_TaskCompletionSource<T>
_に基づいて、リクエストへの応答を保留するTask
ベースのAPIを提供するライブラリ(ソケットネットワーキング)コードがあります。ただし、TPLには、同期の継続を防ぐことは不可能と思われるという点で厄介な点があります。 likeできるようにすることは次のいずれかです。
TaskCompletionSource<T>
_でアタッチできないようにする_TaskContinuationOptions.ExecuteSynchronously
_を伝える、またはTaskContinuationOptions.ExecuteSynchronously
_を無視するように指定する方法で結果(SetResult
/TrySetResult
)を設定します具体的には、私が抱えている問題は、着信データが専用のリーダーで処理されていることです。呼び出し元が_TaskContinuationOptions.ExecuteSynchronously
_でアタッチできる場合、リーダーを停止させる可能性があります。以前は、any継続が存在するかどうかを検出するハッカーによってこれを回避し、それが完了した場合はThreadPool
に完了をプッシュしますが、これは呼び出し元が大きな影響を与えます完了がタイムリーに処理されないため、作業キューが飽和しました。 Task.Wait()
(または同様の)を使用している場合、それらは本質的にそれ自体でデッドロックします。同様に、これが読者がワーカーを使用するのではなく、専用のスレッドにいる理由です。
そう; TPLチームにナグする前に:オプションがありませんか?
キーポイント:
ThreadPool
を実装として使用することはできません。プールが飽和状態のときに機能する必要があるためです。以下の例は出力を生成します(順序はタイミングによって異なる場合があります)。
_Continuation on: Main thread
Press [return]
Continuation on: Thread pool
_
問題は、ランダムな呼び出し元が「メインスレッド」で継続を取得できたという事実です。実際のコードでは、これはプライマリリーダーを中断します。悪いこと!
コード:
_using System;
using System.Threading;
using System.Threading.Tasks;
static class Program
{
static void Identify()
{
var thread = Thread.CurrentThread;
string name = thread.IsThreadPoolThread
? "Thread pool" : thread.Name;
if (string.IsNullOrEmpty(name))
name = "#" + thread.ManagedThreadId;
Console.WriteLine("Continuation on: " + name);
}
static void Main()
{
Thread.CurrentThread.Name = "Main thread";
var source = new TaskCompletionSource<int>();
var task = source.Task;
task.ContinueWith(delegate {
Identify();
});
task.ContinueWith(delegate {
Identify();
}, TaskContinuationOptions.ExecuteSynchronously);
source.TrySetResult(123);
Console.WriteLine("Press [return]");
Console.ReadLine();
}
}
_
。NET 4.6の新機能:
.NET 4.6には、新しいTaskCreationOptions
:RunContinuationsAsynchronously
が含まれています。
Reflectionを使用してプライベートフィールドにアクセスすることができるため...
TCSのタスクをTASK_STATE_THREAD_WAS_ABORTED
フラグでマークできます。これにより、すべての継続がインライン化されなくなります。
const int TASK_STATE_THREAD_WAS_ABORTED = 134217728;
var stateField = typeof(Task).GetField("m_stateFlags", BindingFlags.NonPublic | BindingFlags.Instance);
stateField.SetValue(task, (int) stateField.GetValue(task) | TASK_STATE_THREAD_WAS_ABORTED);
編集:
Reflectionを使用する代わりに、式を使用することをお勧めします。これははるかに読みやすく、PCL互換性があるという利点があります。
var taskParameter = Expression.Parameter(typeof (Task));
const string stateFlagsFieldName = "m_stateFlags";
var setter =
Expression.Lambda<Action<Task>>(
Expression.Assign(Expression.Field(taskParameter, stateFlagsFieldName),
Expression.Or(Expression.Field(taskParameter, stateFlagsFieldName),
Expression.Constant(TASK_STATE_THREAD_WAS_ABORTED))), taskParameter).Compile();
リフレクションを使用せずに:
誰かが興味があるなら、Reflectionなしでこれを行う方法を見つけましたが、それは少し「汚い」ものであり、もちろん無視できないパフォーマンスペナルティがあります:
try
{
Thread.CurrentThread.Abort();
}
catch (ThreadAbortException)
{
source.TrySetResult(123);
Thread.ResetAbort();
}
TPLにはTaskCompletionSource.SetResult
継続に対するexplicitAPIコントロールを提供するものはないと思います。 async/await
シナリオでこの動作を制御するために 初期回答 を維持することにしました。
ContinueWith
が呼び出されたのと同じスレッドでtcs.SetResult
- triggered継続が行われた場合、SetResult
に非同期を課す別のソリューションを次に示します。
public static class TaskExt
{
static readonly ConcurrentDictionary<Task, Thread> s_tcsTasks =
new ConcurrentDictionary<Task, Thread>();
// SetResultAsync
static public void SetResultAsync<TResult>(
this TaskCompletionSource<TResult> @this,
TResult result)
{
s_tcsTasks.TryAdd(@this.Task, Thread.CurrentThread);
try
{
@this.SetResult(result);
}
finally
{
Thread thread;
s_tcsTasks.TryRemove(@this.Task, out thread);
}
}
// ContinueWithAsync, TODO: more overrides
static public Task ContinueWithAsync<TResult>(
this Task<TResult> @this,
Action<Task<TResult>> action,
TaskContinuationOptions continuationOptions = TaskContinuationOptions.None)
{
return @this.ContinueWith((Func<Task<TResult>, Task>)(t =>
{
Thread thread = null;
s_tcsTasks.TryGetValue(t, out thread);
if (Thread.CurrentThread == thread)
{
// same thread which called SetResultAsync, avoid potential deadlocks
// using thread pool
return Task.Run(() => action(t));
// not using thread pool (TaskCreationOptions.LongRunning creates a normal thread)
// return Task.Factory.StartNew(() => action(t), TaskCreationOptions.LongRunning);
}
else
{
// continue on the same thread
var task = new Task(() => action(t));
task.RunSynchronously();
return Task.FromResult(task);
}
}), continuationOptions).Unwrap();
}
}
コメントに対応するために更新されました:
私は呼び出し側を制御しません-特定の継続型バリアントを使用するようにそれらを取得することはできません:できれば、問題はそもそも存在しません
あなたが発信者をコントロールしていないことに気づきませんでした。それにも関わらず、もしあなたがそれを制御しなければ、おそらくTaskCompletionSource
オブジェクトを直接呼び出し元に渡さないでしょう。論理的には、token部分、つまりtcs.Task
を渡すことになります。その場合、上記に別の拡張メソッドを追加することで、ソリューションはさらに簡単になる可能性があります。
// ImposeAsync, TODO: more overrides
static public Task<TResult> ImposeAsync<TResult>(this Task<TResult> @this)
{
return @this.ContinueWith(new Func<Task<TResult>, Task<TResult>>(antecedent =>
{
Thread thread = null;
s_tcsTasks.TryGetValue(antecedent, out thread);
if (Thread.CurrentThread == thread)
{
// continue on a pool thread
return antecedent.ContinueWith(t => t,
TaskContinuationOptions.None).Unwrap();
}
else
{
return antecedent;
}
}), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
使用する:
// library code
var source = new TaskCompletionSource<int>();
var task = source.Task.ImposeAsync();
// ...
// client code
task.ContinueWith(delegate
{
Identify();
}, TaskContinuationOptions.ExecuteSynchronously);
// ...
// library code
source.SetResultAsync(123);
これは実際にはawait
とContinueWith
(fiddle))およびリフレクションハッキングはありません。
シミュレートアボート アプローチは本当に良さそうに見えましたが、TPLハイジャックスレッドにつながりました 一部のシナリオでは 。
その後、 継続オブジェクトのチェック に似た実装を行いましたが、実際には与えられたコードに対してあまりにも多くのシナリオがあるため、any継続をチェックするだけです。うまく機能しますが、それはTask.Wait
は、スレッドプールのルックアップになりました。
最終的に、多くのILを検査した後、唯一の安全で有用なシナリオはSetOnInvokeMres
シナリオ(manual-reset-event-slim continuation)です。他にも多くのシナリオがあります。
そのため、最終的に、非ヌルの継続オブジェクトをチェックすることにしました。 nullの場合は問題ありません(継続なし)。 nullでない場合、SetOnInvokeMres
の特殊なケースのチェック-それである場合:fine(呼び出しても安全);それ以外の場合、スプーフィングアボートなどの特別なことをタスクに指示せずに、スレッドプールにTrySetComplete
を実行させます。 Task.Wait
はSetOnInvokeMres
アプローチを使用します。これは、デッドロックにならないように本当に頑張りたい特定のシナリオです。
Type taskType = typeof(Task);
FieldInfo continuationField = taskType.GetField("m_continuationObject", BindingFlags.Instance | BindingFlags.NonPublic);
Type safeScenario = taskType.GetNestedType("SetOnInvokeMres", BindingFlags.NonPublic);
if (continuationField != null && continuationField.FieldType == typeof(object) && safeScenario != null)
{
var method = new DynamicMethod("IsSyncSafe", typeof(bool), new[] { typeof(Task) }, typeof(Task), true);
var il = method.GetILGenerator();
var hasContinuation = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, continuationField);
Label nonNull = il.DefineLabel(), goodReturn = il.DefineLabel();
// check if null
il.Emit(OpCodes.Brtrue_S, nonNull);
il.MarkLabel(goodReturn);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
// check if is a SetOnInvokeMres - if so, we're OK
il.MarkLabel(nonNull);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, continuationField);
il.Emit(OpCodes.Isinst, safeScenario);
il.Emit(OpCodes.Brtrue_S, goodReturn);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ret);
IsSyncSafe = (Func<Task, bool>)method.CreateDelegate(typeof(Func<Task, bool>));
する代わりに
var task = source.Task;
代わりにこれを行います
var task = source.Task.ContinueWith<Int32>( x => x.Result );
したがって、非同期に実行される継続を常に1つ追加し、サブスクライバーが同じコンテキストで継続を望んでいるかどうかは関係ありません。タスクをカリー化するようなものですね。
更新、ContinueWith
ではなくawait
を処理するために 個別の回答 を投稿しました(ContinueWith
は現在の同期コンテキストを考慮しないため)。
ダム同期コンテキストを使用して、TaskCompletionSource
で_SetResult/SetCancelled/SetException
_を呼び出すことによってトリガーされる継続時に非同期を課すことができます。現在の同期コンテキスト(_await tcs.Task
_の時点)は、そのような継続を同期または非同期のどちらにするかを決定するためにTPLが使用する基準であると思います。
以下は私のために働く:
_if (notifyAsync)
{
tcs.SetResultAsync(null);
}
else
{
tcs.SetResult(null);
}
_
SetResultAsync
は次のように実装されます。
_public static class TaskExt
{
static public void SetResultAsync<T>(this TaskCompletionSource<T> tcs, T result)
{
FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
}
// FakeSynchronizationContext
class FakeSynchronizationContext : SynchronizationContext
{
private static readonly ThreadLocal<FakeSynchronizationContext> s_context =
new ThreadLocal<FakeSynchronizationContext>(() => new FakeSynchronizationContext());
private FakeSynchronizationContext() { }
public static FakeSynchronizationContext Instance { get { return s_context.Value; } }
public static void Execute(Action action)
{
var savedContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(FakeSynchronizationContext.Instance);
try
{
action();
}
finally
{
SynchronizationContext.SetSynchronizationContext(savedContext);
}
}
// SynchronizationContext methods
public override SynchronizationContext CreateCopy()
{
return this;
}
public override void OperationStarted()
{
throw new NotImplementedException("OperationStarted");
}
public override void OperationCompleted()
{
throw new NotImplementedException("OperationCompleted");
}
public override void Post(SendOrPostCallback d, object state)
{
throw new NotImplementedException("Post");
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotImplementedException("Send");
}
}
}
_
_SynchronizationContext.SetSynchronizationContext
_ 非常に安い 追加するオーバーヘッドの点で。実際、非常によく似たアプローチが WPF _Dispatcher.BeginInvoke
_ の実装によって採用されています。
TPLは、await
のポイントのターゲット同期コンテキストを_tcs.SetResult
_のポイントのそれと比較します。同期コンテキストが同じ場合(または両方の場所に同期コンテキストがない場合)、継続は直接、同期的に呼び出されます。それ以外の場合、ターゲット同期コンテキストで_SynchronizationContext.Post
_を使用して、つまり通常のawait
動作でキューに入れられます。このアプローチは、常に_SynchronizationContext.Post
_動作を強制します(ターゲット同期コンテキストがない場合はプールスレッドの継続)。
Updated、ContinueWith
は現在の同期コンテキストを考慮しないため、これは_task.ContinueWith
_では機能しません。ただし、_await task
_( fiddle )では機能します。 await task.ConfigureAwait(false)
でも機能します。
OTOH、 このアプローチ はContinueWith
に対して機能します。
リフレクションを使用でき、準備ができている場合は、これを実行する必要があります。
public static class MakeItAsync
{
static public void TrySetAsync<T>(this TaskCompletionSource<T> source, T result)
{
var continuation = typeof(Task).GetField("m_continuationObject", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
var continuations = (List<object>)continuation.GetValue(source.Task);
foreach (object c in continuations)
{
var option = c.GetType().GetField("m_options", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
var options = (TaskContinuationOptions)option.GetValue(c);
options &= ~TaskContinuationOptions.ExecuteSynchronously;
option.SetValue(c, options);
}
source.TrySetResult(result);
}
}