【问题标题】:Threading & implicit memory barriers线程和隐式内存屏障
【发布时间】:2018-04-18 18:23:19
【问题描述】:

在线程方面尝试了解 .net 的内存模型。这个问题是严格的理论问题,我知道它可以通过其他方式解决,例如使用lock 或将_task 标记为volatile

以下面这段代码为例:

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*/ });
        }
    }
}

现在做以下假设:

  1. Run 可以被多次调用(从不同的线程)并且在Dispose 被调用之后永远不会被调用。
  2. Dispose 只会被调用一次。

现在我的问题是,_task 的值(在Dispose 方法中)是否始终是“新”值,这意味着它将从“主存储器”中读取,而不是从寄存器中读取?从我一直在阅读的内容来看,Interlocked 创建了一个完整的内存屏障,所以我假设 _task 将从主内存中读取,还是我完全关闭了?

【问题讨论】:

  • 几乎没有理由假设任何代码在启动后都会修改 _task 变量。只能读取的变量不能有错误的值。使用 lock 语句的最大优点是它更容易推理。您将有一些机会避免您在代码中放置的不可调试的线程竞争错误。您无法保证该任务实际上会继续执行添加的任务。
  • 阅读您的答案我假设您并没有真正阅读我的问题。首先,如果再次调用 Run,_task 变量会发生变化(看看 Run 方法中的延续)。我很清楚使用锁会更容易调试、读取等,而且我不会在真实场景中使用上述代码。然而,这只是我试图深入研究的一个例子,即整个内存屏障模型。
  • 我不会称之为“.NET 的内存模型”。障碍归结为汇编指令 - 跨核心缓存的缓存同步并不完全是 .NET 问题。
  • @TomTom C# 语言对给定变量的行为方式做出了某些保证,您可以将其称为语言内存模型。只要根据语言规范的规则,代码应该可以工作,那么您就不需要关心 C# 语言如何强制执行其内存模型的约束的细节。也就是说,除非您发现 C# 语言中的错误,但通常不会经常遇到这种错误。
  • 来自 Joe Albahari 的“C# 中的线程”:“Interlocked 的所有方法都会生成一个完整的栅栏。因此,您通过 Interlocked 访问的字段不需要额外的栅栏——除非它们在其他在没有互锁或锁定的情况下放置在您的程序中。” albahari.com/threading/part4.aspx#_Nonblocking_Synchronization

标签: c# multithreading task-parallel-library memory-barriers


【解决方案1】:

除了过于松散地使用“新读”这个短语的复杂性之外,_task 将从主内存中重新获取。但是,您的代码可能存在单独的甚至更微妙的问题。为您的代码考虑一个替代但完全等效的结构,这样可以更容易地发现潜在问题。

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 对_working 执行了其他操作,导致它在寄存器中缓存它。
  • 线程 B 完成任务并从 ContinueWith 委托调用 Exchange
  • 线程 A 调用 Dispose

在上述场景中,_working 将变为 1,然后变为 0,然后 Dispose 将其翻转回 1(因为该值已缓存在寄存器中),甚至无需进入 if 语句。此时_working 可能处于不一致状态。

就我个人而言,我认为这种情况不太可能发生,主要是因为我认为_working 不会以这种方式被缓存,尤其是如果您始终确保通过互锁操作保护对它的访问。

如果没有别的,我希望它能让您思考一下无锁技术可以变得多么复杂。

【讨论】:

  • 我明白你的观点并完全同意你的看法。除非我绝对有,否则我不会在现实世界的场景中使用这样的代码,因为它使理解/阅读变得有点困难。我想要的是更好地理解我认为我拥有的案例。感谢您的意见。
  • @zaf:我和你在一起。我也很少使用无锁。但是,无论如何,了解一切在幕后如何运作有很多好处。我自己还有很多东西要学。
【解决方案2】:

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

我还找到了这个答案,清楚地解释了事实确实如此,因此您阅读的文档似乎是正确的:Does Interlocked.CompareExchange use a memory barrier?

【讨论】:

    猜你喜欢
    • 2012-05-27
    • 2014-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-05
    • 1970-01-01
    • 2012-09-08
    相关资源
    最近更新 更多