如何使用 C# 在异步方法中重用同步代码

How to reuse sync code in async method using C#

假设一个接口并考虑以下代码,这是我的合同

public interface ISomeContract
{
    Task<int> SomeMethodAsync(CancellationToken cancellationToken);
    int SomeMethod();
}

现在用以下代码想象一下合约对 ISomeContract 的实现

public class SomeImplementation : ISomeContract
    {
        public int SomeMethod()
        {
            // a lot of code ...
            return 10;
        }

        public async Task<int> SomeMethodAsync(CancellationToken cancellationToken)
        {
            return SomeMethod();

            // another example: I can remove async modifier from SomeMethodAsync method and this following.

            //try
            //{
            //    return Task.FromResult<int>(SomeMethod());
            //}
            //catch (Exception ex)
            //{
            //    return Task.FromException<int>(ex);
            //}
        }
    }

如您所见,我那里没有可等待的代码, 如果我真的没有可等待的代码,我该如何重用 SomeMethod 主体? 更重要的是,我在方法头中测试了 Task.FromResult 没有异步符号的情况,但是如何才能获得针对此问题的最佳解决方案?

我认为这里没有一个完美的答案:这在某种程度上取决于你想做什么。

首先,我们应该决定合同的消费者是否会期望 SomeMethodAsync() 到 return 很快,因为它是一个异步方法。在那种情况下,如果 SomeMethod() 很慢,我们可能希望使用 Task.Run() 来实现它,即使这通常被认为是一种不好的做法:

// if you think that "returns quickly" is an important guarantee of SomeMethodAsync
public Task<int> SomeMethodAsync(CancellationToken cancellationToken)
{
    return Task.Run(() => SomeMethod(), cancellationToken);
}

例如,您可以看到 MSFT 在 default implementation of TextReader.ReadAsync 中采用了这种方法。

如果 SomeMethod() 相当快或者我们不关心 returning 快速(技术上异步方法不能保证),那么我们需要一些模拟异步行为的方法运行 同步发生的方法。特别是,failures/cancellation 应该导致 faulted/canceled 任务。一种方法是简单地使用异步方法,如您所示:

public async Task<int> SomeMethodAsync(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    return SomeMethod();
}

这种方法的优点是它非常简单而且完全正确。另一个优点是某些原始任务结果在 运行 时间以一种方式缓存,而在使用 Task.FromResult 时则不会。比如缓存零任务:

public async Task<int> Z() => 0;

void Main()
{
    Console.WriteLine(Task.FromResult(0) == Task.FromResult(0)); // false
    Console.WriteLine(Z() == Z()); // true
}

因此,如果您经常 return 这些常用值,您可能会获得一些性能优势。

这样做的主要缺点是它会生成构建警告,因为您有一个没有等待的异步方法。您可以使用 #pragma 来抑制它,但这会使代码变得丑陋,并且可能仍然会混淆其他阅读代码的开发人员。另一个轻微的缺点是,在某些情况下,这可能会比手动构建任务的性能略差。例如,在传入的令牌被取消的情况下,我们必须通过抛出异常来传达这一点。

这使我们得出类似于您的第二个选项的结果,稍作调整以正确处理取消:

public Task<int> SomeMethodAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return Task.FromCanceled<int>(cancellationToken);
    }

    try { return Task.FromResult(SomeMethod()); }
    catch (OperationCanceledException oce)
    {
        var canceledTaskBuilder = new TaskCompletionSource<int>();
        canceledTaskBuilder.SetCanceled();
        return canceledTaskBuilder.Task;
    }
    catch (Exception e)
    {
        return Task.FromException<int>(e);
    }
}

这很笨拙,所以大多数时候我会选择前两个选项之一,或者编写一个辅助方法来包装第三个选项的代码。

我想补充一点,如果您在 SomeMethod 中执行任何 IO,您实际上并不想重用它(使用 Task.Run 或任何其他方法)。相反,您想使用异步 IO 重写它,因为使用异步的要点之一就是利用异步 IO。例如,假设您有:

public long SomeMethod(string url)
{
    var request = (HttpWebRequest)WebRequest.Create(url);
    var response = request.GetResponse();
    return response.ContentLength;
}

在这种情况下,您不想在 SomeMethodAsync 中重用此方法,因为您做 request.GetResponse(),这是 IO,并且它具有异步版本。所以你必须这样做:

public async Task<long> SomeMethodAsync(string url, CancellationToken cancellationToken) {
    var request = (HttpWebRequest) WebRequest.Create(url);
    using (cancellationToken.Register(() => request.Abort(), false)) {
        try {
            var response = await request.GetResponseAsync();
            return response.ContentLength;
        }
        catch (WebException ex) {
            if (cancellationToken.IsCancellationRequested)
                throw new OperationCanceledException(ex.Message, ex, cancellationToken);
            throw;
        }
    }
}

如您所见,在这种情况下它有点长(因为 GetResponseAsync 不接受取消令牌),但如果您使用任何 IO,这将是正确的方法。