在ManualResetEvent.WaitOne()上捕获ObjectDisposedException是否安全
本文关键字:ObjectDisposedException 是否 安全 ManualResetEvent WaitOne | 更新日期: 2023-09-27 18:24:59
这与发出信号并立即关闭手动重置事件是否安全密切相关?并且可能为该问题提供一种解决方案。
假设我有一堆线程可能想做同样的工作,但应该只允许一个线程做,其他线程应该等到工作者完成并使用其结果。
所以基本上我希望工作只做一次。
更新: 让我补充一点,这不是一个初始化问题,可以使用.net 4的Lazy<T> 。我所说的一次是指每个任务一次,这些任务是在运行时确定的。从下面的简化示例中可能看不清楚这一点
将Hans Passant对上述问题的回答中的简单例子稍作修改,我想下面的例子是安全的。(它与刚才描述的用例略有不同,但就线程及其关系而言,它是等效的)
static void Main(string[] args)
{
ManualResetEvent flag = new ManualResetEvent(false);
object workResult = null;
for (int ix = 0; ix < 10; ++ix)
{
ThreadPool.QueueUserWorkItem(s =>
{
try
{
flag.WaitOne();
Console.WriteLine("Work Item Executed: {0}", workResult);
}
catch (ObjectDisposedException)
{
Console.WriteLine("Finished before WaitOne: {0}", workResult);
}
});
}
Thread.Sleep(1000);
workResult = "asdf";
flag.Set();
flag.Close();
Console.WriteLine("Finished");
}
我想我问题的核心是:
就内存障碍而言,由于ObjectDisposedException而中止的对WaitOne的调用是否相当于对WaitOne的成功调用
这应该确保其他线程安全访问变量workResult。
我的猜测是:它必须是安全的,否则WaitOne怎么能安全地发现ManualResetEvent对象一开始就被关闭了?
以下是我看到的:
- 您得到
ObjectDisposedException
是因为您的代码显示以下竞赛条件:- 在所有线程都成功调用flag.waitOne之前,可以调用flag.close
如何处理这一问题取决于flag.waitOne.之后代码的执行有多重要
这里有一种方法:
如果所有已经启动的线程都应该执行,那么在调用flag.close之前,您可以进行一些额外的同步。您可以通过在Task.Factory
上使用StartNew
而不是Thread.QueueUserWorkItem
来实现这一点。任务可以等待完成,然后您将调用flag.close,从而消除竞争条件和处理ObjectDisposedException
的需要
然后你的代码会变成:
static void Main(string[] args)
{
ManualResetEvent flag = new ManualResetEvent(false);
object workResult = null;
Task[] myTasks = new Task[10];
for (int ix = 0; ix < myTasks.Length; ++ix)
{
myTasks[ix] = Task.Factory.StartNew(() =>
{
flag.WaitOne();
Console.WriteLine("Work Item Executed: {0}", workResult);
});
}
Thread.Sleep(1000);
workResult = "asdf";
flag.Set();
Task.WaitAll(); // Eliminates race condition
flag.Close();
Console.WriteLine("Finished");
}
正如您在上面看到的,任务允许额外的同步,这将消除您看到的竞争条件。
另请注意,ManualResetEvent.waitOne执行内存屏障,因此工作结果变量将是最新更新的变量,而不需要任何进一步的内存屏障或易失性读取。
因此,为了回答您的问题,如果您确实必须避免额外的同步,并通过使用您的方法来处理ObjectDisposed异常,我认为已处理的对象没有为您执行内存屏障,那么您必须在捕获块中调用Thread.MemoryBarrier
,以确保已读取最新的值。
但是异常是昂贵的,如果可以在正常程序执行中避免它们,我相信这样做是谨慎的。
祝你好运!
几点:
-
如果这是.NET 4,那么Lazy是更好的方法。
-
从内存屏障的角度来看,它是否等效是无关紧要的——异常永远不应该是正常代码路径的一部分。我认为行为是未定义的,因为这不是预期的用例。
考虑到我必须解决的实际问题,这比简化的例子要复杂一些,我决定使用Monitor.Wait和Monitor.PulseAll.
Joe Albahari的C#线程被证明非常有用,在这种特殊情况下,以下部分适用:http://www.albahari.com/threading/part4.aspx#_Signaling_with_Wait_and_Pulse