Before diving into deadlock, it helps to know the patterns real concurrency bugs take โ the first step to writing robust code.
The Crux: How To Handle Common Concurrency Bugs
Concurrency bugs come in a few common patterns. Knowing which to look out for is the first step to writing more robust, correct concurrent code.32.1 What Types Of Bugs Exist?
Lu et al. studied concurrency bugs found and fixed in four mature open-source applications โ a rare quantitative look at what actually goes wrong:
| what it does | non-deadlock | deadlock | |
|---|---|---|---|
| MySQL | database server | 14 | 9 |
| Apache | web server | 13 | 4 |
| Mozilla | web browser | 41 | 16 |
| OpenOffice | office suite | 6 | 2 |
| Total | โ | 74 | 31 |
The headline: non-deadlock bugs are the majority (74 of 105). We look at those first, then deadlock.
32.2 Non-Deadlock Bugs
Lu found two dominant patterns โ together ~97% of non-deadlock bugs.
Atomicity-violation bugs
A sequence of accesses the programmer assumed was atomic gets interrupted. The MySQL example (Fig 32.2):
// Thread 1
if (thd->proc_info) {
fputs(thd->proc_info, ...);
}
// Thread 2
thd->proc_info = NULL;
Thread 1 checks the pointer, then uses it โ assuming nothing changes in between. But it can:
Thread 1 checks: proc_info is non-NULL, so it enters the if โ about to fputs() it.
Formally (Lu): โa code region is intended to be atomic, but the atomicity is not enforced during execution.โ This is an atomicity violation atomicity violation A non-deadlock concurrency bug where a sequence of accesses assumed to be atomic is interrupted, letting another thread interleave and break the assumption โ e.g. one thread checks a pointer is non-NULL and is interrupted before using it while another sets it NULL, causing a crash. Fixed by holding a lock around all the accesses. With order violations, ~97% of non-deadlock bugs (Lu et al.). defined in ch. 32 โ open in glossary . The fix is usually simple โ hold a lock around every access to the shared field (Fig 32.3):
pthread_mutex_t proc_info_lock = PTHREAD_MUTEX_INITIALIZER;
// Thread 1
pthread_mutex_lock(&proc_info_lock);
if (thd->proc_info) {
fputs(thd->proc_info, ...);
}
pthread_mutex_unlock(&proc_info_lock);
// Thread 2
pthread_mutex_lock(&proc_info_lock);
thd->proc_info = NULL;
pthread_mutex_unlock(&proc_info_lock);
Order-violation bugs
The desired order between two accesses is flipped โ A should run before B, but nothing enforces it. The Mozilla example (Fig 32.4):
// Thread 1
void init() {
mThread = PR_CreateThread(mMain, ...);
}
// Thread 2
void mMain(...) {
mState = mThread->State;
}
Thread 2 assumes mThread is already set. If it runs first, it isnโt:
Thread 1's init() calls PR_CreateThread, which creates Thread 2 (mMain). But mThread hasn't been assigned the return value yet โ it's still NULL.
Formally: โA should always be executed before B, but the order is not enforced.โ This is an order violation order violation A non-deadlock concurrency bug where the desired order between two groups of memory accesses is flipped โ A should run before B but the order isn't enforced โ e.g. a thread uses a variable another thread was supposed to initialize first, hitting a NULL dereference. Fixed by enforcing order with a condition variable (or semaphore). defined in ch. 32 โ open in glossary . The fix is to enforce order โ a condition variable condition variable The signaling primitive (pthread_cond_t): wait(cond, mutex) sleeps the caller โ releasing the mutex while asleep, reacquiring before return โ until another thread signal()s. Always called with the lock held; always re-checked in a while loop. The deep treatment arrives with ch30. defined in ch. 27 โ open in glossary (with a state variable) is the robust way (Fig 32.5):
pthread_mutex_t mtLock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mtCond = PTHREAD_COND_INITIALIZER;
int mtInit = 0;
// Thread 1 โ after creating the thread:
pthread_mutex_lock(&mtLock);
mtInit = 1;
pthread_cond_signal(&mtCond);
pthread_mutex_unlock(&mtLock);
// Thread 2 โ before using mThread:
pthread_mutex_lock(&mtLock);
while (mtInit == 0)
pthread_cond_wait(&mtCond, &mtLock);
pthread_mutex_unlock(&mtLock);
mState = mThread->State; // now safe
If Thread 2 runs early it waits for the signal; if it runs late it sees
mtInit == 1 and proceeds. When order matters between threads,
condition variables (or semaphores) come to the rescue.
Because atomicity and order violations together cover the vast majority of non-deadlock bugs, learning to spot these two patterns โ and reaching for a lock or a condition variable โ prevents most of them. Next: the harder, older problem of deadlock.
Check yourself: non-deadlock bugs
1.According to Lu et al.'s study, which two patterns cover ~97% of non-deadlock concurrency bugs?
2.In the MySQL example, Thread 1 does `if (proc_info) { fputs(proc_info); }` and Thread 2 does `proc_info = NULL`. What's the bug?
3.How do you fix an atomicity violation like the proc_info bug?
4.In the Mozilla example, Thread 1's init() sets mThread while Thread 2's mMain() reads mThread->State. What's the bug?
5.What's the robust fix for an order violation?
6.Of the 105 bugs Lu et al. studied, how did non-deadlock and deadlock bugs split?