繁体   English   中英

如何使用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()
        {
            // 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,但是如何才能获得此问题的最佳解决方案?

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

首先,我们应该决定合同的消费者是否期望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采用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);
    }
}

这非常笨重,因此大多数时候我会使用前两个选项中的一个或编写一个帮助方法来包装第三个选项中的代码。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM