digitalmars.D.learn - Debug help - ! in data sharing concurrency
- Brother Bill (66/66) Aug 30 A predicate (!*isDone) vs. (*isDone == false) seems to have
- Steven Schveighoffer (58/88) Aug 30 Thank you for posting the full code. This helps diagnose issues
- Brother Bill (9/13) Aug 31 So this was just 'bad' luck with 'race conditions'.
- Andy Valencia (13/18) Aug 31 Speaking as a guy who did Unix kernel SMP for years, there are
- Steven Schveighoffer (23/38) Aug 31 Yes, the (bad) luck seemed to suggest your changes were causing a
- Andy Valencia (6/10) Aug 31 Do keep in mind that if you're going to be using tightly parallel
A predicate (!*isDone) vs. (*isDone == false) seems to have
different behavior, where I would expect identical behavior.
What am I missing?
This program runs forever, even though isDone changes from false
to true.
```
import std.stdio;
import std.concurrency;
import core.thread;
import core.time : msecs;
void main()
{
shared(bool) isDone = false;
spawn(&worker, &isDone);
writeln("main");
Thread.sleep(1.seconds);
// Signalling the worker to terminate:
isDone = true;
writeln("main() isDone: ", isDone);
}
void worker(shared(bool)* isDone)
{
writeln("worker() before while, isDone: ", *isDone);
while (!*isDone)
{
Thread.sleep(250.msecs);
writeln("worker() isDone: ", *isDone);
}
}
```
This program properly terminates as expected.
```
import std.stdio;
import std.concurrency;
import core.thread;
import core.time : msecs;
void main()
{
shared(bool) isDone = false;
spawn(&worker, &isDone);
writeln("main");
Thread.sleep(1.seconds);
// Signalling the worker to terminate:
isDone = true;
writeln("main() isDone: ", isDone);
}
void worker(shared(bool)* isDone)
{
writeln("worker() before while, isDone: ", *isDone);
while (*isDone == false)
{
Thread.sleep(250.msecs);
writeln("worker() isDone: ", *isDone);
}
}
```
Console output:
```
main
worker() before while, isDone: false
worker() isDone: false
worker() isDone: false
worker() isDone: false
main() isDone: true
worker() isDone: true
```
Aug 30
On Saturday, 30 August 2025 at 22:05:49 UTC, Brother Bill wrote:
A predicate (!*isDone) vs. (*isDone == false) seems to have
different behavior, where I would expect identical behavior.
What am I missing?
This program runs forever, even though isDone changes from
false to true.
```d
import std.stdio;
import std.concurrency;
import core.thread;
import core.time : msecs;
void main()
{
shared(bool) isDone = false;
spawn(&worker, &isDone);
writeln("main");
Thread.sleep(1.seconds);
// Signalling the worker to terminate:
isDone = true;
writeln("main() isDone: ", isDone);
}
void worker(shared(bool)* isDone)
{
writeln("worker() before while, isDone: ", *isDone);
while (!*isDone)
{
Thread.sleep(250.msecs);
writeln("worker() isDone: ", *isDone);
}
}
```
Thank you for posting the full code. This helps diagnose issues
that generally are confusing because you are looking at the wrong
problem.
What you have done here:
```d
// Signalling the worker to terminate:
isDone = true;
writeln("main() isDone: ", isDone);
// exits main
}
```
is you exited the `main` function. However, where does `isDone`
live? It lives in `main`'s stack frame! When you exit the stack
frame, it becomes unallocated.
This means the next function that gets called, will overtake the
value at the address of `isDone`, and write some other value into
it (obviously 0).
If I add a sleep 1 second after setting isDone to true, the
worker exits. this is because the pause gives the worker enough
time to see the value has changed to `true` before it leaves.
Why would your second iteration make a difference? Purely by
chance! In fact, on my machine, it does not exit in either case.
Welcome to the wonderful world of race conditions and
multithreading!
To properly solve this problem, you can:
a) allocate `isDone` on the heap so it doesn't go away.
b) place `isDone` as a global variable.
c) Do not exit the main thread until the worker thread is
finished.
I recommend c in this case:
```d
import std.stdio;
import std.concurrency;
import core.thread;
import core.time : msecs;
void main()
{
shared(bool) isDone = false;
auto thread = spawnLinked(&worker, &isDone); // note spawnLinked
here
writeln("main");
Thread.sleep(1.seconds);
// Signalling the worker to terminate:
isDone = true;
writeln("main() isDone: ", isDone);
receiveOnly!LinkTerminated(); // wait for worker to exit
}
void worker(shared(bool)* isDone)
{
writeln("worker() before while, isDone: ", *isDone);
while (!*isDone)
{
Thread.sleep(250.msecs);
writeln("worker() isDone: ", *isDone);
}
}
```
Aug 30
On Sunday, 31 August 2025 at 01:27:57 UTC, Steven Schveighoffer wrote:Why would your second iteration make a difference? Purely by chance! In fact, on my machine, it does not exit in either case. Welcome to the wonderful world of race conditions and multithreading!So this was just 'bad' luck with 'race conditions'. It is not a failure of the D compiler. FWIW, given that D supports Message Passing Concurrency, is Data Sharing Concurrency just there for D completeness, for those that want to live close to the iron. It would seem that Message Passing Concurrency should be our first, second and third choice for concurrency.
Aug 31
On Sunday, 31 August 2025 at 12:44:33 UTC, Brother Bill wrote:FWIW, given that D supports Message Passing Concurrency, is Data Sharing Concurrency just there for D completeness, for those that want to live close to the iron.Speaking as a guy who did Unix kernel SMP for years, there are times when you just need shared memory, spinlocks, atomic lock->semaphore transitions, atomic operations (increment, compare/exchange, etc.). D has all of'em, and I made sure they worked when I was first looking at D.It would seem that Message Passing Concurrency should be our first, second and third choice for concurrency.Indeed. As the Golang folks have also noted, message passing is almost inevitably a more productive and less error prone way to coordinate parallel threads. If, some day, you find your parallel app dying under the overhead of all the messaging--you can keep shared memory programming in mind as a possible alternative. Andy
Aug 31
On Sunday, 31 August 2025 at 12:44:33 UTC, Brother Bill wrote:On Sunday, 31 August 2025 at 01:27:57 UTC, Steven Schveighoffer wrote:Yes, the (bad) luck seemed to suggest your changes were causing a different behavior, whereas actually it was just hitting the race condition differently. I've spent many hours on issues such as this. You think some behavior is happening because you changed x, but really it's due to y, and the change just happens to be triggering a difference in y, or some coincidence is happening. Race conditions are really hard to figure out. Couple that with memory safety issues, you are in big trouble.Why would your second iteration make a difference? Purely by chance! In fact, on my machine, it does not exit in either case. Welcome to the wonderful world of race conditions and multithreading!So this was just 'bad' luck with 'race conditions'. It is not a failure of the D compiler.FWIW, given that D supports Message Passing Concurrency, is Data Sharing Concurrency just there for D completeness, for those that want to live close to the iron.Correct. You are not absolved of the responsibility of ensuring you don't use pointers after free. If you want this extra check, you need to mark your function as ` safe`. This will prevent, among other things, taking the address of a local variable as you have done here.It would seem that Message Passing Concurrency should be our first, second and third choice for concurrency.Yes, aside from the issue with lifetime here, you technically are not properly synchronizing access to your boolean. But in this case, it should properly work. Other cases may not be correct without more synchronization (mutexes or using atomics). `std.concurrency` is meant to make all these control flow systems easy to avoid races for, even in ` safe` code. Sending the boolean instead of keeping a pointer to a shared boolean can alleviate a lot of these problems. -Steve
Aug 31
On Sunday, 31 August 2025 at 22:53:51 UTC, Steven Schveighoffer wrote:`std.concurrency` is meant to make all these control flow systems easy to avoid races for, even in ` safe` code. Sending the boolean instead of keeping a pointer to a shared boolean can alleviate a lot of these problems.Do keep in mind that if you're going to be using tightly parallel access to shared memory, you'll want to look at the core.atomic facilities. Andy
Aug 31









Andy Valencia <dont spam.me> 