Ketika benar menggunakan Task.Run dan ketika hanya async-tunggu

317

Saya ingin menanyakan pendapat Anda tentang arsitektur yang benar kapan harus digunakan Task.Run . Saya mengalami UI lamban dalam aplikasi WPF .NET 4.5 kami (dengan kerangka kerja Caliburn Micro).

Pada dasarnya saya lakukan (cuplikan kode yang sangat sederhana):

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

Dari artikel / video yang saya baca / lihat, saya tahu itu await asyncbelum tentu berjalan di utas latar dan untuk mulai bekerja di latar belakang Anda harus membungkusnya dengan menunggu Task.Run(async () => ... ). Menggunakan async awaittidak memblokir UI, tetapi tetap berjalan di utas UI, sehingga membuatnya tertinggal.

Di mana tempat terbaik untuk meletakkan Task.Run?

Haruskah saya hanya

  1. Bungkus panggilan luar karena ini kurang bekerja threading untuk .NET

  2. , atau haruskah saya membungkus hanya metode terikat CPU yang berjalan secara internal Task.Runkarena ini membuatnya dapat digunakan kembali untuk tempat lain? Saya tidak yakin di sini jika mulai mengerjakan utas latar belakang jauh di inti adalah ide yang bagus.

Ad (1), solusi pertama akan seperti ini:

public async void Handle(SomeMessage message)
{
    ShowLoadingAnimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    HideLoadingAnimation();
}

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

Ad (2), solusi kedua akan seperti ini:

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}
Lukas K
sumber
BTW, baris dalam (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());seharusnya await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK Anda tidak mendapatkan apa-apa dengan menambahkan sedetik awaitdan asyncdalam Task.Run. Dan karena Anda tidak melewatkan parameter, itu menyederhanakan sedikit lebih banyak await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve
sebenarnya ada sedikit perbedaan jika Anda memiliki kedua menunggu di dalam. Lihat artikel ini . Saya merasa sangat berguna, hanya dengan poin khusus ini saya tidak setuju dan lebih suka mengembalikan Tugas secara langsung daripada menunggu. (seperti yang Anda sarankan dalam komentar Anda)
Lukas K

Jawaban:

365

Perhatikan pedoman untuk melakukan pekerjaan pada utas UI , dikumpulkan di blog saya:

  • Jangan blokir utas UI lebih dari 50 ms sekaligus.
  • Anda dapat menjadwalkan ~ 100 lanjutan pada utas UI per detik; 1000 terlalu banyak.

Ada dua teknik yang harus Anda gunakan:

1) Gunakan ConfigureAwait(false)saat Anda bisa.

Misalnya, await MyAsync().ConfigureAwait(false);bukannya await MyAsync();.

ConfigureAwait(false)memberitahu awaitbahwa Anda tidak perlu melanjutkan pada konteks saat ini (dalam hal ini, "pada konteks saat ini" berarti "pada utas UI"). Namun, untuk sisa asyncmetode itu (setelah ConfigureAwait), Anda tidak dapat melakukan apa pun yang mengasumsikan Anda berada dalam konteks saat ini (misalnya, memperbarui elemen UI).

Untuk informasi lebih lanjut, lihat artikel MSDN saya Praktik Terbaik di Pemrograman Asinkron .

2) Gunakan Task.Rununtuk memanggil metode yang terikat CPU.

Anda harus menggunakan Task.Run, tetapi tidak dalam kode apa pun yang Anda ingin dapat digunakan kembali (yaitu, kode perpustakaan). Jadi Anda gunakan Task.Rununtuk memanggil metode, bukan sebagai bagian dari implementasi metode.

Jadi pekerjaan yang terikat CPU murni akan terlihat seperti ini:

// Documentation: This method is CPU-bound.
void DoWork();

Yang akan Anda panggil menggunakan Task.Run:

await Task.Run(() => DoWork());

Metode yang merupakan campuran dari CPU-terikat dan I / O-terikat harus memiliki Asynctanda tangan dengan dokumentasi menunjukkan sifat terikat-CPU mereka:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Yang juga akan Anda panggil menggunakan Task.Run(karena sebagian terikat CPU):

await Task.Run(() => DoWorkAsync());
Stephen Cleary
sumber
4
Terima kasih atas tanggapan cepat Anda! Saya tahu tautan yang Anda poskan dan melihat video yang dirujuk di blog Anda. Sebenarnya itu sebabnya saya memposting pertanyaan ini - dalam video dikatakan (sama seperti dalam respons Anda) Anda tidak boleh menggunakan Task.Run dalam kode inti. Tetapi masalah saya adalah, bahwa saya perlu membungkus metode tersebut setiap kali saya menggunakannya untuk tidak memperlambat respon (harap dicatat semua kode saya async dan tidak memblokir, tetapi tanpa Thread. Jalankan itu hanya lamban). Saya juga bingung apakah itu pendekatan yang lebih baik untuk hanya membungkus metode terikat CPU (banyak panggilan Task.Run) atau menyelesaikan semuanya dalam satu Task.Run?
Lukas K
12
Semua metode perpustakaan Anda harus menggunakan ConfigureAwait(false). Jika Anda melakukannya pertama kali, maka Anda mungkin merasa itu Task.Runsama sekali tidak perlu. Jika Anda tidak masih perlu Task.Run, maka hal itu tidak membuat banyak perbedaan untuk runtime dalam hal ini apakah Anda menyebutnya sekali atau berkali-kali, jadi hanya melakukan apa yang paling alami untuk kode Anda.
Stephen Cleary
2
Saya tidak mengerti bagaimana teknik pertama akan membantunya. Bahkan jika Anda menggunakan ConfigureAwait(false)metode cpu-terikat Anda, itu masih thread UI yang akan melakukan metode cpu-terikat, dan hanya semuanya setelah itu dapat dilakukan pada thread TP. Atau apakah saya salah paham akan sesuatu?
Darius
4
@ user4205580: Tidak, Task.Runmengerti tanda tangan asinkron, jadi tidak akan selesai sampai DoWorkAsyncselesai. Ekstra async/ awaittidak perlu. Saya menjelaskan lebih banyak tentang "mengapa" dalam seri blogTask.Run saya tentang etiket .
Stephen Cleary
3
@ user4205580: Tidak. Sebagian besar metode async "inti" tidak menggunakannya secara internal. Cara normal untuk menerapkan metode async "inti" adalah dengan menggunakan TaskCompletionSource<T>atau salah satu dari notasi singkatnya seperti FromAsync. Saya memiliki posting blog yang lebih detail mengapa metode async tidak memerlukan utas .
Stephen Cleary
12

Satu masalah dengan ContentLoader Anda adalah bahwa ia beroperasi secara berurutan. Pola yang lebih baik adalah memparalelkan pekerjaan dan kemudian melakukan sinkronisasi pada akhirnya, jadi kita dapatkan

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List<Task>();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

Jelas, ini tidak berfungsi jika ada tugas yang membutuhkan data dari tugas sebelumnya yang lain, tetapi seharusnya memberikan Anda hasil keseluruhan yang lebih baik untuk sebagian besar skenario.

Paul Hatcher
sumber