异步/等待:使后台任务无阻塞的正确方法

时间:2017-11-10 10:05:01

标签: c# asynchronous async-await task-parallel-library

  编辑:OP的评论,目标是   非阻止后台任务,以便其他任务保持响应

说我有这样的功能:

void OnFrameSampleAcquired(VideoCaptureSample sample)
{  
    //Some code here

    //Here I want to introduce an Asynchrnous process
    ProcessAsynchronously(_latestImageBytes);

    //some more code here
}

在评论中,我想引入对异步函数的调用。 现在,我无法修改OnFrameSampleAcquired(意味着我无法使其“异步”)。 我怎么能这样做?

我在想

async void ProcessAsynchronously(byte[] image)
{
    await Process1(image);
    await Process2(image);
    // ...
}

async Task ProcessAsynchronously(byte[] image)

其中ProcessX也声明为async

这是一个好方法吗?

感谢您的任何见解,因为我在异步处理方面的实践经验很少。

1 个答案:

答案 0 :(得分:4)

已审核包含评论中的所有建议,并添加了一些背景信息。

背景&见解

将一个函数转换为async不足以使整个过程无阻塞。为了实现您的目标,整个处理路径(调用堆栈)应转换为非阻塞。调用堆栈上的一种阻塞方法足以使整个进程阻塞。 非阻止并不一定意味着async,如下面的一些示例所示。

将方法转换为async包含两个步骤:

  • 更改签名以返回Task。这将允许您的呼叫者跟踪您的方法的状态和结果。
  • 在签名中添加async关键字。这将允许await - 方法正文中的其他async方法。

CPU绑定与IO绑定任务

请注意,即使您await使用方法,也不一定意味着您立即将线程释放回调用方。 await - ed方法只有在反过来await开始执行IO绑定操作时才会将线程释放回给您。当await -ed方法在第一次await进行IO绑定操作之前执行CPU绑定操作时,您的线程仍会阻塞。

例如:

async Task MyAsyncMethod()
{
    Thread.Sleep(5000); // equivalent to CPU-bound operations
}

await MyAsyncMethod(); // will block for 5 seconds

另一方面,

async Task MyAsyncMethod()
{
    await Task.Delay(5000); // equivalent to IO-bound operations
}

await MyAsyncMethod(); // will return immediately

如果您有CPU绑定任务但仍然不想阻止调用者线程,则解决方法:

async Task MyAsyncMethod()
{
    await Task.Yield(); // this does the magic
    Thread.Sleep(5000); // equivalent to CPU-bound operations
}

await MyAsyncMethod(); // will return immediately thanks to Task.Yield()

在您的案例中做什么

由于我不确定您为何无法将OnFrameSampleAcquired签名更改为async,我会建议几种不同的选项。

选项1

最简单,最真实的异步方法是:

async Task OnFrameSampleAcquired(VideoCaptureSample sample)
{  
    //Some code here

    //Here I want to introduce an Asynchrnous process
    await ProcessAsynchronously(_latestImageBytes);

    //some more code here -- provided it is either async or non-blocking!
}

async Task ProcessAsynchronously(byte[] image)  
{
    await Process1(image);
    await Process2(image);
    // ...
}

如果处理路径上的所有方法都是这样的,那么您就可以正确实现非阻塞后台作业。

选项2

如果您绝对无法更改签名OnFrameSampleAcquired,则有一种解决方法。您可以异步调用其余的处理,如@Fildor所建议的那样:

public void OnFrameSampleAcquired(VideoCaptureSample sample)
{
    //Some code here

    //Here I want to introduce an Asynchrnous process
    ProcessAsynchronously(_latestImageBytes).ContinueWith(task => {
        // this runs on a different thread after ProcessAsynchronously is completed
        // some more code here
    });

    // return without blocking
}

在这里你赢得了双方的胜利:首先,你不必改变OnFrameSampleAcquired的签名;第二,OnFrameSampleAcquired现在是一种非阻塞方法。

选项3

如果您无法更改签名,因为您必须实现如下界面:

public interface ISomeInterface
{
    void OnFrameSampleAcquired(VideoCaptureSample sample);
    // ... other members
}

然后您可以将async关键字添加到您的方法中,并仍然符合界面:

async void OnFrameSampleAcquired(VideoCaptureSample sample)
{  
    //Some code here

    //Here I want to introduce an Asynchrnous process
    await ProcessAsynchronously(_latestImageBytes);

    //some more code here
}

ISomeInterface x;                  // initialized elsewhere
x.OnFrameSampleAcquired(/*....*/); // the same as await, but no error handling

此选项的缺点是调用者无法跟踪任务的状态(仍在运行或已完成?),也无法跟踪其结果(已完成或引发异常?)。您可能需要将整个OnFrameSampleAcquired包装在try/catch中,并编写一个例外来记录。

选项4

从技术上讲,您还可以使用Wait上的Task从非异步OnFrameSampleAcquired异步调用异步Process,但它无法实现您拥有非{I}的目标阻止后台任务Wait()将阻止线程,直到完成异步处理:

void OnFrameSampleAcquired(VideoCaptureSample sample)
{  
    //Some code here

    //Here I want to introduce an Asynchrnous process
    ProcessAsynchronously(_latestImageBytes).Wait();

    //some more code here
}