This article needs more citations. (December 2016) |
In concurrent programming, an Array-Based Queuing Lock (ABQL) is a synchronization mechanism used to control access to shared resources and ensure fairness among competing threads. It is a variation of the ticket lock algorithm. Traditional locking mechanisms often involve threads contending for a single lock variable (a shared data element used to control access). In contrast, an ABQL uses an array to represent a queue of waiting threads. Each thread, upon attempting to acquire the lock, is assigned a unique position, or "slot," in this array. This approach significantly improves scalability and fairness because threads spin on their individual array slots rather than a single shared location, reducing contention and ensuring a first-come, first-served acquisition order. This distributed spinning also minimizes cache coherency traffic (the communication required to keep data consistent across multiple processor cores' caches), which further enhances performance, especially in multi-core and multi-processor systems.
Synchronization is a major issue in the designing and programming of shared memory multiprocessors. The common problem with lock implementations is the high network contention due to the processors spinning on a shared synchronization flag or memory location. Thus the scalability of the lock is reduced significantly in terms of the number of contending processors.
The Array Based Queuing Lock is an extension to the ticket lock algorithm which ensures that, on a lock release, only one processor attempts to acquire the lock, decreasing the number of cache misses. This effect can be achieved by having all the processors spin on unique memory locations. One analogy used to explain the lock mechanism is the relay race where the athlete passes on the baton to the next athlete in the queue, which ensures that only one athlete acquires the baton at a time.
ABQL also guarantees fairness in lock acquisition by using a first in, first out (FIFO) queue-based mechanism. Additionally, the amount of invalidation is significantly less than ticket-based lock implementations since only one processor incurs a cache miss on a lock release.
The foremost requirement of the implementation of array based queuing lock is to ensure that all the threads spin on unique memory locations. This is achieved with an array of length equal to the number of threads which are in contention for the lock. The elements of the array are all initialized to 0 except the first element which is takes the value 1, thus ensuring successful lock acquisition by the first thread in the queue. On a lock release, the hold is passed to the next thread in queue by setting the next element of the array to 1. The requests are granted to the threads in FIFO ordering.
import java.util.concurrent.atomic.AtomicInteger;
class ArrayBasedQueuingLock {
private static final int MAX_SIZE = /* max size here */;
private AtomicInteger nextTicket;
private int[] canServe;
public ArrayBasedQueuingLock() {
this.next_ticket = 0;
this.canServe[0] = 1;
for (int i = 1; i < MAX_SIZE; i++) {
this.canServe[i] = 0;
}
}
public void acquire() {
int myTicket = nextTicket.getAndIncrement();
while (canServe[myTicket] != 1) {
// Do nothing...
}
}
public void release(int myTicket) {
canServe[myTicket] = 0;
canServe[myTicket + 1] = 1;
}
}
To implement ABQL in the pseudo code above, three variables are introduced:
canServe is an array that represents the unique memory locations that the threads waiting in the queue for the lock acquisitions spin on.nextTicket represents the next available ticket number that is assigned to a new thread.myTicket represents the ticket thread of each unique thread in the queue.In the constructor, the variable nextTicket is initialized to 0. All the elements of the canServe array except the first element are initialized to 0. Initialization of the first element in the array canServe to 1, ensures successful lock acquisition by the first thread in the queue.
The acquire method uses an atomic operation getAndIncrement() to fetch the next available ticket number (afterwards the ticket number is incremented by 1) that the new thread will use to spin on. The threads in the queue spin on their locations until the value of myTicket is set to 1 by the previous thread. On acquiring the lock the thread enters the critical section of the code.
On release of a lock by a thread, the control is passed to the next thread by setting the next element in the array canServe to 1. The next thread which was waiting to acquire the lock can now do so successfully.
The working of ABQL can be depicted in the table below by assuming 4 processors contending to enter the critical section with the assumption that a thread enters the critical section only once.
| Execution Steps | nextTicket |
canServe | myTicket(P1) |
myTicket(P2) |
myTicket(P3) |
myTicket(P4) |
Comments |
|---|---|---|---|---|---|---|---|
| initially | 0 | [1, 0, 0, 0] | 0 | 0 | 0 | 0 | initial value of all variables is 0 |
| P1: getAndIncrement | 1 | [1, 0, 0, 0] | 0 | 0 | 0 | 0 | P1 attempts and successfully acquires the lock |
| P2: getAndIncrement | 2 | [1, 0, 0, 0] | 0 | 1 | 0 | 0 | P2 attempts to acquire the lock |
| P3: getAndIncrement | 3 | [1, 0, 0, 0] | 0 | 1 | 2 | 0 | P3 attempts to acquire the lock |
| P4: getAndIncrement | 4 | [1, 0, 0, 0] | 0 | 1 | 2 | 3 | P4 attempts to acquire the lock |
| P1: canServe[1] = 1;
canServe[0] = 0 |
4 | [0, 1, 0, 0] | 0 | 1 | 2 | 3 | P1 releases the lock and P2 successfully acquires the lock |
| P2: canServe[2] = 1;
canServe[1] = 0 |
4 | [0, 0, 1, 0] | 0 | 1 | 2 | 3 | P2 releases the lock and P3 successfully acquires the lock |
| P3: canServe[3] = 1;
canServe[2] = 0 |
4 | [0, 0, 0, 1] | 0 | 1 | 2 | 3 | P3 releases the lock and P4 successfully acquires the lock |
| P4: canServe[3] = 0 | 4 | [0, 0, 0, 0] | 0 | 1 | 2 | 3 | P4 releases the lock |
The following performance criteria can be used to analyse the lock implementations:
canServe.