【问题标题】:How to synchronize TPL Tasks, by using Monitor / Mutex / Semaphore? Or should one use something else entirely?如何使用 Monitor / Mutex / Semaphore 同步 TPL 任务?还是应该完全使用其他东西?
【发布时间】:2016-05-10 15:16:52
【问题描述】:

我正在尝试将我的一些旧项目从 ThreadPool 和独立的 Thread 移动到 TPL Task,因为它支持一些非常方便的功能,例如 Task.ContinueWith 的延续(以及从 C# 5 和 @ 987654327@)、更好的取消、异常捕获等等。我很想在我的项目中使用它们。但是我已经看到了潜在的问题,主要是同步问题。

我使用经典的独立Thread 编写了一些显示生产者/消费者问题的代码:

class ThreadSynchronizationTest
{
    private int CurrentNumber { get; set; }
    private object Synchro { get; set; }
    private Queue<int> WaitingNumbers { get; set; }

    public void TestSynchronization()
    {
        Synchro = new object();
        WaitingNumbers = new Queue<int>();

        var producerThread = new Thread(RunProducer);
        var consumerThread = new Thread(RunConsumer);

        producerThread.Start();
        consumerThread.Start();

        producerThread.Join();
        consumerThread.Join();
    }

    private int ProduceNumber()
    {
        CurrentNumber++;
        // Long running method. Sleeping as an example
        Thread.Sleep(100);
        return CurrentNumber;
    }

    private void ConsumeNumber(int number)
    {
        Console.WriteLine(number);
        // Long running method. Sleeping as an example
        Thread.Sleep(100);
    }

    private void RunProducer()
    {
        while (true)
        {
            int producedNumber = ProduceNumber();

            lock (Synchro)
            {
                WaitingNumbers.Enqueue(producedNumber);
                // Notify consumer about a new number
                Monitor.Pulse(Synchro);
            }
        }
    }

    private void RunConsumer()
    {
        while (true)
        {
            int numberToConsume;
            lock (Synchro)
            {
                // Ensure we met out wait condition
                while (WaitingNumbers.Count == 0)
                {
                    // Wait for pulse
                    Monitor.Wait(Synchro);
                }
                numberToConsume = WaitingNumbers.Dequeue();
            }
            ConsumeNumber(numberToConsume);
        }
    }
}

在此示例中,ProduceNumber 生成一个递增整数序列,而ConsumeNumber 将它们写入Console。如果生产运行得更快,数字将排队等待稍后使用。如果消费运行得更快,消费者将等到有一个数字可用。所有同步均使用Monitorlock(内部也使用Monitor)完成。

在尝试“TPL 化”类似代码时,我已经看到了一些我不知道该如何解决的问题。如果我将new Thread().Start() 替换为Task.Run()

  1. TPL Task 是一种抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,则无限循环将导致消费者甚至永远无法启动。根据 MSDN,在运行任务时提供 TaskCreationOptions.LongRunning 参数应该提示 TaskScheduler 以适当地运行该方法,但是我没有找到任何方法来确保它确实如此。据说 TPL 足够聪明,可以按照程序员的意图运行任务,但这对我来说似乎有点神奇。而且我不喜欢编程中的魔法。
  2. 如果我理解它是如何正常工作的,则不能保证 TPL Task 在启动时在同一线程上恢复。如果是这样,在这种情况下,它会尝试释放它不拥有的锁,而另一个线程永远持有锁,从而导致死锁。我记得不久前 Eric Lippert 写道,这就是为什么 await 不允许出现在 lock 块中的原因。回到我的例子,我什至不知道如何解决这个问题。

这些是我脑海中闪过的少数几个问题,尽管可能还有(可能是)更多。我应该如何解决它们?

另外,这让我想到,使用通过MonitorMutexSemaphore 进行同步的经典方法甚至是执行 TPL 代码的正确方法吗?也许我错过了我应该使用的东西?

【问题讨论】:

  • 我相信你的假设是正确的。因为您的代码是 producer-consumer,您可能需要查看 TPL Dataflow。顺便说一句,使用明确的Thread 而不是使用Task 在某些事情上仍然占有一席之地。例如,长时间运行的非 IO 绑定作业。
  • 长期运行的非 IO 绑定作业正是我的项目所做的。大多。但是我认为,由于 TPL 在内部使用ThreadPool,因此您可以使用ThreadPool 以及“糖”来做所有您可以做的事情。因此,我决定投资转换。这个问题被称为生产者-消费者,而不是提供者-消费者...... doh!现在我觉得很傻。
  • lol :) 通常,TPL 任务用​​于短期工作块,而Thread 更多用于长期运行的非 I/O 工作。此外,对于消费者而言,另一项非常棒的技术是微软的反应式扩展 (RX)。它可能看起来有点“what-tha”?起初,但它可能很优雅。 This site 有一些很棒的教程。全部检查并选择您认为适合您的技术。

标签: c# .net multithreading synchronization task-parallel-library


【解决方案1】:

您的问题突破了 Stack Overflow 的广泛性限制。从普通的Thread 实现迁移到基于Task 和其他TPL 特性的实现涉及到各种各样的考虑。单独来看,几乎可以肯定每个问题都在之前的 Stack Overflow Q&A 中得到了解决,而总的来说,有太多的考虑因素无法在单个 Stack Overflow Q&A 中充分而全面地解决。

所以,话虽如此,让我们看看你在这里问过的具体问题。

  1. TPL 任务是一种抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,则无限循环将导致消费者甚至永远无法启动。根据 MSDN,在运行任务时提供 TaskCreationOptions.LongRunning 参数应该提示 TaskScheduler 正确运行该方法,但是我没有找到任何方法来确保它确实如此。据说 TPL 足够聪明,可以按照程序员的意图运行任务,但这对我来说似乎有点神奇。而且我不喜欢编程中的魔法。

Task 对象本身确实不保证异步行为。例如,返回 Task 对象的 async 方法可能根本不包含异步操作,并且可以在返回已经完成的 Task 对象之前运行较长时间。

另一方面,Task.Run() 保证异步操作。是documented as such:

将指定的工作排入队列以在 ThreadPool 上运行并返回该工作的任务或 Task 句柄

虽然Task 对象本身抽象了“未来”或“承诺”的概念(使用编程中的同义词),但具体实现与线程池密切相关。如果使用得当,可以保证异步操作。

  1. 如果我理解它是如何正常工作的,则不能保证 TPL 任务在启动时在同一个线程上恢复。如果是这样,在这种情况下,它会尝试释放它不拥有的锁,而另一个线程永远持有该锁,从而导致死锁。我记得不久前 Eric Lippert 写道,这就是为什么在锁定块中不允许等待的原因。回到我的例子,我什至不知道如何解决这个问题。

只有一些同步对象是线程特定的。例如,Monitor 是。但Semaphore 不是。这对您是否有用取决于您要实现的内容。例如,您可以使用使用BlockingCollection&lt;T&gt; 的长时间运行的线程来实现生产者/消费者模式,而根本不需要调用任何显式同步对象。如果您确实想使用 TPL 技术,可以使用 SemaphoreSlim 及其 WaitAsync() 方法。

当然,您也可以使用 Dataflow API。对于某些情况,这将是可取的。对于非常简单的生产者/消费者来说,这可能是矫枉过正。 :)

另外,这让我想到,使用通过 Monitor、Mutex 或 Semaphore 进行同步的经典方法甚至是执行 TPL 代码的正确方法吗?也许我错过了我应该使用的东西?

恕我直言,这是问题的症结所在。从基于Thread 的编程迁移到TPL 不仅仅是从一个构造到另一个构造的直接映射问题。在某些情况下,这样做效率低下,而在其他情况下,它根本行不通。

确实,我想说 TPL 尤其是 async/await 的一个关键特性是线程同步的必要性要小得多。总体思路是异步执行操作,线程之间的交互最少。线程之间的数据仅在定义明确的点(即从已完成的Task 对象中检索)流动,减少甚至消除了显式同步的需要。

不可能提出具体的技术,因为如何最好地实施某事将取决于确切的目标是什么。但是简短的版本是要理解,在使用 TPL 时,通常根本不需要使用同步原语,例如您习惯于使用较低级别的 API 的同步原语。您应该努力积累足够的 TPL 习语经验,以便您能够识别哪些适用于哪些编程问题,以便您直接应用它们,而不是试图在脑海中映射您的旧知识。

在某种程度上,这(我认为)类似于学习一种新的人类语言。一开始,一个人会花很多时间在心理上翻译字面意思,可能会重新映射以适应语法、习语等。但理想情况下,一个人会内化该语言并能够直接用该语言表达自己。就个人而言,我在人类语言方面从未达到过这一点,但我在理论上理解这个概念:)。我可以直接告诉你,它在编程语言环境中运行良好。


顺便说一句,如果您有兴趣了解如何将 TPL 想法发挥到极致,您可能想通读有关该主题的 Joe Duffy's recent blog articles。事实上,.NET 和相关语言的最新版本大量借鉴了他所描述的 Midori 项目中开发的概念。

【讨论】:

  • 你说得对,关于其他陷阱的内容会使我的问题过于宽泛。我删除了它,留下了细节。我喜欢学习新人类语言的类比:) 很好的答案,谢谢。
【解决方案2】:

.Net 中的任务是混合的。 TPL 在 .Net 4.0 中引入了任务,但 async-await 仅在 .Net 4.5 中提供。

原始任务与 async-await 附带的真正异步任务之间存在差异。第一个只是对在某个线程上运行的“工作单元”的抽象,但异步任务不需要线程,或者根本不需要在任何地方运行。

常规任务(或Delegate Tasks)在某个TaskScheduler(通常由使用ThreadPoolTask.Run)上排队,并在任务的整个生命周期内由同一线程执行。 在这里使用传统的lock 完全没有问题。

异步任务(或Promise Tasks)通常没有要执行的代码,它们只是表示将在未来完成的异步操作。以Task.Delay(10000) 为例。任务已创建,并在 10 秒后完成,但在此期间没有任何运行。 在这里,您仍然可以在适当的时候使用传统的lock(但不能在临界区中使用await),但您也可以使用SemaphoreSlim.WaitAsync(或其他异步同步结构)进行异步锁定

使用通过Monitor、Mutex 或 Semaphore 进行同步的经典方法甚至是执行 TPL 代码的正确方法吗?

这可能是,这取决于代码实际执行的操作以及它是使用 TPL(即任务)还是异步等待。但是,您现在可以使用许多其他工具,例如异步同步构造 (AsyncLock) 和异步数据结构 (TPL Dataflow)

【讨论】:

  • 感谢您的回答,我阅读了 Stephen Cleary 的博客,非常有用。我还阅读了有关构建 AsyncLock 的文章,并且在这样做的同时,发现了一个似乎完全可以做到这一点的库,甚至更多。 github.com/StephenCleary/AsyncEx 它具有 Monitor、Mutex 等的异步实现。如果你推荐使用 AsyncLock,你会发现这个库很有用。
  • @GediminasMasaitis 我很熟悉斯蒂芬和他的作品。您可能还想查看Microsoft.VisualStudio.Threading
猜你喜欢
  • 2011-04-03
  • 2011-06-11
  • 2017-05-22
  • 1970-01-01
  • 1970-01-01
  • 2014-02-10
  • 1970-01-01
  • 2016-08-03
  • 2011-11-04
相关资源
最近更新 更多