Yeah you could a race condition there. Actual, several...
Lets say you would do
ThreadB.StopSending;
ThreadA.Terminate;
ThreadA.WaitFor;
ThreadA.Free;
Which you noted crashes.
"ThreadB.StopSending;" => this removes the OnData from ThreadA.
Except, you can't assume it happens simultaneously.
if Assigned(OnData) then
OnData(Self);
For the call "OnData(Self) the thread loads the event-address into a register.
Now this can hapen
- Thread A: copy OnData into register
* Scheduler switches thread
- Main thread, calls ThreadB.StopSending
- Main thread destroys ThreadB
* Scheduler switches thread
- Thread A: call to ThreadB.SendData (from what it has in its register)
And that gives an error as the object for ThreadB is gone.
Now, this can be a hard to catch race condition, because the time slot in which the error happens is tiny.
I am not sure, even if you compile with maximum optimization, the field should be reloaded from memory quite often.
You put in a lot of sleeps, so this should be really really unlikely....
I wonder...
You said the debugger was somewhere in CThreads. But that may be misleading.
Because if you call a method on a freed object, then you could end up following virtual methods to random places.
Also, your real code will have more workload than the example. So it is possible that instead of calling an event on the destroyed object, you are still inside an event within the object "ThreadB" when THreadB gets destroyed. Leads to similar errors.
More so, even
Is not thread save.
This is an event, a method pointer. OnData has 2 fields, the function-address and the object instance.
So the assignment is not an atomic operation. While highly unlikely (less than a lottery jackpot chance), the other thread could be reading OnData, when only half of it was written.
And
if Assigned(OnData) then
OnData(Self);
Is also bad (and it is not that unlikely to go wrong)>
You could have
Thread A: if Assigned(OnData) then
Thread B: ThreadA.OnData := nil;
// even if it was atomic, even if it does both fields before the other thread goes on
Thread A OnData(Self);
Only that by the time the call is made, OnData is nil.
In other words quite a few race conditions.
The simplest way is to use critical sections.
If that is not wanted, you can use unprotected writes/reads but (very roughly described)
- you must the fields by hand in the right order
- have the check do the matching order
- potentially add Read/Write barriers.
In other words, without critical sections => you need a lot of knowledge about thread safety.