Non-blocking algorithm: Difference between revisions

Content deleted Content added
Bender the Bot (talk | contribs)
m top: HTTP to HTTPS for Brown University
 
(277 intermediate revisions by more than 100 users not shown)
Line 1:
{{Short description|Algorithm in a thread whose failure cannot cause another thread to fail}}
In [[computer science]], '''non-blocking synchronization''' ensures that [[thread (software engineering)|thread]]s competing for a shared [[resource (computer science)|resource]] do not have their [[execution (computers)|execution]] indefinitely postponed by [[mutual exclusion]]. Literature up to the turn of the century used "non-blocking" synonymously with ''lock-free'': an algorithm that was free from [[deadlock]] and [[livelock]]. However, since 2003 {{ref|obs-free}}, the term has been weakened to deny only deadlock.
{{Distinguish|non-blocking I/O}}
 
In [[computer science]], an [[algorithm]] is called '''non-blocking''' if failure or [[Scheduling (computing)|suspension]] of any [[thread (computing)|thread]] cannot cause failure or suspension of another thread;<ref>{{cite book|last1=Göetz|first1=Brian|last2=Peierls|first2=Tim|last3=Bloch|first3=Joshua|last4=Bowbeer|first4=Joseph|last5=Holmes|first5=David|last6=Lea|first6=Doug|title=Java concurrency in practice|date=2006|publisher=Addison-Wesley|___location=Upper Saddle River, NJ|isbn=9780321349606|page=[https://archive.org/details/javaconcurrencyi00goet/page/41 41]|url-access=registration|url=https://archive.org/details/javaconcurrencyi00goet/page/41}}</ref> for some operations, these algorithms provide a useful alternative to traditional [[lock (computer science)|blocking implementations]]. A non-blocking algorithm is '''lock-free''' if there is guaranteed system-wide [[Resource starvation|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.<ref name=obs-free>{{cite conference|last1=Herlihy|first1=M.|last2=Luchangco|first2=V.|last3=Moir|first3=M.|title=Obstruction-Free Synchronization: Double-Ended Queues as an Example|conference=23rd [[International Conference on Distributed Computing Systems]]|year=2003|pages=522|url=https://www.cs.brown.edu/people/mph/HerlihyLM03/main.pdf}}</ref>
In modern usage, therefore, an algorithm is ''non-blocking'' if the suspension of one or more threads will not stop the potential progress of the remaining threads.
 
The word "non-blocking" was traditionally used to describe [[telecommunications network]]s that could route a connection through a set of relays "without having to re-arrange existing calls"{{Quote without source|date=November 2024}} (see [[Clos network]]). Also, if the telephone exchange "is not defective, it can always make the connection"{{Quote without source|date=November 2024}} (see [[nonblocking minimal spanning switch]]).
{{mergefrom|Lock-free and wait-free algorithms}}
 
== Motivation ==
{{Main|Lock (computer science)#Disadvantages|l1=Disadvantages of locks}}
 
The traditional approach to multi-threaded programming is to use [[lock (computer science)|locks]] to synchronize access to shared [[resource (computer science)|resources]]. Synchronization primitives such as [[mutual exclusion|mutexes]], [[Semaphore (programming)|semaphores]], and [[critical section|critical sections]]s are all mechanisms by which a programmer can ensure that certain sections of code do not execute concurrently, if doing so would corrupt shared memory structures. If one thread attempts to acquire a lock that is already held by another thread, the thread will block until the lock is free.
 
Blocking a thread, though,can isbe undesirable for many reasons. An obvious reason is that while the thread is blocked, it cannot accomplish anything. : Ifif the blocked thread ishad been performing a high-priority or [[real-time computing|real-time]] task, it iswould be highly undesirable to halt its progress. Other problems are less obvious. Certain interactions between locks can lead to error conditions such as [[deadlock]], [[livelock]], and [[priority inversion]]. Using locks also involves a trade-off between coarse-grained locking, which can significantly reduce opportunities for [[parallel computing|parallelism]], and fine-grained locking, which requires more careful design, increases overhead and is more prone to bugs.
 
Other problems are less obvious. For example, certain interactions between locks can lead to error conditions such as [[Deadlock (computer science)|deadlock]], [[livelock]], and [[priority inversion]]. Using locks also involves a trade-off between coarse-grained locking, which can significantly reduce opportunities for [[parallel computing|parallelism]], and fine-grained locking, which requires more careful design, increases locking overhead and is more prone to bugs.
Non-blocking algorithms are also safe for use in [[interrupt handler]]s: even though the [[Pre-emptive multitasking|preempted]] thread cannot be resumed, progress is still possible without it. In contrast, global data structures protected by mutual exclusion cannot safely be accessed in a handler, as the preempted thread may be the one holding the lock.
 
Unlike blocking algorithms, non-blocking algorithms do not suffer from these downsides, and in addition are safe for use in [[interrupt handler]]s: even though the [[Pre-emptive multitasking|preempted]] thread cannot be resumed, progress is still possible without it. In contrast, global data structures protected by mutual exclusion cannot safely be accessed in an interrupt handler, as the preempted thread may be the one holding the lock. While this can be rectified by masking interrupt requests during the critical section, this requires the code in the critical section to have bounded (and preferably short) running time, or excessive [[interrupt latency]] may be observed.<ref name="monit">{{cite journal | doi = 10.1145/358818.358824 | url = http://research.microsoft.com/lampson/23-ProcessesInMesa/Abstract.html | title = Experience with Processes and Monitors in Mesa | author = Butler W. Lampson | author-link = Butler W. Lampson |author2=David D. Redell |author2-link=David D. Redell | journal = Communications of the ACM | volume = 23 | issue = 2 | pages = 105–117 |date=February 1980| citeseerx = 10.1.1.142.5765 | s2cid = 1594544 }}</ref>
Non-blocking synchronization has the potential to prevent [[priority inversion]], as no thread is forced to wait for a suspended thread to complete. However, as livelock is still possible in the modern definition, threads have to wait when they encounter contention; hence, priority inversion is still possible depending upon the contention management system used. Lock-free algorithms, below, avoid priority inversion.
 
A lock-free data structure can be used to improve performance.
A lock-free data structure increases the amount of time spent in parallel execution rather than serial execution, improving performance on a [[multi-core processor]], because access to the shared data structure does not need to be serialized to stay coherent.<ref>
Guillaume Marçais, and Carl Kingsford.
[https://web.archive.org/web/20140518060917/http://bioinformatics.oxfordjournals.org/content/27/6/764.abstract "A fast, lock-free approach for efficient parallel counting of occurrences of k-mers"].
Bioinformatics (2011) 27(6): 764-770.
{{doi|10.1093/bioinformatics/btr011}}
[http://www.genome.umd.edu/jellyfish.html "Jellyfish mer counter"].
</ref>
 
== Implementation ==
With few exceptions, non-blocking algorithms use [[Linearizability|atomic]] [[read-modify-write]] primitives that the hardware must provide, the most notable of which is [[Compare-and-swap|compare and swap (CAS)]]. [[Critical section]]s are almost always implemented using standard interfaces over these primitives (in the general case, critical sections will be blocking, even when implemented with these primitives). In the 1990s all non-blocking algorithms had to be written "natively" with the underlying primitives to achieve acceptable performance. However, the emerging field of [[software transactional memory]] promises standard abstractions for writing efficient non-blocking code.<ref name=lightweight-transactions>{{cite journal|last1=Harris|first1=Tim|last2=Fraser|first2=Keir|title=Language support for lightweight transactions|journal=ACM SIGPLAN Notices|date=26 November 2003|volume=38|issue=11|pages=388|doi=10.1145/949343.949340|url=http://research.microsoft.com/en-us/um/people/tharris/papers/2003-oopsla.pdf|citeseerx=10.1.1.58.8466}}</ref><ref name=composable-memory-transactions>{{cite book|last1=Harris|first1=Tim|last2=Marlow|first2=S.|last3=Peyton-Jones|first3=S.|last4=Herlihy|first4=M.|title=Proceedings of the 2005 ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, PPoPP '05 : Chicago, Illinois|publisher=ACM Press|___location=New York, NY|isbn=978-1-59593-080-4|pages=48–60|chapter=Composable memory transactions|date=June 15–17, 2005|doi=10.1145/1065944.1065952|s2cid=53245159 }}</ref>
 
Much research has also been done in providing basic [[data structure]]s such as [[stack (data structure)|stacks]], [[Queue (data structure)|queues]], [[Set (computer science)|sets]], and [[hash table]]s. These allow programs to easily exchange data between threads asynchronously.
Non-blocking algorithms use [[Atomicity|atomic]] [[read-modify-write]] primitives that the hardware must provide, the most notable of which is [[Compare-and-swap|compare and swap (CAS)]]. Ultimately, all synchronizing algorithms must use these; however, [[critical section|critical sections]] are almost always implemented using standard interfaces over these primitives. Until recently, all non-blocking algorithms had to be written "natively" with the underlying primitives to achieve acceptable performance. However, the emerging field of [[software transactional memory]] promises standard abstractions for writing efficient non-blocking code.
 
Additionally, some non-blocking data structures are weak enough to be implemented without special atomic primitives. These exceptions include:
Much research has also been done in providing basic [[data structure]]s such as [[stack (computing)|stacks]], [[queue|queues]], [[set|sets]], and [[hash table|hash tables]]. These allow programs to easily exchange data between threads asynchronously.
 
* a single-reader single-writer [[Circular buffer|ring buffer]] [[FIFO (computing and electronics)|FIFO]], with a size which evenly divides the overflow of one of the available unsigned integer types, can unconditionally be [[Producer–consumer problem#Without semaphores or monitors|implemented safely]] using only a [[memory barrier]]
* [[Read-copy-update]] with a single writer and any number of readers. (The readers are wait-free; the writer is usually lock-free, until it needs to reclaim memory).
* Read-copy-update with multiple writers and any number of readers. (The readers are wait-free; multiple writers generally serialize with a lock and are not obstruction-free).
 
Several libraries internally use lock-free techniques,<ref>
[https://libcds.sourceforge.net/ libcds] - C++ library of lock-free containers and safe memory reclamation schema
</ref><ref>
[https://www.liblfds.org/ liblfds] - A library of lock-free data structures, written in C
</ref><ref>
[http://concurrencykit.org Concurrency Kit] - A C library for non-blocking system design and implementation
</ref> but it is difficult to write lock-free code that is correct.<ref name="A_FALSE_SENSE_OF_SECURITY">Herb Sutter. {{cite web | url=http://www.drdobbs.com/article/print?articleId=210600279&siteSectionName=cpp | title=Lock-Free Code: A False Sense of Security | archive-url=https://web.archive.org/web/20150901211737/http://www.drdobbs.com/article/print?articleId=210600279&siteSectionName=cpp | archive-date=2015-09-01 |url-status=dead}}</ref><ref name="A_CORRECTED_QUEUE">Herb Sutter. {{cite web | archive-url=https://web.archive.org/web/20081205072023/http://www.ddj.com/cpp/210604448 | title=Writing Lock-Free Code: A Corrected Queue | archive-date=2008-12-05 | url-status=dead | url=http://www.ddj.com/cpp/210604448 }}</ref><ref>
Herb Sutter. [http://www.ddj.com/cpp/211601363 "Writing a Generalized Concurrent Queue"].
</ref><ref>
Herb Sutter. [http://www.ddj.com/cpp/184401930 "The Trouble With Locks"].
</ref>
 
Non-blocking algorithms generally involve a series of read, read-modify-write, and write instructions in a carefully designed order.
Optimizing compilers can aggressively re-arrange operations.
Even when they don't, many modern CPUs often re-arrange such operations (they have a "weak [[consistency model]]"),
unless a [[memory barrier]] is used to tell the CPU not to reorder.
[[C++11]] programmers can use <code>std::atomic</code> in <code>&lt;atomic&gt;</code>,
and [[C11 (C standard revision)|C11]] programmers can use <code>&lt;stdatomic.h&gt;</code>,
both of which supply types and functions that tell the [[compiler]] not to re-arrange such instructions, and to insert the appropriate memory barriers.<ref>
Bruce Dawson.
[https://randomascii.wordpress.com/2020/11/29/arm-and-lock-free-programming/ "ARM and Lock-Free Programming"].
</ref>
 
== Wait-freedom ==
Wait-freedom is the strongest non-blocking guarantee of progress, combining guaranteed system-wide throughput with [[Resource starvation|starvation]]-freedom. An algorithm is wait-free if every operation has a bound on the number of steps the algorithm will take before the operation completes.<ref name="awilliams">
Anthony Williams.
[https://www.justsoftwaresolutions.co.uk//files/safety_off.pdf "Safety: off: How not to shoot yourself in the foot with C++ atomics"].
2015.
p. 20.
</ref>
This property is critical for real-time systems and is always nice to have as long as the performance cost is not too high.
 
It was shown in the 1980s<ref name=imp>{{cite conference |last=Herlihy |first=Maurice P. |conference=Proc. 7th Annual ACM Symp. on Principles of Distributed Computing |isbn=0-89791-277-2 |pages=276–290 |doi=10.1145/62546.62593 |title=Impossibility and universality results for wait-free synchronization |year=1988|doi-access=free }}</ref> that all algorithms can be implemented wait-free, and many transformations from serial code, called ''universal constructions'', have been demonstrated. However, the resulting performance does not in general match even naïve blocking designs. Several papers have since improved the performance of universal constructions, but still, their performance is far below blocking designs.
Wait-freedom is the strongest non-blocking guarantee of progress, combining livelock-freedom with [[Resource starvation|starvation]]-freedom. An algorithm is wait-free if every operation has a bound on the number of steps it will take before completing.
 
ItSeveral waspapers shownhave ininvestigated the 1980sdifficulty thatof all algorithms can implementedcreating wait-free algorithms. For example, andit manyhas transformationsbeen fromshown<ref serialname=cond-sync>{{cite code,conference called|last1=Fich ''universal|first1=Faith|author1-link=Faith constructions'',Ellen have|last2=Hendler been|first2=Danny demonstrated|last3=Shavit |first3=Nir |conference=Proc. However,23rd theAnnual resultingACM performanceSymp.on doesPrinciples notof inDistributed generalComputing match(PODC) even|year=2004 naive|isbn=1-58113-802-4 blocking|pages=80–87 designs|doi=10.1145/1011767.1011780 It|title=On hasthe alsoinherent beenweakness shownof {{ref|cond-syncconditional synchronization primitives}}</ref> that the widely- available [[atomic]] ''conditional'' primitives, [[compareCompare-and-swap|CAS]] and [[Load-Linkedlink/Storestore-Conditionalconditional|llLL/scSC]], cannot provide starvation-free implementations of many common data structures without memory costs growing linearly in the number of threads. Wait-free algorithms are therefore rare, both in research and in practice.
 
However, these lower bounds do not present a real barrier in practice, as spending a cache line or exclusive reservation granule (up to 2&nbsp;KB on ARM) of store per thread in the shared memory is not considered too costly for practical systems. Typically, the amount of store logically required is a word, but physically CAS operations on the same cache line will collide, and LL/SC operations in the same exclusive reservation granule will collide, so the amount of store physically required{{citation needed|date=June 2014}} is greater.{{Clarification needed|date=October 2024|reason=Does this imply that there actually is a barrier in practice?}}
 
Wait-free algorithms were rare until 2011, both in research and in practice. However, in 2011 Kogan and [[Erez Petrank|Petrank]]<ref name=wf-queue>{{cite conference |last1=Kogan |first1=Alex |last2=Petrank |first2=Erez |conference=Proc. 16th ACM SIGPLAN Symp. on Principles and Practice of Parallel Programming (PPOPP) |year=2011 |isbn=978-1-4503-0119-0 |pages=223–234 |doi=10.1145/1941553.1941585 |title=Wait-free queues with multiple enqueuers and dequeuers|url=http://www.cs.technion.ac.il/~erez/Papers/wfquque-ppopp.pdf}}</ref> presented a wait-free queue building on the [[Compare-and-swap|CAS]] primitive, generally available on common hardware. Their construction expanded the lock-free queue of Michael and Scott,<ref name=lf-queue>{{cite conference |last1=Michael |first1=Maged |last2=Scott |first2=Michael |conference=Proc. 15th Annual ACM Symp. on Principles of Distributed Computing (PODC) |year=1996 |isbn=0-89791-800-2 |pages=267–275 |doi=10.1145/248052.248106 |title=Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms|doi-access=free }}</ref> which is an efficient queue often used in practice. A follow-up paper by Kogan and Petrank<ref name=wf-fpsp>{{cite conference |last1=Kogan |first1=Alex |last2=Petrank |first2=Erez |conference=Proc. 17th ACM SIGPLAN Symp. on Principles and Practice of Parallel Programming (PPOPP) |year=2012 |isbn=978-1-4503-1160-1 |pages=141–150 |doi=10.1145/2145816.2145835 |title=A method for creating fast wait-free data structures}}</ref> provided a method for making wait-free algorithms fast and used this method to make the wait-free queue practically as fast as its lock-free counterpart. A subsequent paper by Timnat and Petrank<ref name=wf-simulation>{{cite conference |last1=Timnat |first1=Shahar |last2=Petrank |first2=Erez |conference=Proc. 17th ACM SIGPLAN Symp. on Principles and Practice of Parallel Programming (PPOPP) |year=2014 | isbn=978-1-4503-2656-8 | pages = 357–368 | doi=10.1145/2692916.2555261 | title= A Practical Wait-Free Simulation for Lock-Free Data Structures}}</ref> provided an automatic mechanism for generating wait-free data structures from lock-free ones. Thus, wait-free implementations are now available for many data-structures.
 
Under reasonable assumptions, Alistarh, Censor-Hillel, and Shavit showed that lock-free algorithms are practically wait-free.<ref name=lf-wf>{{cite conference |last1=Alistarh |first1=Dan |last2=Censor-Hillel |first2=Keren |last3=Shavit |first3=Nir |conference=Proc. 46th Annual ACM Symposium on Theory of Computing (STOC’14) | year=2014 | isbn=978-1-4503-2710-7 | pages = 714–723 | doi=10.1145/2591796.2591836 | title=Are Lock-Free Concurrent Algorithms Practically Wait-Free?|arxiv=1311.3200 }}</ref> Thus, in the absence of hard deadlines, wait-free algorithms may not be worth the additional complexity that they introduce.
 
== Lock-freedom ==
Lock-freedom allows individual threads to starve but guarantees system-wide throughput. An algorithm is lock-free if, when the program threads are run for a sufficiently long time, at least one of the threads makes
progress (for some sensible definition of progress).
All wait-free algorithms are lock-free.
 
In particular, if one thread is suspended, then a lock-free algorithm guarantees that the remaining threads can still make progress. Hence, if two threads can contend for the same mutex lock or [[spinlock]], then the algorithm is ''not'' lock-free. (If we suspend one thread that holds the lock, then the second thread will block.)
Lock-freedom allows individual threads to starve but denies livelock. An algorithm is lock-free if every step taken achieves global progress (for some sensible definition of progress). All wait-free algorithms are lock-free.
 
An algorithm is lock-free if infinitely often operation by some processors will succeed in a finite number of steps. For instance, if {{var|N}} processors are trying to execute an operation, some of the {{var|N}} processes will succeed in finishing the operation in a finite number of steps and others might fail and retry on failure. The difference between wait-free and lock-free is that wait-free operation by each process is guaranteed to succeed in a finite number of steps, regardless of the other processors.
 
In general, a lock-free algorithm can run in four phases: completing one's own operation, assisting an obstructing operation, aborting an obstructing operation, and waiting. Completing one's own operation is complicated by the possibility of concurrent assistance and abortion, but is invariably the fastest path to completion.
Line 38 ⟶ 95:
 
== Obstruction-freedom ==
Obstruction-freedom is the weakest natural non-blocking progress guarantee. An algorithm is obstruction-free if at any point, a single thread executed in isolation (i.e., with all obstructing threads suspended) for a bounded number of steps will complete its operation.<ref name="awilliams" /> All lock-free algorithms are obstruction-free.
 
Obstruction-freedom demands only that any partially completed operation can be aborted and the changes made rolled back. Dropping concurrent assistance can often result in much simpler algorithms that are easier to validate. Preventing the system from continually [[livelock|live-locking]] is the task of a contention manager.
Obstruction-freedom denies only deadlock. An algorithm is obstruction-free if at any point, a single thread executed in isolation (i.e. with all obstructing threads suspended) for a bounded number of steps will complete its operation. All lock-free algorithms are obstruction-free.
 
Obstruction-freedom demands only that any partially-completed operation can be aborted and the changes made rolled back. Dropping concurrent assistance can often result in much simpler algorithms that are easier to validate. Preventing the system from continually live-locking is once again the task of a contention manager.
 
Recent research {{ref|polka}} has yielded a promising practical contention manager, whimsically named ''Polka'', combining [[exponential backoff]] with "priority accumulation". As an operation progresses, it gains "priority"; when an operation is obstructed by another with higher priority, it will back off, with backoff intervals increasing exponentially. Each backoff increases the operation's priority; only when its priority is greater than that of its obstructor will it abort it. Aborted operations retain their former priority, giving their next attempt a greater chance of success.
 
Some obstruction-free algorithms use a pair of "consistency markers" in the data structure. Processes reading the data structure first read one consistency marker, then read the relevant data into an internal buffer, then read the other marker, and then compare the markers. The data is consistent if the two markers are identical. Markers may be non-identical when the read is interrupted by another process updating the data structure. In such a case, the process discards the data in the internal buffer and tries again.
Polka achieves good throughput in benchmarks because it minimizes both wasted effort, by prioritizing long transactions, and memory interconnect contention, using exponential backoff. This can inform other parallel algorithms, such as lock-free ones, to achieve greater throughput in the common case.
 
== See also ==
* [[Deadlock (computer science)|Deadlock]]
* [[Concurrency control]]
* [[Java ConcurrentMap#Lock-free atomicity]]
* [[Linearizability]]
* [[Liveness]]
* [[Lock (computer science)]]
* [[Mutual exclusion]]
* [[Priority inversion]]
* [[Resource starvation]]
* [[Non-lock concurrency control]]
* [[Optimistic concurrency control]]
 
==External linksReferences ==
{{Reflist|30em}}
*Article "[http://www.research.ibm.com/people/m/michael/podc-1996.pdf Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms]" by [[Maged M. Michael]] and [[Michael L. Scott]]
*[http://citeseer.ist.psu.edu/cis?q=Fast+and+Practical+Non-Blocking+Concurrent+Synchronization Citations from CiteSeer]
*Discussion "[http://groups.google.com/groups?group=comp.programming.threads&threadm=c2s1qn%24mrj%247%40newsserv.zdv.uni-tuebingen.de Communication between Threads, without blocking]"
 
== ResourcesExternal links ==
* [http://preshing.com/20120612/an-introduction-to-lock-free-programming/ An Introduction to Lock-Free Programming]
* {{note|cond-sync}} F. Fich, D. Hendler, N. Shavit. [http://www.cs.tau.ac.il/~afek/Handler-conditionals.pdf|"On the inherent weakness of conditional synchronization primitives."] 23rd Annual ACM Symposium on Principles of Distributed Computing, 2004, pp. 80-87.
* [http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html Non-blocking Algorithms]
* {{note|obs-free}} M. Herlihy, V. Luchangco and M. Moir. [http://www.cs.brown.edu/people/mph/HerlihyLM03/main.pdf|"Obstruction-Free Synchronization: Double-Ended Queues as an Example."] 23rd International Conference on Distributed Computing Systems, 2003, p.522.
* {{note|polka}} W. Scherer and M. Scott. [http://www.cs.rochester.edu/u/scott/papers/2005_PODC_CM.pdf"Advanced Contention Management for Dynamic Software Transactional Memory."] 24th annual ACM SIGACT-SIGOPS Symposium on Principles of Distributed Computing, 2005, pp. 240-248.
 
{{DEFAULTSORT:Non-Blocking Algorithm}}
[[Category:Synchronization]]
[[Category:Concurrency control]]
[[Category:AlgorithmsConcurrency control algorithms]]
[[Category:Operating system technology]]