.net 发出信号并立即关闭手动复位事件是否安全?
我觉得我应该知道这个问题的答案,但我还是要问一下,以防我犯了一个潜在的灾难性错误 以下代码按预期执行,没有错误/异常:.net 发出信号并立即关闭手动复位事件是否安全?,.net,multithreading,manualresetevent,.net,Multithreading,Manualresetevent,我觉得我应该知道这个问题的答案,但我还是要问一下,以防我犯了一个潜在的灾难性错误 以下代码按预期执行,没有错误/异常: static void Main(string[] args) { ManualResetEvent flag = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(s => { flag.WaitOne(); Console.WriteLine("W
static void Main(string[] args)
{
ManualResetEvent flag = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(s =>
{
flag.WaitOne();
Console.WriteLine("Work Item 1 Executed");
});
ThreadPool.QueueUserWorkItem(s =>
{
flag.WaitOne();
Console.WriteLine("Work Item 2 Executed");
});
Thread.Sleep(1000);
flag.Set();
flag.Close();
Console.WriteLine("Finished");
}
当然,与多线程代码通常的情况一样,成功的测试并不能证明这实际上是线程安全的。如果我将关闭
放在设置
之前,测试也会成功,即使文档中明确指出在关闭
之后尝试执行任何操作都会导致未定义的行为
我的问题是,当我调用ManualResetEvent.Set
方法时,是否保证在将控制返回给调用者之前向所有等待的线程发出信号?换句话说,假设我能够保证不再调用WaitOne
,那么在这里关闭句柄是否安全,或者在某些情况下,该代码是否可能阻止某些等待者收到信号或导致ObjectDisposedException
文档只说
Set
将其置于“信号状态”-它似乎没有声明服务员何时会真正收到信号,所以我想确定一下。这不好。您在这里很幸运,因为您只启动了两个线程。当您在双核机器上调用Set时,它们将立即开始运行。试试这个,然后看着它爆炸:
static void Main(string[] args) {
ManualResetEvent flag = new ManualResetEvent(false);
for (int ix = 0; ix < 10; ++ix) {
ThreadPool.QueueUserWorkItem(s => {
flag.WaitOne();
Console.WriteLine("Work Item Executed");
});
}
Thread.Sleep(1000);
flag.Set();
flag.Close();
Console.WriteLine("Finished");
Console.ReadLine();
}
static void Main(字符串[]args){
ManualResetEvent标志=新的ManualResetEvent(错误);
对于(int-ix=0;ix<10;++ix){
ThreadPool.QueueUserWorkItem(s=>{
flag.WaitOne();
Console.WriteLine(“已执行的工作项”);
});
}
睡眠(1000);
flag.Set();
flag.Close();
控制台。写入线(“完成”);
Console.ReadLine();
}
当旧机器或当前机器忙于其他任务时,您的原始代码也会同样失败。我认为存在竞争条件。基于条件变量编写事件对象后,您会得到如下代码:
mutex.lock();
while (!signalled)
condition_variable.wait(mutex);
mutex.unlock();
因此,虽然事件可能会发出信号,但等待事件的代码可能仍然需要访问部分事件
根据上的文档,这仅释放非托管资源。因此,如果事件仅使用托管资源,您可能会很幸运。但这可能会在将来发生变化,因此我会在预防方面出错,在您知道事件不再使用之前不会关闭该事件。当您使用
ManualResetEvent.Set
发出信号时,可以保证等待该事件的所有线程(即flag.WaitOne上处于阻塞状态)将在将控制权返回给调用方之前发出信号
当然,在某些情况下,您可能会设置标志,而您的线程却看不到它,因为它在检查标志之前做了一些工作(如果您正在创建多个线程,则按照nobugs的建议):
标志上存在争用,现在可以在关闭标志时创建未定义的行为。您的标志是线程之间的共享资源,您应该创建一个倒计时锁存器,每个线程在完成时都会在该锁存器上发出信号。这将消除标志上的争用
public class CountdownLatch
{
private int m_remain;
private EventWaitHandle m_event;
public CountdownLatch(int count)
{
Reset(count);
}
public void Reset(int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException();
m_remain = count;
m_event = new ManualResetEvent(false);
if (m_remain == 0)
{
m_event.Set();
}
}
public void Signal()
{
// The last thread to signal also sets the event.
if (Interlocked.Decrement(ref m_remain) == 0)
m_event.Set();
}
public void Wait()
{
m_event.WaitOne();
}
}
对我来说,这似乎是一种危险的模式,即使由于[当前]的实现,它也没有问题。您正在尝试处置可能仍在使用中的资源
这就像更新和构建一个对象,然后在该对象的消费者完成之前盲目地删除它
即使在其他方面,这里也有一个问题。程序可能会退出,甚至在其他线程有机会运行之前。线程池线程是后台线程
考虑到您必须等待其他线程,您最好在之后进行清理。一个有趣的实验是将睡眠移动到WaitOne()之前。这将允许您测试WaitOne()在“flag”已关闭时所做的操作()'d.@Phillip Ngan:正如文档所示,它实际上是未定义的。如果将代码中的关闭
移动到线程.Sleep
之前,则在WaitOne
期间会出现ObjectDisposedException
(“安全句柄已关闭”)。另一方面,如果在线程.Sleep
之后直接移动Close
,则两个线程都会收到信号,执行成功完成。因此,未定义的行为,在“坏”代码中存在竞争条件。我只是想确保在我认为“好”的代码中没有类似的未定义行为这是完全正确的,但这是因为测试代码,而不是因为Set
的行为。在本例中,事件在某些线程开始运行之前关闭。将睡眠超时增加到5秒可以在四核上正常运行。我可以通过序列化对事件的访问并在关闭后立即将其置零来防止上述情况;我主要关注已经在等待句柄上等待的线程。不能保证线程在任意长时间睡眠后会真正开始运行。长时间睡眠只会降低ObjectDisposedException的风险,但不能消除它。这种时间依赖性是任何人对线程进行假设的最终棺材。当然,我不会在生产代码中使用Thread.Sleep
——实际实现使用了各种同步原语。但事实证明确实存在一个微妙的种族条件,现在已经得到了解决。谢谢你的意见+1.作为第一个提出这个建议的人,这在概念上似乎是一个更好的选择;我很难理解它是如何适用于一个生产商和多个消费者的情况的(这似乎是正确的)
public class CountdownLatch
{
private int m_remain;
private EventWaitHandle m_event;
public CountdownLatch(int count)
{
Reset(count);
}
public void Reset(int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException();
m_remain = count;
m_event = new ManualResetEvent(false);
if (m_remain == 0)
{
m_event.Set();
}
}
public void Signal()
{
// The last thread to signal also sets the event.
if (Interlocked.Decrement(ref m_remain) == 0)
m_event.Set();
}
public void Wait()
{
m_event.WaitOne();
}
}
// In the Producer
ManualResetEvent flag = new ManualResetEvent(false);
CountdownLatch countdown = new CountdownLatch(0);
int numConsumers = 0;
while(hasMoreWork)
{
Consumer consumer = new Consumer(coutndown, flag);
// Create a new thread with each consumer
numConsumers++;
}
countdown.Reset(numConsumers);
flag.Set();
countdown.Wait();// your producer waits for all of the consumers to finish
flag.Close();// cleanup