Lamport's bakery algorithm

Last updated

Lamport's bakery algorithm is a computer algorithm devised by computer scientist Leslie Lamport, as part of his long study of the formal correctness of concurrent systems, which is intended to improve the safety in the usage of shared resources among multiple threads by means of mutual exclusion.

Contents

In computer science, it is common for multiple threads to simultaneously access the same resources. Data corruption can occur if two or more threads try to write into the same memory location, or if one thread reads a memory location before another has finished writing into it. Lamport's bakery algorithm is one of many mutual exclusion algorithms designed to prevent concurrent threads entering critical sections of code concurrently to eliminate the risk of data corruption.

Algorithm

Analogy

Lamport envisioned a bakery with a numbering machine at its entrance so each customer is given a unique number. Numbers increase by one as customers enter the store. A global counter displays the number of the customer that is currently being served. All other customers must wait in a queue until the baker finishes serving the current customer and the next number is displayed. When the customer is done shopping and has disposed of his or her number, the clerk increments the number, allowing the next customer to be served. That customer must draw another number from the numbering machine in order to shop again.

According to the analogy, the "customers" are threads, identified by the letter i, obtained from a global variable.

Due to the limitations of computer architecture, some parts of Lamport's analogy need slight modification. It is possible that more than one thread will get the same number n when they request it; this cannot be avoided (without first solving the mutual exclusion problem, which is the goal of the algorithm). Therefore, it is assumed that the thread identifier i is also a priority. A lower value of i means a higher priority and threads with higher priority will enter the critical section first.

Critical section

The critical section is that part of code that requires exclusive access to resources and may only be executed by one thread at a time. In the bakery analogy, it is when the customer trades with the baker that others must wait.

When a thread wants to enter the critical section, it has to check whether now is its turn to do so. It should check the number n of every other thread to make sure that it has the smallest one. In case another thread has the same number, the thread with the smallest i will enter the critical section first.

In pseudocode this comparison between threads a and b can be written in the form:

// Let na - the customer number for thread a, and // ia - the thread number for thread a, then  (na, ia) < (nb, ib) 

which is equivalent to:

(na < nb) or ((na == nb) and (ia < ib))

Once the thread ends its critical job, it gets rid of its number and enters the non-critical section.

Non-critical section

The non-critical section is the part of code that doesn't need exclusive access. It represents some thread-specific computation that doesn't interfere with other threads' resources and execution.

This part is analogous to actions that occur after shopping, such as putting change back into the wallet.

Implementation of the algorithm

Definitions

In Lamport's original paper, the entering variable is known as choosing, and the following conditions apply:

Code examples

Pseudocode

In this example, all threads execute the same "main" function, Thread. In real applications, different threads often have different "main" functions.

Note that as in the original paper, the thread checks itself before entering the critical section. Since the loop conditions will evaluate as false, this does not cause much delay.

// declaration and initial values of global variablesEntering:array[1..NUM_THREADS]ofbool={false};Number:array[1..NUM_THREADS]ofinteger={0};lock(integeri){Entering[i]=true;Number[i]=1+max(Number[1],...,Number[NUM_THREADS]);Entering[i]=false;for(integerj=1;j<=NUM_THREADS;j++){// Wait until thread j receives its number:while(Entering[j]){/* nothing */}// Wait until all threads with smaller numbers or with the same// number, but with higher priority, finish their work:while((Number[j]!=0)&&((Number[j],j)<(Number[i],i))){/* nothing */}}}unlock(integeri){Number[i]=0;}Thread(integeri){while(true){lock(i);// The critical section goes here...unlock(i);// non-critical section...}}

Each thread only writes its own storage, only reads are shared. It is remarkable that this algorithm is not built on top of some lower level "atomic" operation, e.g. compare-and-swap. The original proof shows that for overlapping reads and writes to the same storage cell only the write must be correct.[ clarification needed ] The read operation can return an arbitrary number. Therefore, this algorithm can be used to implement mutual exclusion on memory that lacks synchronisation primitives, e.g., a simple SCSI disk shared between two computers.

The necessity of the variable Entering might not be obvious as there is no 'lock' around lines 7 to 13. However, suppose the variable was removed and two processes computed the same Number[i]. If the higher-priority process was preempted before setting Number[i], the low-priority process will see that the other process has a number of zero, and enters the critical section; later, the high-priority process will ignore equal Number[i] for lower-priority processes, and also enters the critical section. As a result, two processes can enter the critical section at the same time. The bakery algorithm uses the Entering variable to make the assignment on line 6 look like it was atomic; process i will never see a number equal to zero for a process j that is going to pick the same number as i.

When implementing the pseudo code in a single process system or under cooperative multitasking, it is better to replace the "do nothing" sections with code that notifies the operating system to immediately switch to the next thread. This primitive is often referred to as yield.

Lamport's bakery algorithm assumes a sequential consistency memory model. Few, if any, languages or multi-core processors implement such a memory model. Therefore, correct implementation of the algorithm typically requires inserting fences to inhibit reordering. [1]

PlusCal code

We declare N to be the number of processes, and we assume that N is a natural number.

CONSTANT N ASSUME N \in Nat 

We define P to be the set {1, 2, ... , N} of processes.

P == 1..N 

The variables num and flag are declared as global.

--algorithm AtomicBakery { variable num = [i \in P |-> 0], flag = [i \in P |-> FALSE]; 

The following defines LL(j, i) to be true iff <<num[j], j>> is less than or equal to <<num[i], i>> in the usual lexicographical ordering.

define { LL(j, i) == \/ num[j] < num[i]                      \/ /\ num[i] = num[j]                         /\ j =< i        } 

For each element in P there is a process with local variables unread, max and nxt. Steps between consecutive labels p1, ..., p7, cs are considered atomic. The statement with (x \in S) { body } sets id to a nondeterministically chosen element of the set S and then executes body. A step containing the statement await expr can be executed only when the value of expr is TRUE.

process (p \in P)   variables unread \in SUBSET P,              max \in Nat,              nxt \in P; { p1: while (TRUE) {       unread := P \ {self} ;       max := 0;       flag[self] := TRUE; p2:   while (unread # {}) {         with (i \in unread) { unread := unread \ {i};                               if (num[i] > max) { max := num[i]; }          }        }; p3:   num[self] := max + 1; p4:   flag[self] := FALSE;       unread := P \ {self} ; p5:   while (unread # {}) {         with (i \in unread) { nxt := i ; };         await ~ flag[nxt]; p6:     await \/ num[nxt] = 0               \/ LL(self, nxt) ;         unread := unread \ {nxt};         } ; cs:   skip ;  \* the critical section; p7:   num[self] := 0;  }} } 

Java code

We use the AtomicIntegerArray class not for its built in atomic operations but because its get and set methods work like volatile reads and writes. Under the Java Memory Model this ensures that writes are immediately visible to all threads.

AtomicIntegerArrayticket=newAtomicIntegerArray(threads);// ticket for threads in line, n - number of threads// Java initializes each element of 'ticket' to 0AtomicIntegerArrayentering=newAtomicIntegerArray(threads);// 1 when thread entering in line// Java initializes each element of 'entering' to 0publicvoidlock(intpid)// thread ID{entering.set(pid,1);intmax=0;for(inti=0;i<threads;i++){intcurrent=ticket.get(i);if(current>max){max=current;}}ticket.set(pid,1+max);entering.set(pid,0);for(inti=0;i<ticket.length();++i){if(i!=pid){while(entering.get(i)==1){Thread.yield();}// wait while other thread picks a ticketwhile(ticket.get(i)!=0&&(ticket.get(i)<ticket.get(pid)||(ticket.get(i)==ticket.get(pid)&&i<pid))){Thread.yield();}}}// The critical section goes here...}publicvoidunlock(intpid){ticket.set(pid,0);}

See also

Related Research Articles

Dekker's algorithm is the first known correct solution to the mutual exclusion problem in concurrent programming where processes only communicate via shared memory. The solution is attributed to Dutch mathematician Th. J. Dekker by Edsger W. Dijkstra in an unpublished paper on sequential process descriptions and his manuscript on cooperating sequential processes. It allows two threads to share a single-use resource without conflict, using only shared memory for communication.

<span class="mw-page-title-main">Mutual exclusion</span> In computing, restricting data to be accessible by one thread at a time

In computer science, mutual exclusion is a property of concurrency control, which is instituted for the purpose of preventing race conditions. It is the requirement that one thread of execution never enters a critical section while a concurrent thread of execution is already accessing said critical section, which refers to an interval of time during which a thread of execution accesses a shared resource or shared memory.

<span class="mw-page-title-main">Semaphore (programming)</span> Variable used in a concurrent system

In computer science, a semaphore is a variable or abstract data type used to control access to a common resource by multiple threads and avoid critical section problems in a concurrent system such as a multitasking operating system. Semaphores are a type of synchronization primitive. A trivial semaphore is a plain variable that is changed depending on programmer-defined conditions.

In computer science, a lock or mutex is a synchronization primitive that prevents state from being modified or accessed by multiple threads of execution at once. Locks enforce mutual exclusion concurrency control policies, and with a variety of possible methods there exist multiple unique implementations for different applications.

Peterson's algorithm is a concurrent programming algorithm for mutual exclusion that allows two or more processes to share a single-use resource without conflict, using only shared memory for communication. It was formulated by Gary L. Peterson in 1981. While Peterson's original formulation worked with only two processes, the algorithm can be generalized for more than two.

<span class="mw-page-title-main">OpenMP</span> Open standard for parallelizing

OpenMP is an application programming interface (API) that supports multi-platform shared-memory multiprocessing programming in C, C++, and Fortran, on many platforms, instruction-set architectures and operating systems, including Solaris, AIX, FreeBSD, HP-UX, Linux, macOS, and Windows. It consists of a set of compiler directives, library routines, and environment variables that influence run-time behavior.

In computer science, an algorithm is called non-blocking if failure or suspension of any thread cannot cause failure or suspension of another thread; for some operations, these algorithms provide a useful alternative to traditional blocking implementations. A non-blocking algorithm is lock-free if there is guaranteed system-wide progress, and wait-free if there is also guaranteed per-thread progress. "Non-blocking" was used as a synonym for "lock-free" in the literature until the introduction of obstruction-freedom in 2003.

In concurrent programming, concurrent accesses to shared resources can lead to unexpected or erroneous behavior, so parts of the program where the shared resource is accessed need to be protected in ways that avoid the concurrent access. One way to do so is known as a critical section or critical region. This protected section cannot be entered by more than one process or thread at a time; others are suspended until the first leaves the critical section. Typically, the critical section accesses a shared resource, such as a data structure, a peripheral device, or a network connection, that would not operate correctly in the context of multiple concurrent accesses.

In computing, a futex is a kernel system call that programmers can use to implement basic locking, or as a building block for higher-level locking abstractions such as semaphores and POSIX mutexes or condition variables.

In computer science, a ticket lock is a synchronization mechanism, or locking algorithm, that is a type of spinlock that uses "tickets" to control which thread of execution is allowed to enter a critical section.

In computer science, the fetch-and-add (FAA) CPU instruction atomically increments the contents of a memory location by a specified value.

In computer science, a readers–writer is a synchronization primitive that solves one of the readers–writers problems. An RW lock allows concurrent access for read-only operations, whereas write operations require exclusive access. This means that multiple threads can read the data in parallel but an exclusive lock is needed for writing or modifying data. When a writer is writing the data, all other writers and readers will be blocked until the writer is finished writing. A common use might be to control access to a data structure in memory that cannot be updated atomically and is invalid until the update is complete.

The Ricart–Agrawala algorithm is an algorithm for mutual exclusion on a distributed system. This algorithm is an extension and optimization of Lamport's Distributed Mutual Exclusion Algorithm, by removing the need for messages. It was developed by computer scientists Glenn Ricart and Ashok Agrawala.

Maekawa's algorithm is an algorithm for mutual exclusion on a distributed system. The basis of this algorithm is a quorum-like approach where any one site needs only to seek permissions from a subset of other sites.

In computer science, synchronization is the task of coordinating multiple of processes to join up or handshake at a certain point, in order to reach an agreement or commit to a certain sequence of action.

In computing, the producer-consumer problem is a family of problems described by Edsger W. Dijkstra since 1965.

The Suzuki–Kasami algorithm is a token-based algorithm for achieving mutual exclusion in distributed systems. The process holding the token is the only process able to enter its critical section.

The Eisenberg & McGuire algorithm is an algorithm for solving the critical sections problem, a general version of the dining philosophers problem. It was described in 1972 by Murray A. Eisenberg and Michael R. McGuire.

Szymański's Mutual Exclusion Algorithm is a mutual exclusion algorithm devised by computer scientist Dr. Bolesław Szymański, which has many favorable properties including linear wait, and which extension solved the open problem posted by Leslie Lamport whether there is an algorithm with a constant number of communication bits per process that satisfies every reasonable fairness and failure-tolerance requirement that Lamport conceived of.

Array-Based Queuing Lock (ABQL) is an advanced lock algorithm that ensures that threads spin on unique memory locations thus ensuring fairness of lock acquisition coupled with improved scalability.

References