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 не имеет других асинхронных точек (т. е. нет awaits вообще, или все awaits находятся на уже завершенной задаче) и зовет!--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 - пожалуй, лучшее решение этой проблемы.