Perbedaan apa pun antara “await Task.Run (); kembali;" dan "return Task.Run ()"?

90

Apakah ada perbedaan konseptual antara dua bagian kode berikut:

async Task TestAsync() 
{
    await Task.Run(() => DoSomeWork());
}

dan

Task TestAsync() 
{
    return Task.Run(() => DoSomeWork());
}

Apakah kode yang dihasilkan juga berbeda?

EDIT: Untuk menghindari kebingungan dengan Task.Run, kasus serupa:

async Task TestAsync() 
{
    await Task.Delay(1000);
}

dan

Task TestAsync() 
{
    return Task.Delay(1000);
}

PEMBARUAN TERLAMBAT: Selain jawaban yang diterima, ada juga perbedaan dalam cara LocalCallContextpenanganannya: CallContext.LogicalGetData dipulihkan meskipun tidak ada asinkron. Mengapa?

avo
sumber
1
Ya, ini berbeda. Dan itu sangat berbeda. jika tidak, tidak ada gunanya menggunakan await/ asyncsama sekali :)
MarcinJuraszek
1
Saya rasa ada dua pertanyaan di sini. 1. Apakah implementasi sebenarnya dari metode ini penting bagi pemanggilnya? 2. Apakah representasi yang dikompilasi dari dua metode berbeda?
DavidRR

Jawaban:

80

Satu perbedaan utama adalah dalam propagasi pengecualian. Pengecualian, dilemparkan dalam sebuah async Taskmetode, akan disimpan dalam kembali Taskobjek dan tetap aktif sampai tugas akan diamati melalui await task, task.Wait(), task.Resultatau task.GetAwaiter().GetResult(). Hal ini diperbanyak dengan cara ini bahkan jika terlempar dari sinkron bagian dari asyncmetode.

Pertimbangkan kode berikut, di mana OneTestAsyncdan AnotherTestAsyncberperilaku sangat berbeda:

static async Task OneTestAsync(int n)
{
    await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
    return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
    Task task = null;
    try
    {
        // start the task
        task = whatTest(n);

        // do some other stuff, 
        // while the task is pending
        Console.Write("Press enter to continue");
        Console.ReadLine();
        task.Wait();
    }
    catch (Exception ex)
    {
        Console.Write("Error: " + ex.Message);
    }
}

Jika saya memanggil DoTestAsync(OneTestAsync, -2), itu menghasilkan keluaran berikut:

Tekan enter untuk melanjutkan
Error: Terjadi satu atau lebih error. Tunggu Task.Delay
Kesalahan: 2nd

Catatan, saya harus menekan Enteruntuk melihatnya.

Sekarang, jika saya panggil DoTestAsync(AnotherTestAsync, -2), alur kerja kode di dalamnya DoTestAsyncsangat berbeda, dan begitu juga hasilnya. Kali ini, saya tidak diminta untuk menekan Enter:

Kesalahan: Nilai harus -1 (menandakan waktu tunggu tak terbatas), 0 atau bilangan bulat positif.
Nama parameter: millisecondsDelayError: 1st

Dalam kedua kasus, Task.Delay(-2)lempar di awal, sambil memvalidasi parameternya. Ini mungkin skenario yang dibuat-buat, tetapi secara teori Task.Delay(1000)mungkin juga terjadi, misalnya, ketika API pengatur waktu sistem yang mendasarinya gagal.

Di samping catatan, logika penyebaran kesalahan masih berbeda untuk async voidmetode (sebagai lawan async Taskmetode). Pengecualian yang dimunculkan di dalam async voidmetode akan segera ditampilkan kembali pada konteks sinkronisasi thread saat ini (melalui SynchronizationContext.Post), jika thread saat ini memiliki satu ( SynchronizationContext.Current != null). Jika tidak, akan ditampilkan kembali melalui ThreadPool.QueueUserWorkItem). Penelepon tidak memiliki kesempatan untuk menangani pengecualian ini pada bingkai tumpukan yang sama.

Saya memposting beberapa detail lebih lanjut tentang perilaku penanganan pengecualian TPL di sini dan di sini .


T : Apakah mungkin meniru perilaku propagasi pengecualian dari asyncmetode untuk Taskmetode berbasis non-asinkron , sehingga metode berbasis asinkron tidak menampilkan bingkai tumpukan yang sama?

A : Jika memang dibutuhkan, maka ya, ada trik untuk itu:

// async
async Task<int> MethodAsync(int arg)
{
    if (arg < 0)
        throw new ArgumentException("arg");
    // ...
    return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
    var task = new Task<int>(() => 
    {
        if (arg < 0)
            throw new ArgumentException("arg");
        // ...
        return 42 + arg;
    });

    task.RunSynchronously(TaskScheduler.Default);
    return task;
}

Namun perlu diperhatikan, dalam kondisi tertentu (seperti jika terlalu dalam di tumpukan), RunSynchronouslymasih dapat dijalankan secara asinkron.


Perbedaan penting lain adalah bahwa yang async/ awaitversi lebih rentan terhadap mati-penguncian pada konteks sinkronisasi non-default . Misalnya, berikut ini akan dead-lock di aplikasi WinForms atau WPF:

static async Task TestAsync()
{
    await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
    TestAsync().Wait(); // dead-lock here
}

Ubah ke versi non-asinkron dan tidak akan mengunci:

Task TestAsync() 
{
    return Task.Delay(1000);
}

Sifat kunci buntu dijelaskan dengan baik oleh Stephen Cleary di blognya .

noseratio
sumber
2
Saya yakin kebuntuan pada contoh pertama dapat dihindari dengan menambahkan .ConfigureAwait (false) ke baris await, karena ini hanya terjadi karena metode tersebut mencoba kembali ke konteks eksekusi yang sama. Jadi pengecualian adalah satu-satunya perbedaan yang tersisa.
relative_random
2
@relatives_random, komentar Anda benar, meskipun jawabannya adalah tentang perbedaan antara return Task.Run()dan await Task.Run(); return, bukanawait Task.Run().ConfigureAwait(false); return
noseratio
Jika Anda menemukan program ditutup setelah Anda menekan Enter, pastikan Anda melakukan ctrl + F5, bukan F5.
David Klempfner
54

Apa perbedaan antara

async Task TestAsync() 
{
    await Task.Delay(1000);
}

dan

Task TestAsync() 
{
    return Task.Delay(1000);
}

?

Saya bingung dengan pertanyaan ini. Izinkan saya mencoba menjelaskan dengan menanggapi pertanyaan Anda dengan pertanyaan lain. Apa perbedaannya?

Func<int> MakeFunction()
{
    Func<int> f = ()=>1;
    return ()=>f();
}

dan

Func<int> MakeFunction()
{
    return ()=>1;
}

?

Apapun perbedaan antara kedua hal saya, perbedaan yang sama adalah antara kedua hal Anda.

Eric Lippert
sumber
23
Tentu saja! Anda telah membuka mata saya :) Dalam kasus pertama, saya membuat tugas pembungkus, secara semantik dekat dengan Task.Delay(1000).ContinueWith(() = {}). Yang kedua, hanya saja Task.Delay(1000). Perbedaannya agak halus, tetapi signifikan.
avo
3
Bisakah Anda menjelaskan sedikit perbedaannya? sebenarnya saya tidak .. Terima kasih
zheng yu
4
Mengingat ada perbedaan halus dengan konteks sinkronisasi, dan propagasi pengecualian, saya akan mengatakan perbedaan antara async / await dan pembungkus fungsi tidak sama.
Cameron MacFarland
1
@CameronMacFarland: Itu sebabnya saya meminta klarifikasi. Pertanyaannya adalah apakah ada perbedaan konseptual antara keduanya. Saya tidak tahu. Tentu ada banyak perbedaan; apakah ada di antara mereka yang dianggap sebagai perbedaan "konseptual"? Dalam contoh saya dengan funcs bersarang, ada juga perbedaan dalam propagasi kesalahan; jika fungsi ditutup atas status lokal, ada perbedaan dalam masa hidup lokal, dan seterusnya. Apakah perbedaan "konseptual" ini?
Eric Lippert
6
Ini adalah jawaban lama, tapi saya yakin diberikan hari ini, akan mendapat suara negatif. Itu tidak menjawab pertanyaan, juga tidak mengarahkan OP ke sumber yang bisa dia pelajari.
Daniel Dubovski
11
  1. Metode pertama bahkan tidak dapat dikompilasi.

    Karena ' Program.TestAsync()' adalah metode async yang mengembalikan ' Task', kata kunci return tidak boleh diikuti oleh ekspresi objek. Apakah Anda berniat untuk kembali ' Task<T>'?

    Itu harus

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Ada perbedaan konseptual utama antara keduanya. Yang pertama tidak sinkron, yang kedua tidak. Baca Performa Asinkron: Memahami Biaya Asinkron dan Menunggu untuk mendapatkan lebih banyak informasi tentang internal async/ await.

  3. Mereka menghasilkan kode yang berbeda.

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
            01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
            2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
            79 6e 63 3e 64 5f 5f 31 00 00
        )
        .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x216c
        // Code size 62 (0x3e)
        .maxstack 2
        .locals init (
            [0] valuetype SOTestProject.Program/'<TestAsync>d__1',
            [1] class [mscorlib]System.Threading.Tasks.Task,
            [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
        )
    
        IL_0000: ldloca.s 0
        IL_0002: ldarg.0
        IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
        IL_0008: ldloca.s 0
        IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
        IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0014: ldloca.s 0
        IL_0016: ldc.i4.m1
        IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
        IL_001c: ldloca.s 0
        IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0023: stloc.2
        IL_0024: ldloca.s 2
        IL_0026: ldloca.s 0
        IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
        IL_002d: ldloca.s 0
        IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
        IL_0039: stloc.1
        IL_003a: br.s IL_003c
    
        IL_003c: ldloc.1
        IL_003d: ret
    } // end of method Program::TestAsync
    

    dan

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed 
    {
        // Method begins at RVA 0x21d8
        // Code size 23 (0x17)
        .maxstack 2
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
        IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
        IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
        IL_0012: stloc.0
        IL_0013: br.s IL_0015
    
        IL_0015: ldloc.0
        IL_0016: ret
    } // end of method Program::TestAsync2
    
MarcinJuraszek
sumber
@MarcinJuraszek, memang tidak dikompilasi. Itu salah ketik, saya yakin Anda benar. Jika tidak, jawaban yang bagus, terima kasih! Saya pikir C # mungkin cukup pintar untuk menghindari menghasilkan kelas mesin negara dalam kasus pertama.
avo
9

Kedua contoh itu berbeda. Ketika sebuah metode ditandai dengan asynckata kunci, kompilator menghasilkan mesin negara di belakang layar. Inilah yang bertanggung jawab untuk melanjutkan kelanjutan setelah menunggu ditunggu.

Sebaliknya, ketika sebuah metode tidak ditandai dengan asyncAnda kehilangan kemampuan untuk awaitmenunggu. (Artinya, di dalam metode itu sendiri; metode tersebut masih bisa ditunggu oleh pemanggilnya.) Namun, dengan menghindari asynckata kunci, Anda tidak lagi membuat mesin negara, yang dapat menambahkan sedikit overhead (mengangkat penduduk lokal ke bidang dari mesin negara, objek tambahan ke GC).

Dalam contoh seperti ini, jika Anda dapat menghindari async-awaitdan mengembalikan awaitable secara langsung, ini harus dilakukan untuk meningkatkan efisiensi metode.

Lihat pertanyaan ini dan jawaban ini yang sangat mirip dengan pertanyaan Anda dan jawaban ini.

Lukazoid
sumber