C# ManualResetEvent 与 Thread.Sleep

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1116249/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 08:34:55  来源:igfitidea点击:

ManualResetEvent vs. Thread.Sleep

c#multithreadingsleepmanualresetevent

提问by Matthew Scharley

I implemented the following background processing thread, where Jobsis a Queue<T>:

我实现了以下后台处理线程,其中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.
        }
    }
}

This produced a noticable delay between when the jobs were being entered and when they were actually starting to be run (batches of jobs are entered at once, and each job is only [relatively] small.) The delay wasn't a huge deal, but I got to thinking about the problem, and made the following change:

这在输入作业和实际开始运行之间产生了明显的延迟(一次输入一批作业,每个作业只是 [相对] 小。)延迟并不是什么大问题,但我不得不考虑这个问题,并进行了以下更改:

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

Where the thread adding jobs now locks _workerWaitand calls _workerWait.Set()when it's done adding jobs. This solution (seemingly) instantly starts processing jobs, and the delay is gone altogether.

添加作业的线程现在锁定_workerWait_workerWait.Set()在完成添加作业后调用。这个解决方案(表面上)立即开始处理作业,延迟完全消失。

My question is partly "Why does this happen?", granted that Thread.Sleep(int)can very well sleep for longer than you specify, and partly "How does the ManualResetEventachieve this level of performance?".

我的问题部分是“为什么会发生这种情况?”,当然Thread.Sleep(int)可以比您指定的睡眠时间更长,部分是“如何ManualResetEvent实现这种性能水平?”。

EDIT:Since someone asked about the function that's queueing items, here it is, along with the full system as it stands at the moment.

编辑:由于有人询问了排队项目的功能,这里是,以及目前的完整系统。

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

采纳答案by nos

The events are kernel primitives provided by the OS/Kernel that's designed just for this sort of things. The kernel provides a boundary upon which you can guarantee atomic operations which is important for synchronization(Some atomicity can be done in user space too with hardware support).

事件是由 OS/Kernel 提供的内核原语,专为此类事情而设计。内核提供了一个边界,您可以在该边界上保证对同步很重要的原子操作(某些原子性也可以在硬件支持下在用户空间完成)。

In short, when a thread waits on an event it's put on a waiting list for that event and marked as non-runnable. When the event is signaled, the kernel wakes up the ones in the waiting list and marks them as runnable and they can continue to run. It's naturally a huge benefit that a thread can wake up immediately when the event is signalled, vs sleeping for a long time and recheck the condition every now and then.

简而言之,当一个线程等待一个事件时,它会被放入该事件的等待列表中并标记为不可运行。当事件发出信号时,内核唤醒等待列表中的那些,并将它们标记为可运行,它们可以继续运行。与长时间休眠并时不时地重新检查条件相比,线程可以在发出事件信号时立即唤醒,这自然是一个巨大的好处。

Even one millisecond is a really really long time, you could have processed thousands of event in that time. Also the time resolution is traditionally 10ms, so sleeping less than 10ms usually just results in a 10ms sleep anyway. With an event, a thread can be woken up and scheduled immediately

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

回答by Richard

First locking on _workerWaitis pointless, an Event is a system (kernel) object designed for signaling between threads (and heavily used in the Win32 API for asynchronous operations). Therefore it is quite safe for multiple threads to set or reset it without additional synchronization.

首先锁定_workerWait是没有意义的,事件是一个系统(内核)对象,设计用于线程之间的信号传输(并且在 Win32 API 中大量用于异步操作)。因此,多个线程在没有额外同步的情况下设置或重置它是非常安全的。

As to your main question, need to see the logic for placing things on the queue as well, and some information on how much work is done for each job (is the worker thread spending more time processing work or on waiting for work).

至于您的主要问题,还需要查看将事物放入队列的逻辑,以及有关每个作业完成多少工作的一些信息(工作线程是花费更多时间处理工作还是等待工作)。

Likely the best solution would be to use an object instance to lock on and use Monitor.Pulseand Monitor.Waitas a condition variable.

可能最好的解决方案是使用对象实例来锁定和使用Monitor.PulseMonitor.Wait作为条件变量。

Edit: With a view of the code to enqueue, it appears that answer #1116297has it right: a 1ms delay is too long to wait, given that many of the work items will be extremely quick to process.

编辑:从要排队的代码来看,答案#1116297似乎是正确的:1 毫秒的延迟太长而无法等待,因为许多工作项目的处理速度非常快。

The approach of having a mechanism to wake up the worker thread is correct (as there is no .NET concurrent queue with a blocking dequeue operation). However rather than using an event, a condition variable is going to be a little more efficient (as in non-contended cases it does not require a kernel transition):

具有唤醒工作线程的机制的方法是正确的(因为没有具有阻塞出队操作的 .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 will release the lock on the parameter, wait until Pulse()or PulseAll()is called against the lock, then re-enter the lock and return. Need to recheck the wait condition because some other thread could have read the item off the queue.

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