繁体   English   中英

如果我确保两个线程永远不会并行运行,是否还必须使列表变量可变?

[英]If I ensure two threads never run in parallel do I still have to make my list variable volatile?

想象一下,我有这样的代码,其中Windows窗体内部定时器我可以生成一些线程-但我保证只有一个线程使用下面的方法运行(从答案的一个指示在这里 -马特·约翰逊):

nb:现在让我们假设此_executing方法有效,并且我不使用backgroundworker等。

private volatile bool _executing;

private void TimerElapsed(object state)
{
    if (_executing)
        return;

    _executing = true;

    if(smth)
    {
    Thread myThread = new Thread(MainThread1);
    myThread.IsBackground = true;
    myThread.Start();

    }else
    {
    Thread myThread = new Thread(MainThread2);
    myThread.IsBackground = true;
    myThread.Start();
    }
}


 public void MainThread1()
 {
   try
   {
     methodWhichAddelementTomyList(); // e.g., inside list.add();
   }
   finally
   {_executing = false;}
 }
 public void MainThread2()
 {
   try
   {
     methodWhichAddelementTomyList(); // e.g., inside list.add();
   }
   finally
   {_executing = false;}
 }

现在,我还有List实例变量,您可以从MainThread1MainThread2访问该实例变量,但是由于上述逻辑确保了MainThread1MainThread2不会并行运行,因此我是否还必须使list volatile 我可以遇到与缓存列表变量有关的问题吗?

编辑 :并且这种方法也可以保护我免于并行运行那些线程吗? (链接的问题中的答案有些不同-它在计时器内部运行工作-所以我想再次检查)。

EDIT2 :老实说,下面是否应该在我的list对象上应用volatile关键字没有共识。 这种状况使我感到困惑。 因此,仍然欢迎记录在案的答案; 否则无法完全解决

我将重申您的问题:

如果我确保两个线程永远不会并行运行,是否还必须使列表变量volatile

您没有两个线程,只有三个线程:一个线程启动另外两个线程。 该线程始终与其他任何线程并行运行,并且使用共享标志与它们进行通信。 鉴于这一点,你发布的代码,它不需要标记列表作为volatile


但是在只有两个线程和两个线程的情况下,它将以某种方式一个接一个地执行,而不会受到第三个线程的干扰(即,从共享变量中读取),使列表volatile就足以保证两个线程始终可见相同的数据。

对于两个不能同时运行以查看列表处于一致状态(换句话说, 是最新状态 )的线程,它们始终必须使用内存中最新版本的内存。 这意味着当线程开始使用列表时,必须在先前的写操作结束后从列表中读取。

这意味着内存障碍。 线程在使用列表之前需要获取屏障,在使用列表之后需要释放屏障。 使用Thread.MemoryBarrier ,您不能很好地控制屏障的语义,您总是会遇到完整的屏障(释放获取,比我们需要的要坚固),但是最终结果是相同的。

因此,如果您可以保证线程永远不会并行运行,那么C#内存模型可以保证以下各项能够按预期工作:

private List<int> _list;

public void Process() {
    try {
        Thread.MemoryBarrier(); // Release + acquire. We only need the acquire.
        _list.Add(42);
    } finally {
        Thread.MemoryBarrier(); // Release + acquire. We only need the release.
    }
}

注意list是如何volatile 因为不需要,所以需要障碍。

现在的事情是, ECMA C#语言规范说(强调我的意思):

17.4.3挥发场

  • 易失性字段的读取称为易失性读取。 易失性读取具有“ 获取语​​义 ”; 就是说,它保证在指令序列中对内存的任何引用之前发生。

  • 易失性字段的写入称为易失性写入。 易失性写入具有“ 释放语义 ”; 即,保证在指令序列中的写入指令之前的任何存储器引用之后发生。

(感谢R. Martinho Fernandes在标准中找到了相关段落!)

换句话说,从volatile字段读取具有与获取障碍相同的语义,而对volatile字段进行写入具有与释放障碍相同的语义。 这意味着在给定前提的情况下,以下代码节的行为与上一个相同: 1

private volatile List<int> _list;

public void Process() {
    try {
         // This is an acquire, because we're *reading* from a volatile field.
        _list.Add(42);
    } finally {
        // This is a release, because we're *writing* to a volatile field.
        _list = _list;
    }
}

这就足以保证只要两个线程不并行运行,它们将始终看到列表处于一致状态。

(1) :这两个示例并非严格相同,第一个示例提供了更强的保证,但是在这种特定情况下不需要这些强大的保证。

使对象对列表的引用具有可变性不会对列表本身产生任何影响。 它会影响您在读取和分配该变量时所获得的保证。

您不能在某个地方应用volatile并期望它神奇地使非线程安全的数据结构成为线程安全的。 如果真是那样,那么穿线将很容易。 只需将所有内容标记为易失。 不起作用

从代码和描述中可以看到,假设您仅在一个线程上访问列表。 那不需要同步。 请注意,即使您在第二个线程上读取了不安全的列表。 如果至少有一个写程序,则不能有任何其他并发访问。 甚至不读。

这是一个更简单的方法:

Task.Run(() => Process(smth));

 ...

 public void Process(bool smth)
 {
   try
   {
     if (smth) methodWhichAddelementTomyList();
     else otherThing();
   }
   finally
   {_executing = false;}
 }

没有更多的“两个线程”。 这是一个令人困惑的概念。

这里似乎有2个问题:

  1. 如果使用两个线程,但它们从未异步运行,那么为什么要有两个线程? 只要适当地序列化您的方法,即坚持一个线程。

  2. 但是,如果有两个线程是某种要求(例如,允许一个线程继续处理/保持畅通而另一个线程正在执行其他任务):即使您已对此进行编码以确保没有两个线程可以同时访问列表,为了安全起见,我将添加一个锁定构造,因为列表不是线程安全的。 对我来说,这是最直接的。

您可以为此使用线程安全集合,例如System.Collections.Concurrent中的集合之一。 否则,您将需要同步对列表的所有访问(即:将每个Add调用置于锁中),

我个人避免使用volatile。 Albahari对此有一个很好的解释 :“ volatile关键字可确保字段中始终存在最新的值。这是不正确的,因为正如我们所看到的,可以先进行写操作再进行读操作重新排序。”

挥发只是确保两个线程同时看到相同的数据。 它根本不会阻止他们交错读取和写入操作。

例如:声明一个同步对象,例如:

private static Object _objectLock = new Object();

并在methodWhichAddelementTomyList方法(以及列表的其他任何地方被修改)中使用,以确保从不同线程对资源的串行访问:

lock(_objectLock)
{
    list.Add(object);
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM