我正在玩一个创建一个线程的简单控制台应用程序,我在主线程和工作线程之间进行一些线程间的通信。
我将主线程中的对象发布到并发队列,而工作线程将该队列出列并进行一些处理。
令我感到奇怪的是,当我描述这个应用程序时,即使我有两个核心。 一个核心100%免费,另一个核心完成所有工作,我看到两个线程都在该核心中运行。
为什么会这样?
是因为我在发布消息时使用等待句柄,在处理完成后释放吗?
这是我的示例代码,现在使用2个工作线程。 它仍然表现相同,main,worker1和worker2在同一个核心中运行。 想法?
[编辑] 它现在有点起作用,至少,与昨天相比,我获得了两倍的性能。 诀窍是减慢消费者的速度,以避免使用AutoResetEvent发出信号。
public class SingleThreadDispatcher
{
public long Count;
private readonly ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>();
private volatile bool _hasMoreTasks;
private volatile bool _running = true;
private int _status;
private readonly AutoResetEvent _signal = new AutoResetEvent(false);
public SingleThreadDispatcher()
{
var thread = new Thread(Run)
{
IsBackground = true,
Name = "worker" + Guid.NewGuid(),
};
thread.Start();
}
private void Run()
{
while (_running)
{
_signal.WaitOne();
do
{
_hasMoreTasks = false;
Action task;
while (_queue.TryDequeue(out task) && _running)
{
Count ++;
task();
}
//wait a short while to let _hasMoreTasks to maybe be set to true
//this avoids the roundtrip to the AutoResetEvent
//that is, if there is intense pressure on the pool, we let some new
//tasks have the chance to arrive and be processed w/o signaling
if(!_hasMoreTasks)
Thread.Sleep(5);
Interlocked.Exchange(ref _status, 0);
} while (_hasMoreTasks);
}
}
public void Schedule(Action task)
{
_hasMoreTasks = true;
_queue.Enqueue(task);
SetSignal();
}
private void SetSignal()
{
if (Interlocked.Exchange(ref _status, 1) == 0)
{
_signal.Set();
}
}
}
答案 0 :(得分:7)
是因为我在发布消息时使用等待句柄,在处理完成后释放吗?
如果没有看到你的代码,很难肯定地说,但是根据你的描述,你写的两个线程似乎充当了协同例程:当主线程运行时,工作线程没有任何关系,并且反之亦然。看起来.NET调度程序非常智能,在发生这种情况时不会加载第二个核心。
您可以通过多种方式更改此行为 - 例如
答案 1 :(得分:2)
好的,我已经弄明白问题是什么了。 在这种情况下,生产者和消费者几乎一样快。 这导致消费者快速完成所有工作,然后循环返回以等待AutoResetEvent。 生成器下次发送任务时,必须触摸AutoresetEvent并进行设置。
解决方案是在消费者中添加非常小的延迟,使其比生产者稍慢。 这导致生产者发送任务时,它注意到消费者已经处于活动状态,并且只需要发送到工作队列而不接触AutoResetEvent。
原始行为导致了一种乒乓效应,可以在屏幕截图中看到。
答案 2 :(得分:1)
Dasblinkelight(可能)有正确答案。
除此之外,当你的一个线程被I / O绑定时(也就是说,它没有卡在CPU上),它也是正确的行为 - 在这种情况下,你已经使用多个内核没有任何好处,.NET足够聪明,可以在一个内核上更改上下文。
这通常是UI线程的情况 - 它几乎没有什么工作要做,所以通常没有太多理由让它为自己占用整个核心。是的,如果您的并发队列没有正确使用,它可能只是意味着主线程等待工作线程 - 再次,在这种情况下,没有必要切换核心,因为原始线程无论如何都在等待。
答案 3 :(得分:1)
您应该使用BlockingCollection而不是ConcurrentQueue
。默认情况下,BlockingCollection
使用了ConcurrentQueue
,但它有一个更容易使用的界面。特别是,它会进行非繁忙的等待。此外,BlockingCollection
支持cancellation,因此您的消费者变得非常简单。这是一个例子:
public class SingleThreadDispatcher
{
public long Count;
private readonly BlockingCollection<Action> _queue = new BlockingCollection<Action>();
private readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
public SingleThreadDispatcher()
{
var thread = new Thread(Run)
{
IsBackground = true,
Name = "worker" + Guid.NewGuid(),
};
thread.Start();
}
private void Run()
{
foreach (var task in _queue.GetConsumingEnumerable(_cancellation.Token))
{
Count++;
task();
}
}
public void Schedule(Action task)
{
_queue.Add(task);
}
}
GetConsumingEnumerable
的循环将在队列上执行非繁忙等待。没有必要单独举办活动。它将等待一个项目添加到队列中,或者如果您设置了取消令牌,它将退出。
要正常停止,只需拨打_queue.CompleteAdding()
即可。这告诉消费者不再有任何项目添加到队列中。消费者将清空队列然后退出。
如果您想提前退出,请拨打_cancellation.Cancel()
。这将导致GetConsumingEnumerable
退出。
一般情况下,您不应该直接使用ConcurrentQueue
。 BlockingCollection
更易于使用,并提供相同的性能。