C# 穿线及;内隐记忆障碍

C# 穿线及;内隐记忆障碍,c#,multithreading,task-parallel-library,memory-barriers,C#,Multithreading,Task Parallel Library,Memory Barriers,试图理解.net在线程方面的内存模型。这个问题是严格理论上的,我知道它可以通过其他方式解决,例如使用锁或将\u任务标记为易失性 以下面的代码为例: class Test { Task _task; int _working = 0; public void Run() { if (Interlocked.CompareExchange(ref _working, 1, 0) == 0) { _task =

试图理解.net在线程方面的内存模型。这个问题是严格理论上的,我知道它可以通过其他方式解决,例如使用
或将
\u任务
标记为
易失性

以下面的代码为例:

class Test
{
    Task _task;
    int _working = 0;

    public void Run()
    {
        if (Interlocked.CompareExchange(ref _working, 1, 0) == 0)
        {
            _task = Task.Factory.StartNew(() =>
            {
                //do some work...
            });
            _task.ContinueWith(antecendent => Interlocked.Exchange(ref _working, 0));
        }
    }

    public void Dispose()
    {
        if (Interlocked.CompareExchange(ref _working, _working, 0) == 1)
        {
            _task.ContinueWith(antecendent => { /*do some other work*/ });
        }
    }
}
现在做出以下假设:

  • Run
    可以被多次调用(从不同的线程调用),并且在调用了
    Dispose
    之后将永远不会被调用
  • Dispose
    将只调用一次
  • 现在我的问题是,
    \u task
    (在
    Dispose
    方法中)的值是否总是一个“新”值,这意味着它将从“主存”读取而不是从寄存器读取?从我一直在读的
    联锁
    创建了一个完整的内存屏障,所以我假设
    \u任务
    将从主内存读取,或者我完全关闭了吗?

    我不使用C编写代码,但是如果使用了完整的内存屏障,那么您的解释是正确的。编译器不应重复使用存储在寄存器中的值,而应以确保内存排序障碍不会掩盖内存子系统中存在的实际值的方式获取该值


    我还发现这个答案清楚地解释了事实上的情况,因此您阅读的文档似乎是正确的:

    除了使用短语“fresh read”过于松散的复杂性之外,yes,
    \u任务将从主内存中重新获取。然而,您的代码可能存在一些单独的甚至更微妙的问题。考虑一个替代的,但完全等价的结构,你的代码应该更容易发现潜在的问题。

    public void Dispose()
    {
        int register = _working;
        if (Interlocked.CompareExchange(ref _working, register, 0) == 1)
        {
            _task.ContinueWith(antecendent => { /*do some other work*/ });
        }
    }
    
    CompareExchange
    的第二个参数是按值传递的,因此它可以缓存在寄存器中。我正在设想以下情景

    • 线程A调用
      Run
    • 线程A通过
      \u工作
      执行其他操作,使其将其缓存在寄存器中
    • 线程B完成任务并从
      ContinueWith
      委托调用
      Exchange
    • 线程A调用
      Dispose
    在上面的场景中,
    \u working
    将更改为1,然后是0,然后是
    Dispose
    将其翻转回1(因为该值缓存在寄存器中),甚至不进入
    if
    语句。此时,
    \u工作
    可能处于不一致状态

    就个人而言,我认为这种情况不太可能发生,主要是因为我不认为
    \u working
    会以这种方式缓存,特别是如果您总是确保使用联锁操作来保护对它的访问


    如果没有其他东西的话,我希望它能让您思考一下无锁技术有多复杂。

    没有理由假设任何代码在启动后都在修改_任务变量。只能读取的变量不能有错误的值。使用lock语句的最大优点是更容易推理。您可能会有一些机会避免您在代码中放入的不可调试的线程竞赛bug。你不能保证这项任务实际上会继续执行添加的任务。阅读你的答案我想你没有真正阅读我的问题。首先,如果再次调用Run,则_task变量可能会更改(请查看Run方法中的continuation)。我很清楚,使用锁会更容易调试、读取等,我不会在实际场景中使用上述代码。然而,这只是一个例子,我正试图深入研究的是整个记忆障碍模型。我不会称之为“.NET的记忆模型”。汇编指令的障碍——跨核心缓存的缓存同步性并不完全是.NET的问题。@C#语言TomTom对给定变量的行为做出了一定的保证,您可以称之为语言内存模型。只要根据语言规范的规则,代码应该可以工作,那么您就不需要真正关心C语言如何强制执行其内存模型约束的细节。也就是说,除非你在C语言中发现一个bug,但这通常不是你经常会遇到的问题“Interlocked的所有方法都会生成一个完整的围栏。因此,通过Interlocked访问的字段不需要额外的围栏,除非在程序中的其他位置访问这些字段时没有Interlocked或锁。“我明白你的意思,完全同意你。我不会在真实场景中使用这样的代码,除非我确实使用过,因为这样会使理解/阅读变得有点困难。我想要的是更好地理解这个案例,我想我已经做到了。谢谢你的意见。@zaf:我同意。我也不怎么使用免锁。但是,不管怎样,理解幕后的一切是如何运作的,还是有很多好处的。我还有很多东西要学。