ManualResetEvent与Thread.Sleep的区别

14

我实现了以下后台处理线程,其中Jobs是一个 Queue<T>:

static void WorkThread()
{
    while (working)
    {
        var job;

        lock (Jobs)
        {
            if (Jobs.Count > 0)
                job = Jobs.Dequeue();
        }

        if (job == null)
        {
            Thread.Sleep(1);
        }
        else
        {
            // [snip]: Process job.
        }
    }
}

这导致作业进入队列时和实际开始执行之间出现了明显的延迟(批处理作业一次性进入,每个作业相对较小)。虽然这个延迟并不是什么大问题,但我开始思考这个问题,并进行了以下更改:

static ManualResetEvent _workerWait = new ManualResetEvent(false);
// ...
    if (job == null)
    {
        lock (_workerWait)
        {
            _workerWait.Reset();
        }
        _workerWait.WaitOne();
    }

现在,线程添加作业时会锁定_workerWait并在完成添加作业后调用_workerWait.Set()。这种解决方案似乎立即开始处理作业,延迟完全消失。

我的问题部分是“为什么会这样发生?”,考虑到Thread.Sleep(int)可能会休眠比您指定的时间更长,部分是“ManualResetEvent如何实现此级别的性能?”。

编辑:由于有人问及正在排队项目的函数,因此在此提供该函数以及当前系统的完整状态。

public void RunTriggers(string data)
{
    lock (this.SyncRoot)
    {
        this.Triggers.Sort((a, b) => { return a.Priority - b.Priority; });

        foreach (Trigger trigger in this.Triggers)
        {
            lock (Jobs)
            {
                Jobs.Enqueue(new TriggerData(this, trigger, data));
                _workerWait.Set();
            }
        }
    }
}

static private ManualResetEvent _workerWait = new ManualResetEvent(false);
static void WorkThread()
{
    while (working)
    {
        TriggerData job = null;

        lock (Jobs)
        {
            if (Jobs.Count > 0)
                job = Jobs.Dequeue();

            if (job == null)
            {
                _workerWait.Reset();
            }
        }

        if (job == null)
            _workerWait.WaitOne();
        else
        {
            try
            {
                foreach (Match m in job.Trigger.Regex.Matches(job.Data))
                    job.Trigger.Value.Action(job.World, m);
            }
            catch (Exception ex)
            {
                job.World.SendLineToClient("\r\n\x1B[32m -- {0} in trigger ({1}): {2}\x1B[m",
                    ex.GetType().ToString(), job.Trigger.Name, ex.Message);
            }
        }
    }
}
2个回答

20

事件是由操作系统/内核提供的内核原语,专门为此类事情设计。内核提供了一个边界,可以保证原子操作,这对于同步非常重要(一些原子性也可以通过硬件支持在用户空间中完成)。

简而言之,当线程等待事件时,它会被放置在该事件的等待列表中,并标记为不可运行状态。当事件被发出信号时,内核会唤醒等待列表中的线程,并将它们标记为可运行状态,然后它们就可以继续运行。如果线程在事件发出信号时可以立即唤醒,与长时间睡眠并每隔一段时间重新检查条件相比,这自然是巨大的优势。

即使一毫秒也是非常长的时间,在那段时间内您可以处理成千上万个事件。另外,时间分辨率传统上为10毫秒,因此睡眠少于10毫秒通常只会导致10毫秒的睡眠。使用事件,线程可以立即被唤醒和调度。


3
更新信息:10毫秒最小分辨率是XP及之前版本的操作系统使用的,因为它们使用静态增量的10毫秒进行调度。我认为Vista可做到,而且我知道Win7可以使用动态“无tick”时间片。在Win7中,我可以启动高分辨率计时器,发出sleep(1)命令,计时非常接近1毫秒,有时甚至更短。 - Bengie

10
首先,在_workerWait上加锁是没有意义的。事件是一种系统(内核)对象,旨在实现线程间的信号传递(并且在Win32 API中广泛用于异步操作)。因此,多个线程设置或重置它而无需额外的同步是相当安全的。
至于您的主要问题,需要查看将事物放入队列的逻辑以及有关每个作业执行多少工作的信息(工作线程在处理工作还是等待工作方面花费的时间更多)。
最好的解决方案可能是使用对象实例进行锁定,并使用Monitor.PulseMonitor.Wait作为条件变量。
编辑:通过查看排队代码,似乎答案#1116297是正确的:1毫秒的延迟时间太长了,因为许多工作项的处理非常快速。
唤醒工作线程的机制是正确的(因为没有.NET并发队列具有阻塞出队操作)。但是,与使用事件相比,使用条件变量会更有效率一些(因为在非争用情况下,它不需要内核转换)。
object sync = new Object();
var queue = new Queue<TriggerData>();

public void EnqueueTriggers(IEnumerable<TriggerData> triggers) {
  lock (sync) {
    foreach (var t in triggers) {
      queue.Enqueue(t);
    }
    Monitor.Pulse(sync);  // Use PulseAll if there are multiple worker threads
  }
}

void WorkerThread() {
  while (!exit) {
    TriggerData job = DequeueTrigger();
    // Do work
  }
}

private TriggerData DequeueTrigger() {
  lock (sync) {
    if (queue.Count > 0) {
      return queue.Dequeue();
    }
    while (queue.Count == 0) {
      Monitor.Wait(sync);
    }
    return queue.Dequeue();
  }
}

Monitor.Wait会释放参数上的锁,等待直到Pulse()PulseAll()针对该锁被调用,然后重新进入锁并返回。需要重新检查等待条件,因为其他线程可能已经读取了队列中的项目。


大多数工作只是匹配(预编译的)正则表达式并退出(因为匹配失败)。这取决于用户放入了多少以及应用程序接收到的数据量(它是一个网络应用程序)。最大负载时可能会达到每秒数百个,甚至可能达到一千个。我不确定是否有人对代码排队项目感兴趣,但既然你这么客气地询问了,我现在正在编辑它。 - Matthew Scharley
我记得在某个地方看到过,Monitor是lock() {}结构背后的支持者?那么你如何能够像那样在同步对象上同时使用lock()和Monitor呢? - Matthew Scharley
哦,等等,我刚刚读懂了最后一段。 - Matthew Scharley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接