Thread overview
Debug help - ! in data sharing concurrency
6 days ago
Andy Valencia
August 30

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
August 31

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.

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:

	// 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:

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);
	}
}
August 31

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.

August 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

6 days ago

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:

>

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.

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.

>

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

6 days ago

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