提供同步和异步方法

时间:2019-10-23 13:57:38

标签: .net asynchronous async-await

对于任何新的库项目,在提供async方法和/或普通同步方法之间,我都感到很痛苦。在此示例中,我尝试提供同步和异步方法以产生相同的总体结果。 Because I'm as lazy as possible,我想简单地包装或拆开其他方法所做的任何事情。

从同步调用异步有很多问题,还有解开异常并返回正确执行上下文的各种困难,但是我找不到关于如何允许消耗性开发人员在异步或同步执行路径之间进行选择的任何问题。 。

让我们看一下我可以用来提供同一工作的同步和异步方法的两种潜在的开发模式。

public interface IMyService
{
    MyResponse DoWork(MyRequest req);
    Task<MyResponse> DoWorkAsync(MyRequest req, CancellationToken token);
}

public class MyServiceSyncWrapped : IMyService
{
    public MyResponse DoWork(MyRequest req)
    {
        //Do the actual work, but sync-d
    }

    //No need to make this async/await, here it's pure overhead
    public Task<MyResponse> DoWorkAsync(MyRequest req, CancellationToken token)
    {
        //Return DoWork wrapped in a task
    }
}

public class MyServiceAsyncWrapped : IMyService
{
    public MyResponse DoWork(MyRequest req)
    {
        //Return DoWorkAsync, unwrapped!
    }

    //async/await actually potentially adds value here
    public async Task<MyResponse> DoWorkAsync(MyRequest req, CancellationToken token)
    {
        //Do the actual work, fully async-ly
    }
}

同时提供同步和异步方法是否有价值(或者这是一种反模式)? 如果认为这是合乎需要的,并牢记解开异步代码的难度与允许异步完全遍历代码库的更好实践是什么,那么驱动每种方法价值的是(或显然更可取的)? 我是否应该放弃同步方法并强迫其他开发人员进行异步开发?

2 个答案:

答案 0 :(得分:2)

Microsoft使用的模式是其方法的同步版本与异步版本是完全不同的实现。同步版本不仅是异步版本的包装。

您可以在他们的源代码中看到它。例如,将File.InternalReadAllText()(由File.ReadAllText()使用)与File.InternalReadAllTextAsync()(由File.ReadAllTextAsync()使用)进行比较。

实际上,同步等待异步方法存在危险,如本文所述:Don't Block on Async Code。当然,只要您知道它正在发生,就有办法避免这种情况。

我的个人哲学是不创建仅包装异步方法的同步方法。如果呼叫者觉得他们需要这样做,请让他们这样做。

答案 1 :(得分:1)

只有当同步版本中有一个空闲等待,通常是等待另一个进程完成(例如等待文件打开)或数据库查询结果时,编写过程的异步版本才有意义或互联网的某些信息。

一个sync方法将闲置地等待其他进程完成,从而锁定了程序的执行。在async-await方法中,线程将环顾四周,看看它是否还能执行其他操作,而不是闲着等待。

如果将其与做早餐的厨师进行比较,则最容易理解,如this interview with Eric Lippert中所述。在中间的某个地方搜索异步等待。

如果厨师必须同时做早餐,他将开始沸腾水,等到水沸腾后,用沸水煮茶,开始烘烤面包,等到面包被烘烤后,开始煮鸡蛋,等到鸡蛋煮沸,等等。您会看到所有空闲的等待。

一个异步等待的厨师会开始煮沸水,但他不会等待水烧开,而是开始烤面包和开水。当他不得不等待这些过程完成时,他可以自由地做其他事情,例如切西红柿。一段时间后,茶水沸腾了。他泡茶并继续工作,直到烤面包,等等。只要有工作要做,厨师就不会闲着等待,而是做他可以做的事而没有他开始的任务的结果。

返回您的问题

如果某个过程不必等待其他过程完成,则为该过程创建异步版本是没有意义的。如果该过程花费大量时间,则让您的调用者决定是否需要响应并启动一个调用您的函数的任务。

另一个原因是实现接口。这可能是没有创建一个同时具有同步和异步版本的接口,而是创建两个接口的原因:两个接口具有同步功能,另一个接口具有异步功能。具有不必等待空闲的函数的类将仅实现接口的同步版本。

如果您的类具有两个功能相同的方法,则唯一的区别是一个是异步的,另一个不是异步的,请考虑将该方法分解为子方法,并仅在可以调用异步函数时创建异步版本:

IEnumerable<int> ReadNumbers(string fileName1)
{
    // Read File 1, wait idly,
    string text1 = ReadTextFile(fileName1);

    // convert the read texts to numbers, and return in ascending order
    IEnumerable<int> result = ConvertToAscendingInt(text1);
    return result;
}

此方法在读取文本文件时空闲。为防止这种情况,请调用它的异步版本。所有其他事情都不是闲散的,不需要异步版本:

async Task<IEnumerable<int>> ReadNumbersAsync(string fileName1)
{
    // Start reading File 1 async
    string text1 = await ReadTextFileAsync(fileName1);

    // convert the read texts to numbers, and return in ascending order
    IEnumerable<int> result = ConvertToAscendingInt(text1);
    return result;
}

这样,大多数代码将在子方法中。更容易看出sync和async方法执行相同的操作。如果将来转换方式有所不同,则同步版本和异步版本都会自动完成。

最后,您可以决定让同步版本调用异步版本。任务开销将减慢同步版本:

IEnumerable<int> ReadNumbers(string fileName1)
{
   var taskReadNumbers = Task.Run( () => ReadNumbersAsync(fileName1);
   taskReadNumbers.Wait();
   return taskReadNumbers.Result();
}

为简单起见,我省略了异常处理