[NAME] ALL.dao.tutorial.concurrent [TITLE] Concurrent Programming [DESCRIPTION] Dao has several features that support concurrent programming. The first is a concurrent Garbage Collector (GC), which works behind the scene. The other features come in the following forms: multi-threading module, asynchronous function call and asynchronous object, which are built around a lightweight thread pool with a simple scheduler to map thread tasks (or tasklets) to native threads. 0.1 Multi-threading Module The Multi-threading Module ( mt) offers the traditional multi-threading based on threads with mutexes, condition variables and semaphores. The main functionalities of mt are provided as code section methods for maximum flexibility and expressiveness. For example, a tasklet can be started with the mt.start() method, 1 fut1 = mt.start { 2 sum = 0; 3 for( i = 1 : 10000 ) sum += i * i 4 return sum 5 } 6 fut2 = mt.start( $now ) { 7 sum = 0; 8 for( i = 1 : 10000 ) sum += i * i 9 return sum 10 } 11 io.writeln( fut1.value(), fut2.value() ) mt.start() can execute any expression or block of codes as a tasklet, which may be scheduled to run when a native thread become available to it. If the tasklet needs to be started immediately, the additional parameter $now can be passed to mt.start(), so that a new native thread may be created if there is no other native thread available for the task. mt.start() returns a future value which will be used to store the value returned by the tasklet. Accessing its value by future.value() will cause the current tasklet to block until the value becomes available after the tasklet finishs. If one wants to wait for the task for only a limited amount of time, the method future.value() can be used with a timeout parameter (a float value for seconds or fraction seconds, negative for infinite waiting). 0.2 Parallelized Code Section Methods However, the simplest way to do threaded programming is to use the following parallelized code section methods of mt, * iterate(): Iterate a predefined number of times, or iterate over a list, map or array, and execute the code block on each of the items; * map() Map a list, map or array to another list, map or array, using the values returned by the code block; * apply() Apply the values returned by the code block to a list, map or array; * find() Find the first item in a list or map the satisfy the condition as tested by the code block. The default number of threads used by these methods are 2. Examples, 1 ls = {1,2,3,4,5,6} 2 3 # Use 2 threads to iterate 10 times: 4 mt.iterate( 10 ) { io.writeln( X ) } 5 6 # Use 3 threads to iterate over a list: 7 mt.iterate( ls, 3 ) { io.writeln( X ) } 8 9 mt.map( ls, 2 ) { X*X } # produces new list: {1,4,9,16,25,36} 10 mt.apply( ls, 2 ) { X*X } # ls becomes: {1,4,9,16,25,36} 0.3 Asynchronous Function Call Asynchronous Function Call (AFC) allows a function call to be executed as a tasklet, and return immediately a future value that can be use to block on the tasklet and wait for its completion. Any standard function call followed by !! will start an AFC. 1 routine MyFunction( n = 10 ) 2 { 3 for( i = 1 : n ) io.writeln( i ) 4 } 5 f = MyFunction( 20 ) !! 6 io.writeln( f.value() ) When multiple methods of a class instance are called in asynchronous mode, their execution will be mutually exclusive, namely, at any given time only call (tasklet) can be active. So in this way, the class instance acts just like a monitor. 0.4 Asynchronous Object An asynchronous object is a class instance that is created with asynchronous call mode, 1 async_objet = some_class( ... ) !! When an asynchronous object invokes a method, it will be automatically called in asynchronous mode, and start a tasklet and return a future value. Such tasklets are queued and and executed in the order they are queued. So such asynchronous object acts like an actor in the actor model considering that calling methods of such object is just like sending messages to the object with function parameters being the message contents. For each instance, these messages are processed one by one in the order of receiving. Here is an simple example, 1 class Account 2 { 3 private 4 5 var balance = 0 6 7 public 8 9 routine Account( init = 0 ){ 10 balance = init 11 } 12 routine Withdraw( amount : int ) => enum<false,true> 13 { 14 if ( balance < amount ) return $false 15 balance -= amount 16 return $true 17 } 18 routine Deposit( amount : int ) => int 19 { 20 balance += amount 21 return balance 22 } 23 routine Balance() => int 24 { 25 return balance 26 } 27 } 28 29 acount1 = Account( 100 ) !! 30 acount2 = Account( 100 ) !! 31 32 future1 = acount1.Withdraw( 10 ) 33 if( future1.value() == $true ) future2 = acount2.Deposit( 10 ) 34 35 future3 = acount1.Deposit( 20 ) 36 37 io.writeln( 'Balance in account1:', acount1.Balance().value() ) 38 io.writeln( 'Balance in account2:', acount2.Balance().value() ) Like calling mt.start(), calling a method on an asynchronous object will return a future value, which can be used to check the status of the asynchronous call. 0.5 Communication Channels Tasklets (represented by future values) created from mt::start, asynchronous function call and the methods of asynchronous object can be assigned to native threads to run concurrently. Tasklets can communicate and synchronize with each other using communication channels. The channel type is implemented as a customized C data type that supports template-like type arguments. So to construct a channel that can send and receive integers, one can use, 1 chan = mt::channel<int>() Here channel is prefixed with mt::, because channel is defined in the built-in mt multi-threading module. The type argument int indicates this channel can only be used to transmit integers. Currently, only primitive types ( int, float, double, complex, string, enum) plus array types, and their composition through list, map and tuple types are supported for channels. Each channel has a capacity for transmitting data, which are buffered in the channel. When the channel's buffer reached its capacity, any attempt to send data to it will result in the blocking of the sender, which will be resumed either after a timeout or after some data of the channel have been read out of the buffer to make room for the new data being sent by the blocked sender. Channel capacity can be passed to the constructor of the channel as an optional parameter, which is one by default. The essential methods of channel are: * send(): for sending data to the channel; * receive(): for receiving data from the channel; * select(): for selecting a group of channels; See dao.concurrent.channel for details. Here is a simple example, 1 chan = mt::channel<int>(2) 2 3 produce = mt.start { 4 index = 0; 5 while( ++index <= 10 ){ 6 io.writeln( 'sending', index ) 7 chan.send( index ) 8 } 9 chan.cap(0) # set channel buffer size to zero to close the channel; 10 } 11 12 consume = mt.start { 13 while(1){ 14 data = chan.receive() 15 io.writeln( "received", data ); 16 if( data.status == $finished ) break 17 } 18 }