Синхронно чекаючи операції асинхронізації, і чому Wait () заморожує програму тут


318

Передмова : Я шукаю пояснення, а не просто рішення. Я вже знаю рішення.

Незважаючи на те, що витратив кілька днів на вивчення статей MSDN про асинхронний візерунок на основі завдань (TAP), асинхронізуйте його та очікуйте, я все ще трохи розгублений щодо деяких тонких деталей.

Я пишу реєстратор для додатків 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, тому я не можу використовувати деякі операції, але стикаюся з тими ж проблемами.
амадіб

3
@amadib, ver.1 та 4 були пояснені у [надані відповіді. Ver.2 anv 3 спробуйте почати заново вже розпочате завдання. Залиште своє запитання. Незрозуміло, як можна мати проблеми .NET 4.5 з асинхронністю / очікуванням проблем на .NET 4.0
Геннадій Ванін Геннадій Ванін

1
Версія 4 - найкращий варіант для форм Xamarin. Ми спробували решту варіантів, і не працювали та не мали досвід у всіх випадках
Ramakrishna

Дякую! Версія 4 працювала на мене. Але чи все одно це працює асинхронно? Я припускаю, що ключове слово async є.
sshirley

Відповіді:


189

awaitУсередині асинхронного методу намагається повернутися в потік призначеного для користувача інтерфейсу.

Оскільки потік користувальницького інтерфейсу зайнятий очікуванням завершення всього завдання, у вас є глухий кут.

Переміщення виклику асинхронізації для Task.Run()вирішення проблеми.
Оскільки виклик асинхронізації зараз працює на потоці пулу потоків, він не намагається повернутися до потоку інтерфейсу, і тому все працює.

Крім того, ви можете зателефонувати StartAsTask().ConfigureAwait(false)до очікування внутрішньої операції, щоб змусити її повернутися до пулу потоків, а не до потоку інтерфейсу, повністю уникаючи тупикової ситуації.



13
У ConfigureAwait(false)цьому випадку відповідне рішення. Оскільки йому не потрібно викликати зворотні дзвінки у захопленому контексті, він не повинен. Будучи методом API, він повинен керувати нею внутрішньо, а не змушувати всіх абонентів виходити з контексту інтерфейсу користувача.
Сервіс

@Servy Я прошу, оскільки ви згадали ConfigureAwait. Я використовую .net3.5, і мені довелося видалити конфігурацію, чекаючи, оскільки це не було в бібліотеці async, яку я використовував. Як я можу написати свій власний або чи є інший спосіб очікування дзвінка з асинхронністю. Тому що мій метод теж висить. У мене немає завдання, але не завдання. Це, мабуть, буде питанням самостійно.
flexxxit

@flexxxit: Ви повинні використовувати Microsoft.Bcl.Async.
Слакс

48

Виклик asyncкоду з синхронного коду може бути досить складним.

Я пояснюю всі причини цього глухого кута в своєму блозі . Коротше кажучи, є "контекст", який зберігається за замовчуванням на початку кожного awaitта використовується для відновлення методу.

Отже, якщо це викликається в контексті інтерфейсу, коли awaitзавершується, asyncметод намагається повторно ввести цей контекст для продовження виконання. На жаль, код, що використовує Wait(або Result), блокує потік у цьому контексті, тому asyncметод не може завершитися.

Вказівки для цього уникнути:

  1. Використовуйте ConfigureAwait(continueOnCapturedContext: false)якомога більше. Це дозволяє вашим asyncметодам продовжувати виконувати без необхідності повторного введення контексту.
  2. Використовуйте asyncвесь шлях. Використовуйте awaitзамість Resultабо Wait.

Якщо ваш метод є природним асинхронним, то ви (ймовірно) не повинні виставляти синхронну обгортку .


Мені потрібно виконати завдання 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();
}

працює чудово і не блокує потік інтерфейсу користувача


0

При невеликому користувальницькому контексті синхронізації функція синхронізації може чекати завершення функції асинхронізації, не створюючи тупик. Ось невеликий приклад для програми 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.