Asynchronous Exceptions

Note: Unlike the other examples on this page, this one doesn't actually work. The problem is that Charcoal inherited a lack of exceptions from C. If one were to implement activities in a language with exception handling, this example would be relevant. Read on if that sounds interesting to you.

An asynchronous exception is one that is thrown (or raised) by some entity other than the currently executing code. One of the classic use cases for asynchronous exceptions is an algorithm with multiple implementation approaches with very different performance profiles. Different approaches perform dramatically better or worse based on some hard-to-predict characteristic of the input data.

If we have asynchronous exceptions, we can try all approaches concurrently. When one of them finishes, it kills the other ones. Here's a sketch of how it would work in Charcoal (if it had exception handling with Java-ish syntax).

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
result_type algo_X( input data )
{
    
result_type rv;
    
activity_t a1, a2;
    
barrier_t b;
    
barrier_init( &b, 2 );
    
a1 = activate
    
{
        
try
        
{
            
barrier_wait( &b );
            
rv = algo_X_approach_1( input data );
            
deliver a2 terminate_thyself;
        
}
        
catch( terminate_thyself ) { }
        
/* clean up approach 1 */
    
}
    
a2 = activate
    
{
        
try
        
{
            
barrier_wait( &b );
            
rv = algo_X_approach_2( input data );
            
deliver a1 terminate_thyself;
        
}
        
catch( terminate_thyself ) { }
        
/* clean up approach 2 */
    
}
    
join( a1 );
    
join( a2 );
    
return rv;
}

Quick side note: Remember, activities do not run in parallel, so the goal here is not to benefit from processor parallelism. Rather we should expect this code to take about twice as much time as the better of the two approaches on any given input. If the two approaches have sufficiently different performance profiles, this is a big win in terms of worst-case performance, relative to simply doing one or the other.

The first important thing to say about asynchronous exceptions (or their cousin, thread cancelation) is that it is a sufficiently tempting feature that many multithreading APIs include it in version 1. However, it is sufficiently problematic to implement correctly that most API documentation strongly discourages its use, and many APIs/languages have explicitly deprecated it. The one notable exception [sorry] I am aware of is Haskell (authors' copy).

Why is the implementation of asynchronous exceptions so hairy? I encourage you to look at the paper linked above; it's a good one. The short story is that compilers and processors can and do reorder expressions and statements all the time for the purpose of optimization. As a result, if one interrupts a running thread at any given time there may be no valid high-level program state that accurately reflects the low-level state. So where should the exception appear that it was thrown from? Threads can stay in such "ambiguous" states for arbitrarily long periods of time.

With activities (and really anything in the cooperative thread family), we can actually implement asynchronous exceptions sensibly. When one activity delivers an exception to another activity, the exception will appear to be raised by the next yield expression that the receiver activity evaluates. This pattern seems a little better-suited to activities than conventional cooperative threads because yields generally happen quite frequently, so there is a better chance of the exception being delivered in a timely manner.

(By the way, I used deliver instead of throw because throwing and delivering exceptions are wildly different. Throwing changes the control flow of the current activity to the relevant handler. Delivering affects some other activity, and the current activity goes along its merry way.)

We Can, But Should We?

So delivering asynchronous exceptions between activities is not the train wreck that it is in the context of threads, but is it worth actually offering as a language feature? This is a tricky question. The main problem is that asynchronous exceptions can arrive at any time, so developers have to be extremely cautious to avoid getting interrupted in the middle of a delicate sequence of operations. I think activities have a fighting chance because unyielding can be used to block asynchronous exceptions for regions of code. However, I think more research is needed on this topic.

Even "synchronous" (i.e. normal) exception handling has its skeptics (1, 2, 3, 4, 5, 6). The big-picture issue here is that non-local control transfers always carry a cost in terms of code understandability (and thus maintainability and robustness). My own current rule of thumb is that exception handling should mostly be restricted to weird near-catastrophic situations, not "normal" error conditions.

For asynchronous exception handling, my intuition is that it has some legitimate use cases, but it should be used only when there is some huge win relative to alternative patterns.

This is on the back burner for the Charcoal project, because C doesn't have exception handling, but I am very interested in hearing other peoples' thoughts on the topic.


Creative Commons License
Charcoal Programming Language by Benjamin Ylvisaker is licensed under a Creative Commons Attribution 4.0 International License