Utilizzare Task.Run () nel metodo sincrono per evitare deadlock in attesa sul metodo async?

AGGIORNAMENTO Lo scopo di questa domanda è ottenere una risposta semplice su Task.Run() e deadlocking. Capisco molto il ragionamento teorico per non mischiare asincrono e sincronizzazione, e li prendo a cuore. Non sto sopra imparando cose nuove dagli altri; Cerco di farlo ogni volta che posso. Ci sono solo momentjs in cui tutto ciò di cui un ragazzo ha bisogno è una risposta tecnica …

Ho un metodo Dispose() che deve chiamare un metodo asincrono. Poiché il 95% del mio codice è asincrono, il refactoring non è la scelta migliore. Avere un IAsyncDisposable (tra le altre caratteristiche) supportato dal framework sarebbe l’ideale, ma non siamo ancora arrivati. Quindi, nel frattempo, ho bisogno di trovare un modo affidabile per chiamare i metodi asincroni da un metodo sincrono senza deadlock.

Preferirei non usare ConfigureAwait(false) perché lascia la responsabilità sparsa per tutto il mio codice affinché il callee si comporti in un determinato modo nel caso in cui il chiamante sia sincrono. Preferirei fare qualcosa nel metodo sincrono visto che è il bugiardo deviante.

Dopo aver letto il commento di Stephen Cleary in un’altra domanda che Task.Run() pianifica sempre sul pool di thread anche con metodi asincroni, mi ha fatto pensare.

In .NET 4.5 in ASP.NET o in qualsiasi altro contesto di sincronizzazione che pianifica le attività sul thread corrente / sullo stesso thread, se si dispone di un metodo asincrono:

 private async Task MyAsyncMethod() { ... } 

E voglio chiamarlo da un metodo sincrono, posso semplicemente usare Task.Run() con Wait() per evitare deadlock poiché accoda il metodo asincrono del pool di thread?

 private void MySynchronousMethodLikeDisposeForExample() { // MyAsyncMethod will get queued to the thread pool // so it shouldn't deadlock with the Wait() ?? Task.Run((Func)MyAsyncMethod).Wait(); } 

Sembra che tu capisca i rischi coinvolti nella tua domanda, quindi salterò la lezione.

Per rispondere alla tua domanda effettiva: Sì, puoi semplicemente usare Task.Run per scaricare quel lavoro su un thread ThreadPool che non ha un SynchronizationContext e quindi non c’è alcun rischio reale per un deadlock.

Tuttavia , l’uso di un altro thread solo perché non ha SC è un po ‘un trucco e potrebbe essere costoso poiché la pianificazione del lavoro da eseguire sul ThreadPool ha i suoi costi.

Una soluzione migliore e più chiara IMO sarebbe semplicemente rimuovere l’SC per il momento utilizzando SynchronizationContext.SetSynchronizationContext e ripristinandolo in seguito. Questo può essere facilmente incapsulato in un IDisposable modo da poterlo utilizzare in un ambito di using :

 public static class NoSynchronizationContextScope { public static Disposable Enter() { var context = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); return new Disposable(context); } public struct Disposable : IDisposable { private readonly SynchronizationContext _synchronizationContext; public Disposable(SynchronizationContext synchronizationContext) { _synchronizationContext = synchronizationContext; } public void Dispose() => SynchronizationContext.SetSynchronizationContext(_synchronizationContext); } } 

Uso:

 private void MySynchronousMethodLikeDisposeForExample() { using (NoSynchronizationContextScope.Enter()) { MyAsyncMethod().Wait(); } } 

Questo codice non si bloccherà esattamente per i motivi evidenziati nella domanda: il codice viene sempre eseguito senza contesto di sincronizzazione (poiché utilizza il pool di thread) e Wait semplicemente bloccherà il thread fino a quando il metodo restituisce.

Questo è il mio modo di evitare il deadlock quando devo chiamare il metodo asincrono in modo sincrono e il thread può essere un thread dell’interfaccia utente:

  public static T GetResultSafe(this Task task) { if (SynchronizationContext.Current == null) return task.Result; if (task.IsCompleted) return task.Result; var tcs = new TaskCompletionSource(); task.ContinueWith(t => { var ex = t.Exception; if (ex != null) tcs.SetException(ex); else tcs.SetResult(t.Result); }, TaskScheduler.Default); return tcs.Task.Result; } 

Se si deve assolutamente chiamare il metodo async da uno sincrono, assicurarsi di utilizzare ConfigureAwait(false) all’interno delle chiamate del metodo asincrono per evitare l’acquisizione del contesto di sincronizzazione.

Questo dovrebbe valere, ma nel migliore dei casi è traballante. Consiglierei di pensare al refactoring. anziché.

Con un piccolo contesto di sincronizzazione personalizzato, la funzione di sincronizzazione può attendere il completamento della funzione asincrona, senza creare deadlock. Il thread originale viene mantenuto, quindi il metodo di sincronizzazione utilizza lo stesso thread prima e dopo la chiamata alla funzione asincrona. Ecco un piccolo esempio per l’app 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  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