web-dev-qa-db-fra.com

Utilisez Task.Run () dans la méthode synchrone pour éviter le blocage en attente sur la méthode asynchrone?

UPDATE Le but de cette question est d'obtenir une réponse simple à propos de Task.Run() et du blocage. Je comprends très bien le raisonnement théorique pour ne pas mélanger async et synchronisation, et je les prends à cœur. Je ne suis pas au-dessus d'apprendre de nouvelles choses des autres; Je cherche à le faire chaque fois que je le peux. Il y a des moments où tout ce dont un homme a besoin est une réponse technique ...

J'ai une méthode Dispose() qui doit appeler une méthode asynchrone. Étant donné que 95% de mon code est asynchrone, le refactoring n'est pas le meilleur choix. Avoir un IAsyncDisposable (entre autres fonctionnalités) supporté par le framework serait idéal, mais nous n'en sommes pas encore là. Donc, en attendant, je dois trouver un moyen fiable d'appeler des méthodes asynchrones à partir d'une méthode synchrone sans blocage.

Je préférerais pas utiliser ConfigureAwait(false) car cela laisse la responsabilité éparpillée tout au long de mon code pour que l'appelé se comporte d'une certaine manière au cas où l'appelant serait synchrone. Je préfère faire quelque chose dans la méthode synchrone car c'est le bug déviant.

Après avoir lu le commentaire de Stephen Cleary dans une autre question que Task.Run() planifie toujours sur le pool de threads même les méthodes asynchrones, cela m'a fait réfléchir.

Dans .NET 4.5 dans ASP.NET ou tout autre contexte de synchronisation qui planifie des tâches sur le thread actuel/même thread, si j'ai une méthode asynchrone:

private async Task MyAsyncMethod()
{
    ...
}

Et je veux l'appeler à partir d'une méthode synchrone, puis-je simplement utiliser Task.Run() avec Wait() pour éviter les blocages car il met en file d'attente la méthode asynchrone le pool de threads?

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

Il semble que vous compreniez les risques liés à votre question, je vais donc sauter la leçon.

Pour répondre à votre question réelle: Oui, vous pouvez simplement utiliser Task.Run pour décharger ce travail sur un thread ThreadPool qui n'a pas de SynchronizationContext et il n'y a donc pas de risque réel de blocage.

Cependant, en utilisant un autre thread juste parce qu'il n'a pas SC est en quelque sorte un hack et pourrait être coûteux car planifier ce travail à faire sur le ThreadPool a ses coûts.

Une solution IMO meilleure et plus claire consisterait simplement à supprimer le SC pour le moment en utilisant SynchronizationContext.SetSynchronizationContext et le restaurer ensuite. Cela peut facilement être encapsulé dans un IDisposable afin que vous puissiez l'utiliser dans un using portée:

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

Usage:

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

C'est ma façon d'éviter le blocage lorsque je dois appeler la méthode asynchrone de manière synchrone et que le thread peut être un thread d'interface utilisateur:

    public static T GetResultSafe<T>(this Task<T> task)
    {
        if (SynchronizationContext.Current == null)
            return task.Result;

        if (task.IsCompleted)
            return task.Result;

        var tcs = new TaskCompletionSource<T>();
        task.ContinueWith(t =>
        {
            var ex = t.Exception;
            if (ex != null)
                tcs.SetException(ex);
            else
                tcs.SetResult(t.Result);
        }, TaskScheduler.Default);

        return tcs.Task.Result;
    }
3
Dmitry Naumov

Ce code ne bloquera pas exactement pour les raisons que vous avez soulignées dans la question - le code s'exécute toujours sans contexte de synchronisation (depuis l'utilisation du pool de threads) et Wait bloquera simplement le thread jusqu'à ce que/si la méthode retourne.

3
Alexei Levenkov

Avec un petit contexte de synchronisation personnalisé, la fonction de synchronisation peut attendre la fin de la fonction asynchrone, sans créer de blocage. Le thread d'origine est conservé, donc la méthode de synchronisation utilise le même thread avant et après l'appel à la fonction asynchrone. Voici un petit exemple pour l'application 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
1
codefox

Si vous absolument devez appeler la méthode async à partir d'une méthode synchrone, assurez-vous d'utiliser ConfigureAwait(false) dans vos appels de méthode async pour éviter la capture du contexte de synchronisation.

Cela devrait tenir mais est au mieux fragile. Je conseillerais de penser au refactoring. au lieu.

1
Yuval Itzchakov