【问题标题】:Producer/Consumer Thread Pool w/ Main Thread Support - Infrequent Deadlock?带主线程支持的生产者/消费者线程池 - 不常见的死锁?
【发布时间】:2016-09-14 21:45:26
【问题描述】:

我有一个 C# 线程池类,它在很大程度上基于来自 https://stackoverflow.com/a/1656662/782181 的生产者/消费者代码。注意:我正在这样做而不是使用 BlockingCollection,因为我坚持使用 .NET2.0!

我在类中添加了一个可以从主线程调用的函数,以允许主线程做一些工作。我的想法是,在某些时候,主线程会等待工作完成,但我也可以让主线程做一些工作以加快速度,而不是等待。

这里有一个精简版的类来演示:

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() 来获取一个任务并执行它。

我遇到了一个不常见的问题,主线程会尝试在 PerformTask() 中“锁定”,但锁定已经被占用。主线程等待,但由于某种原因,锁永远不可用。

代码中的任何内容都没有作为导致死锁的问题而跳出来 - 我希望其他人能够发现问题。我已经看了几个小时了,我不确定为什么主线程会卡在 PerformTask() 中的“lock()”调用中。似乎没有其他线程会无限期地持有锁?让主线程以这种方式与池交互是不是一个坏主意?

【问题讨论】:

  • 也许,你想试试BlockingCollection而不是队列+锁
  • @L.B,我实际上认为那会很棒,但我在 Unity 工作,暂时停留在 .NET2.0!我将更新问题以表明这一点。

标签: c# multithreading threadpool deadlock


【解决方案1】:

嗯,所以,虽然我仍然想知道为什么上面的代码在某些情况下可能会死锁,但我想我已经找到了一种解决方法。

如果主线程要在这里工作,我想确保主线程不会长时间阻塞。毕竟,一个通用的开发规则:不要阻塞主线程!

所以,我正在尝试的解决方案是直接使用 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() 时,编译器无论如何都会生成类似的代码,所以我认为这个解决方案不会有任何性能问题。

【讨论】:

  • 您的方法是正确的,但我只想指出您的出队代码 - 我建议您断言两个队列中的计数相等。不平等似乎不真实,但我认为应该这样做。只是为了检查你的不变量。
猜你喜欢
  • 1970-01-01
  • 2018-05-22
  • 1970-01-01
  • 2018-03-08
  • 1970-01-01
  • 2013-03-06
  • 2018-09-24
  • 2017-02-01
  • 2012-04-30
相关资源
最近更新 更多