目前,我在.NET中执行async / await和tasks的第一步,我非常高兴异步运行是多么容易!但是,目前我必须通过SerialPort与设备通信。由于同时只能实现一个连接,我只是编写了一些扩展方法来运行所有这些方法,来自不同的任务/线程,同步并以先进先出的顺序运行:
public static class Extensions
{
private readonly static object LockObject = new object();
public static Task<TResult> RunAfter<TResult>(this Task<TResult> task, ConcurrentQueue<Task> others)
=> (Task<TResult>)task.RunAllSynchronously(others);
public static Task RunAfter(this Task task, ConcurrentQueue<Task> others)
=> task.RunAllSynchronously(others);
private static Task RunAllSynchronously(this Task task, ConcurrentQueue<Task> others)
{
if (others == null) throw new ArgumentNullException("The value of " + nameof(others) + " is null!");
lock (LockObject)
{
others.Enqueue(task);
Task currentTask;
while (others.TryDequeue(out currentTask))
{
currentTask.RunSynchronously();
if (currentTask == task) break;
}
}
return task;
}
}
这种方法似乎是一种好方法,还是应该区别对待这种情况?
答案 0 :(得分:2)
为什么要同步运行 ?
您应该异步运行任务并使用async
和await
逐个执行它们:
Task currentTask;
while (others.TryDequeue(out currentTask))
{
await currentTask;
if (currentTask == task) break;
}
另一方面,查看代码,我找不到使用lock
(线程同步)的理由。 您将线程与某些共享资源同步(,某些对象可能会或可能不会被多个线程读取/修改)。您可以将方法重新编写为:
private static async Task RunAllAsync(this Task task, ConcurrentQueue<Task> others)
{
// Design by contract rocks ;)
// See: https://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx
Contracts.Requires(task != null);
Contracts.Requires(others != null);
others.Enqueue(task);
// See how I've improved your loop. Since ConcurrentQueue.TryDequeue
// will return false if other thread has called it already, your loop
// should try to dequeue again until it returns true, and it should
// break if dequeued task is the task against which the extension method
// was called or the concurrent queue has no more items, to prevent a
// possible infinite loop
do
{
Task currentTask;
if(others.TryDequeue(out currentTask))
await currentTask;
}
while (currentTask == task || others.Count > 0);
return task;
}
我可能忘了说,ConcurrentQueue就是 应该在线程之间共享的资源。即 在每个新任务上调用Task.RunAllSynchronously()(访问 SerialPort)和这个调用可能来自不同的线程。也, 我无法确保只调用RunAllSynchronously() 当前正在运行(或排队)的任务已经完成(我可以,但是 因此我不得不在扩展名外面使用像锁一样的东西 方法,这对扩展方法来说并不是那么好。
这就是您使用ConcurrentQueue<T>
的原因。线程安全在内部进行管理。如果您拨打ConcurrentQueue<T>.TryDequeue
并且多个线程一次调用它,则只有一个会获胜而其他人将获得false
作为返回值并且out
参数赢了&# 39;被分配。查看MSDN says for this:
ConcurrentQueue在内部处理所有同步。如果两个 线程在同一时刻调用TryDequeue,两者都没有 操作被阻止。当在两个线程之间检测到冲突时, 一个线程必须再次尝试检索下一个元素,并且 同步在内部处理。
TryDequeue尝试从队列中删除元素。如果方法是 成功后,项目将被删除,方法返回true; 否则,它返回false。这就是原子地发生的 队列上的其他操作。如果队列填充了代码 例如q.Enqueue(&#34; a&#34;); q.Enqueue(&#34; B&#34); q.Enqueue(&#34; C&#34);和两个 线程同时尝试将一个元素出列,一个线程将 出队一个,另一个线程将出列队列b。两人都打来电话 TryDequeue将返回true,因为它们都能够出列 元件。如果每个线程返回以使其他元素出列, 其中一个线程将使c退出并返回true,而另一个则返回true 线程会发现队列为空,并返回false。
答案 1 :(得分:1)
首先:
如果您的程序还有其他内容,您只能从async-await中受益 在你的任务运行时做。
如果您的主线程将启动任务,并且只是等待此任务完成,则您的主线程可以自己完成工作。那甚至会更快。
在您的示例中,我可以想象通过串行线路发送比处理速度慢得多。所以我可以想象,当一个线程忙于通过串行线发送数据时,您的线程可能忙于创建要发送的下一个数据。或者可能有10个线程正在创建一个接一个地发送的数据。当然,在后一种情况下,无法保证数据的发送顺序。
Buf让我们看到它更简单:一个线程以自己的速度创建数据,而另一个线程通过串行线独立发送数据。
这对于生产者 - 消费者模式来说是尖叫:一个线程是生产者,它生成消费者阅读和处理的项目。一段时间后,生产者告诉消费者不再需要数据了。
其中的关键对象是System.Threading.Tasks.Dataflow.BufferBlock。请参阅MSDN。备注部分说它是通过nuget分发的。
bufferBlock实现了两个接口:
<T
&GT;让生产者将其输出发送到<T
&GT;供消费者阅读来自的输入。假设您使用System.IO.Ports.SerialPort发送数据。这个类没有异步支持,所以我们必须自己创建它。假设您要将类型为T的对象转换为可通过串行线发送的格式。代码如下所示:
private void Write(T t)
{
var dataToSend = ConvertToData(t);
serialPort.Write(dataToSend);
}
不是非常异步。所以让我们做一个异步函数:
private async Task WriteAsync(T t)
{
return await Task.Run ( () =>
{
var dataToSend = ConvertToData(t);
serialPort.Write(dataToSend);
}
}
或者你可以调用另一个写函数:
return await Task.Run ( () => Write(t));
注意:如果您确定只有一个线程将使用此功能,则您不必将其锁定。
现在我们确实有一个异步函数来通过串行线发送类型为T的对象,让我们创建一个生成器,它将创建类型为T的对象并将它们发送到缓冲区块。
我将它变为异步,因此调用线程可以在生成数据时执行其他操作:
private BufferBlock<T> bufferBlock = new BufferBlock<T>();
private async Task ProduceAsync()
{
while (objectsToProcessAvailable())
{
T nextObject = GetNextObjectToProcess()
await bufferBlock.SendAsync(nextObject);
}
// nothing to process anymore: mark complete:
bufferBlock.Complete();
}
接收方将由另一个线程完成:
private Task ConsumeAsync()
{
// as long as there is something to process: fetch it and process it
while (await bufferBlock.OutputAvailableAsync())
{
T nextToProcess = await bufferBlock.ReceiveAsync();
// use WriteAsync to send to the serial port:
await WriteAsync(nextToProcess);
}
// if here: no more data to process. Return
}
现在我们需要的是一个创建两个线程的过程,并等待两个任务完成:
private async Task ProduceConsumeAsync()
{
var taskProducer = ProduceAsync();
// while the producer is busy producing, you can start the consumer:
var taskConsumer = ConsumeAsync();
// while both tasks are busy, you can do other things,
// like keep the UI responsive
// after a while you need to be sure the tasks are finished:
await Task.WhenAll(new Task[] {taskProducer, taskConsumer});
}
注意:由于bufferBlock,生成器没有问题 消费者尚未开始生产。
我们需要的只是一个启动异步的函数,如果你有一个事件处理程序只是声明它是异步的:
private async void OnButton1_clicked(object sender, ...)
{
await ProduceConsumeAsync()
}
如果您没有异步功能,则必须自己创建一个任务:
private void MyFunction()
{
// start produce consume:
var myTask = Task.Run( () => ProduceConsumeAsync());
// while the task is running, do other things.
// when you need the task to finish:
await myTask;
}
有关消费者 - 生产者模式的更多信息。见MSDN
答案 2 :(得分:0)
在玩了各种各样的东西后,我发现了一个简单的解决方案,这对我来说已经足够了,并且有点类似于Matías Fidemraizer的解决方案:
private static ConcurrentQueue<Task> Tasks { get; } = new ConcurrentQueue<Task>();
public async static Task RunAlone(this Task task)
{
Tasks.Enqueue(task);
do
{
var nextTask = Tasks.First();
if (nextTask == task)
{
nextTask.Start();
await nextTask;
Task deletingTask;
Tasks.TryDequeue(out deletingTask);
break;
}
else
{
nextTask.Wait();
}
} while (Tasks.Any());
}
public async static Task<TResult> RunAlone<TResult>(this Task<TResult> task)
{
TResult result = default(TResult);
Tasks.Enqueue(task);
do
{
var nextTask = Tasks.First();
if (nextTask == task)
{
nextTask.Start();
result = await (Task<TResult>)nextTask;
Task deletingTask;
Tasks.TryDequeue(out deletingTask);
break;
}
else
{
nextTask.Wait();
}
} while (Tasks.Any());
return result;
}