En mutex är enkel. Du låser den, ändrar delat tillstånd, låser upp den. Varje tråd som vill ha åtkomst väntar på sin tur. Problemet är att "vänta på sin tur", vid högt antal kärnor och hög dataflöde blir den serialiseringen taket. En tråd som har ett lås kan förgripas, vilket gör att alla andra trådar blir inaktiva. Prioritetsinversion kan få lågprioriterade trådar att blockera högprioriterade. I system som bearbetar miljontals operationer per sekund över dussintals kärnor, saktar inte bara ner mutex-konkurrens; den minskar dataflödet.
Analysera samtidighetslogik
SMART TS XL spårar atomära operationsvägar, åtkomstmönster för delade minnesminnen och data över trådar.
Utforska nuLåsfria datastrukturer ersätter ömsesidig uteslutning med atomära operationer. Istället för att förhindra konflikter tolererar och återhämtar de sig från dem. En tråd som misslyckas med ett jämförelse-och-växlingsförsök med uppdaterat tillstånd. Ingen tråd blockerar någonsin en annan. Minst en tråd gör alltid framsteg. Resultatet är dramatiskt bättre dataflöde under hög konkurrens, förutsägbar latens utan konvojeffekter och eliminering av dödläge och prioritetsinversion genom design. Priset är implementeringskomplexitet: ABA-problemet, risker för minnesåtervinning, falsk delning och subtila krav på minnesordning är alla fällor som inte finns i låsbaserad kod. Den här artikeln täcker alla dessa med konkret kod.
Låsfritt vs. Väntefritt vs. Mutex: Vilken ska man använda?
Innan man väljer en implementeringsmetod är den rätta frågan vilken framstegsgaranti systemet faktiskt behöver. De tre modellerna skiljer sig åt i vad de lovar när trådar konkurrerar.
| Modell | Framstegsgaranti | Typisk latens | Komplexitet | bäst för |
|---|---|---|---|---|
| Mutex / låsbaserad | Blockering, trådar väntar | Oförutsägbar under strid | Låg | Delat tillstånd med låg konkurrens, enkla korrekthetskrav |
| Låsfritt | Systemövergripande, minst en tråd fortskrider | Låg konkurrens | Hög | Köer, stackar och räknare med hög genomströmning |
| Väntefri | Per tråd avslutas varje tråd i begränsade steg | Begränsat värsta tänkbara fall | Mycket högt | Realtidssystem, säkerhetskritiska, strikta latens-SLA:er |
| Hindringsfri | Ensam framsteg, endast när det är obestridligt | Låg utan strid | Medium | Transaktionella minnesprototyper, forskningskontexter |
Låsfria är den praktiska standarden för produktionssystem med hög samtidighet. Michael-Scott-kön, Treiber-stacken och de flesta MPMC-ringbuffertar i produktionen är låsfria. Enskilda trådar kan svälta under extrem konkurrens, men systemet som helhet gör framsteg.
Väntefria algoritmer garanterar en trådbegränsad progression men kräver betydligt mer komplexa algoritmer. Universella konstruktioner finns men har höga kostnader. Väntefria algoritmer är lämpliga för hårda realtidskontexter där svansfördröjning är viktigare än genomsnittlig dataflöde.
Obstruktionsfri används sällan direkt i produktion. Den förekommer i vissa implementeringar av transaktionsminne och fungerar som en språngbräda när man bevisar algoritmens korrekthet.
För de flesta system med hög samtidighet: använd en mutex när konkurrensen är låg och korrektheten är enkel, använd lock-free när dataflödet under konkurrens är viktigt, använd wait-free endast när värsta tänkbara latens per tråd är ett svårt krav.
Vad är en låsfri datastruktur?
En datastruktur är låsfri om den garanterar att minst en tråd bland alla trådar som körs på den kommer att slutföra sin operation i ett begränsat antal steg, oavsett vad andra trådar gör eller hur de är schemalagda. Denna formella definition har en precis innebörd: ingen låsfri algoritm kan fastna i ett dödläge. Om en tråd stoppas, pausas eller körs långsamt, fortsätter andra trådar att göra framsteg.
Mekanismen är atomära operationer, CPU-instruktioner som exekveras som en enda odelbar enhet. Den universella primitiva är Compare-and-Swap (CAS) :
CAS(location, expected, new_value):
if *location == expected:
*location = new_value
return true
else:
return false // someone else changed it first
CAS är atomärt på hårdvarunivå. På x86 är det CMPXCHG instruktion. På ARM implementeras den via LDXR/STXR (load-exclusive/store-exclusive), vilket är LL/SC-varianten (Load-Linked/Store-Conditional). En tråd läser ett värde, beräknar ett nytt värde och använder CAS för att installera det endast om ingen annan har ändrat det däremellan. Om CAS misslyckas försöker tråden igen med det nya värdet.
I C++11 och senare exponeras detta genom std::atomic:
cpp
#include <atomic>
// Atomic increment using CAS loop
void atomic_add(std::atomic<int>& counter, int delta) {
int expected = counter.load(std::memory_order_relaxed);
while (!counter.compare_exchange_weak(
expected,
expected + delta,
std::memory_order_release,
std::memory_order_relaxed))
{
// expected is updated on failure -- retry with fresh value
}
}
compare_exchange_weak är CAS-primitiven. Vid fel uppdateras den expected till det aktuella värdet automatiskt, vilket gör återförsöksslingan idiomatisk.
ABA-problemet: Lock-Frees farligaste fallgrop
ABA-problemet är den mest kontraintuitiva risken med korrekthet i låsfri programmering. CAS kontrollerar om en minnesplats fortfarande innehåller det förväntade värdet innan en ny installeras. Den kan inte upptäcka om det värdet ändrades och ändrades tillbaka mellan läsningen och CAS. Platsen ser fortfarande ut som det ursprungliga värdet, men systemets underliggande tillstånd har ändrats på sätt som CAS inte kan se.
Scenariot steg för steg
Betrakta en låsfri stack med en enda pekare top:
- Tråd A läser
top = Node1Nod1:s nästa pekare är Nod2. - Tråd A preemptas innan sin pop-up är klar.
- Tråd B poppar Nod1 (topp blir Nod2), sedan poppar Nod2 (topp blir null).
- Tråd B skickar en ny nod, som råkar vara allokerad på samma minnesadress som Nod1 (vanligt med frilista-allokatorer). Topp = Nod1 (samma pekare, annat innehåll).
- Tråd A återupptas. Dess CAS ser
top == Node1(förväntat värde), lyckas och sättertop = Node2, men Node2 var redan fri. Katastrof.
ABA-problemet manifesterar sig olika i varje datastruktur:
- StackCAS lyckas men installerar en inaktuell nästapekare, vilket orsakar use-after-free
- KöHuvud- eller svanspekaren verkar oförändrad men strukturen har ändrats
- Länkad listaEn nod verkar fortfarande vara på plats men har tagits bort och omfördelats
Undviker LL/SC ABA-problemet?
Ja, LL/SC (Load-Linked/Store-Conditional) ger starkare semantik än CAS och undviker naturligtvis ABA. LL markerar den laddade minnesadressen som "länkad". SC på samma adress misslyckas om någon lagring till den adressen har skett sedan LL, även om värdet återställdes till sitt ursprungliga tillstånd. Modifieringshistoriken spåras på hårdvarunivå, inte bara det aktuella värdet.
LL/SC-implementeringar på verklig hårdvara har dock praktiska begränsningar. På ARM och POWER kan falska LL/SC-fel uppstå även utan konflikt (kontextväxlar, cacheutkastningar). Koden måste ta hänsyn till återförsöksloopar även utan verkliga konflikter. På x86 finns ingen inbyggd LL/SC; CAS är hårdvaruprimitiven. För x86-kod måste ABA förhindras i programvaran.
Åtgärda ABA: Tre metoder
1. Versionsräknare (taggade pekare)
Packa en versionsräknare i samma atomord som pekaren. Varje lyckad CAS ökar räknaren. Även om en pekare återgår till sitt ursprungliga värde, skiljer sig räknaren åt:
cpp
#include <atomic>
#include <cstdint>
struct TaggedPtr {
uintptr_t ptr : 48; // pointer (48-bit virtual address space)
uintptr_t tag : 16; // version counter
};
std::atomic<TaggedPtr> top;
bool cas_with_tag(std::atomic<TaggedPtr>& loc,
TaggedPtr expected,
void* new_ptr) {
TaggedPtr desired = { (uintptr_t)new_ptr, expected.tag + 1 };
return loc.compare_exchange_strong(expected, desired);
}
Detta är standardmetoden i praktiken. 16-bitarstaggen lindas runt efter 65 536 operationer, vilket är teoretiskt osäkert men praktiskt tillräckligt för de flesta system. Det är extremt osannolikt att en tråd preempteras för exakt 65 536 CAS-cykler på samma plats.
2. Varningssignaler
Varje tråd har en liten uppsättning "hazard pekare", pekare till noder som den för närvarande använder. Innan en nod frigörs kontrollerar en tråd alla hazard pekarregister för att bekräfta att ingen annan tråd använder den. En nod som visas i en hazard pekare skjuts upp tills pekaren rensas:
cpp
// Simplified hazard pointer pattern
thread_local void* hazard_ptr = nullptr;
void* safe_load(std::atomic<void*>& head) {
void* ptr;
do {
ptr = head.load(std::memory_order_acquire);
hazard_ptr = ptr; // announce we are using this
// memory fence ensures announcement is visible before validation
std::atomic_thread_fence(std::memory_order_seq_cst);
} while (ptr != head.load(std::memory_order_acquire)); // validate still valid
return ptr;
}
void safe_release() {
hazard_ptr = nullptr;
}
Hazardpekare förhindrar ABA på minnesåtervinningsnivån: en nod kan inte återanvändas medan en tråd innehåller en hazardpekare. Detta kräver att alla hazardpekare i tråden skannas innan de frigörs, vilket ökar overheadkostnaden proportionellt mot trådantalet.
3. Epokbaserad återvinning (EBR)
Trådar fungerar i globalt spårade "epoker". Pensionerade noder buffras per epok och frigörs endast när alla trådar har passerat den epok då noden pensionerades. EBR är enklare att använda än hazard-pekare och har lägre overhead per operation, på bekostnad av begränsad men oförutsägbar minnestillväxt under viloperioder.
I Java, AtomicStampedReference åtgärdar direkt ABA-problemet genom att para ihop en referens med en heltalsstämpel:
Java
import java.util.concurrent.atomic.AtomicStampedReference;
AtomicStampedReference<Node> top =
new AtomicStampedReference<>(null, 0);
void push(Node newNode) {
int[] stamp = new int[1];
Node current;
do {
current = top.get(stamp);
newNode.next = current;
} while (!top.compareAndSet(current, newNode, stamp[0], stamp[0] + 1));
}
Låsfria köimplementeringar
Köer är den vanligast förekommande låsfria strukturen. Den kanoniska låsfria kön är Michael-Scott-kön, som använder två pekare (huvud och svans) och en sentinelnod för att tillåta samtidiga operationer för att lägga till och ta bort kön.
SPSC-kö: Maximal prestanda med minimal synkronisering
En kö med en enda producent och en enda konsument eliminerar all skriv-skriv- och läs-läs-konflikt. En tråd skriver till svansen; en tråd läser från huvudet. Endast huvudpekaren delas mellan producent och konsument:
cpp
#include <atomic>
#include <array>
template<typename T, size_t Capacity>
class SPSCQueue {
std::array<T, Capacity> buffer;
alignas(64) std::atomic<size_t> head{0}; // consumer reads head
alignas(64) std::atomic<size_t> tail{0}; // producer writes tail
// alignas(64) puts each on a separate cache line -- prevents false sharing
public:
bool push(const T& item) {
size_t t = tail.load(std::memory_order_relaxed);
size_t next = (t + 1) % Capacity;
if (next == head.load(std::memory_order_acquire))
return false; // full
buffer[t] = item;
tail.store(next, std::memory_order_release);
return true;
}
bool pop(T& item) {
size_t h = head.load(std::memory_order_relaxed);
if (h == tail.load(std::memory_order_acquire))
return false; // empty
item = buffer[h];
head.store((h + 1) % Capacity, std::memory_order_release);
return true;
}
};
Ocuco-landskapet alignas(64) på head och tail är avgörande. Utan den passar båda på samma cacherad. Varje skrivning till tail utlöser en ogiltigförklaring av cacheraden som är synlig för kärnans läshuvud, och vice versa, falsk delning som serialiserar det som borde vara oberoende operationer.
MPMC-kö: Multi-Producer Multi-Consumer
En helt samtidig MPMC-kö är betydligt mer komplex. Produktionsimplementeringar använder ett sekvensnummer per plats för att koordinera producenter och konsumenter utan ett lås:
cpp
#include <atomic>
#include <array>
template<typename T, size_t Capacity>
class MPMCQueue {
struct Slot {
std::atomic<size_t> sequence;
T data;
};
alignas(64) std::array<Slot, Capacity> slots;
alignas(64) std::atomic<size_t> enqueue_pos{0};
alignas(64) std::atomic<size_t> dequeue_pos{0};
public:
MPMCQueue() {
for (size_t i = 0; i < Capacity; ++i)
slots[i].sequence.store(i, std::memory_order_relaxed);
}
bool push(const T& item) {
size_t pos = enqueue_pos.fetch_add(1, std::memory_order_relaxed);
Slot& slot = slots[pos % Capacity];
size_t seq = slot.sequence.load(std::memory_order_acquire);
// wait for the slot to be ready for writing
while (seq != pos) {
seq = slot.sequence.load(std::memory_order_acquire);
}
slot.data = item;
slot.sequence.store(pos + 1, std::memory_order_release);
return true;
}
bool pop(T& item) {
size_t pos = dequeue_pos.fetch_add(1, std::memory_order_relaxed);
Slot& slot = slots[pos % Capacity];
size_t seq = slot.sequence.load(std::memory_order_acquire);
while (seq != pos + 1) {
seq = slot.sequence.load(std::memory_order_acquire);
}
item = slot.data;
slot.sequence.store(pos + Capacity, std::memory_order_release);
return true;
}
};
.NET ConcurrentQueue: Så fungerar det internt
.NET ConcurrentQueue<T> I .NET 5+ används en segmenterad arraystruktur. Varje segment är en array med fast storlek med ett huvud- och svansindex som hanteras med Interlocked operationer (som mappas till CAS på den underliggande hårdvaran). Segmenten är länkade via en volatil _nextSegment pekare. Enqueue läggs till i det aktuella svanssegmentet och utökar segmentkedjan när den är full. Dequeue läser från huvudsegmentet och flyttar huvudindexet atomärt.
Det viktigaste designbeslutet är att undvika en global låsning. Producenter konkurrerar endast på det aktuella segmentets svansindex. Konsumenter konkurrerar endast på huvudindex. Ingen producent konkurrerar någonsin med en konsument. Detta gör ConcurrentQueue<T> mycket effektiv för producent-konsument-rörledningar:
csharp
using System.Collections.Concurrent;
using System.Threading;
// .NET ConcurrentQueue -- lock-free, thread-safe, FIFO
var queue = new ConcurrentQueue<int>();
// Producer thread
var producer = Task.Run(() => {
for (int i = 0; i < 1_000_000; i++)
queue.Enqueue(i);
});
// Consumer thread
var consumer = Task.Run(() => {
long sum = 0;
while (!queue.IsEmpty || !producer.IsCompleted) {
if (queue.TryDequeue(out int item))
sum += item;
else
Thread.SpinWait(1); // brief spin before retry
}
return sum;
});
await Task.WhenAll(producer, consumer);
Cachekoherens och falsk delning i låsfri kod
Cachekoherens är den osynliga prestandavariabeln i låsfria implementeringar. När en kärna skriver till en minnesplats ogiltigförklaras hela den 64-byte cacheraden som innehåller den platsen i alla andra kärnors cacher. De måste hämta den uppdaterade raden innan de läses. I låsfri kod med frekventa atomuppdateringar kan denna ogiltigförklaring av cacheraden bli den dominerande kostnaden.
Falsk delning inträffar när två trådar modifierar olika variabler som råkar dela en cache-rad. Ingen av trådarna har åtkomst till samma data, men cache-koherensprotokollet behandlar hela cache-raden som omtvistad:
cpp
// BAD: head and tail on the same cache line
struct BadQueue {
std::atomic<size_t> head; // bytes 0-7
std::atomic<size_t> tail; // bytes 8-15
// both fit in the same 64-byte cache line
// producer writing tail invalidates head in consumer's cache
};
// GOOD: head and tail on separate cache lines
struct GoodQueue {
alignas(64) std::atomic<size_t> head;
alignas(64) std::atomic<size_t> tail;
// each on its own cache line -- no false sharing
};
MESI-protokollet (Modified, Exclusive, Shared, Invalid) beskriver hur x86-processorer koordinerar ägande av cachelinjer. När en kärna vill skriva till en plats den delar med andra kärnor (Shared state) måste den sända en skrivbegäran, vänta på bekräftelse från alla delare och övergå till Modified state. Denna återgång till koherensprotokollet tar 50–300+ cykler på ett system med flera socklar. Falsk delning omvandlar oberoende trådlokala operationer till cachekoherenstrafik, vilket minskar prestanda som borde skalas linjärt med kärnorna.
Upptäcka falsk delning: använda sig av perf stat -e cache-misses,L1-dcache-load-misses på Linux eller Visual Studios CPU-prestandaprofilerare på Windows. En hög andel L1-cachemissar i ett flertrådat program som får plats i cachen är en stark indikator på falsk delning.
Minnesordning: Varför memory_order_relaxed Är inte alltid tillräckligt
C + + std::atomic Operationer exponerar hela spektrumet av minnesordning från C++11-minnesmodellen. Att välja fel ordning är det näst vanligaste felet i låsfri kod efter ABA-problemet.
cpp
// The five memory orderings and when each applies:
// relaxed: no synchronization, only atomicity
// Use for: counters where only the final value matters
counter.fetch_add(1, std::memory_order_relaxed);
// acquire: reads see all writes by threads that released this location
// Use for: reading shared state protected by a flag
if (ready.load(std::memory_order_acquire)) {
use(data); // guaranteed to see all writes made before ready was set
}
// release: writes are visible to threads that acquire this location
// Use for: publishing shared state
data = compute();
ready.store(true, std::memory_order_release);
// acq_rel: acquire + release in one operation
// Use for: read-modify-write operations like CAS in the middle of a chain
node.compare_exchange_strong(expected, desired,
std::memory_order_acq_rel, // success
std::memory_order_acquire); // failure
// seq_cst: total order across all seq_cst operations, all cores
// Use for: when you need a global consistent view (but slowest)
flag.store(true, std::memory_order_seq_cst);
Ett vanligt misstag: att använda relaxed för en släppflagga. En tråd som anger ready = true med relaxed beställning garanterar inte att föregående skrivningar till data är synliga för andra trådar som läser readyParet förvärva/släppa skapar den "händer-innan"-relation som förbinder författare och läsare.
Treiber-stacken: Låsfri stack i C++
Treiber-stacken är den enklaste icke-triviala låsfria datastrukturen. Den använder en CAS-slinga på en enda top pekare:
cpp
#include <atomic>
template<typename T>
class TreiberStack {
struct Node {
T data;
Node* next;
explicit Node(T d) : data(std::move(d)), next(nullptr) {}
};
std::atomic<Node*> top{nullptr};
public:
void push(T data) {
Node* node = new Node(std::move(data));
node->next = top.load(std::memory_order_relaxed);
while (!top.compare_exchange_weak(
node->next, // expected -- updated on failure
node, // desired
std::memory_order_release,
std::memory_order_relaxed))
{ /* retry */ }
}
bool pop(T& result) {
Node* node = top.load(std::memory_order_acquire);
while (node) {
if (top.compare_exchange_weak(
node,
node->next, // new top
std::memory_order_acquire,
std::memory_order_relaxed))
{
result = std::move(node->data);
// WARNING: cannot free node here safely without hazard pointers or EBR
// delete node; -- ABA hazard if another thread reads node->next
return true;
}
}
return false; // empty
}
};
Kommentaren till delete node är kritisk: naiv borttagning är den ABA-risk som beskrivits tidigare. I en produktionsbaserad Treiber-stack kräver nodåterställning riskpekare, epokbaserad återställning eller sophämtning (Java/C# hanterar detta automatiskt).
Java Lock-Free Datastrukturer
Javas standardbibliotek tillhandahåller låsfria implementeringar av produktionskvalitet i java.util.concurrent:
| Klass | Structure | Framsteg | Anmärkningar |
|---|---|---|---|
AtomicReference<T> | Enkelt värde | Låsfritt | CAS på objektreferens |
AtomicStampedReference<T> | Värde + stämpel | Låsfritt | ABA-skydd via versionsräknare |
ConcurrentLinkedQueue<T> | Michael-Scott-kö | Låsfritt | FIFO, obegränsad |
ConcurrentLinkedDeque<T> | Låsfri deque | Låsfritt | Båda ändarna |
LongAdder | Motverka | Låsfritt | Randig räkning med hög genomströmning |
LongAdder är värt att notera specifikt. Istället för en enda atomär räknare som konkurrerar över alla trådar, underhåller den en stripedansamling av räknare, som var och en nås av en delmängd av trådar. Konflikten sprids över stripes snarare än koncentreras till en plats. Totalen summeras långsamt på sum()För högfrekventa stegoperationer över många trådar, LongAdder dramatiskt överträffar AtomicLong.incrementAndGet():
Java
import java.util.concurrent.atomic.LongAdder;
// BAD for high-concurrency counting: all threads contend on one location
AtomicLong counter = new AtomicLong(0);
counter.incrementAndGet(); // single CAS -- all threads collide
// GOOD for high-concurrency counting: contention distributed across stripes
LongAdder adder = new LongAdder();
adder.increment(); // updates thread-local stripe -- minimal contention
long total = adder.sum(); // sum all stripes lazily
Hur SMART TS XL Stöder låsfri systemutveckling
Låsfri kod är bland den svåraste koden att få rätt. Buggarna är icke-deterministiska, uppträder bara under specifika sammanflätningar och ofta bara i storskalig produktion. Statisk analys åtgärdar detta genom att undersöka kodens strukturella egenskaper före exekvering snarare än att vänta på att ett kappvillkor ska manifestera sig.
SMART TS XL analyserar de fullständiga exekveringsvägarna för samtidig kod och spårar hur atomära operationer relaterar till de minnesplatser de skyddar. För system som blandar låsfri kod med äldre komponenter eller flerspråkiga arkitekturer ger den den gränsöverskridande insyn som enspråkiga verktyg inte kan: hur en delad minnesregion som nås av en låsfri C++-komponent relaterar till Java-tjänsten som läser från den, eller hur en samtidig kö matas in i en COBOL-baserad bearbetningspipeline.
Funktionen för statisk kodanalys identifierar mönster associerade med samtidighetsrisker: atombelastningar utan motsvarande förvärvsemantik, CAS-loopar som inte uppdaterar det förväntade värdet vid fel, delade datastrukturer där fält som nås av olika trådar är samlokaliserade utan justeringsfyllning. Funktionen för konsekvensanalys spårar vilka nedströmskomponenter som är beroende av en delad samtidig datastruktur, så att ändringar i strukturens gränssnitt eller återställningsstrategi kan omfattas korrekt innan ändringen görs.
För företagssystem där låsfria komponenter måste samexistera med äldre batchbehandling, meddelandekö-mellanprogramvara och moderna tjänstelager, SMART TS XLÄr beroendekartläggning ger en arkitektonisk vy över hur samtidiga datavägar kopplar samman komponenter skrivna på olika språk över hela systemstacken.
När låsfritt är fel val
Låsfritt är inte alltid bättre än en mutex. Argumenten för låsfria strukturer beror på arbetsbelastningens faktiska konkurrensprofil. Vid lågt trådantal eller låg konkurrens är en väl implementerad mutex snabbare eftersom den undviker overhead från atomära återförsöksloopar och ogiltigförklaring av cachelinjer över kärnor.
Använd en mutex när:
- Trådantalet är lågt (under 4-8) eller så är det sällsynt med trådkonflikter
- Den kritiska sektionen är lång och komplex (låsfria slingor blir dyra i förhållande till sektionen)
- Korrekthet är viktigare än dataflöde och algoritmen är enklare med ett lås
- Plattformen har effektiv futex-baserad låsning (Linux
pthread_mutex, Windows SRWLOCK)
Använd låsfri när:
- Trådantalet är högt och konkurrensen fortsätter
- Operationerna är korta (CAS-loopoverhead är liten i förhållande till operationen)
- Blockering är oacceptabelt (realtidstrådar, avbrottshanterare, signalhanterare)
- Komponera med andra låsfria strukturer där ett lås skulle introducera beroenden mellan låsordning och
Använd väntefri när:
- Varje tråd måste slutföras inom begränsad tid oavsett andra trådar
- Hårda realtids- eller säkerhetskritiska krav där utarmning är oacceptabel
- Algoritmen kan struktureras för att stödja avslutande av begränsade steg per tråd
Disciplinen för låsfri programmering är inte att välja den överallt, utan att välja den exakt där dess egenskaper, icke-blockerande framsteg, eliminering av dödlägen, tolerans för förköp, matchar systemets faktiska krav.