Apa sebenarnya yang terjadi ketika utas menunggu tugas di dalam loop sementara?

10

Setelah berurusan dengan pola async / await C # untuk sementara waktu sekarang, saya tiba-tiba menyadari bahwa saya tidak benar-benar tahu bagaimana menjelaskan apa yang terjadi dalam kode berikut:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()diasumsikan mengembalikan yang ditunggu-tunggu Taskyang mungkin atau mungkin tidak menyebabkan ulir ketika kelanjutan dijalankan.

Saya tidak akan bingung jika penantian tidak berada di dalam satu lingkaran. Saya secara alami berharap bahwa sisa metode (yaitu kelanjutan) berpotensi mengeksekusi di utas lain, yang baik-baik saja.

Namun, di dalam satu lingkaran, konsep "sisa metode" menjadi agak berkabut bagi saya.

Apa yang terjadi pada "sisa loop" jika utas diaktifkan kelanjutan vs jika tidak diaktifkan? Di thread mana iterasi loop berikutnya dieksekusi?

Pengamatan saya menunjukkan (tidak diverifikasi secara meyakinkan) bahwa setiap iterasi dimulai pada utas yang sama (yang asli) sementara kelanjutan dijalankan pada yang lain. Bisakah ini benar-benar terjadi? Jika ya, apakah ini suatu tingkat paralelisme tak terduga yang perlu dipertanggungjawabkan terkait keamanan benang metode GetWorkAsync?

UPDATE: Pertanyaan saya bukan duplikat, seperti yang disarankan oleh beberapa orang. The while (!_quit) { ... }Pola Kode hanyalah penyederhanaan kode saya yang sebenarnya. Pada kenyataannya, utas saya adalah loop yang berumur panjang yang memproses antrian input item pekerjaan secara berkala (setiap 5 detik secara default). Pemeriksaan kondisi berhenti aktual juga bukan pemeriksaan lapangan sederhana seperti yang disarankan oleh kode sampel, melainkan pemeriksaan pegangan acara.

aoven
sumber
1
Lihat juga Bagaimana hasil dan menunggu menerapkan aliran kontrol di .NET? untuk beberapa informasi hebat tentang bagaimana semua ini disatukan.
John Wu
@ John Wu: Saya belum melihat utas itu. Banyak nugget info menarik di sana. Terima kasih!
aoven

Jawaban:

6

Anda sebenarnya bisa memeriksanya di Try Roslyn . Metode menunggu Anda akan ditulis ulang void IAsyncStateMachine.MoveNext()pada kelas async yang dihasilkan.

Yang akan Anda lihat adalah sesuatu seperti ini:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

Pada dasarnya, tidak masalah Anda berada di thread mana; mesin keadaan dapat melanjutkan dengan benar dengan mengganti loop Anda dengan struktur if / goto yang setara.

Karena itu, metode async tidak harus dijalankan pada utas yang berbeda. Lihat penjelasan Eric Lippert "Ini bukan sihir" untuk menjelaskan bagaimana Anda dapat bekerja async/awaithanya pada satu utas.

Mason Wheeler
sumber
2
Saya tampaknya meremehkan sejauh mana penulisan ulang yang dilakukan kompiler pada kode async saya. Intinya, tidak ada "loop" setelah penulisan ulang! Itu adalah bagian yang hilang bagi saya. Luar biasa dan terima kasih untuk tautan 'Coba Roslyn' juga!
aoven
GOTO adalah konstruksi loop asli . Jangan lupakan itu.
2

Pertama, Servy telah menulis beberapa kode dalam jawaban untuk pertanyaan serupa, yang menjadi dasar jawaban ini:

/programming/22049339/how-to-create-a-cancellable-task-loop

Jawaban Servy mencakup ContinueWith()loop serupa menggunakan konstruksi TPL tanpa penggunaan eksplisit asyncdan awaitkata kunci; jadi untuk menjawab pertanyaan Anda, perhatikan seperti apa kode Anda saat loop Anda tidak digunakanContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Ini membutuhkan waktu untuk membungkus kepala Anda, tetapi dalam ringkasan:

  • continuationmewakili penutupan untuk "iterasi saat ini"
  • previousmewakili keadaan yang Taskberisi "iterasi sebelumnya" (yaitu ia tahu kapan 'iterasi' selesai dan digunakan untuk memulai yang berikutnya ..)
  • Dengan asumsi GetWorkAsync()mengembalikan a Task, itu berarti ContinueWith(_ => GetWorkAsync())akan mengembalikan Task<Task>maka karenanya panggilan Unwrap()untuk mendapatkan 'tugas batin' (yaitu hasil aktual dari GetWorkAsync()).

Begitu:

  1. Awalnya tidak ada iterasi sebelumnya , jadi hanya diberi nilai Task.FromResult(_quit) - negaranya dimulai sebagai Task.Completed == true.
  2. The continuationdijalankan untuk pertama kalinya menggunakanprevious.ContinueWith(continuation)
  3. yang continuationupdate penutupan previousuntuk mencerminkan keadaan selesainya_ => GetWorkAsync()
  4. Ketika _ => GetWorkAsync()selesai, ia "melanjutkan" _previous.ContinueWith(continuation)- yaitu memanggil continuationlambda lagi
    • Jelas pada titik ini, previoustelah diperbarui dengan keadaan _ => GetWorkAsync()sehingga continuationlambda dipanggil ketika GetWorkAsync()kembali.

The continuationlambda selalu memeriksa keadaan _quitbegitu, jika _quit == falsekemudian tidak ada lagi lanjutan, dan TaskCompletionSourceakan diatur ke nilai _quit, dan semuanya selesai.

Adapun pengamatan Anda tentang kelanjutan dieksekusi di utas yang berbeda, itu bukan sesuatu yang kata kunci async/ awaitakan lakukan untuk Anda, sesuai blog ini "Tugas (masih) bukan utas dan async tidak paralel" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

Saya sarankan memang perlu melihat lebih dekat pada GetWorkAsync()metode Anda sehubungan dengan threading dan keamanan benang. Jika diagnostik Anda mengungkapkan bahwa itu telah dieksekusi pada utas yang berbeda sebagai konsekuensi dari kode async / wait Anda yang diulang, maka sesuatu di dalam atau yang terkait dengan metode itu harus menyebabkan utas baru dibuat di tempat lain. (Jika ini tidak terduga, mungkin ada suatu .ConfigureAwaittempat?)

Ben Cottrell
sumber
2
Kode yang saya tunjukkan disederhanakan. Di dalam GetWorkAsync () ada beberapa lagi yang menunggu. Beberapa dari mereka mengakses database dan jaringan, yang berarti I / O sejati. Seperti yang saya pahami, sakelar ulir adalah konsekuensi alami (walaupun tidak diharuskan) dari penantian tersebut, karena utas awal tidak menetapkan konteks sinkronisasi yang akan mengatur di mana kelanjutan harus dijalankan. Jadi mereka mengeksekusi pada thread pool thread. Apakah alasan saya salah?
aoven
@oven poin bagus - Saya tidak mempertimbangkan berbagai jenis SynchronizationContext- yang tentu saja penting karena .ContinueWith()menggunakan SynchronizationContext untuk mengirim kelanjutan; itu memang akan menjelaskan perilaku yang Anda lihat jika awaitdipanggil pada utas ThreadPool atau utas ASP.NET. Kelanjutan tentu dapat dikirim ke utas berbeda dalam kasus-kasus tersebut. Di sisi lain, memanggil awaitkonteks single-threaded seperti WPF Dispatcher atau konteks Winforms harus cukup untuk memastikan kelanjutan terjadi pada aslinya. utas
Ben Cottrell