【发布时间】: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。如果生产运行得更快,数字将排队等待稍后使用。如果消费运行得更快,消费者将等到有一个数字可用。所有同步均使用Monitor 和lock(内部也使用Monitor)完成。
在尝试“TPL 化”类似代码时,我已经看到了一些我不知道该如何解决的问题。如果我将new Thread().Start() 替换为Task.Run():
- TPL
Task是一种抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,则无限循环将导致消费者甚至永远无法启动。根据 MSDN,在运行任务时提供TaskCreationOptions.LongRunning参数应该提示TaskScheduler以适当地运行该方法,但是我没有找到任何方法来确保它确实如此。据说 TPL 足够聪明,可以按照程序员的意图运行任务,但这对我来说似乎有点神奇。而且我不喜欢编程中的魔法。 - 如果我理解它是如何正常工作的,则不能保证 TPL
Task在启动时在同一线程上恢复。如果是这样,在这种情况下,它会尝试释放它不拥有的锁,而另一个线程永远持有锁,从而导致死锁。我记得不久前 Eric Lippert 写道,这就是为什么await不允许出现在lock块中的原因。回到我的例子,我什至不知道如何解决这个问题。
这些是我脑海中闪过的少数几个问题,尽管可能还有(可能是)更多。我应该如何解决它们?
另外,这让我想到,使用通过Monitor、Mutex 或Semaphore 进行同步的经典方法甚至是执行 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