Skip to content

Commit

Permalink
runtime: fix thread management deadlock analysis for mutexes
Browse files Browse the repository at this point in the history
  • Loading branch information
mertcandav committed Jan 4, 2025
1 parent 3e8247e commit 1fc2814
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 27 deletions.
6 changes: 3 additions & 3 deletions std/runtime/sema.jule
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn cansemacquire(mut &sema: u32): bool {
}

// Puts the current thread into a waiting state and unlocks the lock.
fn semapark(&lock: fmutex, &deq: bool, opt: u32) {
fn semapark(&lock: fmutex, &sema: u32, &deq: bool, opt: u32) {
mut reason := u32(reasonNA | reasonStrict)
if opt&semaWaitGroup == semaWaitGroup {
reason |= reasonWaitGroup
Expand All @@ -150,7 +150,7 @@ fn semapark(&lock: fmutex, &deq: bool, opt: u32) {
}
lock.unlock()
for !deq {
yield(0, reason)
yield(uintptr(&sema), reason)
reason &= ^reasonStrict
}
}
Expand Down Expand Up @@ -192,7 +192,7 @@ fn semacquire(mut &sema: u32, lifo: bool, opt: u32) {
// Any semrelease after the cansemacquire knows we're waiting
// (we set nwait above), so go to sleep.
root.queue(sema, sl, lifo)
semapark(root.lock, sl.deq, opt)
semapark(root.lock, sema, sl.deq, opt)
// Try to acquire semaphore before enqueue again.
if cansemacquire(sema) {
break
Expand Down
45 changes: 31 additions & 14 deletions std/runtime/thread.jule
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ struct thread {
// See documentation of the checkDeadlock function.
frame: int

// Unique identifier for mutex-sensitive primitives like channels.
// Unique identifier for various purposes, usually it is a pointer.
// It used to detect a specific primitive.
// It may be a mutex pointer for channel, of a sema pointer for mutex.
mu: uintptr

// Pointer to the next thread.
Expand Down Expand Up @@ -111,6 +112,7 @@ fn getCurrentThread(): &thread {

// Suspends the current thread and yields the CPU.
// If the mu is not zero, assumes it already locked and releases before yield.
// If reason is related with a sema, will not handle mu as a mutex.
fn yield(mu: uintptr, mut reason: u32) {
threadMutex.lock()
mut t := getCurrentThread()
Expand All @@ -127,7 +129,8 @@ fn yield(mu: uintptr, mut reason: u32) {
// Unlock the mutex becase other threads may need to lock.
// There is nothing to do for this thread for now, so release lock.
threadMutex.unlock()
if mu != 0 {
// Release mutex if reason is not related with a sema.
if mu != 0 && reason&reasonMutex != reasonMutex && reason&reasonWaitGroup != reasonWaitGroup {
unsafe { (*fmutex)(mu).unlock() }
}
// Yield the CPU if possible, it may return immediately for the same thread.
Expand All @@ -137,7 +140,7 @@ fn yield(mu: uintptr, mut reason: u32) {
// Lock mutex again and wake up.
threadMutex.lock()
t.mu = 0
t.state &= ^threadSuspended
t.state &= ^(threadSuspended | reason)
threadMutex.unlock()
}

Expand All @@ -153,19 +156,19 @@ fn closeThread(tptr: *unsafe) {
t.state &= ^(threadRunning | threadSuspended)
t.state |= threadClosed
t.frame = 0
// We have empty select special case.
// We have to check deadlocks after any thread closed. Because at least
// one thread is in deep sleep and we do not know when this thread will wake up.
// So, if we have a deadlock, detection may be impossible because empty selects
// does not checks deadlocks. So check deadlock after closed a thread to
// caught special case deadlock; all threads are in the deep sleep.
if threadCases&threadSC_EmptySelect == threadSC_EmptySelect {
checkDeadlock(0, reasonNA)
}
threadMutex.unlock()
break
}
}
// We have empty select special case.
// We have to check deadlocks after any thread closed. Because at least
// one thread is in deep sleep and we do not know when this thread will wake up.
// So, if we have a deadlock, detection may be impossible because empty selects
// does not checks deadlocks. So check deadlock after closed a thread to
// caught special case deadlock; all threads are in the deep sleep.
if threadCases&threadSC_EmptySelect == threadSC_EmptySelect {
checkDeadlock(0, reasonNA)
}
threadMutex.unlock()
}

// Checks deadlock and panics if exist.
Expand Down Expand Up @@ -335,6 +338,19 @@ fn checkDeadlock(mu: uintptr, reason: u32) {
// channels do not require excessive attention because concurrency rarely
// triggers such situations. For this edge case to be triggered,
// the program must have advanced to the frame analysis stage.
//
// A quick check before heavy analysis:
// mu is no zero and reason is mutex. For a fast check, we can try sema value.
// If sema is not locked, mutex have a chance, so no deadlock risk.
// It also prevents fake deadlock analysis results for mutexes.
// For example: all threads tries to lock a mutex and they in the suspended
// state but no one locked the mutex yet. In this case common analysis will
// result as deadlock due to all threads suspended.
if mu != 0 && reason&reasonMutex == reasonMutex {
if atomicLoad(unsafe { *(*u32)(mu) }, atomicSeqCst) > 0 {
ret
}
}
mut wgRuns := 0
mut condRuns := 0
mut nonlocked := 0
Expand Down Expand Up @@ -423,7 +439,8 @@ fn checkDeadlock(mu: uintptr, reason: u32) {
// removed a frame from thread.
t = threads
for t != nil; t = t.next {
if t.state&threadRunning == threadRunning {
if t.state&threadRunning == threadRunning &&
t.state&threadSuspended == threadSuspended {
if t.frame > 0 {
t.frame--
ret
Expand Down
8 changes: 4 additions & 4 deletions std/runtime/thread_unix.jule
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ struct threadData {
// So, the head fields of the thread data should be matched fields of the threadData.
#export "__jule_coSpawn"
unsafe fn coSpawn(func: *unsafe, mut args: *unsafe): bool {
mut thread := pushNewThread()
(*threadData)(args).handle = &thread.os.handle
if cpp.pthread_create(&thread.os.handle, nil, integ::Emit[*unsafe]("(void*(*)(void*))({})", func), args) != 0 {
mut t := pushNewThread()
(*threadData)(args).handle = &t.os.handle
if cpp.pthread_create(&t.os.handle, nil, integ::Emit[*unsafe]("(void*(*)(void*))({})", func), args) != 0 {
ret false
}
threadMutex.unlock()
cpp.pthread_detach(thread.os.handle)
cpp.pthread_detach(t.os.handle)
ret true
}

Expand Down
12 changes: 6 additions & 6 deletions std/runtime/thread_windows.jule
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ struct threadData {
// So, the head fields of the thread data should be matched fields of the threadData.
#export "__jule_coSpawn"
unsafe fn coSpawn(func: *unsafe, mut args: *unsafe): bool {
mut thread := pushNewThread()
(*threadData)(args).handle = &thread.os.handle
thread.os.handle = cpp.CreateThread(
mut t := pushNewThread()
(*threadData)(args).handle = &t.os.handle
t.os.handle = cpp.CreateThread(
nil,
0,
integ::Emit[*unsafe]("(unsigned long(*)(void*))({})", func),
args,
0,
integ::Emit[*unsafe]("(LPDWORD)({})", &thread.os.id))
if thread.os.handle == nil {
integ::Emit[*unsafe]("(LPDWORD)({})", &t.os.id))
if t.os.handle == nil {
ret false
}
threadMutex.unlock()
sys::CloseHandle(sys::Handle(thread.os.handle))
sys::CloseHandle(sys::Handle(t.os.handle))
ret true
}

Expand Down

0 comments on commit 1fc2814

Please sign in to comment.