如果我确保两个线程从来没有并行运行,我仍然必须使我的列表变量易失性



想象我有这段代码,在Windows窗体定时器我可以生成一些线程-但我确保只有一个线程正在使用以下方法(正如Matt Johnson在这里给出的答案之一所示):

注:让我们假设现在这种_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吗?我会遇到问题吗?与缓存列表变量相关吗?

EDIT:这种方法也可以保护我不并行运行这些线程吗?(链接问题的答案有点不同-它在计时器内运行工作-所以我想仔细检查)。

EDIT2:老实说,下面没有共同的意见,我是否应该在我的list对象上应用volatile关键字。这种情况使我感到困惑。所以书面回答仍然是受欢迎的;否则这是不完全回答

我重申你的问题:

如果我确保两个线程永远不会并行运行我仍然必须使我的列表变量volatile吗?

你没有两个线程,你有三个:一个线程启动另外两个线程。该线程总是与其他线程并行运行,并使用共享标志与它们通信。考虑到这一点和你发布的代码,它是不是需要将列表标记为volatile


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

对于没有并发运行的两个线程,要查看处于一致状态的列表(换句话说,最新的),它们总是必须使用驻留在内存中的最新版本。这意味着当一个线程开始使用列表时,它必须在之前的写操作完成后从列表中读取。

这意味着内存屏障。线程在使用列表之前需要一个acquire barrier,在使用列表之后需要一个release barrier。使用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 Volatile字段
  • 对volatile字段的读取称为volatile read。volatile read具有获取语义";也就是说,它保证发生在指令序列中对内存的任何引用之前。

  • 对易失性字段的写操作称为易失性写操作。易失性写入具有"释放语义";也就是说,它保证发生在指令序列中写指令之前的任何内存引用之后。

(感谢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对列表本身没有任何影响。它会影响您在读取和赋值该变量时获得的保证。

你不能在某个地方应用volatile并期望它神奇地使非线程安全的数据结构成为线程安全的。如果是那么简单的话,线程就很容易了。只要标记所有不稳定的东西。不工作。

从给出的代码和描述来看,您只在一个线程上访问列表。这并不需要同步。注意,即使在第二个线程上读取列表也是不安全的。如果至少有一个写器,则不能有任何其他并发访问。

这里有一个更简单的方法:

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

不再有"两个线程"。这是一个令人困惑的概念。

这里似乎有两个问题:

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

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

你可以使用线程安全的集合来代替,比如System.Collections.Concurrent中的一个集合。否则,你需要同步所有对List的访问(即:把每个Add调用都放在一个锁内),

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

Volatile只是确保两个线程同时看到相同的数据。它根本不能阻止它们的读和写操作交错。

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

private static Object _objectLock = new Object();

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

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

最新更新