Threads can be interrupted. An interrupt is a notification of some event such as a keystroke, a timer expiring, the reception of a network packet, the completion of a disk operation, and so on. We distinguish interrupts and exceptions. An exception is caused by the thread executing an invalid machine instruction such as divide-by-zero. An interrupt is caused by some peripheral device and can be handled in Harmony. In other words: an interrupt is a notification, while an exception is an error.
sequential done count = 0 done = False def handler(): count += 1 done = True def main(): trap handler() await done assert count == 1 spawn main()
Harmony allows modeling interrupts using the
trap handler argument
handlerargument at some later, unspecified time. Thus you can think of trap as setting a timer. Only one of these asynchronous events can be outstanding at a time; a new call to trap overwrites any outstanding one. Figure 22.1 gives an example of how trap might be used. Here, the
main() thread loops until the interrupt has occurred and the done flag has been set.
sequential done count = 0 done = False def handler(): count += 1 done = True def main(): trap handler() count += 1 await done assert count == 2 spawn main()
But now consider Figure 22.2. The difference with Figure 22.1 is that
handler() methods increment count. This is not
unlike the example we gave in Figure 3.2, except that only a single
thread is involved now. And, indeed, it suffers from a similar race
condition; run it through Harmony to see for yourself. If the interrupt
main() reads count (and thus still has value 0) but
main() writes the updated value 1, then the interrupt handler
will also read value 0 and write value 1. We say that the code in
Figure 22.2 is not interrupt-safe (as opposed to not being
from synch import Lock, acquire, release sequential done countlock = Lock() count = 0 done = False def handler(): acquire(?countlock) count += 1 release(?countlock) done = True def main(): trap handler() acquire(?countlock) count += 1 release(?countlock) await done assert count == 2 spawn main()
You would be excused if you wanted to solve the problem using locks,
similar to Figure 8.3. Figure 22.3 shows how one might go about
this. But locks are intended to solve synchronization issues between
multiple threads. But an interrupt handler is not run by another
thread---it is run by the same thread that experienced the interrupt. If
you run the code through Harmony, you will find that the code may not
terminate. The issue is that a thread can only acquire a lock once. If
the interrupt happens after
main() acquires the lock but before
main() releases it, the
handler() method will block trying to
acquire the lock, even though it is being acquired by the same thread
that already holds the lock.
sequential done count = 0 done = False def handler(): count += 1 done = True def main(): trap handler() setintlevel(True) count += 1 setintlevel(False) await done assert count == 2 spawn main()
Instead, the way one fixes interrupt-safety issues is through disabling
interrupts temporarily. In Harmony, this can be done by setting the
interrupt level of a thread to
True using the setintlevel
interface. Figure 22.4 illustrates how this is done. Note that it is
not necessary to change the interrupt level during servicing an
interrupt, because it is automatically set to
True upon entry to the
interrupt handler and restored to
False upon exit. It is important
main() code re-enables interrupts after incrementing count.
What would happen if
main() left interrupts disabled?
sequential done count = 0 done = False def increment(): let prior = setintlevel(True): count += 1 setintlevel(prior) def handler(): increment() done = True def main(): trap handler() increment() await done assert count == 2 spawn main()
setintlevel(il) sets the interrupt level to il and returns the prior interrupt level. Returning the old level is handy when writing interrupt-safe methods that can be called from ordinary code as well as from an interrupt handler. Figure 22.5 shows how one might write a interrupt-safe method to increment the counter.
from synch import Lock, acquire, release sequential t_done, i_done count = 0 countlock = Lock() t_done = i_done = [ False, False ] def increment(): let prior = setintlevel(True): acquire(?countlock) count += 1 release(?countlock) setintlevel(prior) def handler(self): increment() i_done[self] = True def thread(self): trap handler(self) increment() await i_done[self] t_done[self] = True await all(t_done) assert count == 4 spawn thread(0) spawn thread(1)
It will often be necessary to write code that is both interrupt-safe
and thread-safe. As you might expect, this involves both managing
locks and interrupt levels. To increment count, the interrupt level
True and countlock must be held. Figure 22.6 gives an
example of how this might be done. One important rule to remember is
that a thread should disable interrupts before attempting to acquire a
acquire() to the beginning of the
release() to the end of
increment and see what happens.
This incorrect code can lead to threads getting blocked indefinitely.
(Another option is to use synchronization techniques that do not use locks. See Chapter 23 for more information.)
There is another important rule to keep in mind. Just like locks should never be held for long, interrupts should never be disabled for long. With locks the issue is to maximize concurrent performance. For interrupts the issue is fast response to asynchronous events. Because interrupts may be disabled only briefly, interrupt handlers must run quickly and cannot wait for other events.
put method you implemented in Example 18.1 cannot be used in
interrupt handlers for two reasons: (1) it is not interrupt-safe, and
(2) it may block for a long time if the buffer is full. Yet, it would be
useful if, say, a keyboard interrupt handler could place an event on a
shared queue. Implement a new method
i_put(item) that does not
block. Instead, it should return
False if the buffer is full and
True if the item was successfully enqueued. The method also needs to