Producer/Consumer 具有主线程支持的线程池 - 不常见的死锁?
Producer/Consumer Thread Pool w/ Main Thread Support - Infrequent Deadlock?
我有一个 C# 线程池 class,它主要基于 中的 producer/consumer 代码。注意:我这样做而不是使用 BlockingCollection,因为我坚持使用 .NET2.0!
我在class中添加了一个可以从主线程调用的函数,让主线程做一些工作。我的想法是,在某些时候,主线程等待工作完成,但我也可以让主线程做一些工作来加快速度,而不是等待。
这里有一个 class 的精简版来演示:
public static class SGThreadPool
{
// Shared object to lock access to the queue between threads.
private static object locker = new object();
// The various threads that are doing our work.
private static List<Thread> workers = null;
// A queue of tasks to be completed by the workers.
private static Queue<object> taskQueue = new Queue<object>();
private static Queue<WaitCallback> taskCallbacks = new Queue<WaitCallback>();
//OMMITTED: Init function (starts threads)
// Enqueues a task for a thread to do.
public static void EnqueueTask(WaitCallback callback, object context)
{
lock(locker)
{
taskQueue.Enqueue(context);
taskCallbacks.Enqueue(callback);
Monitor.PulseAll(locker); //Q: should I just use 'Pulse' here?
}
}
// Can be called from main thread to have it "help out" with tasks.
public static bool PerformTask()
{
WaitCallback taskCallback = null;
object task = null;
lock(locker)
{
if(taskQueue.Count > 0)
{
task = taskQueue.Dequeue();
}
if(taskCallbacks.Count > 0)
{
taskCallback = taskCallbacks.Dequeue();
}
}
// No task means no work, return false.
if(task == null || taskCallback == null) { return false; }
// Do the work!
taskCallback(task);
return true;
}
private static void Consume()
{
while(true)
{
WaitCallback taskCallback = null;
object task = null;
lock(locker)
{
// While no tasks in the queue, wait.
while(taskQueue.Count == 0)
{
Monitor.Wait(locker);
}
// Get a task.
task = taskQueue.Dequeue();
taskCallback = taskCallbacks.Dequeue();
}
// Null task signals an exit.
if(task == null || taskCallback == null) { return; }
// Call consume callback with task as context.
taskCallback(task);
}
}
}
基本上,我可以将一些要由后台线程执行的任务加入队列。但是主线程也可以通过调用PerformTask()来获取一个任务并执行它。
我 运行 遇到了一个不常见的问题,主线程将尝试 "lock" 在 PerformTask() 中,但锁已被占用。主线程等待,但出于某种原因锁永远不会可用。
代码中的任何内容都没有跳出来作为导致死锁的问题 - 我希望其他人能够发现问题。我已经看了几个小时了,我不确定为什么主线程会卡在 PerformTask() 中的 "lock()" 调用处。似乎没有其他线程会无限期地持有锁?让主线程以这种方式与池交互是不是一个坏主意?
嗯,所以,虽然我仍然想知道为什么 上面的代码在某些情况下可能会死锁,但我想我已经找到了解决方法。
如果主线程要在这里工作,我想确保主线程不会被阻塞很长时间。毕竟,一般的开发规则:不要阻塞主线程!
所以,我正在尝试的解决方案是直接使用 Monitor.TryEnter,而不是对主线程使用 lock()。这允许我指定主线程愿意等待锁的超时时间。
public static bool PerformTask()
{
WaitCallback taskCallback = null;
object task = null;
// Use TryEnter, rather than "lock" because
// it allows us to specify a timeout as a failsafe.
if(Monitor.TryEnter(locker, 500))
{
try
{
// Pull a task from the queue.
if(taskQueue.Count > 0)
{
task = taskQueue.Dequeue();
}
if(taskCallbacks.Count > 0)
{
taskCallback = taskCallbacks.Dequeue();
}
}
finally
{
Monitor.Exit(locker);
}
}
// No task means no work, return false.
if(task == null || taskCallback == null) { return false; }
// Do the work!
taskCallback(task);
return true;
}
在这段代码中,线程将等待获取锁长达 500 毫秒。如果它出于某种原因不能执行任何任务,但至少不会卡住。不把主线程放在可以无限期等待的位置似乎是个好主意。
我相信当你使用 lock() 时,编译器无论如何都会生成类似的代码,所以我认为这个解决方案不会有任何性能问题。
我有一个 C# 线程池 class,它主要基于 中的 producer/consumer 代码。注意:我这样做而不是使用 BlockingCollection,因为我坚持使用 .NET2.0!
我在class中添加了一个可以从主线程调用的函数,让主线程做一些工作。我的想法是,在某些时候,主线程等待工作完成,但我也可以让主线程做一些工作来加快速度,而不是等待。
这里有一个 class 的精简版来演示:
public static class SGThreadPool
{
// Shared object to lock access to the queue between threads.
private static object locker = new object();
// The various threads that are doing our work.
private static List<Thread> workers = null;
// A queue of tasks to be completed by the workers.
private static Queue<object> taskQueue = new Queue<object>();
private static Queue<WaitCallback> taskCallbacks = new Queue<WaitCallback>();
//OMMITTED: Init function (starts threads)
// Enqueues a task for a thread to do.
public static void EnqueueTask(WaitCallback callback, object context)
{
lock(locker)
{
taskQueue.Enqueue(context);
taskCallbacks.Enqueue(callback);
Monitor.PulseAll(locker); //Q: should I just use 'Pulse' here?
}
}
// Can be called from main thread to have it "help out" with tasks.
public static bool PerformTask()
{
WaitCallback taskCallback = null;
object task = null;
lock(locker)
{
if(taskQueue.Count > 0)
{
task = taskQueue.Dequeue();
}
if(taskCallbacks.Count > 0)
{
taskCallback = taskCallbacks.Dequeue();
}
}
// No task means no work, return false.
if(task == null || taskCallback == null) { return false; }
// Do the work!
taskCallback(task);
return true;
}
private static void Consume()
{
while(true)
{
WaitCallback taskCallback = null;
object task = null;
lock(locker)
{
// While no tasks in the queue, wait.
while(taskQueue.Count == 0)
{
Monitor.Wait(locker);
}
// Get a task.
task = taskQueue.Dequeue();
taskCallback = taskCallbacks.Dequeue();
}
// Null task signals an exit.
if(task == null || taskCallback == null) { return; }
// Call consume callback with task as context.
taskCallback(task);
}
}
}
基本上,我可以将一些要由后台线程执行的任务加入队列。但是主线程也可以通过调用PerformTask()来获取一个任务并执行它。
我 运行 遇到了一个不常见的问题,主线程将尝试 "lock" 在 PerformTask() 中,但锁已被占用。主线程等待,但出于某种原因锁永远不会可用。
代码中的任何内容都没有跳出来作为导致死锁的问题 - 我希望其他人能够发现问题。我已经看了几个小时了,我不确定为什么主线程会卡在 PerformTask() 中的 "lock()" 调用处。似乎没有其他线程会无限期地持有锁?让主线程以这种方式与池交互是不是一个坏主意?
嗯,所以,虽然我仍然想知道为什么 上面的代码在某些情况下可能会死锁,但我想我已经找到了解决方法。
如果主线程要在这里工作,我想确保主线程不会被阻塞很长时间。毕竟,一般的开发规则:不要阻塞主线程!
所以,我正在尝试的解决方案是直接使用 Monitor.TryEnter,而不是对主线程使用 lock()。这允许我指定主线程愿意等待锁的超时时间。
public static bool PerformTask()
{
WaitCallback taskCallback = null;
object task = null;
// Use TryEnter, rather than "lock" because
// it allows us to specify a timeout as a failsafe.
if(Monitor.TryEnter(locker, 500))
{
try
{
// Pull a task from the queue.
if(taskQueue.Count > 0)
{
task = taskQueue.Dequeue();
}
if(taskCallbacks.Count > 0)
{
taskCallback = taskCallbacks.Dequeue();
}
}
finally
{
Monitor.Exit(locker);
}
}
// No task means no work, return false.
if(task == null || taskCallback == null) { return false; }
// Do the work!
taskCallback(task);
return true;
}
在这段代码中,线程将等待获取锁长达 500 毫秒。如果它出于某种原因不能执行任何任务,但至少不会卡住。不把主线程放在可以无限期等待的位置似乎是个好主意。
我相信当你使用 lock() 时,编译器无论如何都会生成类似的代码,所以我认为这个解决方案不会有任何性能问题。