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

时间:2016-11-19 14:55:28

标签: c# asynchronous

假设一个界面并考虑以下代码,它是我的合同

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

现在想象一下使用以下代码

合同实施ISomeContract
public class SomeImplementation : ISomeContract
    {
        public int SomeMethod()
        {
            // lots of codes ...
            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,但是如何才能获得这个问题的最佳解决方案呢?

2 个答案:

答案 0 :(得分:2)

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

首先,我们应该决定合同的消费者是否期望SomeMethodAsync()快速返回,因为它是异步方法。在这种情况下,如果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()速度相当快或者我们不关心快速返回(技术上异步方法无法保证),那么我们需要一些方法来模拟碰巧运行的异步方法的行为同步。特别是,故障/取消应导致故障/取消任务。一种方法是在显示时简单地使用异步方法:

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
}

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

这样做的主要缺点是它会生成一个构建警告,因为你有一个没有等待的异步方法。您可以使用#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);
    }
}

这非常笨重,所以大部分时间我都会使用前两个选项中的一个,或者编写一个辅助方法来包装第三个选项中的代码。

答案 1 :(得分:1)

我想补充一点,如果您在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,这将是正确的方法。