Стоит ли мне беспокоиться о предупреждении «В этом асинхронном методе отсутствуют операторы await и он будет работать синхронно»

93

У меня есть интерфейс, который предоставляет некоторые асинхронные методы. В частности, в нем определены методы, возвращающие либо Task, либо Task <T>. Я использую ключевые слова async / await.

Я в процессе реализации этого интерфейса. Однако в некоторых из этих методов этой реализации нечего ждать. По этой причине я получаю предупреждение компилятора: «В этом асинхронном методе отсутствуют операторы await, и он будет работать синхронно ...»

Я понимаю, почему я получаю сообщение об ошибке, но мне интересно, должен ли я что-нибудь с ними делать в этом контексте. Неправильно игнорировать предупреждения компилятора.

Я знаю, что могу исправить это, ожидая выполнения Task.Run, но это неправильно для метода, который выполняет всего несколько недорогих операций. Также похоже, что это добавит ненужные накладные расходы на выполнение, но я также не уверен, есть ли это уже, потому что присутствует ключевое слово async.

Должен ли я просто игнорировать предупреждения или есть способ обойти это, которого я не вижу?

dannykay1710
источник
2
Это будет зависеть от специфики. Вы действительно уверены, что хотите, чтобы эти операции выполнялись синхронно? Если вы действительно хотите, чтобы они выполнялись синхронно, почему метод отмечен как async?
Servy
11
Просто удалите asyncключевое слово. Вы все еще можете вернуть Taskusing Task.FromResult.
Майкл Лю
1
@BenVoigt Google полон информации об этом, если OP еще не знает.
Servy
1
@BenVoigt Разве Майкл Лю уже не намекнул? Используйте Task.FromResult.
1
@hvd: Позже это было отредактировано в его комментарии.
Бен Войт

Ответы:

144

Асинхронная ключевое слово является лишь деталью реализации способа; это не часть сигнатуры метода. Если при реализации или переопределении одного конкретного метода нечего ожидать, просто опустите ключевое слово 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
1
Для этой цели Task может использоваться статический член типа Task.Empty. Намерение было бы немного яснее, и мне больно думать обо всех этих послушных Задачах, которые возвращают ноль, который никогда не нужен.
Руперт Ронсли, 08
await Task.FromResult(0)? Как насчет await Task.Yield()?
Sushi271
1
@ Sushi271: Нет, без asyncметода вы возвращаетесь Task.FromResult(0) вместо того, чтобы ждать его.
Майкл Лю,
1
На самом деле НЕТ, async - это не просто деталь реализации, есть много деталей, о которых нужно знать :). Нужно знать, какая часть выполняется синхронно, какая асинхронно, каков текущий контекст синхронизации и только для записи. Задачи всегда немного быстрее, так как за занавесками нет конечного автомата :).
ipavlu 09
16

Совершенно разумно, что некоторые «асинхронные» операции выполняются синхронно, но все же соответствуют модели асинхронного вызова ради полиморфизма.

Реальный пример этого - API-интерфейсы ввода-вывода ОС. Асинхронные и перекрывающиеся вызовы на некоторых устройствах всегда завершаются встроенными (например, запись в конвейер, реализованный с использованием общей памяти). Но они реализуют тот же интерфейс, что и операции с несколькими частями, которые продолжаются в фоновом режиме.

Бен Фойгт
источник
4

Майкл Лю хорошо ответил на ваш вопрос о том, как можно избежать предупреждения: вернув Task.FromResult.

Я собираюсь ответить на часть вашего вопроса «Стоит ли мне беспокоиться о предупреждении».

Ответ - да!

Причина этого в том, что предупреждение часто возникает, когда вы вызываете метод, который возвращается Taskвнутри асинхронного метода без awaitоператора. Я только что исправил ошибку параллелизма, возникшую из-за того, что я вызвал операцию в Entity Framework, не дожидаясь предыдущей операции.

Если вы можете тщательно написать свой код, чтобы избежать предупреждений компилятора, то при появлении предупреждения оно будет выделяться, как больной палец. Я мог бы избежать нескольких часов отладки.

Река Вивиан
источник
5
Это неверный ответ. И вот почему: awaitвнутри метода может быть хотя бы один в одном месте (CS1998 не будет), но это не значит, что не будет другого вызова метода asnyc, в котором не будет синхронизации (using awaitили любой другой). Теперь, если кто-то хочет знать, как убедиться, что вы случайно не пропустите синхронизацию, просто убедитесь, что вы не проигнорируете другое предупреждение - CS4014. Я бы даже рекомендовал угрожать этому как ошибкой.
Виктор Ярема
3

Может быть, уже слишком поздно, но расследование может оказаться полезным:

О внутренней структуре скомпилированного кода ( 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 https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

async-методы должны иметь ключевое слово await в своем теле, иначе они никогда не сработают! Об этом важно помнить. Если ожидание не используется в теле асинхронного метода, компилятор C # выдаст предупреждение, но код будет компилироваться и запускаться, как если бы это был обычный метод. Обратите внимание, что это также было бы невероятно неэффективным, поскольку конечный автомат, сгенерированный компилятором C # для метода async, ничего не будет выполнять.

Олег Бондаренко
источник
2
Кроме того, ваш окончательный вывод об использовании async/awaitзначительно упрощен, поскольку вы основываете его на своем нереалистичном примере единственной операции, связанной с процессором. Tasks при правильном использовании позволяет повысить производительность и
скорость
Это всего лишь упрощенный тестовый пример, как я сказал в этом посте. Также я упомянул о запросах к api и хендлерам событий, где возможно использование обеих версий методов (асинхронных и обычных). Также ПО сказал об использовании асинхронных методов без ожидания внутри. Мой пост был об этом, но не о правильном использовании Tasks. Печальная история, что вы не читаете весь текст сообщения и не делаете быстрых выводов.
Олег Бондаренко
1
Существует разница между методом, который возвращает int(как в вашем случае), и методом, который возвращает, Taskнапример, как обсуждалось OP. Прочтите его сообщение и принятый ответ еще раз, вместо того, чтобы принимать вещи лично. Ваш ответ в этом случае бесполезен. Вы даже не удосужитесь показать разницу между методом, имеющимся awaitвнутри, или нет. Если бы вы это сделали, это было бы очень хорошо, и за это стоит проголосовать
MickyD
Я думаю, вы действительно не понимаете разницы между асинхронным методом и обычными методами, которые вызываются с помощью 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.

(Перекрестное сообщение моего ответа на вопрос, когда async Task <T> требуется интерфейсом, как получить возвращаемую переменную без предупреждения компилятора )

тымтам
источник