Карта сайта Kansoftware
НОВОСТИУСЛУГИРЕШЕНИЯКОНТАКТЫ
Разработка программного обеспечения

Motivation

Boost , Chapter 1. Coroutine2 , Chapter 1. Coroutine2

Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Для поддержки широкого диапазона поведения управления исполнением корутинные типы coroutine<> могут использоваться для циклов escape-and-reenter, для escape-and-reenter рекурсивных вычислений и для cooperative многозадачности, помогающей решать задачи гораздо проще и элегантнее, чем при одном потоке управления.

event-driven model

Модель, управляемая событиями, представляет собой парадигму программирования, в которой поток программы определяется событиями. События генерируются несколькими независимыми источниками, и диспетчер событий, ожидающий всех внешних источников, запускает функции обратного вызова (операторы событий) всякий раз, когда одно из этих событий обнаруживается (петля событий). Приложение делится на выбор событий (обнаружение) и обработку событий.

event_model

Полученные приложения являются высокомасштабируемыми, гибкими, имеют высокую отзывчивость, а компоненты слабо связаны. Это делает модель, управляемую событиями, подходящей для приложений пользовательского интерфейса, систем производства на основе правил или приложений, имеющих дело с асинхронным I/O (например, сетевые серверы).

event-based asynchronous paradigm

Классическая синхронная консольная программа выдает запрос ввода/вывода (например, для ввода пользователем или данных файловой системы) и блокирует, пока запрос не будет завершен.

Напротив, асинхронная функция ввода/вывода инициирует физическую операцию, но сразу же возвращается к абоненту, даже если операция еще не завершена. Программа, написанная для использования этой функции, не блокирует: она может выполнять другую работу (включая другие запросы ввода/вывода параллельно), пока исходная операция еще не завершена. После завершения операции программа уведомляется. Поскольку асинхронные приложения тратят меньше времени на ожидание операций, они могут превзойти синхронные программы.

События являются одной из парадигм асинхронного исполнения, но не все асинхронные системы используют события. Хотя асинхронное программирование может быть выполнено с использованием потоков, они имеют свои собственные затраты:

  • трудно программировать (ловушки для неосторожных)
  • требования к памяти высокие
  • большие накладные расходы с созданием и поддержанием состояния нити
  • переключение контекста между потоками

Асинхронная модель на основе событий позволяет избежать таких проблем:

  • проще благодаря единому потоку инструкций
  • менее дорогие переключатели контекста

Недостатком этой парадигмы является субоптимальная программная структура. Программа, управляемая событиями, должна разделить свой код на несколько небольших функций обратного вызова, то есть код организован в последовательности небольших шагов, которые выполняются периодически. Алгоритм, который обычно выражается в виде иерархии функций и циклов, должен быть преобразован в обратный вызов. Полное состояние должно храниться в структуре данных, в то время как поток управления возвращается в контур событий. Как следствие, приложения, управляемые событиями, часто утомительны и запутанны в написании. Каждый возврат вызова вводит новый объем, обратный вызов ошибки и т. Д. Последовательный характер алгоритма разделен на несколько calltacks, что затрудняет отладку приложения. Обработчики исключений ограничены локальными обработчиками: невозможно обернуть последовательность событий в один блок поиска. Использование локальных переменных, в то время как/для циклов, рекурсий и т.д. вместе с контуром событий невозможно. Код становится менее выразительным.

В прошлом код, использующий асинхронные операции , был извлечён функциями обратного вызова.

class session
{
public:
    session(boost::asio::io_service& io_service) :
          socket_(io_service) // construct a TCP-socket from io_service
    {}
    tcp::socket& socket(){
        return socket_;
    }
    void start(){
        // initiate asynchronous read; handle_read() is callback-function
        socket_.async_read_some(boost::asio::buffer(data_,max_length),
            boost::bind(&session::handle_read,this,
                boost::asio::placeholders::error,
                boost::asio::placeholders::bytes_transferred));
    }
private:
    void handle_read(const boost::system::error_code& error,
                     size_t bytes_transferred){
        if (!error)
            // initiate asynchronous write; handle_write() is callback-function
            boost::asio::async_write(socket_,
                boost::asio::buffer(data_,bytes_transferred),
                boost::bind(&session::handle_write,this,
                    boost::asio::placeholders::error));
        else
            delete this;
    }
    void handle_write(const boost::system::error_code& error){
        if (!error)
            // initiate asynchronous read; handle_read() is callback-function
            socket_.async_read_some(boost::asio::buffer(data_,max_length),
                boost::bind(&session::handle_read,this,
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));
        else
            delete this;
    }
    boost::asio::ip::tcp::socket socket_;
    enum { max_length=1024 };
    char data_[max_length];
};

В этом примере простой эхо-сервер, логика разделена на три функции-члена - локальное состояние (например, буфер данных) перемещается в переменные-члена.

Boost.Asio обеспечивает своим новым асинхронным результатом новый фреймворк, объединяющий событийно-управляемую модель и корутины, скрывающий сложность событийно-управляемого программирования и позволяющий стиль классического последовательного кода. Приложение не обязано передавать функции обратного вызова асинхронным операциям, а локальное состояние сохраняется как локальные переменные. Поэтому код намного проще читать и понимать. [4].

void session(boost::asio::io_service& io_service){
    // construct TCP-socket from io_service
    boost::asio::ip::tcp::socket socket(io_service);
    try{
        for(;;){
            // local data-buffer
            char data[max_length];
            boost::system::error_code ec;
            // read asynchronous data from socket
            // execution context will be suspended until
            // some bytes are read from socket
            std::size_t length=socket.async_read_some(
                    boost::asio::buffer(data),
                    boost::asio::yield[ec]);
            if (ec==boost::asio::error::eof)
                break; //connection closed cleanly by peer
            else if(ec)
                throw boost::system::system_error(ec); //some other error
            // write some bytes asynchronously
            boost::asio::async_write(
                    socket,
                    boost::asio::buffer(data,length),
                    boost::asio::yield[ec]);
            if (ec==boost::asio::error::eof)
                break; //connection closed cleanly by peer
            else if(ec)
                throw boost::system::system_error(ec); //some other error
        }
    } catch(std::exception const& e){
        std::cerr<<"Exception: "<<e.what()<<"\n";
    }
}

В отличие от предыдущего примера этот дает представление о последовательном коде и локальных данных (data) при использовании асинхронных операций (async_read(), async_write()). Алгоритм реализован в одной функции, а обработка ошибок выполняется одним блоком поиска.

recursive descent parsing

Coroutines позволяет вам перевернуть поток управления, чтобы вы могли попросить рекурсивный парсер спуска для парсированных символов.

class Parser{
   char next;
   std::istream& is;
   std::function<void(char)> cb;
   char pull(){
        return std::char_traits<char>::to_char_type(is.get());
   }
   void scan(){
       do{
           next=pull();
       }
       while(isspace(next));
   }
public:
   Parser(std::istream& is_,std::function<void(char)> cb_) :
      next(), is(is_), cb(cb_)
    {}
   void run() {
      scan();
      E();
   }
private:
   void E(){
      T();
      while (next=='+'||next=='-'){
         cb(next);
         scan();
         T();
      }
   }
   void T(){
      S();
      while (next=='*'||next=='/'){
         cb(next);
         scan();
         S();
      }
   }
   void S(){
      if (std::isdigit(next)){
         cb(next);
         scan();
      }
      else if(next=='('){
         cb(next);
         scan();
         E();
         if (next==')'){
             cb(next);
             scan();
         }else{
             throw parser_error();
         }
      }
      else{
         throw parser_error();
      }
   }
};
typedef boost::coroutines2::coroutine< char > coro_t;
int main() {
    std::istringstream is("1+1");
    // invert control flow
    coro_t::pull_type seq(
            boost::coroutines2::fixedsize_stack(),
            [&is](coro_t::push_type & yield) {
                // create parser with callback function
                Parser p( is,
                          [&yield](char ch){
                            // resume user-code
                            yield(ch);
                          });
                // start recursive parsing
                p.run();
            });
    // user-code pulls parsed data from parser
    // invert control flow
    for(char c:seq){
        printf("Parsed: %c\n",c);
    }
}

Эта проблема совсем не подходит для общения между независимыми потоками. Нет смысла, чтобы обе стороны действовали независимо друг от друга. Вы хотите, чтобы они передавали контроль туда и обратно.

Есть еще одно преимущество использования корутин. Этот рекурсивный парсер спуска бросает исключение, когда парсинг терпит неудачу. При реализации корутина вам нужно только обернуть код вызова в try/catch.

С помощью коммуникационных потоков вам придется договориться, чтобы поймать исключение и передать указатель исключения в той же очереди, которую вы используете для доставки других событий. Затем вам придется перебросить исключение, чтобы отключить рекурсивную обработку документов.

Корутинное решение очень естественно отображает проблемное пространство.

'same fringe' problem

Преимущества суспендирования на произвольной глубине вызова можно увидеть особенно четко с использованием рекурсивной функции, такой как обход деревьев. Если пересечение двух разных деревьев в одном и том же детерминистском порядке приводит к одному и тому же списку листовых узлов, то оба дерева имеют один и тот же край.

same_fringe

Оба дерева на картине имеют одинаковую оконечность, хотя структура деревьев отличается.

Та же самая пограничная проблема может быть решена с помощью корутин путем итерации по листовым узлам и сравнения этой последовательности через std::equal(). Диапазон значений данных генерируется функцией traverse(), которая рекурсивно пересекает дерево и передает значение данных каждого узла его coroutine<>::push_type. coroutine<>::push_type приостанавливает рекурсивные вычисления и передает значение данных в основной контекст исполнения. coroutine<>::pull_type::iterator, созданный из coroutine<>::pull_type, перешагивает эти значения данных и доставляет их к std::equal() для сравнения. Каждое приращение coroutine<>::pull_type::iterator возобновляет traverse(). По возвращении из iterator::operator++() либо доступно новое значение данных, либо завершено прохождение дерева (iterator признан недействительным).

По сути, корутинный итератор представляет сглаженный вид рекурсивной структуры данных.

struct node{
    typedef std::shared_ptr<node> ptr_t;
    // Each tree node has an optional left subtree,
    // an optional right subtree and a value of its own.
    // The value is considered to be between the left
    // subtree and the right.
    ptr_t       left,right;
    std::string value;
    // construct leaf
    node(const std::string& v):
        left(),right(),value(v)
    {}
    // construct nonleaf
    node(ptr_t l,const std::string& v,ptr_t r):
        left(l),right(r),value(v)
    {}
    static ptr_t create(const std::string& v){
        return ptr_t(new node(v));
    }
    static ptr_t create(ptr_t l,const std::string& v,ptr_t r){
        return ptr_t(new node(l,v,r));
    }
};
node::ptr_t create_left_tree_from(const std::string& root){
    /* --------
         root
         / \
        b   e
       / \
      a   c
     -------- */
    return node::create(
            node::create(
                node::create("a"),
                "b",
                node::create("c")),
            root,
            node::create("e"));
}
node::ptr_t create_right_tree_from(const std::string& root){
    /* --------
         root
         / \
        a   d
           / \
          c   e
       -------- */
    return node::create(
            node::create("a"),
            root,
            node::create(
                node::create("c"),
                "d",
                node::create("e")));
}
typedef boost::coroutines2::coroutine<std::string>   coro_t;
// recursively walk the tree, delivering values in order
void traverse(node::ptr_t n,
              coro_t::push_type& out){
    if(n->left) traverse(n->left,out);
    out(n->value);
    if(n->right) traverse(n->right,out);
}
// evaluation
{
    node::ptr_t left_d(create_left_tree_from("d"));
    coro_t::pull_type left_d_reader([&](coro_t::push_type & out){
                                        traverse(left_d,out);
                                    });
    node::ptr_t right_b(create_right_tree_from("b"));
    coro_t::pull_type right_b_reader([&](coro_t::push_type & out){
                                        traverse(right_b,out);
                                     });
    std::cout << "left tree from d == right tree from b? "
              << std::boolalpha
              << std::equal(begin(left_d_reader),
                            end(left_d_reader),
                            begin(right_b_reader))
              << std::endl;
}
{
    node::ptr_t left_d(create_left_tree_from("d"));
    coro_t::pull_type left_d_reader([&](coro_t::push_type & out){
                                        traverse(left_d,out);
                                    });
    node::ptr_t right_x(create_right_tree_from("x"));
    coro_t::pull_type right_x_reader([&](coro_t::push_type & out){
                                         traverse(right_x,out);
                                     });
    std::cout << "left tree from d == right tree from x? "
              << std::boolalpha
              << std::equal(begin(left_d_reader),
                            end(left_d_reader),
                            begin(right_x_reader))
              << std::endl;
}
std::cout << "Done" << std::endl;
output:
left tree from d == right tree from b? true
left tree from d == right tree from x? false
Done

chaining coroutines

Этот код показывает, как могут быть прикованы корутины.

typedef boost::coroutines2::coroutine<std::string> coro_t;
// deliver each line of input stream to sink as a separate string
void readlines(coro_t::push_type& sink,std::istream& in){
    std::string line;
    while(std::getline(in,line))
        sink(line);
}
void tokenize(coro_t::push_type& sink, coro_t::pull_type& source){
    // This tokenizer doesn't happen to be stateful: you could reasonably
    // implement it with a single call to push each new token downstream. But
    // I've worked with stateful tokenizers, in which the meaning of input
    // characters depends in part on their position within the input line.
    for(std::string line:source){
        std::string::size_type pos=0;
        while(pos<line.length()){
            if(line[pos]=='"'){
                std::string token;
                ++pos;              // skip open quote
                while(pos<line.length()&&line[pos]!='"')
                    token+=line[pos++];
                ++pos;              // skip close quote
                sink(token);        // pass token downstream
            } else if (std::isspace(line[pos])){
                ++pos;              // outside quotes, ignore whitespace
            } else if (std::isalpha(line[pos])){
                std::string token;
                while (pos < line.length() && std::isalpha(line[pos]))
                    token += line[pos++];
                sink(token);        // pass token downstream
            } else {                // punctuation
                sink(std::string(1,line[pos++]));
            }
        }
    }
}
void only_words(coro_t::push_type& sink,coro_t::pull_type& source){
    for(std::string token:source){
        if (!token.empty() && std::isalpha(token[0]))
            sink(token);
    }
}
void trace(coro_t::push_type& sink, coro_t::pull_type& source){
    for(std::string token:source){
        std::cout << "trace: '" << token << "'\n";
        sink(token);
    }
}
struct FinalEOL{
    ~FinalEOL(){
        std::cout << std::endl;
    }
};
void layout(coro_t::pull_type& source,int num,int width){
    // Finish the last line when we leave by whatever means
    FinalEOL eol;
    // Pull values from upstream, lay them out 'num' to a line
    for (;;){
        for (int i = 0; i < num; ++i){
            // when we exhaust the input, stop
            if (!source) return;
            std::cout << std::setw(width) << source.get();
            // now that we've handled this item, advance to next
            source();
        }
        // after 'num' items, line break
        std::cout << std::endl;
    }
}
// For example purposes, instead of having a separate text file in the
// local filesystem, construct an istringstream to read.
std::string data(
    "This is the first line.\n"
    "This, the second.\n"
    "The third has \"a phrase\"!\n"
    );
{
    std::cout << "\nfilter:\n";
    std::istringstream infile(data);
    coro_t::pull_type reader(std::bind(readlines, _1, std::ref(infile)));
    coro_t::pull_type tokenizer(std::bind(tokenize, _1, std::ref(reader)));
    coro_t::pull_type filter(std::bind(only_words, _1, std::ref(tokenizer)));
    coro_t::pull_type tracer(std::bind(trace, _1, std::ref(filter)));
    for(std::string token:tracer){
        // just iterate, we're already pulling through tracer
    }
}
{
    std::cout << "\nlayout() as coroutine::push_type:\n";
    std::istringstream infile(data);
    coro_t::pull_type reader(std::bind(readlines, _1, std::ref(infile)));
    coro_t::pull_type tokenizer(std::bind(tokenize, _1, std::ref(reader)));
    coro_t::pull_type filter(std::bind(only_words, _1, std::ref(tokenizer)));
    coro_t::push_type writer(std::bind(layout, _1, 5, 15));
    for(std::string token:filter){
        writer(token);
    }
}
{
    std::cout << "\nfiltering output:\n";
    std::istringstream infile(data);
    coro_t::pull_type reader(std::bind(readlines,_1,std::ref(infile)));
    coro_t::pull_type tokenizer(std::bind(tokenize,_1,std::ref(reader)));
    coro_t::push_type writer(std::bind(layout,_1,5,15));
    // Because of the symmetry of the API, we can use any of these
    // chaining functions in a push_type coroutine chain as well.
    coro_t::push_type filter(std::bind(only_words,std::ref(writer),_1));
    for(std::string token:tokenizer){
        filter(token);
    }
}

PrevUpHomeNext

Статья Motivation раздела Chapter 1. Coroutine2 Chapter 1. Coroutine2 может быть полезна для разработчиков на c++ и boost.




Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.



:: Главная :: Chapter 1. Coroutine2 ::


реклама


©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007
Top.Mail.Ru

Время компиляции файла: 2024-08-30 11:47:00
2025-05-19 21:41:00/0.010951995849609/0