如何使用 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,这将是正确的方法。
假设一个接口并考虑以下代码,这是我的合同
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,这将是正确的方法。