Lecture 20: Asynchronous Queue

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

Uses

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.

Interface

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.

Data structure

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.

The code

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