Ged*_*tis 5 .net c# multithreading synchronization task-parallel-library
我正在尝试将我的一些旧项目从TPLThreadPool和独立移动Thread到 TPL Task,因为它支持一些非常方便的功能,例如延续 with Task.ContinueWith(以及从 C# 5 with async\await)、更好的取消、异常捕获等。我很想在我的项目中使用它们。但是我已经看到了潜在的问题,主要是同步问题。
我写了一些代码来显示生产者/消费者问题,使用经典的独立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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
在这个例子中,ProduceNumber生成一个递增的整数序列,同时ConsumeNumber将它们写入Console. 如果生产运行得更快,数字将排队等待稍后消费。如果消费运行得更快,则消费者将等到一个数字可用。所有同步都是使用Monitorand完成的lock(在内部也是Monitor)。
在尝试“TPL-ify”类似代码时,我已经看到了一些我不知道如何解决的问题。如果我替换new Thread().Start()为Task.Run():
Task是一种抽象,它甚至不能保证代码会在单独的线程上运行。在我的例子中,如果生产者控制方法同步运行,无限循环将导致消费者甚至永远不会启动。根据MSDN,提供了一个TaskCreationOptions.LongRunning运行任务时,应参数暗示的TaskScheduler适当运行的方法,但是我没有找到任何办法,以确保它。据说 TPL 足够聪明,可以按照程序员预期的方式运行任务,但这对我来说似乎有点神奇。而且我不喜欢编程中的魔法。Task是如何正确工作的,则不能保证TPL会在它开始时在同一线程上恢复。如果是这样,在这种情况下,它会尝试释放不属于它的锁,而另一个线程永远持有该锁,从而导致死锁。我记得不久前 Eric Lippert 写道,这await是不允许在lock块中出现的原因。回到我的例子,我什至不确定如何解决这个问题。这些是我想到的少数问题,尽管可能(可能还有)更多。我应该如何解决它们?
此外,这让我想到,是使用同步 via 的经典方法Monitor,Mutex还是Semaphore使用正确的方法来执行 TPL 代码?也许我错过了我应该使用的东西?
你的问题突破了 Stack Overflow 的广泛性极限。从普通Thread实现转向基于 TPLTask和其他 TPL 功能的实现涉及多种考虑因素。单独来看,每个问题几乎肯定已经在之前的 Stack Overflow 问答中得到了解决,而总的来说,有太多的考虑因素无法在单个 Stack Overflow 问答中有效且全面地解决。
因此,话虽如此,让我们看看您在这里询问的具体问题。
- TPL Task 是一个抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,则无限循环将导致消费者甚至永远不会启动。根据 MSDN,
TaskCreationOptions.LongRunning在运行任务时提供参数应该暗示TaskScheduler适当地运行该方法,但是我没有找到任何方法来确保它确实如此。据说 TPL 足够聪明,可以按照程序员预期的方式运行任务,但这对我来说似乎有点神奇。而且我不喜欢编程中的魔法。
确实,Task对象本身不保证异步行为。例如,async返回Task对象的方法可以根本不包含异步操作,并且可以在返回已完成的Task对象之前运行很长一段时间。
另一方面Task.Run() 保证异步操作。它是这样记录的:
将指定的工作排队以在 ThreadPool 上运行,并返回该工作的任务或 Task<TResult> 句柄
虽然Task对象本身抽象了“未来”或“承诺”的概念(使用编程中的同义词),但具体实现与线程池密切相关。如果使用正确,您可以放心进行异步操作。
- 如果我理解这是如何正确工作的,则不能保证 TPL 任务在启动时的同一线程上恢复。如果是这样,在这种情况下,它将尝试释放它不拥有的锁,而另一个线程永远持有该锁,从而导致死锁。我记得不久前 Eric Lippert 写道,这就是为什么在锁块中不允许等待的原因。回到我的例子,我什至不知道如何解决这个问题。
只有一些同步对象是特定于线程的。例如,Monitor是。但Semaphore事实并非如此。这对您是否有用取决于您想要实现的内容。例如,您可以使用使用 的长时间运行的线程来实现生产者/消费者模式BlockingCollection<T>,而根本不需要调用任何显式同步对象。如果您确实想使用 TPL 技术,您可以使用SemaphoreSlim及其WaitAsync()方法。
当然,您也可以使用Dataflow API。对于某些情况,这会更好。对于非常简单的生产者/消费者来说,这可能有点矫枉过正。:)
另外,这让我思考,使用通过 Monitor、Mutex 或 Semaphore 进行同步的经典方法是否是执行 TPL 代码的正确方法?也许我缺少一些我应该使用的东西?
恕我直言,这就是问题的关键。从Thread基于 的编程转向 TPL 不仅仅是从一种构造到另一种构造的直接映射问题。在某些情况下,这样做效率很低,而在其他情况下,根本行不通。
事实上,我想说 TPL(尤其是async/)的一个关键特性await是线程同步的必要性要低得多。总体思路是异步执行操作,线程之间的交互最少。数据仅在明确定义的点(即从已完成的Task对象中检索)在线程之间流动,从而减少甚至消除了显式同步的需要。
不可能提出具体的技术,因为如何最好地实现某件事将取决于确切的目标是什么。但简短的版本是要了解,在使用 TPL 时,很多时候根本没有必要使用同步原语,例如您习惯与较低级别 API 一起使用的同步原语。您应该努力积累足够的 TPL 习惯用法经验,以便能够识别哪些习惯用法适用于哪些编程问题,以便直接应用它们,而不是试图在心里映射您的旧知识。
在某种程度上,(我认为)这类似于学习一种新的人类语言。一开始,人们会花费大量时间在心里进行字面翻译,可能会重新映射以适应语法、习语等。但理想情况下,在某个时刻,人们会内化该语言并能够直接用该语言表达自己。就我个人而言,当谈到人类语言时,我从未达到这一点,但我在理论上理解这个概念:)。我可以直接告诉你,它在编程语言的上下文中运行得很好。
顺便说一句,如果您有兴趣了解 TPL 想法如何发挥到极致,您可能想阅读Joe Duffy 最近关于该主题的博客文章。事实上,最新版本的 .NET 和相关语言大量借鉴了他所描述的 Midori 项目中开发的概念。