How to serialize async/await?

2019-02-03 11:53发布

Let's suppose I have this simple snippet:

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

Obviously, everytime I push that button a new task is started even when a previous task still runs. How would I postpone any new task until all previous tasks have finished?

Some more details:

In the example above, each new task is identical to the task before. However, in the original context the sequence of tasks matters: Parameters may change (I could "simulate" it by using DateTime.Now.Ticks). The tasks should be executed in the order they are "registered". Specificly, my program will talk to a serial device. I've done this before with a background thread utilizing a BlockingCollection. However, this time there's a strict request/response-protocol and I'd like to use async/await if it is possible.

Possible solution:

I could imagine creating tasks and storing them in a list. But how would I execute the tasks with respect to the requirements? Or should I return to the thread-based solution I have used before?

4条回答
乱世女痞
2楼-- · 2019-02-03 12:29

What about trying the Dataflow.ActionBlock<T> with the (default) max degree of parallelism of 1. This way you don't need to worry about any of the thread safety / locking concerns.

It could look something like:

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

You could also setup the ActionBlock to receive a Task or Func<Task>, and simply run / await this input. Which would allow multiple operations to be queued and awaited from different sources.

查看更多
放我归山
3楼-- · 2019-02-03 12:33

I might be missing something, but I don't think SemaphoreSlim is needed for the OP's scenario. I'd do it the following way. Basically, the code just await the previous pending instance of the task before continuing (no exception handling for clarity):

// 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] To address the comment, here's a thread-safe version, when button_Click can be called concurrently:

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;
}
查看更多
做自己的国王
4楼-- · 2019-02-03 12:36

You could wait on a SemaphoreSlim asynchronously and release it once the job is done. Don't forget to configure the semaphore initialcount to 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();
}
查看更多
forever°为你锁心
5楼-- · 2019-02-03 12:42

I recommend using a SemaphoreSlim for synchronization. However, you want to avoid Task.Factory.StartNew (as I explain on my blog), and also definitely avoid async void (as I explain in the MSDN article).

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();
    }
  });
}
查看更多
登录 后发表回答