11

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

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

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");
}

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

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

该文档仅说Set将其置于“信号状态”-它似乎没有声明服务员何时真正收到该信号,所以我想确定一下。

4

4 回答 4

6

当您使用 a 发出信号时,ManualResetEvent.Set您可以保证所有等待该事件的线程(即处于阻塞状态 on 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();
    }
}
  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
于 2010-02-25T19:53:04.220 回答
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();
    }

当旧机器或当前机器忙于其他任务时,您的原始代码将同样失败。

于 2010-02-25T19:38:33.510 回答
0

我的看法是存在竞争条件。根据条件变量编写事件对象后,您将获得如下代码:

mutex.lock();
while (!signalled)
    condition_variable.wait(mutex);
mutex.unlock();

因此,虽然可能会发出事件信号,但等待事件的代码可能仍需要访问部分事件。

根据Close上的文档,这只会释放非托管资源。因此,如果活动仅使用托管资源,您可能会很幸运。但这在未来可能会改变,所以我会在预防方面犯错,在你知道它不再被使用之前不会关闭活动。

于 2010-02-25T19:40:25.793 回答
0

对我来说,这似乎是一种冒险模式,即使由于 [当前] 实施,它也可以。您正在尝试处置可能仍在使用的资源。

这就像新建和构造一个对象,甚至在该对象的消费者完成之前就盲目地删除它。

否则这里也有问题。程序可能会退出,甚至在其他线程有机会运行之前。线程池线程是后台线程。

鉴于无论如何您都必须等待其他线程,您不妨事后进行清理。

于 2010-02-26T08:36:31.163 回答