发信号并立即关闭ManualResetEvent是否安全?

Aar*_*ght 11 .net multithreading manualresetevent

我觉得我应该知道答案,但无论如何我都会问,以防万一我犯了一个潜在的灾难性错误.

以下代码按预期执行,没有错误/异常:

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");
}
Run Code Online (Sandbox Code Playgroud)

当然,正如多线程代码的情况一样,成功的测试并不能证明这实际上是线程安全的.如果我Close之前提出的话,测试也会成功Set,即使文档明确指出在a之后尝试做任何事情Close都会导致未定义的行为.

我的问题是,当我调用该ManualResetEvent.Set方法时,是否保证将控制权返回给调用者之前发出所有等待线程的信号?换句话说,假设我能够保证不会再进一步​​调用,在这里关闭句柄是否安全,或者在某些情况下这个代码可能会阻止一些服务员发出信号或导致一个?WaitOneObjectDisposedException

该文档仅表示Set将其置于"信号状态" - 它似乎没有任何关于服务员何时会获得该信号的声明,所以我想确定.

Kir*_*ril 6

当您发出信号时,ManualResetEvent.Set您可以保证flag.WaitOne在将控制权返回给调用者之前,将发出等待该事件(即处于阻塞状态)的所有线程的信号.

当然,在某些情况下你可能会设置标志并且你的线程没有看到它,因为它在检查标志之前做了一些工作(或者如果你创建了多个线程则建议使用nobugs):

ThreadPool.QueueUserWorkItem(s =>
{
    QueryDB();
    flag.WaitOne();
    Console.WriteLine("Work Item 1 Executed");
});
Run Code Online (Sandbox Code Playgroud)

标志上存在争用,现在您可以在关闭它时创建未定义的行为.您的标志是线程之间的共享资源,您应该创建一个倒计时锁存器,每个线程在完成后发出信号.这将消除你的争用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();
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 每个线程在倒计时锁存器上发出信号.
  2. 你的主线程在倒计时锁存器上等待.
  3. 倒计时锁存信号后主线程清零.

最终,你最后睡觉的时间并不是解决问题的安全方法,而是应该设计你的程序,使其在多线程环境中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
Run Code Online (Sandbox Code Playgroud)

  • "你可以保证等待该事件的所有线程都会在将控制返回给调用者之前发出信号" - >我找不到任何参考资料......另外,我的测试告诉我相反的情况.使用WaitOne()和Console.WriteLine("foo")迭代启动4个线程.然后设置()MRE并立即重置()它.您会注意到并非所有等待线程都被释放(有时甚至不会释放一个).或者我错过了什么? (2认同)

Han*_*ant 5

这不好.你在这里很幸运,因为你只开始了两个主题.当你在双核机器上调用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();
    }
Run Code Online (Sandbox Code Playgroud)

当旧计算机或当前计算机忙于执行其他任务时,原始代码将类似地失败.

  • 这是完全正确的,但是因为测试代码而轰炸,而不是因为"Set"的行为.在这种情况下,事件在一些线程甚至*开始*运行之前被关闭.将睡眠超时增加到5秒使得在四核上运行正常.我可以通过序列化对事件的访问并在关闭后立即将其置零来阻止上述场景; 我主要关注已经在等待句柄的线程. (4认同)