In this lecture we built an asynchronous queue. An asynchronous queue is like a standard queue, except that when you dequeue an element from an empty queue, the computation blocks instead of failing.
In the context of Async, this is reflected by the type of read:
read : 'a Queue.t -> 'a Deferred.t
An asynchronous queue is useful for situations where you have some part of your program producing values, and another part of your program consuming them. They can also be used for managing a finite collection of resources: to gain access to a resource you can remove it from a queue, when you are done using it you can place it back on the queue.
We wish to implement the following interface:
module type QUEUE = sig
type 'a t
val create : unit -> 'a t
val enqueue : 'a t -> 'a -> unit
val dequeue : 'a t -> 'a Deferred.t
end
Note that we are modeling an imperative data structure: enqueue
imperatively adds an element to the queue, dequeue
imperatively updates the queue to indicate that an element has been consumed; two consecutive calls to dequeue
will return different values.
Note also that dequeue
returns a Deferred.t
; this indicates that you may have to wait to get the actual result.
As with a standard queue, we will implement the asynchronous queue using a singly linked list, with one reference into it for enqueueing and one for dequeuing.
read write
| |
V V
[ 17 ] --> [ 12 ] --> [ ] --> None
Unlike a traditional queue, the read reference can actually advance past the write reference. This can happen if read is called more times than write:
write read
| |
V V
[ ] --> [ ] --> [ ] --> None
We maintain the invariant that the write head always refers to the first non-full cell in the list, while the read head refers to the next cell to be returned on a dequeue
. When either enqueue
or dequeue
is called, we either determine or return the corresponding cell, and then advance the corresponding reference. If we walk off the end of the linked list, we extend it by creating a new cell.
module Queue : QUEUE = struct
type 'a cell = {
mutable next : 'a cell option;
value : 'a Ivar.t;
}
type 'a t = {mutable read: 'a cell; mutable write: 'a cell}
(** return the next cell in the queue, creating it if it does not exist *)
let advance cell = match cell.next with
| Some n -> n
| None -> let new_next = { next=None; value=Ivar.create () } in
cell.next <- new_next;
new_next
let create () =
let cell = { next=None; value=Ivar.create() } in
{ read=cell; write=cell }
let enqueue q x =
Ivar.fill q.write.value x;
q.write <- advance q.write
let dequeue q =
let result = Ivar.read q.read.value in
q.read <- advance q.read;
result
end