Чи повинен я хвилюватися з приводу попередження "Цей метод асинхронізації не має операторів" await "і буде працювати синхронно"


93

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

Я перебуваю в процесі впровадження цього інтерфейсу. Однак у деяких із цих методів ця реалізація не має на що чекати. З цієї причини я отримую попередження компілятора "Цей метод асинхронізації не має операторів 'await' і буде працювати синхронно ..."

Я розумію, чому я отримую помилку, але мені цікаво, чи слід мені щось робити з ними в цьому контексті. Неприємно ігнорувати попередження компілятора.

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

Чи повинен я просто ігнорувати попередження, чи є спосіб обійти це, чого я не бачу?


2
Це буде залежати від особливостей. Ви справді впевнені, що хочете, щоб ці операції виконувались синхронно? Якщо ви хочете, щоб вони виконувались синхронно, чому метод позначений як async?
Серві

11
Просто видаліть asyncключове слово. Ви все ще можете повернути Taskвикористання Task.FromResult.
Майкл Лю

1
@BenVoigt Google переповнений інформацією про нього, якщо ОП ще не знає.
Серві

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.Empty для цієї мети. Цей намір став би дещо зрозумілішим, і мені боляче думати про всі ці сумлінні Завдання, які повертають нуль, який ніколи не потрібен.
Руперт Раунслі

await Task.FromResult(0)? Як щодо await Task.Yield()?
Sushi271

1
@ Sushi271: Ні, у неметоді asyncти повертаєшся, Task.FromResult(0) а не чекаєш цього.
Michael Liu

1
Насправді НІ, асинхронізація - це не просто деталь реалізації, навколо потрібно знати багато деталей :). Потрібно знати, яка частина працює синхронно, яка частина асинхронно, який поточний контекст синхронізації, і лише для запису, Завдання завжди трохи швидші, оскільки за шторами немає автомата стану.
ipavlu

16

Цілком розумно, що деякі "асинхронні" операції виконуються синхронно, але все ж відповідають моделі асинхронного виклику заради поліморфізму.

Реальний приклад цього - API API вводу-виводу. Асинхронні та перекриваються дзвінки на деяких пристроях завжди виконуються вбудовано (наприклад, запис у конвеєр, реалізований із використанням спільної пам'яті). Але вони реалізують той самий інтерфейс, що і багатокомпонентні операції, які продовжуються у фоновому режимі.


4

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

Я збираюся відповісти на частину вашого запитання "Чи варто хвилюватися через попередження".

Відповідь - так!

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

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


5
Ця відповідь просто неправильна. Ось чому: awaitвсередині методу може бути принаймні один в одному місці (CS1998 не буде), але це не означає, що не буде жодного іншого виклику методу asnyc, який не матиме синхронізації (з використанням awaitчи будь-яким іншим). Тепер, якщо хтось хотів би знати, як переконатися, що ви випадково не пропустите синхронізацію, просто переконайтеся, що не ігноруєте ще одне попередження - CS4014. Я навіть рекомендую погрожувати цим як помилкою.
Віктор Ярема

3

Це може бути пізно, але може бути корисним розслідування:

Існує про внутрішню структуру скомпільованого коду ( IL ):

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

це стає в Іллінойсі:

.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 :

асинхронні методи повинні мати ключове слово await у своєму тілі, інакше вони ніколи не дадуть результату! Це важливо мати на увазі. Якщо await не використовується в тілі асинхронного методу, компілятор C # генерує попередження, але код компілюється і працює так, ніби це звичайний метод. Зауважте, що це також було б неймовірно неефективно, оскільки автомат стану, сформований компілятором C # для методу async, нічого не досяг би.


2
До того ж ваш остаточний висновок щодо використання async/awaitдуже спрощений, оскільки ви базуєте його на своєму нереальному прикладі однієї операції, яка пов'язана з процесором. Tasks при правильному використанні дозволяє покращити продуктивність програми та швидкість реагування завдяки паралельним завданням (тобто паралельним) та кращому управлінню та використанню потоків
MickyD

Це просто спрощений приклад тесту, як я вже говорив у цій публікації. Також я згадував про запити до api та хендлерів подій, де це можливо, використовуючи обидві версії методів (асинхронні та звичайні). Також PO сказав про використання асинхронних методів, не чекаючи всередині. У моєму дописі було про це, але не про правильне використання 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.

(Перехресний пост моєї відповіді на питання Коли асинхронне завдання <T> потрібне інтерфейсу, як отримати змінну повернення без попередження компілятора )

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.