Saya baru saja membuat pengamatan yang aneh tentang Task.WhenAll
metode ini, ketika berjalan di .NET Core 3.0. Saya melewati Task.Delay
tugas sederhana sebagai argumen tunggal Task.WhenAll
, dan saya berharap bahwa tugas yang dibungkus akan berperilaku identik dengan tugas asli. Tapi ini bukan masalahnya. Kelanjutan dari tugas asli dijalankan secara tidak sinkron (yang diinginkan), dan kelanjutan dari beberapa Task.WhenAll(task)
pembungkus dijalankan secara sinkron satu per satu (yang tidak diinginkan).
Ini adalah demo dari perilaku ini. Empat tugas pekerja sedang menunggu Task.Delay
tugas yang sama untuk diselesaikan, dan kemudian melanjutkan dengan perhitungan yang berat (disimulasikan oleh a Thread.Sleep
).
var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");
await task;
//await Task.WhenAll(task);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");
Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Ini outputnya. Keempat kelanjutan berjalan seperti yang diharapkan di utas yang berbeda (secara paralel).
05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
Sekarang jika saya berkomentar baris await task
dan batalkan komentar pada baris berikut await Task.WhenAll(task)
, hasilnya sangat berbeda. Semua lanjutan berjalan di utas yang sama, sehingga perhitungannya tidak diparalelkan. Setiap perhitungan dimulai setelah penyelesaian yang sebelumnya:
05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await
Anehnya ini hanya terjadi ketika setiap pekerja menunggu pembungkus yang berbeda. Jika saya mendefinisikan pembungkus di muka:
var task = Task.WhenAll(Task.Delay(500));
... dan kemudian await
tugas yang sama di dalam semua pekerja, perilaku identik dengan kasus pertama (kelanjutan asinkron).
Pertanyaan saya adalah: mengapa ini terjadi? Apa yang menyebabkan kelanjutan dari pembungkus berbeda dari tugas yang sama untuk dieksekusi di utas yang sama, secara serempak?
Catatan: menyelesaikan tugas dengan Task.WhenAny
alih - alih Task.WhenAll
menghasilkan perilaku aneh yang sama.
Pengamatan lain: Saya berharap bahwa membungkus bungkus di dalam Task.Run
akan membuat kelanjutan asinkron. Tetapi itu tidak terjadi. Kelanjutan dari baris di bawah ini masih dijalankan di utas yang sama (serempak).
await Task.Run(async () => await Task.WhenAll(task));
Klarifikasi: Perbedaan di atas diamati dalam aplikasi Konsol yang berjalan pada platform .NET Core 3.0. Pada .NET Framework 4.8, tidak ada perbedaan antara menunggu tugas asli atau pembungkus tugas. Dalam kedua kasus, kelanjutan dijalankan secara sinkron, di utas yang sama.
sumber
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
dari100
ke1000
sehingga tidak selesai saatawait
ed.Jawaban:
Jadi, Anda memiliki beberapa metode async yang menunggu variabel tugas yang sama;
Ya, kelanjutan ini akan dipanggil secara seri ketika
task
selesai. Dalam contoh Anda, setiap kelanjutan kemudian memilik utas untuk detik berikutnya.Jika Anda ingin setiap kelanjutan berjalan secara asinkron Anda mungkin perlu sesuatu seperti;
Sehingga tugas Anda kembali dari kelanjutan awal, dan memungkinkan beban CPU berjalan di luar
SynchronizationContext
.sumber
Task.Yield
adalah solusi yang bagus untuk masalah saya. Namun pertanyaan saya lebih tentang mengapa ini terjadi, dan lebih sedikit tentang bagaimana memaksa perilaku yang diinginkan.SynchronizationContext
, memanggilConfigureAwait(false)
sekali pada tugas asli mungkin cukup.SynchronizationContext.Current
ini adalah nol. Tapi saya baru saja memastikannya. Saya menambahkanConfigureAwait(false)
diawait
baris dan tidak ada bedanya. Pengamatannya sama seperti sebelumnya.Ketika tugas dibuat menggunakan
Task.Delay()
, opsi pembuatannya diaturNone
daripadaRunContinuationsAsychronously
.Ini mungkin melanggar perubahan antara .net framework dan .net core. Terlepas dari itu, tampaknya menjelaskan perilaku yang Anda amati. Anda juga dapat memverifikasi ini dari menggali ke dalam kode sumber yang
Task.Delay()
adalah newing up sebuahDelayPromise
yang memanggil defaultTask
constructor tanpa meninggalkan pilihan penciptaan ditentukan.sumber
RunContinuationsAsychronously
telah menjadi default, bukanNone
, ketika membangunTask
objek baru ? Ini akan menjelaskan beberapa pengamatan saya tetapi tidak semua. Secara khusus itu tidak akan menjelaskan perbedaan antara menungguTask.WhenAll
pembungkus yang sama , dan menunggu pembungkus yang berbeda.Dalam kode Anda, kode berikut ini keluar dari badan berulang.
jadi setiap kali Anda menjalankan yang berikut ini akan menunggu tugas dan menjalankannya di utas terpisah
tetapi jika Anda menjalankan yang berikut, ia akan memeriksa keadaan
task
, jadi itu akan berjalan dalam satu Threadtetapi jika Anda memindahkan pembuatan tugas di sampingnya
WhenAll
akan menjalankan setiap tugas di utas terpisah.sumber
Task.WhenAll
hanya biasaTask
, seperti aslinyatask
. Kedua tugas diselesaikan di beberapa titik, yang asli sebagai hasil dari acara pengatur waktu, dan gabungan sebagai hasil dari penyelesaian tugas asli. Mengapa kelanjutan mereka harus menampilkan perilaku yang berbeda? Dalam aspek apa perbedaan tugas satu dengan yang lainnya?