motivation.qbk 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. [/
  2. Copyright Oliver Kowalke 2014.
  3. Distributed under the Boost Software License, Version 1.0.
  4. (See accompanying file LICENSE_1_0.txt or copy at
  5. http://www.boost.org/LICENSE_1_0.txt
  6. ]
  7. [section:motivation Motivation]
  8. In order to support a broad range of execution control behaviour the coroutine
  9. types of __acoro__ can be used to ['escape-and-reenter] loops, to
  10. ['escape-and-reenter] recursive computations and for ['cooperative] multitasking
  11. helping to solve problems in a much simpler and more elegant way than with only
  12. a single flow of control.
  13. [heading event-driven model]
  14. The event-driven model is a programming paradigm where the flow of a program is
  15. determined by events. The events are generated by multiple independent sources
  16. and an event-dispatcher, waiting on all external sources, triggers callback
  17. functions (event-handlers) whenever one of those events is detected (event-loop).
  18. The application is divided into event selection (detection) and event handling.
  19. [$../../../../libs/coroutine2/doc/images/event_model.png [align center]]
  20. The resulting applications are highly scalable, flexible, have high
  21. responsiveness and the components are loosely coupled. This makes the event-driven
  22. model suitable for user interface applications, rule-based productions systems
  23. or applications dealing with asynchronous I/O (for instance network servers).
  24. [heading event-based asynchronous paradigm]
  25. A classic synchronous console program issues an I/O request (e.g. for user
  26. input or filesystem data) and blocks until the request is complete.
  27. In contrast, an asynchronous I/O function initiates the physical operation but
  28. immediately returns to its caller, even though the operation is not yet
  29. complete. A program written to leverage this functionality does not block: it
  30. can proceed with other work (including other I/O requests in parallel) while
  31. the original operation is still pending. When the operation completes, the
  32. program is notified. Because asynchronous applications spend less overall time
  33. waiting for operations, they can outperform synchronous programs.
  34. Events are one of the paradigms for asynchronous execution, but
  35. not all asynchronous systems use events.
  36. Although asynchronous programming can be done using threads, they come with
  37. their own costs:
  38. * hard to program (traps for the unwary)
  39. * memory requirements are high
  40. * large overhead with creation and maintenance of thread state
  41. * expensive context switching between threads
  42. The event-based asynchronous model avoids those issues:
  43. * simpler because of the single stream of instructions
  44. * much less expensive context switches
  45. The downside of this paradigm consists in a sub-optimal program
  46. structure. An event-driven program is required to split its code into
  47. multiple small callback functions, i.e. the code is organized in a sequence of
  48. small steps that execute intermittently. An algorithm that would usually be expressed
  49. as a hierarchy of functions and loops must be transformed into callbacks. The
  50. complete state has to be stored into a data structure while the control flow
  51. returns to the event-loop.
  52. As a consequence, event-driven applications are often tedious and confusing to
  53. write. Each callback introduces a new scope, error callback etc. The
  54. sequential nature of the algorithm is split into multiple callstacks,
  55. making the application hard to debug. Exception handlers are restricted to
  56. local handlers: it is impossible to wrap a sequence of events into a single
  57. try-catch block.
  58. The use of local variables, while/for loops, recursions etc. together with the
  59. event-loop is not possible. The code becomes less expressive.
  60. In the past, code using asio's ['asynchronous operations] was convoluted by
  61. callback functions.
  62. class session
  63. {
  64. public:
  65. session(boost::asio::io_service& io_service) :
  66. socket_(io_service) // construct a TCP-socket from io_service
  67. {}
  68. tcp::socket& socket(){
  69. return socket_;
  70. }
  71. void start(){
  72. // initiate asynchronous read; handle_read() is callback-function
  73. socket_.async_read_some(boost::asio::buffer(data_,max_length),
  74. boost::bind(&session::handle_read,this,
  75. boost::asio::placeholders::error,
  76. boost::asio::placeholders::bytes_transferred));
  77. }
  78. private:
  79. void handle_read(const boost::system::error_code& error,
  80. size_t bytes_transferred){
  81. if (!error)
  82. // initiate asynchronous write; handle_write() is callback-function
  83. boost::asio::async_write(socket_,
  84. boost::asio::buffer(data_,bytes_transferred),
  85. boost::bind(&session::handle_write,this,
  86. boost::asio::placeholders::error));
  87. else
  88. delete this;
  89. }
  90. void handle_write(const boost::system::error_code& error){
  91. if (!error)
  92. // initiate asynchronous read; handle_read() is callback-function
  93. socket_.async_read_some(boost::asio::buffer(data_,max_length),
  94. boost::bind(&session::handle_read,this,
  95. boost::asio::placeholders::error,
  96. boost::asio::placeholders::bytes_transferred));
  97. else
  98. delete this;
  99. }
  100. boost::asio::ip::tcp::socket socket_;
  101. enum { max_length=1024 };
  102. char data_[max_length];
  103. };
  104. In this example, a simple echo server, the logic is split into three member
  105. functions - local state (such as data buffer) is moved to member variables.
  106. __boost_asio__ provides with its new ['asynchronous result] feature a new
  107. framework combining event-driven model and coroutines, hiding the complexity
  108. of event-driven programming and permitting the style of classic sequential code.
  109. The application is not required to pass callback functions to asynchronous
  110. operations and local state is kept as local variables. Therefore the code
  111. is much easier to read and understand.
  112. [footnote Christopher Kohlhoff,
  113. [@ http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3964.pdf
  114. N3964 - Library Foundations for Asynchronous Operations, Revision 1]].
  115. void session(boost::asio::io_service& io_service){
  116. // construct TCP-socket from io_service
  117. boost::asio::ip::tcp::socket socket(io_service);
  118. try{
  119. for(;;){
  120. // local data-buffer
  121. char data[max_length];
  122. boost::system::error_code ec;
  123. // read asynchronous data from socket
  124. // execution context will be suspended until
  125. // some bytes are read from socket
  126. std::size_t length=socket.async_read_some(
  127. boost::asio::buffer(data),
  128. boost::asio::yield[ec]);
  129. if (ec==boost::asio::error::eof)
  130. break; //connection closed cleanly by peer
  131. else if(ec)
  132. throw boost::system::system_error(ec); //some other error
  133. // write some bytes asynchronously
  134. boost::asio::async_write(
  135. socket,
  136. boost::asio::buffer(data,length),
  137. boost::asio::yield[ec]);
  138. if (ec==boost::asio::error::eof)
  139. break; //connection closed cleanly by peer
  140. else if(ec)
  141. throw boost::system::system_error(ec); //some other error
  142. }
  143. } catch(std::exception const& e){
  144. std::cerr<<"Exception: "<<e.what()<<"\n";
  145. }
  146. }
  147. In contrast to the previous example this one gives the impression of sequential
  148. code and local data (['data]) while using asynchronous operations
  149. (['async_read()], ['async_write()]). The algorithm is implemented in one
  150. function and error handling is done by one try-catch block.
  151. [heading recursive descent parsing]
  152. Coroutines let you invert the flow of control so you can ask a recursive descent
  153. parser for parsed symbols.
  154. class Parser{
  155. char next;
  156. std::istream& is;
  157. std::function<void(char)> cb;
  158. char pull(){
  159. return std::char_traits<char>::to_char_type(is.get());
  160. }
  161. void scan(){
  162. do{
  163. next=pull();
  164. }
  165. while(isspace(next));
  166. }
  167. public:
  168. Parser(std::istream& is_,std::function<void(char)> cb_) :
  169. next(), is(is_), cb(cb_)
  170. {}
  171. void run() {
  172. scan();
  173. E();
  174. }
  175. private:
  176. void E(){
  177. T();
  178. while (next=='+'||next=='-'){
  179. cb(next);
  180. scan();
  181. T();
  182. }
  183. }
  184. void T(){
  185. S();
  186. while (next=='*'||next=='/'){
  187. cb(next);
  188. scan();
  189. S();
  190. }
  191. }
  192. void S(){
  193. if (std::isdigit(next)){
  194. cb(next);
  195. scan();
  196. }
  197. else if(next=='('){
  198. cb(next);
  199. scan();
  200. E();
  201. if (next==')'){
  202. cb(next);
  203. scan();
  204. }else{
  205. throw parser_error();
  206. }
  207. }
  208. else{
  209. throw parser_error();
  210. }
  211. }
  212. };
  213. typedef boost::coroutines2::coroutine< char > coro_t;
  214. int main() {
  215. std::istringstream is("1+1");
  216. // invert control flow
  217. coro_t::pull_type seq(
  218. boost::coroutines2::fixedsize_stack(),
  219. [&is](coro_t::push_type & yield) {
  220. // create parser with callback function
  221. Parser p( is,
  222. [&yield](char ch){
  223. // resume user-code
  224. yield(ch);
  225. });
  226. // start recursive parsing
  227. p.run();
  228. });
  229. // user-code pulls parsed data from parser
  230. // invert control flow
  231. for(char c:seq){
  232. printf("Parsed: %c\n",c);
  233. }
  234. }
  235. This problem does not map at all well to communicating between independent
  236. threads. It makes no sense for either side to proceed independently of the
  237. other. You want them to pass control back and forth.
  238. There's yet another advantage to using coroutines. This recursive descent parser
  239. throws an exception when parsing fails. With a coroutine implementation, you
  240. need only wrap the calling code in try/catch.
  241. With communicating threads, you would have to arrange to catch the exception
  242. and pass along the exception pointer on the same queue you're using to deliver
  243. the other events. You would then have to rethrow the exception to unwind the
  244. recursive document processing.
  245. The coroutine solution maps very naturally to the problem space.
  246. [heading 'same fringe' problem]
  247. The advantages of suspending at an arbitrary call depth can be seen
  248. particularly clearly with the use of a recursive function, such as traversal
  249. of trees.
  250. If traversing two different trees in the same deterministic order produces the
  251. same list of leaf nodes, then both trees have the same fringe.
  252. [$../../../../libs/coroutine2/doc/images/same_fringe.png [align center]]
  253. Both trees in the picture have the same fringe even though the structure of the
  254. trees is different.
  255. The same fringe problem could be solved using coroutines by iterating over the
  256. leaf nodes and comparing this sequence via ['std::equal()]. The range of data
  257. values is generated by function ['traverse()] which recursively traverses the
  258. tree and passes each node's data value to its __push_coro__.
  259. __push_coro__ suspends the recursive computation and transfers the data value to
  260. the main execution context.
  261. __pull_coro_it__, created from __pull_coro__, steps over those data values and
  262. delivers them to ['std::equal()] for comparison. Each increment of
  263. __pull_coro_it__ resumes ['traverse()]. Upon return from
  264. ['iterator::operator++()], either a new data value is available, or tree
  265. traversal is finished (iterator is invalidated).
  266. In effect, the coroutine iterator presents a flattened view of the recursive
  267. data structure.
  268. struct node{
  269. typedef std::shared_ptr<node> ptr_t;
  270. // Each tree node has an optional left subtree,
  271. // an optional right subtree and a value of its own.
  272. // The value is considered to be between the left
  273. // subtree and the right.
  274. ptr_t left,right;
  275. std::string value;
  276. // construct leaf
  277. node(const std::string& v):
  278. left(),right(),value(v)
  279. {}
  280. // construct nonleaf
  281. node(ptr_t l,const std::string& v,ptr_t r):
  282. left(l),right(r),value(v)
  283. {}
  284. static ptr_t create(const std::string& v){
  285. return ptr_t(new node(v));
  286. }
  287. static ptr_t create(ptr_t l,const std::string& v,ptr_t r){
  288. return ptr_t(new node(l,v,r));
  289. }
  290. };
  291. node::ptr_t create_left_tree_from(const std::string& root){
  292. /* --------
  293. root
  294. / \
  295. b e
  296. / \
  297. a c
  298. -------- */
  299. return node::create(
  300. node::create(
  301. node::create("a"),
  302. "b",
  303. node::create("c")),
  304. root,
  305. node::create("e"));
  306. }
  307. node::ptr_t create_right_tree_from(const std::string& root){
  308. /* --------
  309. root
  310. / \
  311. a d
  312. / \
  313. c e
  314. -------- */
  315. return node::create(
  316. node::create("a"),
  317. root,
  318. node::create(
  319. node::create("c"),
  320. "d",
  321. node::create("e")));
  322. }
  323. typedef boost::coroutines2::coroutine<std::string> coro_t;
  324. // recursively walk the tree, delivering values in order
  325. void traverse(node::ptr_t n,
  326. coro_t::push_type& out){
  327. if(n->left) traverse(n->left,out);
  328. out(n->value);
  329. if(n->right) traverse(n->right,out);
  330. }
  331. // evaluation
  332. {
  333. node::ptr_t left_d(create_left_tree_from("d"));
  334. coro_t::pull_type left_d_reader([&](coro_t::push_type & out){
  335. traverse(left_d,out);
  336. });
  337. node::ptr_t right_b(create_right_tree_from("b"));
  338. coro_t::pull_type right_b_reader([&](coro_t::push_type & out){
  339. traverse(right_b,out);
  340. });
  341. std::cout << "left tree from d == right tree from b? "
  342. << std::boolalpha
  343. << std::equal(begin(left_d_reader),
  344. end(left_d_reader),
  345. begin(right_b_reader))
  346. << std::endl;
  347. }
  348. {
  349. node::ptr_t left_d(create_left_tree_from("d"));
  350. coro_t::pull_type left_d_reader([&](coro_t::push_type & out){
  351. traverse(left_d,out);
  352. });
  353. node::ptr_t right_x(create_right_tree_from("x"));
  354. coro_t::pull_type right_x_reader([&](coro_t::push_type & out){
  355. traverse(right_x,out);
  356. });
  357. std::cout << "left tree from d == right tree from x? "
  358. << std::boolalpha
  359. << std::equal(begin(left_d_reader),
  360. end(left_d_reader),
  361. begin(right_x_reader))
  362. << std::endl;
  363. }
  364. std::cout << "Done" << std::endl;
  365. output:
  366. left tree from d == right tree from b? true
  367. left tree from d == right tree from x? false
  368. Done
  369. [heading chaining coroutines]
  370. This code shows how coroutines could be chained.
  371. typedef boost::coroutines2::coroutine<std::string> coro_t;
  372. // deliver each line of input stream to sink as a separate string
  373. void readlines(coro_t::push_type& sink,std::istream& in){
  374. std::string line;
  375. while(std::getline(in,line))
  376. sink(line);
  377. }
  378. void tokenize(coro_t::push_type& sink, coro_t::pull_type& source){
  379. // This tokenizer doesn't happen to be stateful: you could reasonably
  380. // implement it with a single call to push each new token downstream. But
  381. // I've worked with stateful tokenizers, in which the meaning of input
  382. // characters depends in part on their position within the input line.
  383. for(std::string line:source){
  384. std::string::size_type pos=0;
  385. while(pos<line.length()){
  386. if(line[pos]=='"'){
  387. std::string token;
  388. ++pos; // skip open quote
  389. while(pos<line.length()&&line[pos]!='"')
  390. token+=line[pos++];
  391. ++pos; // skip close quote
  392. sink(token); // pass token downstream
  393. } else if (std::isspace(line[pos])){
  394. ++pos; // outside quotes, ignore whitespace
  395. } else if (std::isalpha(line[pos])){
  396. std::string token;
  397. while (pos < line.length() && std::isalpha(line[pos]))
  398. token += line[pos++];
  399. sink(token); // pass token downstream
  400. } else { // punctuation
  401. sink(std::string(1,line[pos++]));
  402. }
  403. }
  404. }
  405. }
  406. void only_words(coro_t::push_type& sink,coro_t::pull_type& source){
  407. for(std::string token:source){
  408. if (!token.empty() && std::isalpha(token[0]))
  409. sink(token);
  410. }
  411. }
  412. void trace(coro_t::push_type& sink, coro_t::pull_type& source){
  413. for(std::string token:source){
  414. std::cout << "trace: '" << token << "'\n";
  415. sink(token);
  416. }
  417. }
  418. struct FinalEOL{
  419. ~FinalEOL(){
  420. std::cout << std::endl;
  421. }
  422. };
  423. void layout(coro_t::pull_type& source,int num,int width){
  424. // Finish the last line when we leave by whatever means
  425. FinalEOL eol;
  426. // Pull values from upstream, lay them out 'num' to a line
  427. for (;;){
  428. for (int i = 0; i < num; ++i){
  429. // when we exhaust the input, stop
  430. if (!source) return;
  431. std::cout << std::setw(width) << source.get();
  432. // now that we've handled this item, advance to next
  433. source();
  434. }
  435. // after 'num' items, line break
  436. std::cout << std::endl;
  437. }
  438. }
  439. // For example purposes, instead of having a separate text file in the
  440. // local filesystem, construct an istringstream to read.
  441. std::string data(
  442. "This is the first line.\n"
  443. "This, the second.\n"
  444. "The third has \"a phrase\"!\n"
  445. );
  446. {
  447. std::cout << "\nfilter:\n";
  448. std::istringstream infile(data);
  449. coro_t::pull_type reader(std::bind(readlines, _1, std::ref(infile)));
  450. coro_t::pull_type tokenizer(std::bind(tokenize, _1, std::ref(reader)));
  451. coro_t::pull_type filter(std::bind(only_words, _1, std::ref(tokenizer)));
  452. coro_t::pull_type tracer(std::bind(trace, _1, std::ref(filter)));
  453. for(std::string token:tracer){
  454. // just iterate, we're already pulling through tracer
  455. }
  456. }
  457. {
  458. std::cout << "\nlayout() as coroutine::push_type:\n";
  459. std::istringstream infile(data);
  460. coro_t::pull_type reader(std::bind(readlines, _1, std::ref(infile)));
  461. coro_t::pull_type tokenizer(std::bind(tokenize, _1, std::ref(reader)));
  462. coro_t::pull_type filter(std::bind(only_words, _1, std::ref(tokenizer)));
  463. coro_t::push_type writer(std::bind(layout, _1, 5, 15));
  464. for(std::string token:filter){
  465. writer(token);
  466. }
  467. }
  468. {
  469. std::cout << "\nfiltering output:\n";
  470. std::istringstream infile(data);
  471. coro_t::pull_type reader(std::bind(readlines,_1,std::ref(infile)));
  472. coro_t::pull_type tokenizer(std::bind(tokenize,_1,std::ref(reader)));
  473. coro_t::push_type writer(std::bind(layout,_1,5,15));
  474. // Because of the symmetry of the API, we can use any of these
  475. // chaining functions in a push_type coroutine chain as well.
  476. coro_t::push_type filter(std::bind(only_words,std::ref(writer),_1));
  477. for(std::string token:tokenizer){
  478. filter(token);
  479. }
  480. }
  481. [endsect]