Getting started with lock-free concurrent data structure III - Behaviour oriented concurrency
Abstract
The purpose of behaviour oriented concurrency (boc) is to eliminate deadlock completely. It enforces a global total order for every shared variable. Moreover, it tries to prevent blocking when trying to gain access to a shared variable, achieving more efficient scheduling.
To implement this, BOC used asynchronous scheduling of threads, and using Cowns as a lock to protect the inner value.
Preventing deadlock
A deadlock may happen when these conditions met:
Mutual exclusion: Multiple threads cannot share a resource at the same time.
No preemption: A thread cannot take resources which other thread is using.
Hold and wait: There may be a thread holding at least one resource and requesting resources which are being used by other threads.
Circular wait: There is a set of threads in which every thread owns a resource, and requesting resources held by other threads in the same set, forming a loop of acquisition.
BOC breaks the Circular wait condition by enforcing an order of acquiring and releasing locks. According to the original article of BOC:
The dependency graph is at the heart of the BoC runtime. It is an directed acyclic graph (DAG) of behaviours, whose edges express the holding of a cown needed by a successor.
(A directed acyclic graph does not have a loop)
Cowns and Behaviours
BOC introduces a protected structure - Cown (Concurrent Owner). A cown wraps the protected resource and acts as a lock. A behaviour consist of — Resources (Cowns) needed and the clause body (code).
A Behaviour is defined by when clause.
Both a and b are cowns. When the program reaches the when clause, it will try to asynchronously acquire locks of a and b in a specific order, meaning that it will summon a new thread every time when the locks are acquired. (Note that acquiring locks are not blocked)
Example - Fibonacci
Implementation based on the article
Cown
A cown wraps the protected object and a pointer of the latest Request to maintain a list structure.
Behaviour
A behaviour consist of thunk (function body or clause), requests, and an atomic counter to count how many cowns that are currently holding by another thread. When the counter reaches 0, the thunk will be executed.
Request
A request maintained a list structure of Behaviours. So that when the request finishes, it can release the lock by decrementing the reference counter of the next behaviour.
Queueing for acquiring a lock
To ensure an order of execution of threads and prevent starvation, the implementation used a queue structure like a linked list, but instead of maintaining a list, it uses swap operation (atomic) to insert to the list, and get the address of previous node on stack.
Releasing the lock
When releasing the lock, we will consider three scenarios.
This is the last Request of the cown
In this situation, next should always be null, and the cown (target)'s last Request must always be this.
So we can easily return and end the Request.
This is not the last Request of the cown and next is not null
There exist a next behaviour of this Request.
So we Resolve the next behaviour (Releasing the cown) and give it to the next.
This is not the last Request of the cown but next is null
This situation occurs when the request list has not been completely scheduled.
We will wait for scheduling, and then resolve it.
Start a behaviour
We need to sort the cowns so that the order of acquiring/releasing cowns are always the same in every thread. (This can be done by sorting by pointer value, assuming all cowns' address would not change)
Then we will append the request (schedule request) to the cowns. After that, we will notify all requests that request appending is completed.
To kickstart the routine, we will fire ResolveOne() on this behaviour, but this will make the counter being subtracted once more. So we will have the counter be the count of cowns + 1
.
Rust implementation
Reference
When Concurrency Matters: Behaviour-Oriented Concurrency: https://dl.acm.org/doi/10.1145/3622852
Last updated