Apakah Anda harus meletakkan Task.Run dalam metode untuk membuatnya async?

304

Saya mencoba memahami async menunggu dalam bentuk yang paling sederhana. Saya ingin membuat metode yang sangat sederhana yang menambahkan dua angka demi contoh ini, memang, tidak ada waktu pemrosesan sama sekali, itu hanya masalah merumuskan contoh di sini.

Contoh 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Contoh 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Jika saya menunggu DoWork1Async()kode akan berjalan secara sinkron atau tidak sinkron?

Apakah saya perlu membungkus kode sinkronisasi dengan Task.Runagar metode ini bisa ditunggu DAN tidak sinkron agar tidak memblokir utas UI?

Saya mencoba mencari tahu apakah metode saya adalah Taskatau mengembalikan Task<T>apakah saya harus membungkus kode dengan Task.Runagar asinkron.

Pertanyaan bodoh saya yakin tetapi saya melihat contoh di internet di mana orang menunggu kode yang tidak memiliki async di dalam dan tidak dibungkus dengan a Task.Runatau StartNew.

Neal
sumber
30
Bukankah potongan pertama Anda memberi Anda peringatan?
svick

Jawaban:

587

Pertama, mari kita jelaskan beberapa terminologi: "asynchronous" ( async) berarti bahwa ia dapat menghasilkan kontrol kembali ke utas panggilan sebelum dimulai. Dalam suatu asyncmetode, titik "hasil" itu adalah awaitekspresi.

Ini sangat berbeda dari istilah "asinkron", seperti (mis) yang digunakan oleh dokumentasi MSDN selama bertahun-tahun yang berarti "dieksekusi di utas latar belakang".

Lebih lanjut membingungkan masalah, asyncsangat berbeda dari "ditunggu"; ada beberapa asyncmetode yang jenis pengembaliannya tidak menunggu, dan banyak metode mengembalikan jenis yang ditunggu tidak async.

Cukup tentang apa yang bukan mereka ; inilah yang mereka adalah :

  • Kata asynckunci memungkinkan metode asinkron (yaitu, memungkinkan awaitekspresi). asyncmetode dapat kembali Task,, Task<T>atau (jika Anda harus) void.
  • Jenis apa pun yang mengikuti pola tertentu dapat ditunggu. Jenis yang paling umum ditunggu adalah Taskdan Task<T>.

Jadi, jika kami merumuskan kembali pertanyaan Anda ke "bagaimana saya bisa menjalankan operasi pada utas latar belakang dengan cara yang bisa ditunggu", jawabannya adalah menggunakan Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Tetapi pola ini adalah pendekatan yang buruk; lihat di bawah).

Tetapi jika pertanyaan Anda adalah "bagaimana cara membuat asyncmetode yang dapat menghasilkan kembali ke pemanggilnya alih-alih memblokir", jawabannya adalah untuk mendeklarasikan metode asyncdan gunakan awaituntuk poin "menghasilkan":

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Jadi, pola dasar segala sesuatu adalah memiliki asynckode bergantung pada "yang dapat ditunggu" dalam awaitekspresi. "Barang yang bisa ditunggu-tunggu" ini bisa berupa asyncmetode lain atau hanya metode biasa untuk mengembalikan barang yang bisa ditunggu. Metode biasa kembali Task/ Task<T> dapat menggunakan Task.Rununtuk mengeksekusi kode di thread latar belakang, atau (lebih umum) mereka dapat menggunakan TaskCompletionSource<T>atau salah satu cara pintas nya ( TaskFactory.FromAsync, Task.FromResult, dll). Saya tidak merekomendasikan membungkus seluruh metode di Task.Run; Metode sinkron harus memiliki tanda tangan sinkron, dan harus diserahkan kepada konsumen apakah harus dibungkus dengan Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

Saya memiliki async/ awaitintro di blog saya; pada akhirnya adalah beberapa sumber daya tindak lanjut yang baik. Dokumen MSDN asyncjuga luar biasa bagus.

Stephen Cleary
sumber
8
@sgnsajgon: Ya. asyncmetode harus kembali Task, Task<T>atau void. Taskdan Task<T>bisa ditunggu; voidtidak.
Stephen Cleary
3
Sebenarnya, async voidtanda tangan metode akan dikompilasi, itu hanya ide yang cukup mengerikan ketika Anda kehilangan pointer ke tugas async Anda
IEatBagels
4
@ TopinFrassi: Ya, mereka akan mengkompilasi, tetapi voidtidak bisa ditunggu.
Stephen Cleary
4
@ohadinho: Tidak, apa yang saya bicarakan di posting blog adalah ketika seluruh metode hanyalah panggilan Task.Run(seperti DoWorkAsyncdalam jawaban ini). Menggunakan Task.Rununtuk memanggil metode dari konteks UI sesuai (seperti DoVariousThingsFromTheUIThreadAsync).
Stephen Cleary
2
Ya persis. Ini valid untuk digunakan Task.Rununtuk memanggil metode, tetapi jika ada Task.Runsekitar semua (atau hampir semua) kode metode, maka itu merupakan anti-pola - biarkan metode itu tetap sinkron dan Task.Runnaikkan level.
Stephen Cleary
22

Salah satu hal yang paling penting untuk diingat ketika mendekorasi suatu metode dengan async adalah bahwa setidaknya ada satu operator yang menunggu di dalam metode tersebut. Dalam contoh Anda, saya akan menerjemahkannya seperti yang ditunjukkan di bawah ini menggunakan TaskCompletionSource .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
sumber
26
Mengapa Anda menggunakan TaskCompletionSource, alih-alih hanya mengembalikan tugas yang dikembalikan oleh metode Task.Run () (dan mengubah tubuhnya untuk mengembalikan hasil)?
ironis
4
Hanya sebuah catatan. Metode yang memiliki tanda tangan "async void" umumnya merupakan praktik yang buruk dan dianggap sebagai kode buruk karena dapat menyebabkan kebuntuan UI dengan mudah. Pengecualian utama adalah pengendali acara asinkron.
Jazzeroki
12

Ketika Anda menggunakan Task.Run untuk menjalankan metode, Tugas mendapat utas dari threadpool untuk menjalankan metode itu. Jadi dari perspektif utas UI, ini "asinkron" karena tidak memblokir utas UI. Ini bagus untuk aplikasi desktop karena Anda biasanya tidak memerlukan banyak utas untuk menjaga interaksi pengguna.

Namun, untuk aplikasi web setiap permintaan dilayani oleh utas-utas dan dengan demikian jumlah permintaan aktif dapat ditingkatkan dengan menyimpan utas-utas tersebut. Sering menggunakan utas threadpool untuk mensimulasikan operasi async tidak dapat diskalakan untuk aplikasi web.

True Async tidak perlu melibatkan penggunaan utas untuk operasi I / O, seperti akses file / DB dll. Anda dapat membaca ini untuk memahami mengapa operasi I / O tidak memerlukan utas. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

Dalam contoh sederhana Anda, ini adalah perhitungan murni yang terikat CPU, jadi menggunakan Task.Run baik-baik saja.

zheng yu
sumber
Jadi jika saya harus mengkonsumsi api eksternal sinkron dalam web api controller, saya TIDAK boleh membungkus panggilan sinkron di Task.Run ()? Seperti yang Anda katakan, melakukan hal itu akan membuat utas permintaan awal tidak terblokir tetapi ia menggunakan utas pool lain untuk memanggil api eksternal. Sebenarnya saya pikir ini masih merupakan ide yang bagus karena secara teori dapat menggunakan dua utas untuk memproses banyak permintaan, misalnya satu utas dapat memproses banyak permintaan masuk dan satu lainnya dapat memanggil api eksternal untuk semua permintaan ini?
stt106
Saya setuju. Saya tidak mengatakan Anda tidak boleh benar-benar membungkus semua panggilan sinkron dalam Task.Run (). Saya hanya menunjukkan potensi masalah.
zheng yu
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()itu benar. Jika ya, Anda hanya perlu mengganti utas. yaitu Anda membuka blokir utas permintaan awal tetapi Anda mengambil utas lain dari threadpool yang bisa digunakan untuk memproses permintaan lain. Satu-satunya hasil adalah overhead saklar konteks ketika panggilan selesai untuk mendapatkan benar-benar nol
Saeb Amini