我有一个简单的测试用例,我在这里借用了另一个问题但是用略有不同但简单的人为例子进行了修改。给出:
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();
}
鉴于ThreadProc正在调用IsComplete(),编译器似乎不会缓存Complete变量。但是,我无法保证编译器不会为从不同线程传递的对象上的方法调用生成缓存优化。
但我担心这种情况: 如果ThreadProc在与主线程不同的处理器上运行 它可以将对象foo的整个代码深入复制到其线程缓存中吗? 意思是我将更新一个完全不同的对象实例。 如果是这样会使参考volatile变得必要吗?
我不明白这里发生了什么。但它似乎证明了我的担忧,它永远不会退出(但在调试模式下退出):
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();
}
}
然而,下面的内容完成了!几乎相同的东西写得不同。 我无法看到匿名lambda如何改变事物。它仍然应该指向同一个对象实例:
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();
}
}
委托方案......:
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();
}
}
这些都是人为的例子。但我不确定为什么1个场景不起作用。 我只看到2个线程在这里更新一个布尔值。如此不稳定不应该是必要的。 代码应该合理地锁定,因为Foo中的一个或两个脏读是正常的。 FooThread不会很快发出信号。 (我知道TaskCancellationSource,这个问题不是关于取消,而是通过对象实例的方法从不同的线程更新布尔标志)
编辑: 请在发布模式下测试。
编辑:
失败的测试用例即代码块2的更新。
似乎编译器正在对!foo.IsComplete()
方法调用进行优化。它似乎假设实例变量没有用于其他地方,因此优化了调用 - 可能是它的简单性?
通过引用foo
的实例变量,编译器适用于不做出这样的假设,使得现在修改的第一个代码块失败:
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();
}
}
此外,通过在Foo上引入一个接口,使得对foo.IsComplete的调用现在是虚拟的(IFoo.IsComplete),编译器将删除优化。
这有保证吗?
答案 0 :(得分:0)
在我看来,你的大部分问题都得到了充分的回答:
When should the volatile keyword be used in C#?,
和What is the purpose of 'volatile' keyword in C#,
甚至Why this program does not go into infinite loop in absence of volatility of a boolean condition variable?
为了解决您已表达的一些更具体的问题......
传递给Thread的对象引用是否应标记为volatile?
在代码示例中标记引用并不是问题所在。它是有问题的Complete
字段,需要volatile
。对象引用永远不会更改,因此将其设置为volatile
并不重要。
你的问题中有很多不同的代码,很多行为都取决于编译器的确切版本,运行时,操作系统和CPU类型。它太宽泛了。
但是,您似乎要问的基本问题可以简单地回答:在.NET环境中,您必需提供线程之间的某种同步方式。所有涉及的组件都可以自由地进行任何他们想要的优化,只要它们符合您提供的同步语义。
如果您不提供,则代码可能会或可能不会按预期工作。当你不提供同步时,运行时不是必需不工作(是的,所有这些都是负面的),所以即使你没有&#在某些情况下它确实有效。 39;提供同步不会以任何方式使你无法提供同步。
代表受影响吗?
不清楚你的意思。您在上面描述的一个示例是"委托方案" 并不涉及对数据的任何并发访问。与其他示例不同,Complete
字段只能由您启动的额外线程访问,因此无需解决同步问题。
我只有2个线程在这里更新布尔值。如此不稳定应该是必要的
除了语法上令人困惑的陈述外,volatile
关键字不是程度问题。它只适用于原始类型或引用(因此值为bool
没有理由省略它),并且您需要的只是两个线程,因为需要同步。为什么你认为让"只有2个线程在这里更新一个布尔值" 会导致得出volatile
是不必要的结论?
要明确:这不是一个有效的结论。