如何通过使用Monitor/Mutex/Semaphore同步TPL任务?还是应该完全使用其他东西? [英] How to synchronize TPL Tasks, by using Monitor / Mutex / Semaphore? Or should one use something else entirely?

查看:74
本文介绍了如何通过使用Monitor/Mutex/Semaphore同步TPL任务?还是应该完全使用其他东西?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm trying to move some of my old projects from ThreadPool and standalone Thread to TPL Task, because it supports some very handy features, like continuations with Task.ContinueWith (and from C# 5 with async\await), better cancellation, exception capturing, and so on. I'd love to use them in my project. However I already see potential problems, mostly with synchronization.

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

I've written some code which shows a Producer / Consumer problem, using a classic stand-alone 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)完成的.

In this example, ProduceNumber generates a sequence of increasing integers, while ConsumeNumber writes them to the Console. If producing runs faster, numbers will be queued for consumption later. If consumption runs faster, the consumer will wait until a number is available. All synchronization is done using Monitor and lock (internally also Monitor).

在尝试"TPL验证"类似代码时,我已经看到了一些不确定的问题.如果我将new Thread().Start()替换为Task.Run():

When trying to 'TPL-ify' similar code, I already see a few issues I'm not sure how to go about. If I replace new Thread().Start() with Task.Run():

  1. TPL Task是一种抽象,甚至不能保证代码将在单独的线程上运行.在我的示例中,如果生产者控制方法同步运行,则无限循环将导致使用者永远无法启动.根据MSDN,在运行任务时提供TaskCreationOptions.LongRunning参数应该提示 TaskScheduler以正确运行该方法,但是我没有找到任何方法来确保它能够运行. TPL应该足够聪明,可以按照程序员的意图运行任务,但这对我来说似乎有点神奇.而且我不喜欢编程中的魔术.
  2. 如果我了解这是如何正常工作的,则不能保证TPL Task会在启动时在同一线程上恢复.如果确实如此,则在这种情况下它将尝试释放一个不拥有的锁,而另一个线程将永远持有该锁,从而导致死锁.我记得前一段时间Eric Lippert写道,这就是lock块中不允许await的原因.回到我的例子,我什至不知道如何解决这个问题.
  1. TPL Task is an abstraction, which does not even guarantee that the code will run on a separate thread. In my example, if the producer control method runs synchronously, the infinite loop will cause the consumer to never even start. According to MSDN, providing a TaskCreationOptions.LongRunning parameter when running the task should hint the TaskScheduler to run the method appropriately, however I didn't find any way to ensure that it does. Supposedly TPL is smart enough to run tasks the way the programmer intended, but that just seems like a bit of magic to me. And I don't like magic in programming.
  2. If I understand how this works correctly, a TPL Task is not guaranteed to resume on the same thread as it started. If it does, in this case it would try to release a lock it doesn't own while the other thread holds the lock forever, resulting in a deadlock. I remember a while ago Eric Lippert writing that it's the reason why await is not allowed in a lock block. Going back to my example, I'm not even sure how to go about solving this issue.

这些是我想到的几个问题,尽管可能还有(可能是).我应该如何解决它们?

These are the few issues that crossed my mind, although there may be (probably are) more. How should I go about solving them?

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

Also, this made me think, is using the classical approach of synchronizing via Monitor, Mutex or Semaphore even the right way to do TPL code? Perhaps I'm missing something that I should be using instead?

推荐答案

您的问题推动了Stack Overflow泛滥的局限.从简单的Thread实现过渡到基于Task和其他TPL功能的实现,涉及许多考虑因素.单独考虑,几乎可以肯定,每个问题都可以在先前的堆栈溢出Q& A中得到解决,总的来说,有太多考虑因素无法在单个堆栈溢出Q& A中胜任和全面解决.

Your question pushes the limits of broadness for Stack Overflow. Moving from plain Thread implementations to something based on Task and other TPL features involves a wide variety of considerations. Taken individually, each concern has almost certainly been addressed in a prior Stack Overflow Q&A, and taken in aggregate there are too many considerations to address competently and comprehensively in a single Stack Overflow Q&A.

因此,话虽如此,让我们来看一下您在此处询问的特定于 的问题.

So, with that said, let's look just at the specific issues you've asked about here.

  1. TPL任务是一种抽象,它甚至不能保证代码将在单独的线程上运行.在我的示例中,如果生产者控制方法同步运行,则无限循环将导致使用者永远无法启动.根据MSDN,在运行任务时提供TaskCreationOptions.LongRunning参数应该提示TaskScheduler适当地运行该方法,但是我没有找到任何方法来确保它能够正常运行. TPL应该足够聪明,可以按照程序员的意图运行任务,但这对我来说似乎有点神奇.而且我不喜欢编程中的魔术.
  1. TPL Task is an abstraction, which does not even guarantee that the code will run on a separate thread. In my example, if the producer control method runs synchronously, the infinite loop will cause the consumer to never even start. According to MSDN, providing a TaskCreationOptions.LongRunning parameter when running the task should hint the TaskScheduler to run the method appropriately, however I didn't find any way to ensure that it does. Supposedly TPL is smart enough to run tasks the way the programmer intended, but that just seems like a bit of magic to me. And I don't like magic in programming.

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

It is true that the Task object itself does not guarantee asynchronous behavior. For example, an async method which returns a Task object could contain no asynchronous operations at all, and could run for an extended period of time before returning an already-completed Task object.

另一方面,保证Task.Run() 可以异步运行.它被记录为这样的:

On the other hand, Task.Run() is guaranteed to operate asynchronously. It is documented as such:

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

Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work

虽然Task对象本身抽象了未来"或承诺"的概念(使用编程中的同义词),但特定的实现与线程池紧密相关.如果使用正确,则可以确保异步操作.

While the Task object itself abstracts the idea of a "future" or "promise" (to use synonymous terms found in programming), the specific implementation is very much tied to the thread pool. When used correctly, you can be assured of asynchronous operation.

  1. 如果我了解这是如何正常工作的,则不能保证TPL任务可以在启动时在相同的线程上继续执行.如果确实如此,则在这种情况下它将尝试释放一个不拥有的锁,而另一个线程将永远持有该锁,从而导致死锁.我记得前一段时间Eric Lippert写道,这就是为什么在锁块中不允许等待的原因.回到我的例子,我什至不知道如何解决这个问题.

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

Only some synchronization objects are thread-specific. For example, Monitor is. But Semaphore is not. Whether this is useful to you or not depends on what you are trying to implement. For example, you can implement the producer/consumer pattern with a long running thread that uses BlockingCollection<T>, without needing to call any explicit synchronization objects at all. If you did want to use TPL techniques, you could use SemaphoreSlim and its WaitAsync() method.

当然,您也可以使用Dataflow API.在某些情况下,这将是更可取的.对于非常简单的生产者/消费者来说,这可能是过大了. :)

Of course, you could also use the Dataflow API. For some scenarios this would be preferable. For very simple producer/consumer, it would probably be overkill. :)

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

Also, this made me think, is using the classical approach of synchronizing via Monitor, Mutex or Semaphore even the right way to do TPL code? Perhaps I'm missing something that I should be using instead?

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

IMHO, this is the crux of the matter. Moving from Thread-based programming to the TPL is not simply a matter of a straight-forward mapping from one construct to another. In some cases, doing so would be inefficient, and in other cases it simply won't work.

实际上,我想说TPL尤其是async/await的一项关键功能是,线程同步的必要性大大降低.一般的想法是异步执行操作,线程之间的交互最少.数据仅在明确定义的点(即从完成的Task对象中检索到)之间在线程之间流动,从而减少甚至消除了显式同步的需要.

Indeed, I would say a key feature of TPL and especially of async/await is that synchronization of threads is much less necessary. The general idea is to perform operations asynchronously, with minimal interaction between threads. Data flows between threads only at well-defined points (i.e. retrieved from the completed Task objects), reducing or even eliminating the need for explicit synchronization.

不可能提出具体的技术,因为如何最好地实现某些东西将取决于确切的目标.但是,简短的版本是要理解的是,在使用TPL时,通常根本不需要使用同步原语,例如您以前在低级API中使用的原语.您应该努力利用TPL惯用法发展足够的经验,以便可以识别哪些问题适用于哪些编程问题,以便您直接应用它们,而不是试图在思维上映射您的旧知识.

It's impossible to suggest specific techniques, as how best to implement something will depend on what exactly the goal is. But the short version is to understand that when using TPL, very often it is simply unnecessary to use synchronization primitives such as what you're used to using with the lower-level API. You should strive to develop enough experience with the TPL idioms that you can recognize which ones apply to which programming problems, so that you apply them directly rather than trying to mentally map your old knowledge.

在某种程度上,(我认为)这类似于学习一种新的人类语言.最初,人们花费大量时间进行直译的心理翻译,可能需要重新映射以适应语法,习语等.但是理想情况下,人们可以将语言内化并能够直接用该语言表达自己.就个人语言而言,我从没有到过这一点,但是我理解理论上的概念:).而且我可以直接告诉您,它在编程语言的上下文中效果很好.

In a way, this is (I think) analogous to learning a new human language. At first, one spends a lot of time mentally translating literally, possibly remapping to adjust to grammar, idioms, etc. But ideally at some point, one internalizes the language and is able to express oneself in that language directly. Personally, I've never gotten to that point when it comes to human languages, but I understand the concept in theory :). And I can tell you firsthand, it works quite well in the context of programming languages.


顺便说一句,如果您有兴趣了解将TPL理念发挥到极致的方法,您可能想通读


By the way, if you are interested in seeing how TPL ideas taken to extremes work out, you might like to read through Joe Duffy's recent blog articles on the topic. Indeed, the most recent version of .NET and associated languages have borrowed heavily from concepts developed in the Midori project he's describing.

这篇关于如何通过使用Monitor/Mutex/Semaphore同步TPL任务?还是应该完全使用其他东西?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆