การรอการดำเนินการแบบอะซิงโครนัสพร้อมกันและทำไม Wait () ทำให้โปรแกรมค้างที่นี่


318

คำนำ : ฉันกำลังมองหาคำอธิบายไม่ใช่แค่ทางออก ฉันรู้วิธีแก้ปัญหาแล้ว

แม้ว่าจะใช้เวลาหลายวันในการศึกษาบทความ MSDN เกี่ยวกับ Asynchronous Pattern (TAP) แบบใช้งาน, async และรอฉันยังคงสับสนเล็กน้อยเกี่ยวกับรายละเอียดปลีกย่อยบางส่วน

ฉันกำลังเขียนตัวบันทึกสำหรับแอพ Windows Store และฉันต้องการสนับสนุนทั้งการบันทึกแบบอะซิงโครนัสและการซิงโครนัส วิธีการแบบอะซิงโครนัสตาม TAP วิธีการแบบอะซิงโครนัสควรซ่อนทั้งหมดนี้และดูและทำงานเหมือนกับวิธีการทั่วไป

นี่คือวิธีการหลักของการบันทึกแบบอะซิงโครนัส:

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);
}

ตอนนี้วิธีการซิงโครนัสที่สอดคล้องกัน ...

รุ่น 1 :

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

สิ่งนี้ดูถูกต้อง แต่ใช้งานไม่ได้ โปรแกรมทั้งหมดค้างตลอดไป

รุ่น 2 :

อืม .. บางทีงานอาจไม่ได้เริ่มเลยเหรอ?

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

สิ่งนี้จะพ่น InvalidOperationException: Start may not be called on a promise-style task.

รุ่น 3:

อืม .. Task.RunSynchronouslyดูเหมือนว่าจะมีแนวโน้ม

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

สิ่งนี้จะพ่น InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

เวอร์ชัน 4 (โซลูชัน):

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

วิธีนี้ใช้ได้ผล ดังนั้น 2 และ 3 จึงเป็นเครื่องมือที่ผิด แต่ 1 มีอะไรผิดปกติกับ 1 และอะไรคือความแตกต่างของ 4? สิ่งที่ทำให้ 1 ทำให้เกิดการแช่แข็ง? มีปัญหากับวัตถุภารกิจหรือไม่? มีการหยุดชะงักที่ไม่ชัดเจนหรือไม่?


โชคดีที่ได้รับคำอธิบายที่อื่น? คำตอบด้านล่างไม่ได้ให้ข้อมูลเชิงลึกอย่างแท้จริง ฉันใช้. net 4.0 ไม่ใช่ 4.5 / 5 ดังนั้นฉันจึงไม่สามารถใช้การดำเนินการบางอย่างได้ แต่พบปัญหาเดียวกัน
amadib

3
@amadib, ver.1 และ 4 ถูกอธิบายใน [คำตอบ rpvided Ver.2 และ 3 พยายามที่จะเริ่มต้นอีกครั้งแล้วเริ่มงาน โพสต์คำถามของคุณ ยังไม่ชัดเจนว่าคุณจะมีปัญหา. NET 4.5 async / await บน. NET 4.0 ได้อย่างไร
Gennady Vanin ГеннадийВанин

1
เวอร์ชัน 4 เป็นตัวเลือกที่ดีที่สุดสำหรับฟอร์ม Xamarin เราลองใช้ตัวเลือกที่เหลือและไม่ทำงานและหยุดชะงักที่มีประสบการณ์ในทุกกรณี
Ramakrishna

ขอบคุณ! เวอร์ชัน 4 ใช้งานได้สำหรับฉัน แต่มันยังคงทำงานแบบอะซิงโครนัสไหม ฉันสมมติว่าเป็นเช่นนั้นเพราะคำหลัก async อยู่ที่นั่น
sshirley

คำตอบ:


189

awaitภายในวิธีการไม่ตรงกันของคุณพยายามที่จะกลับมาด้าย UI

เนื่องจากเธรด UI ไม่ว่างกำลังรอให้งานทั้งหมดเสร็จสมบูรณ์คุณจึงมีการหยุดชะงัก

การย้ายสาย async เพื่อTask.Run()แก้ไขปัญหา
เนื่องจากขณะนี้การเรียกใช้ async ทำงานบนเธรดพูลเธรดจึงไม่พยายามกลับมาที่เธรด UI และทุกอย่างจึงใช้งานได้

หรือคุณสามารถโทรหาStartAsTask().ConfigureAwait(false)ก่อนรอการดำเนินการด้านในเพื่อกลับมาที่เธรดพูลแทนที่จะเป็นเธรด UI เพื่อหลีกเลี่ยงการหยุดชะงักโดยสิ้นเชิง


9
+1 นี่คืออีกหนึ่งคำอธิบาย - รอและ UI และการหยุดชะงัก! พุทโธ่!
Alexei Levenkov

13
นี่ConfigureAwait(false)เป็นทางออกที่เหมาะสมในกรณีนี้ เนื่องจากไม่จำเป็นต้องโทรกลับในบริบทที่บันทึกไว้จึงไม่ควร เป็นวิธีการ API ที่ควรจัดการกับมันภายในแทนที่จะบังคับให้ผู้โทรทั้งหมดย้ายออกจากบริบท UI
Servy

@Servy ฉันถามตั้งแต่คุณพูดถึง ConfigureAwait ฉันใช้. net3.5 และฉันต้องลบการกำหนดค่ารอเพราะมันไม่สามารถใช้ได้ในไลบรารี async ที่ฉันใช้ ฉันจะเขียนของฉันเองหรือมีวิธีอื่นในการรอสาย async ของฉันได้อย่างไร เพราะวิธีการของฉันก็เช่นกัน ฉันไม่มีงาน แต่ไม่ใช่งาน นี่อาจเป็นคำถามของตัวเอง
flexxxit

@flexxxit: Microsoft.Bcl.Asyncคุณควรใช้
SLaks

48

การเรียกasyncรหัสจากรหัสซิงโครนัสนั้นค่อนข้างยุ่งยาก

ผมอธิบายเหตุผลเต็มรูปแบบสำหรับการหยุดชะงักนี้บนบล็อกของฉัน กล่าวโดยย่อมี "บริบท" ที่ถูกบันทึกตามค่าเริ่มต้นที่จุดเริ่มต้นของแต่ละบริบทawaitและใช้เพื่อเริ่มวิธีการทำงานต่อ

ดังนั้นหากสิ่งนี้ถูกเรียกในบริบท UI เมื่อวิธีที่awaitสมบูรณ์นั้นasyncจะพยายามป้อนบริบทนั้นอีกครั้งเพื่อดำเนินการต่อ น่าเสียดายที่การใช้รหัสWait(หรือResult) จะบล็อกเธรดในบริบทนั้นดังนั้นasyncวิธีการจึงไม่สมบูรณ์

แนวทางในการหลีกเลี่ยงปัญหานี้คือ:

  1. ใช้ConfigureAwait(continueOnCapturedContext: false)มากที่สุด สิ่งนี้ทำให้asyncวิธีการของคุณเพื่อดำเนินการต่อโดยไม่ต้องป้อนบริบทอีกครั้ง
  2. ใช้asyncตลอดทาง ใช้awaitแทนหรือResultWait

หากวิธีการของคุณเป็นแบบอะซิงโครนัสโดยธรรมชาติคุณ (อาจ) ไม่ควรเปิดเผย wrapper แบบซิงโครนั


ฉันจำเป็นต้องเรียกใช้งาน Async ใน catch () ซึ่งไม่สนับสนุนasyncวิธีการที่ฉันจะทำเช่นนี้และป้องกันไฟและลืมสถานการณ์
Zapnologica

1
@Zapnologica: awaitได้รับการสนับสนุนในcatchบล็อกตั้งแต่ VS2015 หากคุณอยู่ในรุ่นเก่า, คุณสามารถกำหนดข้อยกเว้นให้กับตัวแปรท้องถิ่นและทำawaitหลังจากการป้องกันการจับ
สตีเฟ่นเคลียร์

5

นี่คือสิ่งที่ฉันทำ

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();
}

ทำงานได้ดีและไม่ปิดกั้นเธรด UI


0

ด้วยบริบทการซิงโครไนซ์ที่กำหนดเองขนาดเล็กฟังก์ชั่นการซิงค์สามารถรอฟังก์ชั่น async ให้เสร็จสมบูรณ์โดยไม่ต้องสร้างการหยุดชะงัก นี่คือตัวอย่างเล็ก ๆ สำหรับแอป 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.