与FIFO同步运行任务的好方法?

时间:2015-08-10 08:33:43

标签: c# asynchronous async-await task fifo

目前,我在.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;
    }
}

这种方法似乎是一种好方法,还是应该区别对待这种情况?

3 个答案:

答案 0 :(得分:2)

为什么要同步运行

您应该异步运行任务并使用asyncawait逐个执行它们:

 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;
}

更新

OP说:

  

我可能忘了说,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实现了两个接口:

  • ITargetBlock <T&GT;让生产者将其输出发送到
  • ISourceBlock <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

How to: Implement a Producer-Consumer Dataflow Pattern

答案 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;
}