I came across Odin a few days ago and must say I’ve very much enjoyed learning it so far (I suppose I am the intended audience since C has been my mainstay language for many years). However, while having some fun testing threads, I came across what seems to be a race condition when starting threads in the following code:
The code runs fine if every thread prints its hello message before any join occurs. If a join occurs before one of the threads prints its message however, the program seems to deadlock. The only case in which it happens always seems to be that thread 0 (that is, the first one created and started) doesn’t print its message and immediately joins with the main thread, apparently ignoring the barrier as well (so I imagine the body of the say_hello proc isn’t executed at all on this thread). This also happens if we reverse the joining order, i.e. the last thread may be joined without printing its message and then the deadlock occurs. The additional 1 second sleep resolves the problem in any case, so it does look like a race condition.
I don’t think similar code using C/Pthreads would lock in this manner, so maybe am I missing some different semantics ? Maybe an explicit memory barrier is required after thread creation ?
Ok, thanks for your answer! Indeed I didn’t think of checking the issue tracker. I don’t have an account on Github, but if it helps, the following lines seem to be involved:
I added a debug message which indeed appears only when there is a deadlock. Commenting the block entirely seems to prevent the race and the deadlock. I didn’t dive too deeply into the code, but in my understanding, this is used to prevent previously joined threads from being created over?
The barrier is unsatisfied and causes the program to halt once it joins one of the threads waiting on the barrier.
You have a barrier in place as your child thread synchronization method.
This barrier depends on all threads starting.
Thread A is marked as Joined by your call to join.
Thread A, per core:thread, eventually starts, sees that it has permission to start, sees that it has been marked as Joined and returns early without ever calling the thread procedure.
Therefore, the barrier never reaches its count, and all the threads stall.
There is in this code the assumption that joining a child thread causes an implicit wait on the main thread, however this is not the case in Odin currently. This means it is possible for a thread to never start the user procedure if you issue a join before waiting and confirming some signal for it.
The way I handle this is to bring the main thread into the synchronization and always wait before joining. You can use a barrier or a wait group for this. In actual fact, if you change the barrier_init to include the main thread and use barrier_wait before joining, your program works as expected.
I am responsible for the commit that introduced this code. I added it some time ago to prevent a thread from blocking on the start_ok semaphore if it joined another thread which was created but never started.
However, I had to think for a moment on why I added the other check in the entry proc which you’ve pointed out, since it’s been a while. This early exit prevents a thread which has been created using the Odin core:thread API - but not started using the Odin API - from ever performing any work.
There is no pthreads start, since as soon as you create a thread, it’s running off to do some work. You have to start it if you eventually want to join it, otherwise it’ll just sit on that semaphore and block the main thread, and we’re back to the original situation we were trying to avoid.
In short, this condition helpfully keeps a thread from ever doing anything if it’s never started but is eventually joined.