Синхронно ожидает асинхронную операцию и почему Wait () останавливает программу здесь

318

Предисловие : я ищу объяснение, а не просто решение. Я уже знаю решение.

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

Я пишу регистратор для приложений Магазина Windows и хочу поддерживать как асинхронную, так и синхронную регистрацию. Асинхронные методы следуют за 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 ответах. Вер.2 и 3 попробуйте снова запустить уже запущенное задание. Отправьте свой вопрос. Непонятно, как у вас могут возникнуть проблемы .NET 4.5 с асинхронным ожиданием в .NET 4.0
Геннадий Ванин Геннадий Ванин
1
Версия 4 является лучшим вариантом для форм Xamarin. Мы опробовали остальные варианты и не сработали, и во всех случаях имели место взаимоблокировки
Рамакришна
Спасибо! Версия 4 работала на меня. Но все еще работает асинхронно? Я так предполагаю, потому что здесь есть ключевое слово async.
Ширли

Ответы:

189

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

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

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

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

SLaks
источник
9
+1. Вот еще одно объяснение - жду и UI, и тупики! Боже мой!
Алексей Левенков
13
Это ConfigureAwait(false)подходящее решение в этом случае. Поскольку ему не нужно вызывать обратные вызовы в захваченном контексте, это не нужно. Будучи методом API, он должен обрабатывать его внутренне, а не заставлять всех вызывающих программ выходить из контекста пользовательского интерфейса.
Служба
@Servy Прошу, так как вы упомянули ConfigureAwait. Я использую .net3.5, и мне пришлось удалить configure await, потому что он не был доступен в асинхронной библиотеке, которую я использовал. Как мне написать свой или есть другой способ ожидания моего асинхронного вызова. Потому что мой метод тоже зависает. У меня нет задачи, но нет задачи. Это, вероятно, вопрос сам по себе.
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после блока catch .
Стивен Клири
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
codefox
источник