One Thing At A Time
Limiting Concurrency in Future-based Programming
Sometimes a Future-based program needs to actively avoid concurrency in one specific way or area, and ensure that only one instance of a given operation is outstanding at any one time. While the program as a whole can make use of concurrency to do multiple things at the same time, maybe there's one kind of operation that must avoid this.
For example, a program of my own communicates with some hardware attached to a serial port. The part of the program dealing with that hardware has to ensure that only one operation is outstanding at any time over that serial port, as the hardware on the other side would get confused if two requests were sent before either had finished. What it needs to do then is defer, or lock out subsequent attempts to invoke the operation until the first has completed.
The Mutex
This kind of locking is often called a mutex - a contraction of a mutual exclusion lock. With a Future this is easily provided by the Future::Mutex
class. An instance of this class provides a single method call, enter
, which performs the locking. It takes a block of code provided by a code reference, and executes this code once the lock is free. The enter
method returns a Future which will eventually complete with the same result as whatever the Future returned by the inner code completes with.
use Future::Mutex;
my $mutex = Future::Mutex->new;
sub perform_serial
{
my ($tx_data) = @_;
return $mutex->enter(sub {
$serial->transmit($tx_data)->then(sub {
return $serial->receive;
});
});
}
The locking operation provided by this mutex prevents more than one concurrent call to perform_serial
from operating on the $serial
instance at the same time, thus preventing the attached hardware from getting confused.
This mutex does not prevent there from being concurrency within the wrapped operation; such as the $serial->receive
method making use of a Future->wait_any
convergence to await either some data from the serial port or a timeout. Nor does the mutex prevent the protected operation from executing concurrently with other unrelated activity within the program; such as operating a GUI or communicating with a network.
Queued Requests
If the operation wrapped inside the mutex is fast enough and it isn't called often enough, then it will run uncontended and not appear any different to how the program would have behaved if the lock wasn't there. In effect, the enter
method appears to be transparent to the return value.
Alternatively, if a subsequent attempt to invoke the operation arrives before the first one is finished, this second attempt will have to wait. Because the enter
method takes a reference to the code the caller wishes to perform, it can simply save that to execute it later on once the mutex becomes free. And as the enter
method returns a Future instance, again no problem there. It can simply return a still-pending Future knowing it can fill in the answer later on.
If more contention happens with more callers attempting to invoke the operation while it is busy, they are placed into a queue to be handled in the order in which the requests were originally made.
No exit
?
It may surprise readers to notice that there is just a single enter
method here, with no corresponding exit
as they might be expecting from some other styles of concurrent programming, such as via threads. This is no accident.
The entire purpose of a future is to represent an outstanding operation and be able to observe its completion. This means that the mutex object is aware of when the operation it is wrapping has completed (either with success or failure; it doesn't mind which). It does not need us to remember to invoke a corresponding exit
method, because it can already observe this fact for itself.
Because of this power and flexibility of future-based programming we have been able to express this form of concurrency-limiting mutual exclusion locking in a simple and elegant way. We have made it impossible to forget to unlock the mutex at the end of the operation. This simplicity will doubtless help to prevent an entire class of bug.
Note: This post has been ported from https://tech.binary.com/ (our old tech blog). Author of this post is https://metacpan.org/author/PEVANS