如果我确保两个线程从来没有并行运行,我仍然必须使我的列表变量易失性
本文关键字:我的 易失性 变量 列表 并行 确保 两个 如果 从来没有 线程 运行 | 更新日期: 2023-09-27 17:49:14
想象我有这段代码,在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
实例变量,你可以看到我从MainThread1
和MainThread2
访问-但由于我上面的逻辑,我确保MainThread1
和MainThread2
永远不会并行运行,我还必须使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
。因为它是不需要的:需要的是障碍。
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;}
}
不再有"两个线程"。这是一个令人困惑的概念。
这里似乎有两个问题:
-
如果你正在使用两个线程,但他们从来没有异步运行,那么为什么有两个线程?只要适当地序列化你的方法,即坚持一个线程。
-
然而,如果两个线程是某种要求(例如允许一个线程继续处理/保持未阻塞,而另一个正在执行其他任务):即使你已经编码了这一点,以确保没有两个线程可以同时访问列表,为了安全起见,我会添加一个锁结构,因为列表不是线程安全的。对我来说,这是最直接的。
你可以使用线程安全的集合来代替,比如System.Collections.Concurrent中的一个集合。否则,你需要同步所有对List的访问(即:把每个Add调用都放在一个锁内),
我个人避免使用volatile。Albahari对此有一个很好的解释:"volatile关键字确保始终在字段中显示最新的值。这是不正确的,因为正如我们所看到的,写操作后跟读操作可以被重新排序。"
Volatile只是确保两个线程同时看到相同的数据。它根本不能阻止它们的读和写操作交错。
例如:声明一个同步对象,例如:
private static Object _objectLock = new Object();
并在您的methodWhichAddelementTomyList
方法(以及您的列表被修改的任何其他地方)中这样使用,以确保从不同的线程串行访问资源:
lock(_objectLock)
{
list.Add(object);
}