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*