Sinkron menunggu operasi async, dan mengapa Tunggu () membekukan program di sini

318

Pendahuluan : Saya mencari penjelasan, bukan hanya solusi. Saya sudah tahu solusinya.

Meskipun telah menghabiskan beberapa hari mempelajari artikel MSDN tentang Asynchronous Pattern (TAP) berbasis Tugas, async dan menunggu, saya masih agak bingung tentang beberapa detail yang lebih baik.

Saya sedang menulis logger untuk Aplikasi Windows Store, dan saya ingin mendukung logging asinkron dan sinkron. Metode asinkron mengikuti TAP, yang sinkron harus menyembunyikan semua ini, dan terlihat dan berfungsi seperti metode biasa.

Ini adalah metode inti pencatatan asinkron:

private async Task WriteToLogAsync(string text)
{
    StorageFolder folder = ApplicationData.Current.LocalFolder;
    StorageFile file = await folder.CreateFileAsync("log.log",
        CreationCollisionOption.OpenIfExists);
    await FileIO.AppendTextAsync(file, text,
        Windows.Storage.Streams.UnicodeEncoding.Utf8);
}

Sekarang metode sinkron yang sesuai ...

Versi 1 :

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Wait();
}

Ini terlihat benar, tetapi tidak berhasil. Seluruh program membeku selamanya.

Versi 2 :

Hmm .. Mungkin tugasnya belum dimulai?

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Start();
    task.Wait();
}

Ini melempar InvalidOperationException: Start may not be called on a promise-style task.

Versi 3:

Hmm .. Task.RunSynchronouslykedengarannya menjanjikan.

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.RunSynchronously();
}

Ini melempar InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

Versi 4 (solusinya):

private void WriteToLog(string text)
{
    var task = Task.Run(async () => { await WriteToLogAsync(text); });
    task.Wait();
}

Ini bekerja. Jadi, 2 dan 3 adalah alat yang salah. Tapi 1? Apa yang salah dengan 1 dan apa bedanya dengan 4? Apa yang membuat saya membeku? Apakah ada masalah dengan objek tugas? Apakah ada jalan buntu yang tidak jelas?

Sebastian Negraszus
sumber
Keberuntungan mendapatkan penjelasan di tempat lain? Jawaban di bawah ini benar-benar tidak memberikan wawasan. Saya sebenarnya menggunakan .net 4.0 bukan 4.5 / 5 jadi saya tidak bisa menggunakan beberapa operasi tetapi mengalami masalah yang sama.
amadib
3
@amadib, ver.1 dan 4 dijelaskan dalam [jawaban yang ditentukan. Ver.2 dan € 3 mencoba memulai lagi sudah memulai tugas. Posting pertanyaan Anda. Tidak jelas bagaimana Anda dapat memiliki .NET 4.5 async / menunggu masalah pada .NET 4.0
Gennady Vanin Геннадий Ванин
1
Versi 4 adalah pilihan terbaik untuk Formulir Xamarin. Kami mencoba sisa opsi dan tidak berhasil dan mengalami kebuntuan dalam semua kasus
Ramakrishna
Terima kasih! Versi 4 bekerja untuk saya. Tetapi apakah masih berjalan secara tidak sinkron? Saya berasumsi demikian karena kata kunci async ada di sana.
sshirley

Jawaban:

189

Bagian awaitdalam metode asinkron Anda sedang mencoba untuk kembali ke utas UI.

Karena utas UI sedang sibuk menunggu seluruh tugas selesai, Anda menemui jalan buntu.

Memindahkan panggilan async untuk Task.Run()menyelesaikan masalah.
Karena panggilan async sekarang berjalan pada utas pool thread, ia tidak mencoba untuk kembali ke utas UI, dan karenanya semuanya berfungsi.

Atau, Anda bisa menelepon StartAsTask().ConfigureAwait(false)sebelum menunggu operasi bagian dalam untuk membuatnya kembali ke kolam thread daripada thread UI, menghindari kebuntuan sepenuhnya.

Slaks
sumber
9
+1. Berikut ini satu penjelasan lagi - Tunggu, UI, dan deadlock! Astaga!
Alexei Levenkov
13
Ini ConfigureAwait(false)adalah solusi yang tepat dalam kasus ini. Karena tidak perlu memanggil callback dalam konteks yang ditangkap, seharusnya tidak. Menjadi metode API itu harus menanganinya secara internal, daripada memaksa semua penelepon untuk keluar dari konteks UI.
Servy
@Servy Am bertanya sejak Anda menyebutkan ConfigureAwait. Saya menggunakan .net3.5 dan saya harus menghapus configure tunggu karena itu tidak tersedia di perpustakaan async yang saya gunakan. Bagaimana cara saya menulis sendiri atau apakah ada cara lain untuk menunggu panggilan async saya. Karena metode saya hang juga. Saya tidak punya Tugas Tapi bukan Tugas. Berjalan. Ini mungkin harus menjadi pertanyaan sendiri.
flexxxit
@flexxxit: Anda harus menggunakan Microsoft.Bcl.Async.
SLaks
48

Memanggil asynckode dari kode sinkron bisa sangat sulit.

Saya menjelaskan alasan lengkap untuk kebuntuan ini di blog saya . Singkatnya, ada "konteks" yang disimpan secara default di awal masing-masing awaitdan digunakan untuk melanjutkan metode.

Jadi jika ini disebut dalam konteks UI, ketika awaitselesai, asyncmetode mencoba memasukkan kembali konteks itu untuk melanjutkan eksekusi. Sayangnya, kode yang menggunakan Wait(atau Result) akan memblokir utas dalam konteks itu, sehingga asyncmetode ini tidak dapat diselesaikan.

Pedoman untuk menghindari ini adalah:

  1. Gunakan ConfigureAwait(continueOnCapturedContext: false)sebanyak mungkin. Ini memungkinkan asyncmetode Anda untuk terus mengeksekusi tanpa harus memasukkan kembali konteksnya.
  2. Gunakan asyncsemuanya. Gunakan awaitsebagai ganti Resultatau Wait.

Jika metode Anda secara alami tidak sinkron, maka Anda (mungkin) tidak boleh mengekspos pembungkus sinkron .

Stephen Cleary
sumber
Saya perlu menjalankan tugas Async dalam tangkapan () yang tidak mendukung asyncbagaimana saya melakukan ini dan mencegah kebakaran dan melupakan situasi.
Zapnologica
1
@Zapnologica: awaitdidukung dalam catchblok pada VS2015. Jika Anda menggunakan versi yang lebih lama, Anda dapat menetapkan pengecualian ke variabel lokal dan melakukan awaitsetelah blok tangkap .
Stephen Cleary
5

Inilah yang saya lakukan

private void myEvent_Handler(object sender, SomeEvent e)
{
  // I dont know how many times this event will fire
  Task t = new Task(() =>
  {
    if (something == true) 
    {
        DoSomething(e);  
    }
  });
  t.RunSynchronously();
}

bekerja dengan baik dan tidak memblokir utas UI

pixel
sumber
0

Dengan konteks sinkronisasi kustom kecil, fungsi sinkronisasi dapat menunggu penyelesaian fungsi async, tanpa membuat jalan buntu. Berikut adalah contoh kecil untuk aplikasi WinForms.

Imports System.Threading
Imports System.Runtime.CompilerServices

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        SyncMethod()
    End Sub

    ' waiting inside Sync method for finishing async method
    Public Sub SyncMethod()
        Dim sc As New SC
        sc.WaitForTask(AsyncMethod())
        sc.Release()
    End Sub

    Public Async Function AsyncMethod() As Task(Of Boolean)
        Await Task.Delay(1000)
        Return True
    End Function

End Class

Public Class SC
    Inherits SynchronizationContext

    Dim OldContext As SynchronizationContext
    Dim ContextThread As Thread

    Sub New()
        OldContext = SynchronizationContext.Current
        ContextThread = Thread.CurrentThread
        SynchronizationContext.SetSynchronizationContext(Me)
    End Sub

    Dim DataAcquired As New Object
    Dim WorkWaitingCount As Long = 0
    Dim ExtProc As SendOrPostCallback
    Dim ExtProcArg As Object

    <MethodImpl(MethodImplOptions.Synchronized)>
    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        Interlocked.Increment(WorkWaitingCount)
        Monitor.Enter(DataAcquired)
        ExtProc = d
        ExtProcArg = state
        AwakeThread()
        Monitor.Wait(DataAcquired)
        Monitor.Exit(DataAcquired)
    End Sub

    Dim ThreadSleep As Long = 0

    Private Sub AwakeThread()
        If Interlocked.Read(ThreadSleep) > 0 Then ContextThread.Resume()
    End Sub

    Public Sub WaitForTask(Tsk As Task)
        Dim aw = Tsk.GetAwaiter

        If aw.IsCompleted Then Exit Sub

        While Interlocked.Read(WorkWaitingCount) > 0 Or aw.IsCompleted = False
            If Interlocked.Read(WorkWaitingCount) = 0 Then
                Interlocked.Increment(ThreadSleep)
                ContextThread.Suspend()
                Interlocked.Decrement(ThreadSleep)
            Else
                Interlocked.Decrement(WorkWaitingCount)
                Monitor.Enter(DataAcquired)
                Dim Proc = ExtProc
                Dim ProcArg = ExtProcArg
                Monitor.Pulse(DataAcquired)
                Monitor.Exit(DataAcquired)
                Proc(ProcArg)
            End If
        End While

    End Sub

     Public Sub Release()
         SynchronizationContext.SetSynchronizationContext(OldContext)
     End Sub

End Class
kodefox
sumber