Bagaimana Anda membuat metode asinkron dalam C #?

196

Setiap posting blog yang saya baca memberi tahu Anda cara mengonsumsi metode asinkron dalam C #, tetapi untuk beberapa alasan aneh tidak pernah menjelaskan cara membangun metode asinkron Anda sendiri untuk dikonsumsi. Jadi saya memiliki kode ini sekarang yang menggunakan metode saya:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

Dan saya menulis metode ini yaitu CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

Apakah ini, penggunaan Task.Factory, cara terbaik untuk menulis metode asinkron, atau haruskah saya menulis dengan cara lain?

Khalid Abuhakmeh
sumber
22
Saya mengajukan pertanyaan umum tentang bagaimana menyusun suatu metode. Saya hanya ingin tahu harus mulai dari mana dalam mengubah metode yang sudah sinkron menjadi asinkron.
Khalid Abuhakmeh
2
OK, jadi apa metode sinkron khas lakukan , dan mengapa Anda ingin membuatnya asynchronous ?
Eric Lippert
Katakanlah saya harus mengelompokkan banyak file dan mengembalikan objek hasil.
Khalid Abuhakmeh
1
OK, jadi: (1) apa operasi latensi tinggi: mendapatkan file - karena jaringan bisa lambat, atau apa pun - atau melakukan pemrosesan - karena CPU intensif, katakanlah. Dan (2) Anda masih belum mengatakan mengapa Anda ingin itu asinkron. Apakah ada utas UI yang tidak ingin Anda blokir, atau apa?
Eric Lippert
21
@EricLippert Contoh yang diberikan oleh op sangat mendasar, itu benar-benar tidak perlu yang rumit.
David B.

Jawaban:

226

Saya tidak menyarankan StartNewkecuali Anda membutuhkan tingkat kompleksitas itu.

Jika metode async Anda bergantung pada metode async lainnya, pendekatan termudah adalah menggunakan asynckata kunci:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Jika metode async Anda melakukan pekerjaan CPU, Anda harus menggunakan Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Anda mungkin menemukan async/ awaitintro saya bermanfaat.

Stephen Cleary
sumber
10
@Stephen: "Jika metode async Anda bergantung pada metode async lainnya" - ok, tetapi bagaimana jika itu tidak terjadi. Bagaimana jika sedang mencoba untuk membungkus beberapa kode yang memanggil BeginInvoke dengan beberapa kode panggilan balik?
Ricibob
1
@Ricibob: Anda harus menggunakannya TaskFactory.FromAsyncuntuk membungkus BeginInvoke. Tidak yakin apa yang Anda maksud tentang "kode panggilan balik"; jangan ragu untuk mengirimkan pertanyaan Anda sendiri dengan kode.
Stephen Cleary
@Stephen: Terima kasih - ya TaskFactory.FromAsync adalah apa yang saya cari.
Ricibob
1
Stephen, dalam for loop, apakah iterasi berikutnya dipanggil segera atau setelah Task.Delaypengembalian?
jp2code
3
@ jp2code: awaitadalah "asynchronous wait", jadi tidak melanjutkan ke iterasi berikutnya sampai setelah tugas dikembalikan dengan Task.Delayselesai.
Stephen Cleary
11

Jika Anda tidak ingin menggunakan async / menunggu di dalam metode Anda, tetapi masih "hiasi" itu agar dapat menggunakan kata kunci tunggu dari luar, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

Dari sini dan sini

Untuk mendukung paradigma seperti itu dengan Tugas, kita perlu cara untuk mempertahankan façade Tugas dan kemampuan untuk merujuk pada operasi asinkron yang sewenang-wenang sebagai suatu Tugas, tetapi untuk mengontrol masa kerja Tugas tersebut sesuai dengan aturan infrastruktur dasar yang menyediakan sinkronisasi, dan untuk melakukannya dengan cara yang tidak memerlukan biaya yang signifikan. Ini adalah tujuan dari TaskCompletionSource.

Saya melihat juga digunakan dalam sumber NET misalnya. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Akhirnya, saya menemukan berguna juga sebagai berikut:

Saya selalu ditanya pertanyaan ini. Implikasinya adalah bahwa harus ada beberapa utas di suatu tempat yang memblokir panggilan I / O ke sumber daya eksternal. Jadi, kode asinkron membebaskan utas permintaan, tetapi hanya dengan mengorbankan utas lain di sistem, kan? Tidak, tidak sama sekali. Untuk memahami mengapa skala permintaan asinkron, saya akan melacak (disederhanakan) contoh panggilan I / O asinkron. Katakanlah permintaan perlu menulis ke file. Utas permintaan memanggil metode penulisan asinkron. WriteAsync diimplementasikan oleh Base Class Library (BCL), dan menggunakan port penyelesaian untuk I / O yang tidak sinkron. Jadi, panggilan WriteAsync diturunkan ke OS sebagai file asynchronous tulis. OS kemudian berkomunikasi dengan tumpukan driver, meneruskan data untuk menulis dalam paket permintaan I / O (IRP). Di sinilah hal-hal menjadi menarik: Jika driver perangkat tidak dapat menangani IRP segera, ia harus menanganinya secara tidak sinkron. Jadi, pengemudi memberi tahu disk untuk mulai menulis dan mengembalikan respons "pending" ke OS. OS meneruskan respons "pending" ke BCL, dan BCL mengembalikan tugas yang tidak lengkap ke kode penanganan permintaan. Kode penanganan permintaan menunggu tugas, yang mengembalikan tugas tidak lengkap dari metode itu dan seterusnya. Akhirnya, kode penanganan permintaan akhirnya mengembalikan tugas yang tidak lengkap ke ASP.NET, dan utas permintaan dibebaskan untuk kembali ke kumpulan utas. Kode penanganan permintaan menunggu tugas, yang mengembalikan tugas tidak lengkap dari metode itu dan seterusnya. Akhirnya, kode penanganan permintaan akhirnya mengembalikan tugas yang tidak lengkap ke ASP.NET, dan utas permintaan dibebaskan untuk kembali ke kumpulan utas. Kode penanganan permintaan menunggu tugas, yang mengembalikan tugas tidak lengkap dari metode itu dan seterusnya. Akhirnya, kode penanganan permintaan akhirnya mengembalikan tugas yang tidak lengkap ke ASP.NET, dan utas permintaan dibebaskan untuk kembali ke kumpulan utas.

Pengantar Async / Menunggu di ASP.NET

Jika targetnya adalah untuk meningkatkan skalabilitas (bukan responsif), semuanya bergantung pada keberadaan I / O eksternal yang memberikan peluang untuk melakukan itu.

Alberto
sumber
1
Jika Anda akan melihat komentar di atas implementasi Task.Run misalnya Antrian pekerjaan yang ditentukan untuk dijalankan di ThreadPool dan mengembalikan pegangan Tugas untuk pekerjaan itu . Anda sebenarnya melakukan hal yang sama. Saya yakin bahwa Task.Run akan lebih baik, karena itu mengelola secara internal CancelationToken dan membuat beberapa optimasi dengan penjadwalan dan menjalankan opsi.
unsafePtr
jadi apa yang Anda lakukan dengan ThreadPool.QueueUserWorkItem dapat dilakukan dengan Task.Run, kan?
Razvan
-1

Salah satu cara yang sangat sederhana untuk membuat metode asinkron adalah dengan menggunakan metode Task.Yield (). Seperti yang dinyatakan MSDN:

Anda dapat menggunakan menunggu Task.Yield (); dalam metode asinkron untuk memaksa metode untuk menyelesaikan asinkron.

Masukkan di awal metode Anda dan kemudian akan segera kembali ke pemanggil dan menyelesaikan sisa metode di utas lainnya.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
SalgoMato
sumber
Dalam aplikasi WinForms sisa metode ini akan selesai di utas yang sama (utas UI). Ini Task.Yield()memang berguna, untuk kasus-kasus yang Anda ingin memastikan bahwa yang dikembalikan Tasktidak akan segera selesai pada saat penciptaan.
Theodor Zoulias