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