序文:私は解決策ではなく説明を探しています。私はすでに解決策を知っています。
タスクベースの非同期パターン(TAP)、非同期、待機についてのMSDNの記事の調査に数日を費やしたにもかかわらず、私はまだ細かい点について少し混乱しています。
私は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がフリーズを引き起こす原因は何ですか?タスクオブジェクトに問題はありますか?明白でないデッドロックはありますか?
非同期メソッド内のawait
は、UIスレッドに戻ろうとしています。
UIスレッドはタスク全体の完了を待って忙しいので、デッドロックがあります。
非同期呼び出しをTask.Run()
に移動すると問題は解決します。
非同期呼び出しは現在スレッドプールスレッドで実行されているため、UIスレッドに戻ってくることはなく、したがってすべてが機能します。
あるいは、内部操作を待つ前にStartAsTask().ConfigureAwait(false)
を呼び出して、UIスレッドではなくスレッドプールに戻すことで、デッドロックを完全に回避することもできます。
同期コードからasync
コードを呼び出すのはかなり面倒です。
私は説明します 私のブログでこのデッドロックの完全な理由 。つまり、デフォルトで各await
の先頭に保存され、メソッドを再開するために使用される「コンテキスト」があります。
したがって、これがUIコンテキストで呼び出された場合、await
が完了すると、async
メソッドはそのコンテキストを再入力して実行を継続しようとします。残念ながら、Wait
(またはResult
)を使用するコードはそのコンテキストでスレッドをブロックするため、async
メソッドは完了できません。
これを回避するためのガイドラインは次のとおりです。
ConfigureAwait(continueOnCapturedContext: false)
を使用してください。これにより、コンテキストを再入力しなくても、async
メソッドを実行し続けることができます。async
をずっと使用してください。 await
またはResult
の代わりにWait
を使用してください。もしあなたのメソッドが本来非同期であれば、 あなたは(おそらく)同期ラッパーを公開すべきではありません 。
これが私がしたことです
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();
}
うまく動作し、UIスレッドをブロックしていない
小さなカスタム同期コンテキストでは、同期機能は、デッドロックを発生させることなく、非同期機能の完了を待つことができます。これが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