我觉得我应该知道答案,但无论如何我都会问,以防万一我犯了一个潜在的灾难性错误。
以下代码按预期执行,没有错误/异常:
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");
}
当然,正如多线程代码的情况一样,成功的测试并不能证明这实际上是线程安全的。如果我放,测试也会成功 Close
之前 Set
即使文档明确指出在尝试之后做任何事情 Close
将导致未定义的行为。
我的问题是,当我调用时 ManualResetEvent.Set
方法,是否保证发出信号 所有 等待线程 之前 将控制权交还给来电者?换句话说,假设我能够保证不会再进一步打电话 WaitOne
,在这里关闭手柄是否安全,或者在某些情况下这个代码可能会阻止一些服务员发出信号或导致 ObjectDisposedException
?
文档只说明了这一点 Set
把它置于一个“信号状态” - 它似乎没有对服务员何时会提出任何要求 实际得到 那个信号,所以我想确定一下。
当你发信号时 ManualResetEvent.Set
保证所有等待该事件的线程(即处于阻塞状态) flag.WaitOne
)在将控制权返回给调用者之前将发出信号。
当然,在某些情况下你可能会设置标志并且你的线程没有看到它,因为它在检查标志之前做了一些工作(或者如果你创建了多个线程则建议使用nobugs):
ThreadPool.QueueUserWorkItem(s =>
{
QueryDB();
flag.WaitOne();
Console.WriteLine("Work Item 1 Executed");
});
标志上存在争用,现在您可以在关闭它时创建未定义的行为。您的标志是线程之间的共享资源,您应该创建一个倒计时锁存器,每个线程在完成后发出信号。这将消除你的争用 flag
。
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();
}
}
- 每个线程在倒计时锁存器上发出信号。
- 你的主线程在倒计时锁存器上等待。
- 倒计时锁存信号后主线程清零。
最终,你在最后睡觉的时间并不是一种安全的方式来处理你的问题,而是你应该设计你的程序,使其在多线程环境中100%安全。
更新:单一生产者/多个消费者
这里的假设是你的生产者知道将创造多少消费者, 创建所有使用者后,重置 CountdownLatch
与给定数量的消费者:
// 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
这不好。你在这里很幸运,因为你只开始了两个主题。当你在双核机器上调用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();
}
当旧计算机或当前计算机忙于执行其他任务时,原始代码将类似地失败。
我认为存在竞争条件。根据条件变量编写事件对象后,您将获得如下代码:
mutex.lock();
while (!signalled)
condition_variable.wait(mutex);
mutex.unlock();
因此,虽然可以发信号通知事件,但是等待事件的代码可能仍然需要访问事件的某些部分。
根据文件 关,这只会释放非托管资源。因此,如果活动仅使用托管资源,您可能会很幸运。但是这可能会在未来发生变化,所以我会在预防措施方面犯错误而不是关闭事件,直到你知道它不再被使用为止。
对我来说看起来像风险模式,即使由于[当前]实施,也没关系。您正在尝试处置可能仍在使用的资源。
这就像新建和构建一个对象,甚至在该对象的消费者完成之前就盲目地删除它。
即便如此,也有一个 问题 这里。即使在其他线程有机会运行之前,程序也可能会退出。线程池线程是后台线程。
鉴于你无论如何都要等待其他线程,你也可以事后清理一下。