【问题标题】:How to serialize async/await?如何序列化异步/等待?
【发布时间】:2014-01-16 13:28:00
【问题描述】:

假设我有这个简单的 sn-p:

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。

可能的解决方案:

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

【问题讨论】:

  • 您仍然可以使用ConcurrentQueueBlockingCollection。既然是生产者-消费者场景,后者应该是最合适的。
  • 当然。但正如我所见,我需要在队列中等待并循环它。我会存储Tasks 或数据来创建任务吗?我会在Task 内循环队列还是循环启动Task?不过,这不是什么大问题,但我希望有另一种更适合该请求/响应模式的方法。

标签: c# asynchronous async-await


【解决方案1】:

我建议使用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();
    }
  });
}

【讨论】:

  • 感谢您链接到您的文章!很不错!顺便说一句,当我尝试您的示例时,我必须用 async 标记 lambda 表达式才能编译它。这是正确的吗?
  • @StephenCleary,我们有什么理由不能只 await 上一个任务,比如 this
  • @Noseratio:这种方法也有效。 IMO SemaphoreSlim 代码具有更清晰的意图;从await-previous-task 代码中并没有明显看出复制上一个任务很重要。
  • 据我了解,如果多次调用button_Click,这并不能保证执行顺序? 1. SemaphoreSlim 不保证获取信号量的顺序,并且 2. 不保证 _mutex.WaitAsync() 在可能单独的工作线程上执行的顺序。
  • @Magnus:不保证订单。它保证一次一个。
【解决方案2】:

您可以异步等待SemaphoreSlim,并在工作完成后释放它。不要忘记将信号量初始计数配置为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();
}

【讨论】:

  • 不要忘记 Try-finally 并在 finally 子句中释放 semaphone !否则,如果在您的任务中抛出异常,可能会导致用户非常沮丧:)
  • 这正是我需要排队的偶尔并发访问!
  • 你可能想把await semaphore.WaitAsync();移到try之外。
  • @Noseratio 原因是什么?这取决于,如果在WaitAsync 之后但在进入try 信号量之前不会释放“线程中止”会发生什么。两者都有自己的优点/缺点
  • @Noseratio 没有记录抛出异常。但无论如何有效点,让我更新我的答案谢谢:)
【解决方案3】:

尝试使用(默认)最大并行度为 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&lt;Task&gt;,然后简单地运行/等待此输入​​。这将允许来自不同来源的多个操作排队等待。

【讨论】:

    【解决方案4】:

    我可能遗漏了一些东西,但我认为 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;
    }
    

    [UPDATE]为了解决评论,这里有一个线程安全的版本,可以同时调用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;
    }
    

    【讨论】:

    • 可能有多个“待处理”任务。
    • @JeffRSon,只要它们都以button_Click(或其他包装方法)开头,它就可以与多个一起使用。这就是我测试initial version 的方法:for (var i = 0; i &lt; 10; i++) button_Click(null, null); 它之所以有效,是因为它将成员_pendingTask 捕获到本地previousTask 中,以便在Task.Run lambda 中使用。
    • 啊-好的。因此,您将依赖 GUI 线程作为“同步”的一种手段。
    • @JeffRSon,没有。这种方法在没有任何同步上下文的控制台应用程序中同样适用,试一试。
    • 不 - 我的意思是,它不是线程安全的:如果在创建新的 _pendingTask 之前有 2 个线程执行 var previousTask = _pendingTask;。当然,如果所有调用都来自同一个线程(如 GUI 线程或主控制台应用程序线程),则不会发生这种情况。
    猜你喜欢
    • 1970-01-01
    • 2019-02-21
    • 2018-07-16
    • 1970-01-01
    • 2023-03-12
    • 2019-11-17
    • 2016-07-07
    • 2016-03-25
    • 2017-10-09
    相关资源
    最近更新 更多