Préface : Je cherche une explication, pas seulement une solution. Je connais déjà la solution.
Bien qu'ayant passé plusieurs jours à étudier des articles MSDN sur le modèle asynchrone basé sur les tâches (TAP), asynchrone et en attente, je suis encore un peu confus au sujet de certains détails plus fins.
J'écris un enregistreur pour Windows Store Apps et je souhaite prendre en charge la journalisation asynchrone et synchrone. Les méthodes asynchrones suivent le TAP, les synchrones doivent masquer tout cela, et ressembler à des méthodes ordinaires.
Voici la méthode principale de la journalisation asynchrone:
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);
}
Maintenant, la méthode synchrone correspondante ...
Version 1 :
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.Wait();
}
Cela semble correct, mais cela ne fonctionne pas. Le programme entier se fige pour toujours.
Version 2 :
Hmm .. Peut-être que la tâche n'a pas été commencée?
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.Start();
task.Wait();
}
Ceci jette InvalidOperationException: Start may not be called on a promise-style task.
Version 3:
Hmm .. Task.RunSynchronously
semble prometteur.
private void WriteToLog(string text)
{
Task task = WriteToLogAsync(text);
task.RunSynchronously();
}
Ceci jette InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.
Version 4 (la solution):
private void WriteToLog(string text)
{
var task = Task.Run(async () => { await WriteToLogAsync(text); });
task.Wait();
}
Cela marche. Donc, 2 et 3 sont les mauvais outils. Mais 1? Quel est le problème avec 1 et quelle est la différence avec 4? Qu'est-ce qui fait que je cause un gel? Y a-t-il un problème avec l'objet de tâche? Y a-t-il une impasse non évidente?
La await
de votre méthode asynchrone tente de revenir au thread d'interface utilisateur.
Le thread d'interface utilisateur étant occupé à attendre la fin de la tâche, vous vous trouvez dans une impasse.
Déplacer l'appel asynchrone vers Task.Run()
résout le problème.
Comme l'appel asynchrone s'exécute maintenant sur un thread de pool de threads, il n'essaie pas de revenir au thread d'interface utilisateur et tout fonctionne donc.
Vous pouvez également appeler StartAsTask().ConfigureAwait(false)
avant d'attendre l'opération interne pour la faire revenir au pool de threads plutôt qu'au thread d'interface utilisateur, évitant ainsi totalement le blocage.
Appeler du code async
à partir de code synchrone peut être assez délicat.
J'explique les raisons complètes de cette impasse sur mon blog . En bref, il existe un "contexte" qui est enregistré par défaut au début de chaque await
et utilisé pour reprendre la méthode.
Donc, si cela est appelé dans un contexte d'interface utilisateur, lorsque la await
est terminée, la méthode async
tente de rentrer de nouveau dans ce contexte pour continuer à s'exécuter. Malheureusement, le code utilisant Wait
(ou Result
) bloquera un thread dans ce contexte, de sorte que la méthode async
ne peut pas se terminer.
Les directives pour éviter cela sont:
ConfigureAwait(continueOnCapturedContext: false)
autant que possible. Cela permet à vos méthodes async
de continuer à s'exécuter sans avoir à ressaisir le contexte.async
jusqu'au bout. Utilisez await
au lieu de Result
ou Wait
.Si votre méthode est naturellement asynchrone, alors vous (probablement) ne devriez pas exposer un wrapper synchrone .
Voici ce que j'ai fait
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();
}
fonctionne bien et ne bloque pas le fil de l'interface utilisateur
Avec un petit contexte de synchronisation personnalisé, la fonction de synchronisation peut attendre l'achèvement de la fonction async, sans créer d'interblocage. Voici un petit exemple d'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