I have a simple test case that I borrowed from another question on here but modified with slightly different but simple contrived examples. Given:
class Foo
{
public bool Complete; // { get; set; }
public bool IsComplete()
{
return Complete ;
}
}
class Program
{
static Foo foo = new Foo();
static void ThreadProc()
{
bool toggle = false;
// while (!foo.Complete) toggle = !toggle;
while (!foo.IsComplete()) toggle = !toggle;
Console.WriteLine("Thread done");
}
static void Main()
{
var t = new Thread(ThreadProc);
t.Start();
Thread.Sleep(1000);
foo.Complete = true;
t.Join();
}
Given that ThreadProc is calling IsComplete() the compiler doesn't seem to cache the Complete variable. But however I can't find a guarantee that the compiler doesn't generate cache optimisations for method calls on an object passed from a different thread.
But I'm worried about this scenario: If ThreadProc is running on a different processor to main thread can it deep copy the entire code of object foo into its thread cache? Meaning I will be updating an entirely different object instance. If so would making the reference volatile necessary?
I don't understand what's happening here. But it seems to prove my worry above, it never exits (but exits in Debug mode):
class Foo
{
public bool Complete; // { get; set; }
public bool IsComplete()
{
return Complete ;
}
}
class Program
{
static void ThreadProc(Foo foo)
{
bool toggle = false;
// while (!foo.Complete) toggle = !toggle;
while (!foo.IsComplete()) toggle = !toggle;
Console.WriteLine("Thread done");
}
static void Main()
{
Foo foo = new Foo();
var t = new Thread(()=>ThreadProc(foo));
t.Start();
Thread.Sleep(1000);
foo.Complete = true;
t.Join();
Console.ReadLine();
}
}
Yet, the below completes! Pretty much the same thing written differently. I can't see how the anonymous lambda is changing things. It should still point to the same object instance:
public class Foo
{
public bool Complete; // { get; set; }
private FooThread fooThread;
public Foo()
{
fooThread = new FooThread(this);
}
public bool IsComplete()
{
return Complete ;
}
public void StartThread()
{
var t = new Thread(fooThread.ThreadProc);
t.Start();
Thread.Sleep(1000);
Complete = true;
t.Join();
}
}
public class FooThread
{
private Foo foo;
public FooThread(Foo f)
{
foo = f;
}
public void ThreadProc()
{
bool toggle = false;
// while (!foo.Complete) toggle = !toggle;
while (!foo.IsComplete()) toggle = !toggle;
Console.WriteLine("Thread done");
}
}
class Program
{
static void Main()
{
Foo foo = new Foo();
foo.StartThread();
Console.ReadLine();
}
}
A delegate scenario...:
public class Foo
{
public bool Complete; // { get; set; }
private FooThread fooThread;
public Foo()
{
fooThread = new FooThread(this);
fooThread.TriggerCompletion += SetComplete;
}
public bool IsComplete()
{
return Complete;
}
public void SetComplete()
{
Complete = true;
}
public Thread StartThread()
{
var t = new Thread(fooThread.ThreadProc);
return t;
}
}
public class FooThread
{
private Foo foo;
public event Action TriggerCompletion;
public FooThread(Foo f)
{
foo = f;
}
public void ThreadProc()
{
bool toggle = false;
// while (!foo.Complete) toggle = !toggle;
int i = 0;
while (!foo.IsComplete())
{
toggle = !toggle;
i++;
if(i == 1200300) // contrived
TriggerCompletion?.Invoke();
}
Console.WriteLine("Thread done");
}
}
class Program
{
static void Main()
{
Foo foo = new Foo();
var t= foo.StartThread();
t.Start();
Thread.Sleep(1000);
t.Join();
Console.ReadLine();
}
}
These are all contrived examples. But I'm not sure why 1 scenario isn't working. I only see 2 threads at work here updating a boolean value. So volatile shouldn't be necessary. Code should reasonably lock free as one or two dirty reads from Foo is OK. FooThread will signal completion infrequently. (I'm aware of TaskCancellationSource, this question isn't about cancellations but updating a boolean flag from a different thread via on object instance's method )
EDIT: Please test in release mode.
EDIT:
Updates on the failing test case i.e. code block 2.
It seems the compiler is making optimizations on !foo.IsComplete()
method call. It appears to assume that the instance variable is not used else where so optimizes out the call - perhaps in its simplicity?
By having an instance variable referencing foo
the compiler applies to make no such assumption such that the first code block now modified fails:
public class Foo
{
public bool Complete; // { get; set; }
private FooThread fooThread;
public Foo()
{
fooThread = new FooThread();
}
public bool IsComplete()
{
return Complete;
}
public void StartThread()
{
var t = new Thread(()=>fooThread.ThreadProc(this));
t.Start();
Thread.Sleep(1000);
Complete = true;
t.Join();
}
}
public class FooThread
{
public void ThreadProc(Foo f)
{
bool toggle = false;
// while (!foo.Complete) toggle = !toggle;
while (!f.IsComplete()) toggle = !toggle;
Console.WriteLine("Thread done");
}
}
class Program
{
static void Main()
{
Foo foo = new Foo();
foo.StartThread();
Console.ReadLine();
}
}
Also, by introducing an interface on Foo such that the call to foo.IsComplete is now virtual (IFoo.IsComplete) the compiler removes the optimization.
Is this guaranteed though?