C# 如何使用监视器/互斥体/信号量同步TPL任务?还是应该完全使用其他东西?
我正在尝试将我的一些旧项目从C# 如何使用监视器/互斥体/信号量同步TPL任务?还是应该完全使用其他东西?,c#,.net,multithreading,synchronization,task-parallel-library,C#,.net,Multithreading,Synchronization,Task Parallel Library,我正在尝试将我的一些旧项目从线程池和独立线程移动到TPL任务,因为它支持一些非常方便的功能,比如继续执行任务。继续执行(从C#5执行异步\等待),更好的取消和异常捕获,等等。我很乐意在我的项目中使用它们。但是,我已经看到了潜在的问题,主要是同步问题 我已经用一个经典的独立线程编写了一些代码,显示了生产者/消费者的问题: class ThreadSynchronizationTest { private int CurrentNumber { get; set; } private
线程池
和独立线程
移动到TPL任务
,因为它支持一些非常方便的功能,比如继续执行任务。继续执行(从C#5执行异步\等待
),更好的取消和异常捕获,等等。我很乐意在我的项目中使用它们。但是,我已经看到了潜在的问题,主要是同步问题
我已经用一个经典的独立线程编写了一些代码,显示了生产者/消费者的问题:
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);
}
}
}
类线程同步测试
{
private int CurrentNumber{get;set;}
私有对象同步{get;set;}
专用队列等待编号{get;set;}
公共void TestSynchronization()
{
Synchro=新对象();
WaitingNumbers=新队列();
var producerThread=新线程(RunProducer);
var consumerThread=新线程(RunConsumer);
producerThread.Start();
consumerThread.Start();
producerThread.Join();
consumerThread.Join();
}
私有int ProduceNumber()
{
CurrentNumber++;
//长时间运行的方法。以睡眠为例
睡眠(100);
返回当前编号;
}
专用编号(整数)
{
控制台写入线(编号);
//长时间运行的方法。以睡眠为例
睡眠(100);
}
私有void RunProducer()
{
while(true)
{
int producedNumber=ProduceNumber();
锁(同步)
{
等待编号。排队(生产编号);
//通知消费者新号码
监视器。脉冲(同步);
}
}
}
私有用户()
{
while(true)
{
整数消费;
锁(同步)
{
//确保我们满足了等待条件
while(WaitingNumbers.Count==0)
{
//等待脉搏
监视器。等待(同步);
}
numberToConsume=WaitingNumbers.Dequeue();
}
消费者编号(numberToConsume);
}
}
}
在本例中,ProduceNumber
生成一系列递增的整数,而consumernumber
将它们写入控制台
。如果生产运行得更快,则数字将排队等待以后使用。如果消费速度加快,消费者将等待一个数字。使用监视器
和锁
(内部也使用监视器
)完成所有同步
在尝试“第三方物流化”类似的代码时,我已经看到了一些问题,我不知道如何处理。如果我将new Thread().Start()
替换为Task.Run()
:
TPLTask
是一种抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,无限循环将导致消费者甚至从未启动。根据MSDN,在运行任务时提供TaskCreationOptions.LongRunning
参数应该提示TaskScheduler
正确运行该方法,但是我没有找到任何方法来确保它。据说TPL足够聪明,可以按照程序员的预期方式运行任务,但这对我来说似乎有点神奇。我不喜欢编程中的魔法
如果我理解这是如何正确工作的,那么TPL任务
就不能保证在启动时在同一线程上恢复。如果它这样做了,在这种情况下,它将尝试释放一个它不拥有的锁,而另一个线程将永远持有该锁,从而导致死锁。我记得不久前Eric Lippert写道,这就是为什么锁
块中不允许等待
的原因。回到我的例子,我甚至不知道如何着手解决这个问题
这些是我脑海中闪过的少数几个问题,尽管可能(可能)还有更多。我应该如何着手解决这些问题
此外,这让我想到,使用经典的同步方法,通过监视器
,互斥体
或信号量
进行同步,这是不是做TPL代码的正确方法?也许我遗漏了应该使用的内容?您的问题突破了堆栈溢出的宽度限制。从普通的Thread
实现转移到基于Task
和其他TPL功能的实现涉及到各种各样的考虑。单独来看,几乎可以肯定,在之前的堆栈溢出Q&a中已经解决了每一个问题,并且总体来看,在单个堆栈溢出Q&a中,有太多的考虑因素需要充分和全面地解决
话虽如此,让我们看看你在这里问的具体问题
TPL任务是一种抽象,它甚至不能保证代码将在单独的线程上运行。在我的示例中,如果生产者控制方法同步运行,无限循环将导致消费者甚至从未启动。根据MSDN,在运行任务时提供TaskCreationOptions.LongRunning
参数应该提示TaskScheduler
正确运行该方法,但是我没有找到任何方法来确保它正确运行。据说TPL足够聪明,可以按照程序员的预期方式运行任务,但这对我来说似乎有点神奇。我不喜欢编程中的魔法
确实,任务
对象本身并不保证异步行为。例如,一个返回任务
对象的async
方法在同一时间内不能包含任何异步操作