我实现了以下后台处理线程,其中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);
}
}
}
}
答案 0 :(得分:16)
事件是由OS / Kernel提供的内核原语,它是专门为这类事物设计的。内核提供了一个边界,您可以在该边界上保证对同步很重要的原子操作(某些原子性也可以在用户空间中通过硬件支持完成)。
简而言之,当一个线程等待某个事件时,它会被置于该事件的等待列表中并被标记为不可运行。 当事件发出信号时,内核唤醒等待列表中的那些并将它们标记为可运行并且它们可以继续运行。当事件发出信号时,线程可以立即醒来,与长时间睡眠并且不时地重新检查状态,这自然是一个巨大的好处。
即使一毫秒真的很长,你也可以在那段时间处理过数千个事件。此外,时间分辨率通常为10毫秒,因此睡眠时间不到10毫秒通常只会导致10毫秒的睡眠。通过事件,可以立即唤醒并安排线程
答案 1 :(得分:10)
首先锁定_workerWait
是没有意义的,Event是一个系统(内核)对象,设计用于线程之间的信令(并且在Win32 API中大量用于异步操作)。因此,多线程在没有额外同步的情况下设置或重置它是非常安全的。
关于你的主要问题,需要看到将事情放在队列中的逻辑,以及关于每项工作完成了多少工作的一些信息(工作者线程花费更多时间处理工作或等待工作)。
可能最好的解决方案是使用对象实例锁定并使用Monitor.Pulse
和Monitor.Wait
作为条件变量。
编辑:有了要排队的代码的视图,似乎答案#1116297使其正确:1ms的延迟太长而无法等待,因为许多工作项将非常快速地处理。
使用唤醒工作线程的机制的方法是正确的(因为没有带有阻塞出列操作的.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将释放对参数的锁定,等待锁定调用{{1}}或Pulse()
,然后重新进入锁定并返回。需要重新检查等待条件,因为某些其他线程可能已从队列中读取该项目。