TaskContinuationOptions.RunContinuations异步和Stack Dives

时间:2015-02-04 12:29:55

标签: c# .net task-parallel-library async-await .net-4.6

this blog post中,Stephan Toub描述了一个将包含在.NET 4.6中的新功能,它为名为RunContinuationsAsynchronously的TaskCreationOptions和TaskContinuationOptions枚举添加了另一个值。

他解释道:

  

“我谈到了调用{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时,它就会   同步开始执行第二个任务,并在它调用时   释放,它将同步开始执行第三个任务,等等   上。 如果上面代码的“// work here”部分不包含任何内容   等待屈服,然后我们可能会在这里潜水   最终可能会爆炸。

我很难掌握他谈论同步执行延续的部分。

问题

这怎么可能导致堆叠潜水?更重要的是,为了解决这个问题,RunContinuationsAsynchronously有效地做了什么?

2 个答案:

答案 0 :(得分:9)

这里的关键概念是任务的延续可以在完成先前任务的同一个线程上同步运行。

让我们想象这是SemaphoreSlim.Release的实施(实际上是Toub' s 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,或者所有await都在已完成的任务上)并且调用_gate.Release()可能在同一个线程上同步完成对_gate.WaitAsync()的挂起调用,您可能会达到单个线程顺序释放信号量,完成下一个挂起调用,执行// work here然后再次释放信号量等状态。等

这意味着同一个线程在堆栈中越来越深,因此堆栈潜水。

RunContinuationsAsynchronously确保延续不同步运行,因此释放信号量的线程继续运行,并且为另一个线程调度延续(哪一个依赖于其他连续参数,例如{{1} }})

这在逻辑上类似于将完成发布到TaskScheduler

ThreadPool

答案 1 :(得分:4)

  

这怎么可能导致堆叠潜水?更重要的是,那是什么   RunContinuations异步有效地去做   解决这个问题?

i3arnon provides非常好地解释了引入RunContinuationsAsynchronously背后的原因。我的回答与他的相反;事实上,我也是为了自己的参考而写这篇文章(我自己也要记得从现在起半年内的任何细微之处:)

首先,让我们看看 TaskCompletionSource RunContinuationsAsynchronously选项与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);
        }
    }
}

注意所有continuation如何在调用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 }

现在如果我们不希望这种情况发生,我们希望每个延续都是异步运行(即,与其他延续并行,可能在另一个线程上,没有任何同步上下文)?

通过安装虚假的临时同步上下文(更多详情here),可以为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逻辑是一个黑客,它仅适用于await个延续。 新的RunContinuationsAsynchronously可以使其与TaskCompletionSource.Task附加的任何类型的延续保持一致。

RunContinuationsAsynchronously的另一个不错的方面是,计划在特定同步上下文中恢复的任何await样式连续将在该上下文异步上运行(使用{{1} },即使SynchronizationContext.Post相同的上下文中完成(与TCS.Task的当前行为不同)。TCS.SetResult - 样式连续也将由它们异步运行相应的任务调度程序(最常见的是ContinueWithTaskScheduler.Default)。他们不会通过TaskScheduler.FromCurrentSynchronizationContext进行内联。我相信Stephen Toub已在对{{3}的评论中澄清了这一点。 }},也可以看到blog post

为什么我们要担心在所有延续中强加不同步?

当我处理合作执行的TaskScheduler.TryExecuteTaskInline方法(共同例程)时,我通常需要它。

一个简单的例子是可暂停的异步处理:一个异步进程暂停/恢复另一个异步处理的执行。他们的执行工作流程在某些async点同步,await直接或间接用于此类同步。

下面是一些现成的示例代码,它使用了Stephen Toub的here in CoreCLR's Task.cs改编版。在这里,一个TaskCompletionSource方法async启动并定期暂停/恢复另一个StartAndControlWorkAsync方法async。尝试将DoWorkAsync更改为asyncAwaitContinuations: true并查看逻辑是否已完全损坏:

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); } } } } ,因为当他们已经安排在具有a的UI线程上异步运行时,将延续推送到Task.Run(() => tcs.SetResult(result))是多余的。适当的同步上下文同时,如果ThreadPoolStartAndControlWorkAsync同时运行在同一个UI同步上下文中,我们也会进行堆栈跳转(如果使用DoWorkAsync而没有tcs.SetResult(result)Task.Run包装)。

现在,SynchronizationContext.Post可能是解决此问题的最佳解决方案。