TaskContinuationOptions.RunContinuationsAsynchronously и стековые погружения
на этот блог, Stephan Toub описывает новую функцию, которая будет включена в .NET 4.6, которая добавляет еще одно значение в перечисления TaskCreationOptions и TaskContinuationOptions под названием RunContinuationsAsynchronously
.
Он объясняет:
" Я говорил о разветвлении вызова методов {Try}Set * на TaskCompletionSource, что любые синхронные продолжения выключены задача TaskCompletionSource может выполняться синхронно как часть звонка. Если мы должны были вызвать SetResult здесь, удерживая блокировка, а затем синхронные продолжения этой задачи будут запущены удерживая замок, и это может привести к очень реальным проблемам. Итак, удерживая замок мы берем TaskCompletionSource в будьте завершены, но мы еще не завершили его, откладывая это до тех пор, пока замок отпущен"
и дает следующий пример для демонстрации:
private SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private async Task WorkAsync()
{
await _gate.WaitAsync().ConfigureAwait(false);
try
{
// work here
}
finally { _gate.Release(); }
}
теперь представьте, что у вас много вызовов WorkAsync:
await Task.WhenAll(from i in Enumerable.Range(0, 10000) select WorkAsync());
мы только что создали 10 000 вызовов WorkAsync, которые будут соответствующим образом сериализованный на семафоре. Одна из задач будет войдите в критическую область, и остальные встанут в очередь на Вызов WaitAsync, внутри SemaphoreSlim эффективно запрашивает задачу завершается, когда кто-то вызывает Release. Если Release завершил это Задача синхронно, затем, когда первая задача вызывает Release, она будет синхронно начать выполнение второй задачи и при ее вызове Отпустите, он синхронно начнет выполнять третью задачу, и так на. если раздел" / / work here " кода выше не включал никаких ждет, что дано, то мы потенциально будем стекать нырять здесь и в конечном итоге потенциально взорвать стек.
мне трудно понять ту часть, где он говорит о выполнении продолжение синхронно.
вопрос
как это может вызвать погружение стека? Более того, и что такое RunContinuationsAsynchronously
эффективно собирается сделать, чтобы решить эту проблему?
2 ответов
ключевая концепция здесь заключается в том, что продолжение задачи может выполняться синхронно в том же потоке, который завершил предшествующую задачу.
представим себе, что это С (это на самом деле AsyncSemphore
С):
public void Release()
{
TaskCompletionSource<bool> toRelease = null;
lock (m_waiters)
{
if (m_waiters.Count > 0)
toRelease = m_waiters.Dequeue();
else
++m_currentCount;
}
if (toRelease != null)
toRelease.SetResult(true);
}
мы видим, что он синхронно выполняет задачу (используя TaskCompletionSource
).
В этом случае, если WorkAsync
не имеет других асинхронных точек (т. е. нет await
s вообще, или все await
s находятся на уже завершенной задаче) и зовет!--8--> может завершить отложенный вызов _gate.WaitAsync()
синхронно в том же потоке вы можете достичь состояния, в котором один поток последовательно освобождает семафор, завершает следующий ожидающий вызов, выполняет // work here
а затем снова выпускает семафор и т. д. так далее.
это означает, что один и тот же поток идет все глубже и глубже в стеке, следовательно, погружение стека.
RunContinuationsAsynchronously
убеждается что продолжение не бежит одновременно и поэтому поток который выпускает семафор движется дальше, и продолжение запланировано для другого потока (который зависит от других параметров продолжения, например TaskScheduler
)
это логически напоминает публикацию завершения в ThreadPool
:
public void Release()
{
TaskCompletionSource<bool> toRelease = null;
lock (m_waiters)
{
if (m_waiters.Count > 0)
toRelease = m_waiters.Dequeue();
else
++m_currentCount;
}
if (toRelease != null)
Task.Run(() => toRelease.SetResult(true));
}
как это может вызвать погружение стека? Более того, и что RunContinuationsAsynchronously эффективно собирается сделать для того, чтобы решить эту проблему?
i3arnon предоставляет очень хорошее объяснение причин введения RunContinuationsAsynchronously
. Мой ответ скорее ортогональна его; в самом деле, я пишу это для моих собственных ссылок (я сам не помню, какие-то тонкости в полгода теперь :)
прежде всего, давайте посмотрим как TaskCompletionSource
' s отличается от Task.Run(() => tcs.SetResult(result))
или подобных. Давайте попробуем простое консольное приложение:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplications
{
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMinThreads(100, 100);
Console.WriteLine("start, " + new { System.Environment.CurrentManagedThreadId });
var tcs = new TaskCompletionSource<bool>();
// test ContinueWith-style continuations (TaskContinuationOptions.ExecuteSynchronously)
ContinueWith(1, tcs.Task);
ContinueWith(2, tcs.Task);
ContinueWith(3, tcs.Task);
// test await-style continuations
ContinueAsync(4, tcs.Task);
ContinueAsync(5, tcs.Task);
ContinueAsync(6, tcs.Task);
Task.Run(() =>
{
Console.WriteLine("before SetResult, " + new { System.Environment.CurrentManagedThreadId });
tcs.TrySetResult(true);
Thread.Sleep(10000);
});
Console.ReadLine();
}
// log
static void Continuation(int id)
{
Console.WriteLine(new { continuation = id, System.Environment.CurrentManagedThreadId });
Thread.Sleep(1000);
}
// await-style continuation
static async Task ContinueAsync(int id, Task task)
{
await task.ConfigureAwait(false);
Continuation(id);
}
// ContinueWith-style continuation
static Task ContinueWith(int id, Task task)
{
return task.ContinueWith(
t => Continuation(id),
CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
}
}
обратите внимание, как все продолжения выполняются синхронно в одном потоке, где TrySetResult
называют:
start, { CurrentManagedThreadId = 1 } before SetResult, { CurrentManagedThreadId = 3 } { continuation = 1, CurrentManagedThreadId = 3 } { continuation = 2, CurrentManagedThreadId = 3 } { continuation = 3, CurrentManagedThreadId = 3 } { continuation = 4, CurrentManagedThreadId = 3 } { continuation = 5, CurrentManagedThreadId = 3 } { continuation = 6, CurrentManagedThreadId = 3 }
теперь что делать, если мы не хотим, чтобы это произошло, и мы хотим, чтобы каждое продолжение работало асинхронно (т. е. параллельно с другими продолжениями и, возможно, на другом нить, при отсутствии какого-либо контекста синхронизации)?
есть трюк, который может сделать это для await
-стиль продолжения, установив поддельный временный контекст синхронизации (подробнее здесь):
public static class TaskExt
{
class SimpleSynchronizationContext : SynchronizationContext
{
internal static readonly SimpleSynchronizationContext Instance = new SimpleSynchronizationContext();
};
public static void TrySetResult<TResult>(this TaskCompletionSource<TResult> @this, TResult result, bool asyncAwaitContinuations)
{
if (!asyncAwaitContinuations)
{
@this.TrySetResult(result);
return;
}
var sc = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(SimpleSynchronizationContext.Instance);
try
{
@this.TrySetResult(result);
}
finally
{
SynchronizationContext.SetSynchronizationContext(sc);
}
}
}
теперь, используя tcs.TrySetResult(true, asyncAwaitContinuations: true)
в нашем тестовом коде:
start, { CurrentManagedThreadId = 1 } before SetResult, { CurrentManagedThreadId = 3 } { continuation = 1, CurrentManagedThreadId = 3 } { continuation = 2, CurrentManagedThreadId = 3 } { continuation = 3, CurrentManagedThreadId = 3 } { continuation = 4, CurrentManagedThreadId = 4 } { continuation = 5, CurrentManagedThreadId = 5 } { continuation = 6, CurrentManagedThreadId = 6 }
обратите внимание, как await
продолжения теперь работают параллельно (хотя, все-таки синхронно ContinueWith
продолжения).
этой asyncAwaitContinuations: true
логика hack и он работает для await
продолжения только. новая RunContinuationsAsynchronously
делает его работать последовательно для любого рода продолжений, прилагается к TaskCompletionSource.Task
.
еще один приятный аспект RunContinuationsAsynchronously
что-либо await
-продолжение стиля, запланированное для возобновления в определенном контексте синхронизации, будет выполняться в этом контексте асинхронно (через SynchronizationContext.Post
, даже если TCS.Task
завершается на то же самое контекст (в отличие от нынешнего поведения TCS.SetResult
). ContinueWith
-продолжения стиля также будут выполняться асинхронно соответствующими планировщиками задач (чаще всего,TaskScheduler.Default
или TaskScheduler.FromCurrentSynchronizationContext
). Они не будут встроены через TaskScheduler.TryExecuteTaskInline
. Я считаю, что Стивен Тоуб уточнил, что в комментариях к его блоге, и это также можно увидеть здесь, в задаче CoreCLR.cs.
почему мы должны беспокоиться о наложении асинхронности на все продолжения?
я обычно это когда я имею дело с async
методы, которые выполняются совместно (совместные процедуры).
простой пример - асинхронная обработка с возможностью паузы:один асинхронный процесс приостанавливает/возобновляет выполнение другого. Их рабочий процесс выполнения синхронизируется при определенных await
очки, и TaskCompletionSource
используется для такого рода синхронизации, прямо или косвенно.
Ниже приведен пример готового к игре кода, который использует адаптацию Стивена Туба PauseTokenSource
. Вот, один async
метод StartAndControlWorkAsync
запускает и периодически приостанавливает / возобновляет другой async
метод DoWorkAsync
. Попробуйте изменить asyncAwaitContinuations: true
to asyncAwaitContinuations: false
и увидеть, что логика полностью нарушена:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static void Main()
{
StartAndControlWorkAsync(CancellationToken.None).Wait();
}
// Do some work which can be paused/resumed
public static async Task DoWorkAsync(PauseToken pause, CancellationToken token)
{
try
{
var step = 0;
while (true)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("Working, step: " + step++);
await Task.Delay(1000).ConfigureAwait(false);
Console.WriteLine("Before await pause.WaitForResumeAsync()");
await pause.WaitForResumeAsync();
Console.WriteLine("After await pause.WaitForResumeAsync()");
}
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e);
throw;
}
}
// Start DoWorkAsync and pause/resume it
static async Task StartAndControlWorkAsync(CancellationToken token)
{
var pts = new PauseTokenSource();
var task = DoWorkAsync(pts.Token, token);
while (true)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("Press enter to pause...");
Console.ReadLine();
Console.WriteLine("Before pause requested");
await pts.PauseAsync();
Console.WriteLine("After pause requested, paused: " + pts.IsPaused);
Console.WriteLine("Press enter to resume...");
Console.ReadLine();
Console.WriteLine("Before resume");
pts.Resume();
Console.WriteLine("After resume");
}
}
// Based on Stephen Toub's PauseTokenSource
// http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperatively-pausing-async-methods.aspx
// the main difference is to make sure that when the consumer-side code - which requested the pause - continues,
// the producer-side code has already reached the paused (awaiting) state.
// E.g. a media player "Pause" button is clicked, gets disabled, playback stops,
// and only then "Resume" button gets enabled
public class PauseTokenSource
{
internal static readonly Task s_completedTask = Task.Delay(0);
readonly object _lock = new Object();
bool _paused = false;
TaskCompletionSource<bool> _pauseResponseTcs;
TaskCompletionSource<bool> _resumeRequestTcs;
public PauseToken Token { get { return new PauseToken(this); } }
public bool IsPaused
{
get
{
lock (_lock)
return _paused;
}
}
// request a resume
public void Resume()
{
TaskCompletionSource<bool> resumeRequestTcs = null;
lock (_lock)
{
resumeRequestTcs = _resumeRequestTcs;
_resumeRequestTcs = null;
if (!_paused)
return;
_paused = false;
}
if (resumeRequestTcs != null)
resumeRequestTcs.TrySetResult(true, asyncAwaitContinuations: true);
}
// request a pause (completes when paused state confirmed)
public Task PauseAsync()
{
Task responseTask = null;
lock (_lock)
{
if (_paused)
return _pauseResponseTcs.Task;
_paused = true;
_pauseResponseTcs = new TaskCompletionSource<bool>();
responseTask = _pauseResponseTcs.Task;
_resumeRequestTcs = null;
}
return responseTask;
}
// wait for resume request
internal Task WaitForResumeAsync()
{
Task resumeTask = s_completedTask;
TaskCompletionSource<bool> pauseResponseTcs = null;
lock (_lock)
{
if (!_paused)
return s_completedTask;
_resumeRequestTcs = new TaskCompletionSource<bool>();
resumeTask = _resumeRequestTcs.Task;
pauseResponseTcs = _pauseResponseTcs;
_pauseResponseTcs = null;
}
if (pauseResponseTcs != null)
pauseResponseTcs.TrySetResult(true, asyncAwaitContinuations: true);
return resumeTask;
}
}
// consumer side
public struct PauseToken
{
readonly PauseTokenSource _source;
public PauseToken(PauseTokenSource source) { _source = source; }
public bool IsPaused { get { return _source != null && _source.IsPaused; } }
public Task WaitForResumeAsync()
{
return IsPaused ?
_source.WaitForResumeAsync() :
PauseTokenSource.s_completedTask;
}
}
}
public static class TaskExt
{
class SimpleSynchronizationContext : SynchronizationContext
{
internal static readonly SimpleSynchronizationContext Instance = new SimpleSynchronizationContext();
};
public static void TrySetResult<TResult>(this TaskCompletionSource<TResult> @this, TResult result, bool asyncAwaitContinuations)
{
if (!asyncAwaitContinuations)
{
@this.TrySetResult(result);
return;
}
var sc = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(SimpleSynchronizationContext.Instance);
try
{
@this.TrySetResult(result);
}
finally
{
SynchronizationContext.SetSynchronizationContext(sc);
}
}
}
}
я не хотел использовать Task.Run(() => tcs.SetResult(result))
здесь, потому что было бы излишним нажимать продолжения на ThreadPool
когда они уже запланированы для асинхронного запуска в потоке пользовательского интерфейса с соответствующим контекстом синхронизации. В то же время, если оба StartAndControlWorkAsync
и DoWorkAsync
запуск на том же UI контекст синхронизации, у нас также будет погружение стека (если tcs.SetResult(result)
используется без Task.Run
или SynchronizationContext.Post
упаковка).
теперь RunContinuationsAsynchronously
- пожалуй, лучшее решение этой проблемы.