Asynchronous operations are getting more and more popular. To the point that we are getting frameworks and environments revolving strictly around that concept. Boost.ASIO, Twisted and node.js are notable example. We will not explore that area. We will focus on techniques for making asynchronous more readable. We will present different currently used solutions. At the end we will introduce coroutines and explain the concept. We will show how these can be integrated with asynchronous code and what we benefit from using coroutines in asynchronous code.
10. Example
//avoid allocating temporary int
void countZeros ( std : : vector <void∗>& a , int ∗ out )
{
out = 0;
for ( auto i = begin ( a ) ; i != end ( a ) ; ++i ) {
i f (!∗ i ) ++out ;
}
}
11. Example
//avoid allocating temporary int
void countZeros ( std : : vector <void∗>& a , int ∗ out )
{
out = 0;
for ( auto i = begin ( a ) ; i != end ( a ) ; ++i ) {
i f (!∗ i ) ++out ;
}
}
template<typename Container >
auto c o u n t n u l l p t r s ( Container const& cont ) −>
std : : s i z e t
{
return count ( begin ( cont ) , end ( cont ) , n u l l p t r ) ;
}
12. Area of interest - asynchronous programming
Synchronous
Your code
Other code
Eg. kernel
Idle
Start operation
Complete operation
Start operation
Complete operation
Executes
other code
Operation results
wait for processing
Asynchronous
Your code
Other code
Eg. kernel
13. Case study: conversation I
We will focus on a example like:
e(d(c(b(a(params)))))
What in synchronous code gets as simple as:
e (d( c (b( a ( params ) ) ) ) )
Or:
resultFromA = a ( params )
resultFromB = b( resultFromA )
resultFromC = c ( resultFromB )
resultFromD = d( resultFromC )
resultFromE = e ( resultFromD )
15. Case study: flow control
Next we will be looking into code that reads two files and compares content:
a = io.open ( ’a.txt’)
a data = a : read ( ’a’)
b = io.open ( ’b.txt’)
b data = b : read ( ’a’)
compare ( a data , b data )
17. Approach #1: callback
C++:
void r e a d h a n d l e r ( e r r o r c o d e const& ec ,
s i z e t bytes ) { . . . }
read ( . . . , r e a d h a n d l e r ) ;
Lua:
function r e a d h a n d l e r ( data ) end
socket : on ("data" , r e a d h a n d l e r )
JavaScript:
function r e a d h a n d l e r ( data ) {}
socket . on ("data" , r e a d h a n d l e r ) ;
18. Approach #1: pitfall
a ( params , function ( resultFromA , e r r )
i f ( ! e r r ) then return end
b( resultFromA , function ( resultFromB , e r r )
i f ( ! e r r ) then return end
c ( resultFromB , function ( resultFromC , e r r )
i f ( ! e r r ) then return end
d( resultFromC , function ( resultFromD , e r r )
i f ( ! e r r ) then return end
e ( resultFromD , function ( resultFromE , e r r )
// . .
end)
end)
end)
end)
end)
19. Approach #1: phony solution
a ( params , HandleResultsOfA ) ;
HandleResultsOfA ( resultFromA , e r r ) {
i f ( ! e r r ) return ;
b( resultFromA , HandleResultsOfB ) ;
}
HandleResultsOfB ( resultFromB , e r r ) {
i f ( ! e r r ) return ;
c ( resultFromB , HandleResultsOfC ) ;
}
HandleResultsOfC ( resultFromC , e r r ) {
i f ( ! e r r ) return ;
d( resultFromC , HandleResultsOfD ) ;
}
HandleResultsOfD ( resultFromD , e r r ) {
i f ( ! e r r ) return ;
21. Approach #1: debugging I
1 f s . s t a t ("1.txt" , function () {
2 f s . s t a t ("2.txt" , function () {
3 //obvious error
4 f s . s t a t ( function (){} , function () {})
5 })
6 })
22. Approach #1: debugging II
Result is ok, error can be easily spotted:
>node t e s t . j s
f s . j s :783
binding . s t a t ( pathModule . makeLong ( path ) , req ) ;
ˆ
TypeError : path must be a s t r i n g
at TypeError ( n a t i v e )
at Object . f s . s t a t ( f s . j s :783:11)
at t e s t . j s : 4 : 8
at FSReqWrap . oncomplete ( f s . j s : 9 5 : 1 5 )
test.js:4 is:
f s . s t a t ( function (){} , function () {})
23. Approach #1: say goodbye to your stacktrace I
What if we use same function in multiple places?
1 var f s = r e q u i r e ( ’fs’ ) ;
2 function bug ( content , cont ) {
3 f s . w r i t e F i l e ("config.json" , content ,
4 function () { f s . s t a t ("config.json" , cont ) ;
5 } ) ;
6 }
7
8 bug ("{}" , "wrong")
24. Approach #1: say goodbye to your stacktrace II
Result:
>node t e s t . j s
throw new TypeError ( ’ c a l l b a c k must be a function ’ ) ;
TypeError : c a l l b a c k must be a f u n c t i o n
at makeCallback ( f s . j s : 7 8 : 1 1 )
at Object . f s . s t a t ( f s . j s :826:14)
at t e s t . j s : 4 : 2 0
at FSReqWrap . oncomplete ( f s . j s : 8 2 : 1 5 )
test.js:4 is:
function () { f s . s t a t ("config.json" , cont ) ;
While there error is in test.js:8:
bug ("{}" , "wrong")
29. Approach #1: pros and cons
Pros
Easy to use,
Integrated by default
Cons
Degrades to callback hell quickly
30. Approach #1: pros and cons
Pros
Easy to use,
Integrated by default
Cons
Degrades to callback hell quickly
Makes stacktraces unusable
31. Approach #1: pros and cons
Pros
Easy to use,
Integrated by default
Cons
Degrades to callback hell quickly
Makes stacktraces unusable
Does not address more complex flow control scenarios
32. Approach #1: pros and cons
Pros
Easy to use,
Integrated by default
Cons
Degrades to callback hell quickly
Makes stacktraces unusable
Does not address more complex flow control scenarios
33. Approach #1: pros and cons
Pros
Easy to use,
Integrated by default
Cons
Degrades to callback hell quickly
Makes stacktraces unusable
Does not address more complex flow control scenarios
34. Approach #2: Promises
Abstract model of asynchronous computation results.
Pending
.then(fulfillCallback,...)
.then(…, rejectCallback)
.catch()
Resolved
Rejected
Cancelled
OnValue
OnError
OnCancel
OnValue
OnError
(no actions)
(action)
(recovery)
(no recovery)
35. Approach #2: Case study I
Do you remember callback hell?
a ( params , function ( resultFromA , e r r )
i f ( ! e r r ) then return end
b( resultFromA , function ( resultFromB , e r r )
i f ( ! e r r ) then return end
c ( resultFromB , function ( resultFromC , e r r )
i f ( ! e r r ) then return end
d( resultFromC , function ( resultFromD , e r r )
i f ( ! e r r ) then return end
e ( resultFromD , function ( resultFromE , e r r )
// . .
end)
end)
end)
end)
end)
36. Approach #2: Case study II
Promises let you write it following way:
a ( params )
: next ( function ( resultFromA )
return b( resultFromA ) end)
: next ( function ( resultFromB )
return c ( resultFromB ) end)
: next ( function ( resultFromC )
return d( resultFromC ) end)
: next ( function ( resultFromD )
. . . end) .
37. Approach #2: Case study III
With a bit of imagination you can read it as:
a ( params )
--:next(function(resultFromA) return
b( resultFromA ) -- end)
--:next(function(resultFromB) return
c ( resultFromB ) -- end)
--:next(function(resultFromC) return
d( resultFromC ) -- end)
--:next(function(resultFromD)
. . . --end)
38. Approach #2: flow control
Promises are abstract model of computation. Therefore it is possible to build
flow control with them. For example:
c(a(paramsOfA), b(paramsOfB))
becomes:
l o c a l a = f s . readAsync ("a.txt")
l o c a l b = f s . readAsync ("b.txt")
d e f e r r e d . a l l ({a , b } ) : next ( function ( r e s u l t s )
return c ( r e s u l t s [ 0 ] , r e s u l t s [ 1 ] )
end )
39. Approach #2: What about callstacks? I
1 var Promise = r e q u i r e ("bluebird" ) ;
2 var f s = r e q u i r e ( ’fs’ ) ;
3 Promise . p r o m i s i f y A l l ( f s ) ;
4 function b( f i l e ) {return function ()
5 {return f s . statAsync ( f i l e )}}
6 f s . statAsync ("1.txt")
7 . then (b("2.txt" ))
8 . then (b( undefined ))
40. Approach #2: It’s a trap!
1 var Promise = r e q u i r e ("bluebird" ) ;
2 var f s = r e q u i r e ( ’fs’ ) ;
3 Promise . p r o m i s i f y A l l ( f s ) ;
4 function b( f i l e ) {return function ()
5 {return f s . statAsync ( f i l e )}}
6 f s . statAsync ("1.txt")
7 . then (b("2.txt" ))
8 . then (b( undefined ))
41. Approach #2: What about callstacks? II
Still not so good:
Unhandled r e j e c t i o n TypeError : path must be a s t r i n g
at TypeError ( n a t i v e )
at Object . f s . s t a t ( f s . j s : 7 8 3 : 1 1 )
at Object . t r y C a t c h e r ( node modules b l u e b i r d j s r e l e a s e u t i l . j s : 1 6 : 2 3 )
at Object . r e t [ as statAsync ] ( e v a l at <anonymous> ( node modules b l u e b i r d j s r e l e a s e p r o m i s i f y . j s : 1 8 4 : 1 2 ) , <a
at t e s t . j s : 5 : 1 3
at t r y C a t c h e r ( node modules b l u e b i r d j s r e l e a s e u t i l . j s : 1 6 : 2 3 )
at Promise . settlePromiseFromHandler ( node modules b l u e b i r d j s r e l e a s e promise . j s : 5 0 9 : 3 1 )
at Promise . s e t t l e P r o m i s e ( node modules b l u e b i r d j s r e l e a s e promise . j s : 5 6 6 : 1 8 )
at Promise . s e t t l e P r o m i s e 0 ( node modules b l u e b i r d j s r e l e a s e promise . j s : 6 1 1 : 1 0 )
at Promise . s e t t l e P r o m i s e s ( node modules b l u e b i r d j s r e l e a s e promise . j s : 6 9 0 : 1 8 )
at Promise . f u l f i l l ( node modules b l u e b i r d j s r e l e a s e promise . j s : 6 3 5 : 1 8 )
at node modules b l u e b i r d j s r e l e a s e nodeback . j s : 4 2 : 2 1
at FSReqWrap . oncomplete ( f s . j s : 9 5 : 1 5 )
test.js:4-5
function b( f i l e ) { return function ()
{ return f s . statAsync ( f i l e )}}
42. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
43. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
44. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
45. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
Cons
Some boiler plate still needed for each call
46. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
Cons
Some boiler plate still needed for each call
Makes stacktraces unusable
47. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
Cons
Some boiler plate still needed for each call
Makes stacktraces unusable
48. Approach #2: pros and cons
Pros
Code structure and business logic are more coherent,
Provide abstraction of flow control
Cons
Some boiler plate still needed for each call
Makes stacktraces unusable
50. Approach #3: Case study I
We will look, again, at e(d(c(b(a(params))))), now with coroutines!
coroutine.wrap ( function ()
resultFromA = a ( params )
resultFromB = b( resultFromA )
resultFromC = c ( resultFromB )
resultFromD = d( resultFromC )
resultFromE = e ( resultFromD )
end ) ( )
51. Approach #3: flow control
Coroutines allow to build flow control with them, too. For example:
c(a(paramsOfA), b(paramsOfB))
becomes:
coroutine.wrap ( function ()
a , b = c o r o s p l i t (
function () return f s. re ad As yn c ( ’a.txt’) end ,
function () return f s. re ad As yn c ( ’b.txt’) end)
c (a , b)
end ) ( )
52. Approach #3: What about callstacks? I
1 local f s = require ’coro-fs’
2 function bug ()
3 f s . s t a t ("2.txt")
4 return f s . s t a t ( function () print "x" end)
5 end
6 coroutine.wrap ( function () xpcall ( function ()
7 f s . s t a t ("1.txt")
8 bug ()
9 end ,
10 function ( e ) print ( debug.traceback ( e ) ) ; return e end)
11 end ) ( )
53. Approach #3: What about callstacks? II
> l u v i t . exe c o r o u t i n e s . lua
deps / coro−f s . lua : 4 4 : bad argument #1 to ’ f s s t a t ’
( s t r i n g expected , got f u n c t i o n )
stack traceback :
t e s t . lua : 1 0 : in f u n c t i o n <t e s t . lua :10>
[C ] : in f u n c t i o n ’ f s s t a t ’
deps / coro−f s . lua : 4 4 : in f u n c t i o n ’ bug ’
t e s t . lua : 4 : in f u n c t i o n ’ bug ’
t e s t . lua : 8 : in f u n c t i o n <t e s t . lua :6>
[C ] : in f u n c t i o n ’ x p c a l l ’
t e s t . lua : 6 : in f u n c t i o n <t e s t . lua :6>
coroutines.lua:6,8
coroutine.wrap ( function () xpcall ( function ()
bug ()
55. Approach #3: pros and cons
Pros
Resembles synchronous code,
Provide abstraction of flow control,
56. Approach #3: pros and cons
Pros
Resembles synchronous code,
Provide abstraction of flow control,
Keeps stack untouched!
57. Approach #3: pros and cons
Pros
Resembles synchronous code,
Provide abstraction of flow control,
Keeps stack untouched!
58. Approach #3: pros and cons
Pros
Resembles synchronous code,
Provide abstraction of flow control,
Keeps stack untouched!
Cons
Some boiler plate still needed for each flow
59. Approach #3: pros and cons
Pros
Resembles synchronous code,
Provide abstraction of flow control,
Keeps stack untouched!
Cons
Some boiler plate still needed for each flow
63. Bibliograpy I
B. Fisher.
Indiana jones and the temple of doom.
https://www.flickr.com/photos/7thstreettheatre/16587334520, 2015.
Cropped out ticket and release details. See licence:
https://creativecommons.org/licenses/by/2.0/ (Attribution 2.0 Generic (CC
BY 2.0)).
Oliver Kowalke.
http://www.boost.org/doc/libs/1 61 0/libs/coroutine/doc/html/coroutine/intro.ht
Distributed under the Boost Software License, Version 1.0. (See
accompanying file LICENSE 1 0.txt or copy at
http://www.boost.org/LICENSE 1 0.txt).