C#中的`is`运算符是线程安全的/原子的吗?

mik*_*ike 5 c# multithreading atomicity

以下代码是线程安全的吗?

public object DemoObject {get;set;}

public void DemoMethod()
{
    if (DemoObject is IDemoInterface demo)
    {
        demo.DoSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

如果其他线程在处理时修改DemoObject(例如设置为空)DemoMethod,是否保证在if块内局部变量demo总是被正确分配(给类型的实例IDemoInterface)?

tcw*_*cks 0

这里的 is 构造是原子的,就像互锁一样。然而,这段代码的行为几乎 100% 是不确定的。除非目标是创建不可预测和不确定的行为,否则这将是一个错误。

\n

此代码的有效使用示例:在模拟某些不确定性事件的可能性的游戏中,例如“来自矩阵的尼奥在半空中捕获子弹”,此方法可能比简单地使用伪随机数生成器更具不确定性。

\n

在任何需要确定性/可预测行为的场景中,此代码都是一个错误。

\n

解释:

\n

if(DemoObject 是 IDemoInterface 演示)

\n

以伪原子方式评估和分配。

\n

此后,在 if 语句中:\n即使 DemoObject 被另一个线程设置为 null,demo 的值也已被分配,并且 DoSomething() 操作将在已分配的实例上执行。

\n

回答您的评论问题:

\n
\n

为什么会有一场比赛?

\n
\n

竞争条件是此代码中设计的。在下面的示例代码中:

\n
    \n
  1. 16个线程正在竞争将DemoObject的值设置为null
  2. \n
  3. 而另外 16 个线程则竞争将 DemoObject 的值设置为 DemoClass 的实例。
  4. \n
  5. 同时,当 DemoObject 不为 null 时,只要赢得竞争条件,16 个线程就会竞争执行 DoSomething()。
  6. \n
\n

请参阅:什么是竞争条件?

\n
\n

为什么我无法预测 DoSomething() 是否会执行?

\n
\n

DoSomething() 每次都会执行

\n
if (DemoObject is IDemoInterface demo)\n
Run Code Online (Sandbox Code Playgroud)\n

评估为真。每次 DemoObject 为 null 或 NOT IDemoInterface 时,它​​都不会执行。\n您无法预测它何时执行。您只能预测,每当执行 DoSomething() 的线程设法获取对 DemoObject 的非 null 实例的引用时,它将执行。或者换句话说,当运行 DemoMethod() 的线程设法赢得竞争条件时:

\n

A) 运行 DemoMethod_Assign() 的线程赢得竞争条件后

\n

B) 在运行 DemoMethod_Null() 的线程赢得竞争条件之前

\n

警告:根据我的理解(请其他人澄清这一点),DemoObject 可能在不同线程中同时为 null 和非 null。

\n

DemoObject可以从高速缓存读取或者可以从主存储器读取。我们不能让它变得易失,因为它是一个对象引用。因此,DemoObject 的状态对于一个线程可能同时为 Null,而对于另一线程则不为 null。这意味着它的值是不确定的。在Schr\xc3\xb6dinger\的猫中,猫同时是死的和活的。我们这里的情况也很相似。

\n

这段代码中没有关于 DemoObject 的锁或内存屏障。然而,线程上下文切换会强制执行相当于内存屏障的操作。因此,上下文切换后恢复的任何线程都将拥有从主内存检索到的 DemoObject 值的准确副本。然而,不同的线程可能已经改变了 DemoObject 的值,但这个改变的值可能尚未刷新到主内存。这就带来了一个问题:哪个是实际的准确值?从主存中获取的值或尚未刷新到主存的值。

\n

注意:其他人请澄清这个警告,因为我可能错过了一些东西。

\n

下面是一些代码,用于验证除警告之外的所有内容。在具有 64 个逻辑核心的计算机上运行此控制台应用程序测试。永远不会抛出空引用异常。

\n
internal class Program\n{\n    private static ManualResetEvent BenchWaitHandle = new ManualResetEvent(false);\n\n    private class DemoClass : IDemoInterface\n    {\n        public void DoSomething()\n        {\n            Interlocked.Increment(ref Program.DidSomethingCount);\n        }\n    }\n    private interface IDemoInterface\n    {\n        void DoSomething();\n    }\n    private static object DemoObject { get; set; }\n    public static volatile int DidSomethingCount = 0;\n\n    private static void DemoMethod()\n    {\n        BenchWaitHandle.WaitOne();\n        for (int i = 0; i < 100000000; i++)\n        {\n            try\n            {\n                if (DemoObject is IDemoInterface demo)\n                {\n                    demo.DoSomething();\n                }\n            }\n            catch (Exception ex)\n            {\n                Console.WriteLine(ex.ToString());\n            }\n        }\n    }\n    private static bool m_IsRunning = false;\n    private static object RunningLock = new object();\n    private static bool IsRunning \n    { \n        get { lock (RunningLock) { return m_IsRunning; } } \n        set { lock(RunningLock) { m_IsRunning = value; } }\n    }\n    private static void DemoMethod_Assign()\n    {\n        BenchWaitHandle.WaitOne();\n        while (IsRunning)\n        {\n            DemoObject = new DemoClass();\n        }\n    }\n    private static void DemoMethod_Null()\n    {\n        BenchWaitHandle.WaitOne();\n        while (IsRunning)\n        {\n            DemoObject = null;\n        }\n    }\n    static void Main(string[] args)\n    {\n        List<Thread> threadsListDoWork = new List<Thread>();\n        List<Thread> threadsList = new List<Thread>();\n        BenchWaitHandle.Reset();\n        for (int I =0; I < 16; I++)\n        {\n            threadsListDoWork.Add(new Thread(new ThreadStart(DemoMethod)));\n            threadsList.Add(new Thread(new ThreadStart(DemoMethod_Assign)));\n            threadsList.Add(new Thread(new ThreadStart(DemoMethod_Null)));\n        }\n        foreach (Thread t in threadsListDoWork)\n        {\n            t.Start();\n        }\n        foreach (Thread t in threadsList)\n        {\n            t.Start();\n        }\n        IsRunning = true;\n        BenchWaitHandle.Set();\n        foreach (Thread t in threadsListDoWork)\n        {\n            t.Join();\n        }\n        IsRunning = false;\n        foreach (Thread t in threadsList)\n        {\n            t.Join();\n        }\n\n        Console.WriteLine(@"Did Something {0} times", DidSomethingCount);\n        Console.ReadLine();\n}\n//On the last run this printed\n//Did Something 112780926 times\n//Which means that DemoMethod() threads won the race condition slightly over 7% of the time.\n
Run Code Online (Sandbox Code Playgroud)\n