struct Dir {
mutex_t lock; HashMap entries;
};
void MoveFile(Dir *from_dir, Dir *to_dir, string filename) {
mutex_lock(&from_dir->lock);
mutex_lock(&to_dir->lock);
Map_put(to_dir->entries, filename,
Map_get(from_dir->entries, filename));
Map_erase(from_dir->entries, filename);
mutex_unlock(&to_dir->lock);
mutex_unlock(&from_dir->lock);
}
Thread 1: MoveFile(A, B, “foo”)
Thread 2: MoveFile(B, A, “bar”)
| Thread 1 | Thread 2 |
|---|---|
MoveFile(A, B, "foo")
|
MoveFile(B, A, "bar")
|
lock(&A->lock);
|
|
lock(&B->lock);
|
|
| (do move) | |
unlock(&B->lock);
|
|
unlock(&A->lock);
|
|
lock(&B->lock);
|
|
lock(&A->lock);
|
|
| (do move) | |
unlock(&A->lock);
|
|
unlock(&B->lock);
|
| Thread 1 | Thread 2 |
|---|---|
MoveFile(A, B, "foo")
|
MoveFile(B, A, "bar")
|
lock(&A->lock);
|
|
lock(&B->lock);
|
|
lock(&B->lock…
|
|
| (do move) | (waiting for B lock) |
unlock(&B->lock);
|
|
lock(&B->lock);
|
|
lock(&A->lock…
|
|
unlock(&A->lock);
|
|
lock(&A->lock);
|
|
| (do move) | |
unlock(&A->lock);
|
|
unlock(&B->lock);
|
| Thread 1 | Thread 2 |
|---|---|
MoveFile(A, B, "foo")
|
MoveFile(B, A, "bar")
|
lock(&A->lock);
|
|
lock(&B->lock)
|
|
lock(&B->lock;…stalled
|
|
| (waiting for lock on B) |
lock(&A->lock;…stalled
|
| (waiting for lock on B) | (waiting for lock on A) |
|
|
|
unlock(&B->lock) |
unlock(&A->lock); |
unlock(&A->lock) |
unlock(&B->lock); |
deadlock — circular waiting for resources
resource = something needed by a thread to do work
often non-deterministic in practice
most common example: when acquiring multiple locks
mutual exclusion
hold and wait
no preemption of resources
circular wait
there exists a set \(\{T_1,\ldots,T_n\}\) of waiting threads such that
Given list: A, B, C, D, E
RemoveNode(LinkedListNode *node) {
pthread_mutex_lock(&node->lock);
pthread_mutex_lock(&node->prev->lock);
pthread_mutex_lock(&node->next->lock);
node->next->prev = node->prev;
node->prev->next = node->next;
pthread_mutex_unlock(&node->next->lock);
pthread_mutex_unlock(&node->prev->lock);
pthread_mutex_unlock(&node->lock);
}
Which of these (all run in parallel) can deadlock? | A. RemoveNode(B) and RemoveNode(C) |
| B. RemoveNode(B) and RemoveNode(D) |
| C. RemoveNode(B) and RemoveNode(C) and RemoveNode(D) |
| D. A and C |
| E. B and C |
| F. all of the above |
| G. none of the above |
| RemoveNode(B) | RemoveNode(C) |
| lock B | lock C |
| lock A (prev) | wait to lock B (prev) |
| wait to lock C (next) |
With B and D — only overlap in in node C — no circular wait possible
(thread can’t be waiting while holding something other thread wants)
abort-and-retry
pthread’s mutexes:
pthread_mutex_trylockpthread_mutex_timedlockhow many times will you retry?
struct Dir { mutex_t lock; HashMap entries; };
void MoveFile(Dir *from_dir, Dir *to_dir, string filename) {
while (true) {
mutex_lock(&from_dir->lock);
if (mutex_trylock(&to_dir->lock) == LOCKED) break;
mutex_unlock(&from_dir->lock);
}
Map_put(to_dir->entries, filename, Map_get(from_dir->entries, filename));
from_dir->entries.erase(filename);
mutex_unlock(&to_dir->lock);
mutex_unlock(&from_dir->lock);
}Thread 1: MoveFile(A, B, “foo”); Thread 2: MoveFile(B, A, “bar”)
livelock: keep aborting and retrying without end
like deadlock — no one’s making progress
unlike deadlock — threads are not waiting
how do we make stealing locks possible
unclean: just kill the thread
clean: have code to undo partial operation
won’t go into detail in this class
try {
AcquireLock();
use shared data
} catch (LockRevokedException le) {
undo operation hopefully?
} finally {
ReleaseLock();
}
MoveFile(Dir* from_dir, Dir* to_dir, string filename) {
if (from_dir->path < to_dir->path) {
lock(&from_dir->lock);
lock(&to_dir->lock);
} else {
lock(&to_dir->lock);
lock(&from_dir->lock);
}
...
}
/*
* ...
* Lock order:
* contex.ldt_usr_sem
* mmap_sem
* context.lock
*/
/*
* ...
* Lock order:
* 1. slab_mutex (Global Mutex)
* 2. node->list_lock
* 3. slab_lock(page) (Only on some arches and for debugging)
* ...
*/
starvation: one+ unlucky (no progress), one+ lucky (yes progress)
deadlock: no one involved in deadlock makes progress
starvation: once starvation happens, taking turns will resolve
deadlock: once it happens, taking turns won’t fix
abort-and-retry
pthread’s mutexes:
pthread_mutex_trylockpthread_mutex_timedlockhow many times will you retry?
struct Dir { mutex_t lock; HashMap entries; };
void MoveFile(Dir *from_dir, Dir *to_dir, string filename) {
while (true) {
mutex_lock(&from_dir->lock);
if (mutex_trylock(&to_dir->lock) == LOCKED) break;
mutex_unlock(&from_dir->lock);
}
Map_put(to_dir->entries, filename, Map_get(from_dir->entries, filename));
from_dir->entries.erase(filename);
mutex_unlock(&to_dir->lock);
mutex_unlock(&from_dir->lock);
}Thread 1: MoveFile(A, B, “foo”); Thread 2: MoveFile(B, A, “bar”)
livelock: keep aborting and retrying without end
like deadlock — no one’s making progress
unlike deadlock — threads are not waiting
how do we make stealing locks possible
unclean: just kill the thread
clean: have code to undo partial operation
won’t go into detail in this class
try {
AcquireLock();
use shared data
} catch (LockRevokedException le) {
undo operation hopefully?
} finally {
ReleaseLock();
}
let’s say I want to detect deadlocks that only involve mutexes
… by modifying my threading library:
struct Thread {
... /* stuff for implementing thread */
/* what extra fields go here? */
};
struct Mutex {
... /* stuff for implementing mutex */
/* what extra fields go here? */
};
why? debugging or fix deadlock by aborting operations
idea: search for cyclic dependencies
need:
deadlock is possible with divislbe resources like memory,…
example: suppose 6MB of RAM for threads total:
cycle: thread 1 waiting on memory owned by thread 2?
not a deadlock — thread 3 can still finish
… but would be deadlock
for each resource: track which threads have those resources
for each thread: resources they are waiting for
repeatedly:
either: all threads eliminated or found deadlock
requires:
common example: for locks in a database
related idea: avoid deadlock with detection on ‘‘what if’’ scenario
BROKEN example:
int child_to_parent_pipe[2], parent_to_child_pipe[2];
pipe(child_to_parent_pipe); pipe(parent_to_child_pipe);
if (fork() == 0) {
/* child */
write(child_to_parent_pipe[1], buffer, HUGE_SIZE);
read(parent_to_child_pipe[0], buffer, HUGE_SIZE);
exit(0);
} else {
/* parent */
write(parent_to_child_pipe[1], buffer, HUGE_SIZE);
read(child_to_parent_pipe[0], buffer, HUGE_SIZE);
}
This will hang forever (if HUGE_SIZE is big enough).
for resources like disk space, memory
figure out maximum allocation when starting thread
only start thread if those resources are available
okay solution for embedded systems?
okay, now what?
pthreads provides pthread_mutex_trylock — ‘‘lock or fail’’
some databases implement revocable locks
suppose you know the maximum resources a process could request
make decision when starting process (‘‘admission control’’)
ask ‘‘what if every process was waiting for maximum resources’’
would it cause deadlock? then don’t let it start
called Banker’s algorithm