Communication between coroutines

Coroutines normally communicate through string source and string sink values. Sometimes, however, two coroutines need to communicate some out-of-band information that cannot be encoded into this stream. Examples of this kind of out-of-band information are meta-data and processing exceptions.

One way of communicating such information is through shared or global variables. The problem with this technique is that it can be difficult to synchronize the modification of a shared variable by one coroutine and its reading by another coroutine. In order to make a variable modification immediately visible, the coroutine that modifies the variable must immediately after switch to the variable-reading coroutine, and the latter must immediately observe the variable change.

The signal action solves the synchronization problem by immediately raising a throw in the target coroutine, and then resuming its execution. After the target coroutine catches the throw, it can handle the communicated data in a catch clause.

Signals can also be used for two-way communication. Since the target of the signal action can be either a string sink or a string source , the catching coroutine can signal back to the other coroutine.

Example of communication through a shared variable

The example string sink function multiple-file-writer defined below writes a long stream of data into multiple files. In this case, file names represent out-of-band information. The first version communicates the file names through the shared variable file-name. The variable is declared read-only in order to pass a reference to the shelf value, not the value itself, to multiple-file-writer. That way any modification to the original variable will be visible to multiple-file-writer.

 define string sink function
 multiple-file-writer read-only string file-name
 as
 repeat scan #current-input
 match lookahead any
 local string current-file-name initial { file-name }
 
 using output as file current-file-name
 repeat scan #current-input
 match (any (when current-file-name = file-name)) => one
 output one
 again
 again
 
 
 process
 local string file-name
 
 using output as multiple-file-writer file-name
 do
 set file-name to "first.txt"
 output "Contents of the first file.%n"
 set file-name to "second.txt"
 output "Contents of the second file.%n"
 output "Some more contents of the second file.%n"
 set file-name to "third.txt"
 ; the third file is empty
 set file-name to "fourth.txt"
 output "Contents of the fourth file.%n"
 done

If we run this program, we'll discover that the empty file "third.txt" has not been created. The reason for this omission is that the multiple-file-writer coroutine does not resume its execution between two modifications of the shared variable file-name. An OmniMark coroutine resumes execution only when it is given or asked for more data, not every time global state should change.

Another problem with multiple-file-writer as written is that it has to manually check if the file-name has changed after every single byte of the input. If it tried to consume and write multiple bytes at once, it could miss a change of the current file name. This is clearly inefficient.

Example of communication with signals

Both of the above problems can be solved if we use signal instead of a shared variable. To that end, we declare a catch file-change that carries information about the file name, and instead of modifying the shared variable we signal throw file-change before we supply the contents of the next file.

 declare catch file-change value string file-name
 
 define string sink function
 multiple-file-writer
 as
 local string current-file-name
 
 do
 assert !(#current-input matches any) message "No file name has been given to write data into."
 
 catch file-change file-name
 set current-file-name to file-name
 repeat
 using output as file current-file-name
 do scan #current-input
 match any* => contents
 output contents
 done
 exit
 
 catch file-change file-name
 set current-file-name to file-name
 again
 done
 
 
 process
 using output as multiple-file-writer
 do
 signal throw file-change "first.txt"
 output "Contents of the first file.%n"
 signal throw file-change "second.txt"
 output "Contents of the second file.%n"
 output "Some more contents of the second file.%n"
 signal throw file-change "third.txt"
 ; the third file is empty
 signal throw file-change "fourth.txt"
 output "Contents of the fourth file.%n"
 done

Note that multiple-file-writer contains no manual check for a file name change after every charater. It simply consumes its entire normal input at the line match any* => contents. This pattern will be terminated when it reaches either a signal or value-end in the input. In the former case, the signal will be propagated into a throw in the local scope as soon as pattern-matching scope do scan is finished, and then it will be caught and handled by the catch clause.

There is one more performance problem remaining in the code. Although the line match any* => contents consumes the input very quickly, all this input is buffered in memory until the next signal or end of input. The entire contents then gets written into the file by the following line. We can eliminate the unnecessary buffering by replacing the pattern-matching scope

 do scan #current-input
 match any* => contents
 output contents
 done

by the following line:

 output #current-input take any*

Prerequisite Concepts
Related Topics

AltStyle によって変換されたページ (->オリジナル) /