我是否应该担心“此异步方法缺少'await'运算符,并且将同步运行”警告


95

我有一个暴露一些异步方法的接口。更具体地说,它具有定义的返回Task或Task <T>的方法。我正在使用async / await关键字。

我正在实现此接口。但是,在其中某些方法中,此实现没有任何等待。因此,我收到编译器警告“此异步方法缺少'await'运算符,将同步运行...”

我知道为什么会收到错误,但想知道在这种情况下是否应该对它们采取任何措施。忽略编译器警告是错误的。

我知道我可以通过等待Task.Run来修复它,但是对于仅执行一些廉价操作的方法来说,这是错误的。听起来也好像会给执行增加不必要的开销,但是由于不确定async关键字,因此我也不确定是否已经存在。

我应该只是忽略警告,还是有办法解决我所没有看到的问题?


2
这将取决于具体情况。您确定要同步执行这些操作吗?如果确实希望它们同步执行,为什么将方法标记为async
2015年

11
只需删除async关键字。您仍然可以返回Taskusing Task.FromResult
Michael Liu

1
如果OP尚不知道,则@BenVoigt Google会提供有关此信息的完整信息。
Servy 2015年

1
@BenVoigt Michael Liu尚未提供该提示吗?使用Task.FromResult

1
@hvd:稍后将其编辑为他的评论。
Ben Voigt 2015年

Answers:


147

异步关键字仅仅是一个方法的实施方案的细节; 它不是方法签名的一部分。如果一个特定的方法实现或重写没有什么可等待的,则只需忽略async关键字,并使用Task.FromResult <TResult>返回完成的任务:

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

如果您的方法返回Task而不是Task <TResult>,那么您可以返回任何类型和值的已完成任务。Task.FromResult(0)似乎是一个流行的选择:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

或者,从.NET Framework 4.6开始,您可以返回Task.CompletedTask

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }

谢谢,我认为我所缺少的是创建已完成任务的概念,而不是返回实际的任务,就像您说的那样,该任务与使用async关键字相同。现在似乎很明显,但我只是没有看到!
dannykay1710 2015年

1
为此,Task可以使用Task.Empty的静态成员。意图会更清晰一些,让我想到所有这些勤勉的任务,这些任务会返回从不需要的零。
罗珀特·罗恩斯利

await Task.FromResult(0)?怎么await Task.Yield()
Sushi271 '16

1
@ Sushi271:不,在非async方法中,您返回 Task.FromResult(0)而不是等待它。
Michael Liu

1
实际上,不,异步不仅是实现细节,还有很多细节需要注意:)。必须知道,哪些部分是同步运行的,哪些部分是异步的,什么是当前的同步上下文,仅出于记录目的,任务总是快一点,因为没有幕后的状态机:)。
ipavlu'4

17

某些“异步”操作可以同步完成,但出于多态性考虑,仍然符合异步调用模型是完全合理的。

OS I / O API就是一个真实的例子。在某些设备上,异步调用和重叠调用总是内联完成(例如,写入使用共享内存实现的管道)。但是它们实现了与在后台继续进行的多部分操作相同的接口。


4

Michael Liu很好地回答了您有关如何避免警告的问题:通过返回Task.FromResult。

我将回答您问题的“我应该担心警告”部分。

答案是肯定的!

这样做的原因是,当您调用一个Task在没有await操作符的情况下返回异步方法内部的方法时,经常会出现警告。我只是修复了发生的并发错误,因为我在不等待上一个操作的情况下在Entity Framework中调用了一个操作。

如果您可以精心编写代码以避免编译器警告,那么当出现警告时,它将像疼痛的拇指一样突出。我本可以避免几个小时的调试。


5
这个答案是错误的。原因如下:await方法内至少有一个地方(没有CS1998),但这并不意味着不会有其他缺少同步的asnyc方法调用(使用await或其他方法)。现在,如果有人想知道如何确保您不会意外丢失同步,那么只需确保您不要忽略其他警告-CS4014。我什至建议威胁说那是错误。
维克多·亚雷玛

4

可能为时已晚,但可能对调查有用:

关于编译后的代码(IL)的内部结构:

 public static async Task<int> GetTestData()
    {
        return 12;
    }

它变成了IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

并且没有异步和任务方法:

 public static int GetTestData()
        {
            return 12;
        }

变成:

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

如您所见,这些方法之间的巨大差异。如果您不在异步方法中使用await并且不关心异步方法的使用(例如API调用或事件处理程序),那么好的主意会将其转换为普通的同步方法(这样可以节省应用程序的性能)。

更新:

还有来自Microsoft docs https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth的其他信息:

异步方法的主体中必须有一个await关键字,否则它们将永远不会屈服!注意这一点很重要。如果异步方法的主体中未使用await,则C#编译器将生成警告,但代码将像正常方法一样进行编译和运行。注意,由于C#编译器为async方法生成的状态机将无法完成任何工作,因此效率也非常低。


2
此外,关于的最终结论在很大程度上async/await是过分简化的,因为您将其基于不切实际的CPU限制操作示例。 Tasks正确使用时,由于并发任务(即并行)以及更好的线程管理和使用,可提高应用程序性能和响应能力
MickyD

正如我在本文中所说的,这只是测试简化的示例。我也提到了使用两种版本的方法(异步和常规)对api和事件处理程序的请求。PO还说过使用异步方法而不在内部等待。我的帖子与此有关,但与正确使用无关Tasks。不幸的是,您没有阅读文章的全文并快速得出结论。
奥列格·邦达连科

2
返回的方法int(如您的情况)与返回的方法(Task例如OP讨论的方法)之间是有区别的。再次阅读他的文章和接受的答案而不是亲自去做。在这种情况下,您的答案无济于事。您甚至不必费心去显示有await内部方法与没有内部方法之间的差异。现在,您是否做过,那将是非常值得进行
投票的

我想您真的不了解异步方法和用api或事件处理程序调用的常规方法之间的区别。在我的帖子中特别提到过。对不起,您又一次错过
奥列格·邦达连科

1

返回时注意异常行为 Task.FromResult

这是一个小演示,展示了标记和未标记的方法在异常处理方面的区别async

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(我的答案的交叉发布是接口需要异步Task <T>时,如何在没有编译器警告的情况下获取返回变量

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.