如何序列化async / await?

时间:2014-01-16 13:28:01

标签: c# asynchronous async-await

我们假设我有这个简单的片段:

async void button_Click(object sender, RoutedEventArgs e)
{
    await Task.Factory.StartNew(() =>
    {
        Console.WriteLine("start");
        Thread.Sleep(5000);
        Console.WriteLine("end");
    });
}

显然,每次按下该按钮,即使前一个任务仍在运行,也会启动新任务。在所有先前的任务完成之前,我将如何推迟任何新任务?

更多细节:

在上面的示例中,每个新任务都与之前的任务相同。但是,在原始上下文中,任务序列很重要:参数可能会发生变化(我可以使用DateTime.Now.Ticks“模拟”它。 任务应按照“注册”的顺序执行。具体来说,我的程序将与串行设备通信。我之前使用BlockingCollection的后台线程完成了这项工作。但是,这次有一个严格的请求/响应协议,如果可能的话,我想使用async / await。

可能的解决方案:

我可以想象创建任务并将它们存储在列表中。但是,我如何执行与要求相关的任务?或者我应该回到之前使用的基于线程的解决方案吗?

4 个答案:

答案 0 :(得分:8)

我建议使用SemaphoreSlim进行同步。但是,您想要avoid Task.Factory.StartNew(正如我在我的博客上解释的那样),还有definitely avoid async void(正如我在MSDN文章中解释的那样)。

private SemaphoreSlim _mutex = new SemaphoreSlim(1);
async void button_Click(object sender, RoutedEventArgs e)
{
  await Task.Run(async () =>
  {
    await _mutex.WaitAsync();
    try
    {
      Console.WriteLine("start");
      Thread.Sleep(5000);
      Console.WriteLine("end");
    }
    finally
    {
      _mutex.Release();
    }
  });
}

答案 1 :(得分:7)

您可以异步等待SemaphoreSlim并在作业完成后释放它。不要忘记将信号量initialcount配置为1

private static SemaphoreSlim semaphore = new SemaphoreSlim(1);

private async static void DoSomethingAsync()
{
     await semaphore.WaitAsync();
     try
     {
        await Task.Factory.StartNew(() =>
        {
            Console.WriteLine("start");
            Thread.Sleep(5000);
            Console.WriteLine("end");
        });
     }
     finally
     {
        semaphore.Release();
     }
}

private static void Main(string[] args)
{
    DoSomethingAsync();
    DoSomethingAsync();
    Console.Read();
}

答案 2 :(得分:2)

我可能会遗漏一些东西,但我不认为OP的情况需要SemaphoreSlim。我会按照以下方式做到这一点。基本上,代码只是await在继续之前先前挂起的任务实例(为清晰起见,没有异常处理):

// the current pending task (initially a completed stub)
Task _pendingTask = Task.FromResult<bool>(true);

async void button_Click(object sender, RoutedEventArgs e)
{
    var previousTask = _pendingTask;

    _pendingTask = Task.Run(async () =>
    {
        await previousTask;

        Console.WriteLine("start");
        Thread.Sleep(5000);
        Console.WriteLine("end");
    });

    // the following "await" is optional, 
    // you only need it if you have other things to do 
    // inside "button_Click" when "_pendingTask" is completed
    await _pendingTask;
}

[更新] 要解决此问题,这里是一个线程安全的版本,可以同时调用button_Click

Task _pendingTask = Task.FromResult<bool>(true);
object _pendingTaskLock = new Object();

async void button_Click(object sender, RoutedEventArgs e)
{
    Task thisTask;

    lock (_pendingTaskLock)
    {
        var previousTask = _pendingTask;

        // note the "Task.Run" lambda doesn't stay in the lock
        thisTask = Task.Run(async () =>
        {
            await previousTask;

            Console.WriteLine("start");
            Thread.Sleep(5000);
            Console.WriteLine("end");
        });

        _pendingTask = thisTask;
    }

    await thisTask;
}

答案 3 :(得分:2)

尝试使用(默认)最大并行度为1的Dataflow.ActionBlock<T>怎么样?这样您就不必担心任何线程安全/锁定问题了。

它可能看起来像:

...
var _block = new ActionBlock<bool>(async b =>
                {
                    Console.WriteLine("start");
                    await Task.Delay(5000);
                    Console.WriteLine("end");
                });
...


async void button_Click(object sender, RoutedEventArgs e)
{
    await _block.SendAsync(true);
}

您还可以设置ActionBlock以接收TaskFunc<Task>,然后只需运行/等待此输入​​。这将允许多个操作排队等待来自不同的来源。