以非阻塞方式调用TaskCompletionSource.SetResult

时间:2013-10-20 19:41:34

标签: c# multithreading asynchronous task-parallel-library async-await

我发现TaskCompletionSource.SetResult();在返回之前调用等待任务的代码。在我的情况下导致死锁。

这是一个以普通Thread

开头的简化版本
void ReceiverRun()
    while (true)
    {
        var msg = ReadNextMessage();
        TaskCompletionSource<Response> task = requests[msg.RequestID];

        if(msg.Error == null)
            task.SetResult(msg);
        else
            task.SetException(new Exception(msg.Error));
    }
}

代码的“异步”部分看起来像这样。

await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();

Wait实际上嵌套在非异步调用中。

SendAwaitResponse(简化)

public static Task<Response> SendAwaitResponse(string msg)
{
    var t = new TaskCompletionSource<Response>();
    requests.Add(GetID(msg), t);
    stream.Write(msg);
    return t.Task;
}

我的假设是第二个SendAwaitResponse将在ThreadPool线程中执行,但它会在为ReceiverRun创建的线程中继续。

无论如何设置任务的结果而不继续等待代码?

该应用程序是控制台应用程序

4 个答案:

答案 0 :(得分:26)

  

我发现了TaskCompletionSource.SetResult();在返回之前调用等待任务的代码。在我的情况下导致死锁。

是的,我有blog post记录了这一点(AFAIK没有记录在MSDN上)。死锁发生的原因有两个:

  1. 混合了async和阻止代码(即async方法正在调用Wait)。
  2. 使用TaskContinuationOptions.ExecuteSynchronously安排任务延续。
  3. 我建议从最简单的解决方案开始:删除第一件事(1)。即,不要混合asyncWait来电:

    await SendAwaitResponse("first message");
    SendAwaitResponse("second message").Wait();
    

    相反,请始终使用await

    await SendAwaitResponse("first message");
    await SendAwaitResponse("second message");
    

    如果需要,您可以在调用堆栈的另一个位置Wait {{1>}方法中而不是)。

    这是我最推荐的解决方案。但是,如果你想尝试删除第二个东西(2),你可以做一些技巧:将async包裹在SetResult中以强制它进入一个单独的线程(my {{3有Task.Run扩展方法可以完成此操作),或者为您的线程提供实际的上下文(例如我的AsyncEx library)并指定*WithBackgroundContinuations,这将AsyncContext type。< / p>

    但这些解决方案比分离ConfigureAwait(false)和阻止代码要复杂得多。

    作为旁注,请看cause the continuation to ignore the ExecuteSynchronously flag;听起来你可能觉得它很有用。

答案 1 :(得分:5)

由于您的应用是一个控制台应用,它运行在默认的synchronization context上,其中await延续回调将在等待任务完成的同一线程上调用。如果要在await SendAwaitResponse之后切换线程,可以使用await Task.Yield()

执行此操作
await SendAwaitResponse("first message");
await Task.Yield(); 
// will be continued on a pool thread
// ...
SendAwaitResponse("second message").Wait(); // so no deadlock

您可以通过在Thread.CurrentThread.ManagedThreadId中存储Task.Result并将其与await之后的当前广告ID进行比较来进一步改善这一点。如果您仍然在同一个主题上,请执行await Task.Yield()

虽然我知道SendAwaitResponse是您实际代码的简化版本,但内部仍然完全同步(您在问题中显示的方式)。你为什么期望有任何线程切换?

无论如何,您可能应该重新设计逻辑,而不是假设您当前使用的是什么线程。避免混合awaitTask.Wait()并使所有代码异步。通常情况下,可以在顶层的某个位置坚持使用Wait()(例如,在Main内)。

[已编辑] task.SetResult(msg)拨打ReceiverRun实际上将控制流转移到awaittask的位置 - 没有线程切换,因为默认的同步上下文的行为。因此,执行实际消息处理的代码将接管ReceiverRun线程。最终,在同一个线程上调用SendAwaitResponse("second message").Wait(),导致死锁。

以下是以您的示例为模型的控制台应用代码。它使用await Task.Yield()内的ProcessAsync来计划单独线程上的延续,因此控制流返回ReceiverRun并且没有死锁。

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        class Worker
        {
            public struct Response
            {
                public string message;
                public int threadId;
            }

            CancellationToken _token;
            readonly ConcurrentQueue<string> _messages = new ConcurrentQueue<string>();
            readonly ConcurrentDictionary<string, TaskCompletionSource<Response>> _requests = new ConcurrentDictionary<string, TaskCompletionSource<Response>>();

            public Worker(CancellationToken token)
            {
                _token = token;
            }

            string ReadNextMessage()
            {
                // using Thread.Sleep(100) for test purposes here,
                // should be using ManualResetEvent (or similar synchronization primitive),
                // depending on how messages arrive
                string message;
                while (!_messages.TryDequeue(out message))
                {
                    Thread.Sleep(100);
                    _token.ThrowIfCancellationRequested();
                }
                return message;
            }

            public void ReceiverRun()
            {
                LogThread("Enter ReceiverRun");
                while (true)
                {
                    var msg = ReadNextMessage();
                    LogThread("ReadNextMessage: " + msg);
                    var tcs = _requests[msg];
                    tcs.SetResult(new Response { message = msg, threadId = Thread.CurrentThread.ManagedThreadId });
                    _token.ThrowIfCancellationRequested(); // this is how we terminate the loop
                }
            }

            Task<Response> SendAwaitResponse(string msg)
            {
                LogThread("SendAwaitResponse: " + msg);
                var tcs = new TaskCompletionSource<Response>();
                _requests.TryAdd(msg, tcs);
                _messages.Enqueue(msg);
                return tcs.Task;
            }

            public async Task ProcessAsync()
            {
                LogThread("Enter Worker.ProcessAsync");

                var task1 = SendAwaitResponse("first message");
                await task1;
                LogThread("result1: " + task1.Result.message);
                // avoid deadlock for task2.Wait() with Task.Yield()
                // comment this out and task2.Wait() will dead-lock
                if (task1.Result.threadId == Thread.CurrentThread.ManagedThreadId)
                    await Task.Yield();

                var task2 = SendAwaitResponse("second message");
                task2.Wait();
                LogThread("result2: " + task2.Result.message);

                var task3 = SendAwaitResponse("third message");
                // still on the same thread as with result 2, no deadlock for task3.Wait()
                task3.Wait();
                LogThread("result3: " + task3.Result.message);

                var task4 = SendAwaitResponse("fourth message");
                await task4;
                LogThread("result4: " + task4.Result.message);
                // avoid deadlock for task5.Wait() with Task.Yield()
                // comment this out and task5.Wait() will dead-lock
                if (task4.Result.threadId == Thread.CurrentThread.ManagedThreadId)
                    await Task.Yield();

                var task5 = SendAwaitResponse("fifth message");
                task5.Wait();
                LogThread("result5: " + task5.Result.message);

                LogThread("Leave Worker.ProcessAsync");
            }

            public static void LogThread(string message)
            {
                Console.WriteLine("{0}, thread: {1}", message, Thread.CurrentThread.ManagedThreadId);
            }
        }

        static void Main(string[] args)
        {
            Worker.LogThread("Enter Main");
            var cts = new CancellationTokenSource(5000); // cancel after 5s
            var worker = new Worker(cts.Token);
            Task receiver = Task.Run(() => worker.ReceiverRun());
            Task main = worker.ProcessAsync();
            try
            {
                Task.WaitAll(main, receiver);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception: " + e.Message);
            }
            Worker.LogThread("Leave Main");
            Console.ReadLine();
        }
    }
}

这与在Task.Run(() => task.SetResult(msg))内执行ReceiverRun没有多大区别。我能想到的唯一优势是你可以明确控制何时切换线程。这样,您可以尽可能长时间保持在同一个线程上(例如,对于task2task3task4,但在task4之后仍需要另一个线程切换避免task5.Wait())出现僵局。

这两种解决方案最终都会使线程池增长,这在性能和可伸缩性方面都很糟糕。

现在,如果我们在上述代码中task.Wait()内的await task替换ProcessAsync await Task.Yield ,我们就不必使用await和仍然没有死锁。但是,在await task1ProcessAsync ReceiverRun内的Wait()调用之后的整个await调用实际上将在WindowsFormsSynchronizationContext线程上执行。只要我们不用其他awaits样式的调用来阻止这个线程,并且在我们处理消息时不做大量的CPU绑定工作,这种方法可能正常工作(异步IO绑定{ {1}} - 样式调用仍然应该没问题,它们实际上可能会触发隐式线程切换)。

那就是说,我认为你需要一个单独的线程,其上安装了序列化同步上下文来处理消息(类似于Task.Wait)。这就是运行包含Task.Run的异步代码的地方。您仍然需要避免在该线程上使用ActionDispatcher。如果单个消息处理需要大量CPU限制工作,则应使用ActionDispatcherSynchronizationContext进行此类工作。对于异步IO绑定调用,您可以保持在同一个线程上。

您可能希望查看来自@StephenCleary的{{1}} / {{1}} Nito Asynchronous Library用于异步消息处理逻辑。希望斯蒂芬跳进来并提供更好的答案。

答案 2 :(得分:0)

“我的假设是第二个SendAwaitResponse将在ThreadPool线程中执行,但它会在为ReceiverRun创建的线程中继续。”

这完全取决于您在SendAwaitResponse中执行的操作。异步和并发are not the same thing

退房:C# 5 Async/Await - is it *concurrent*?

答案 3 :(得分:0)

聚会晚了一点,但这是我的解决方案,我认为这是附加值。

我也一直在为此苦苦挣扎,我通过在等待的方法上捕获SynchronizationContext解决了它。

它看起来像:

// just a default sync context
private readonly SynchronizationContext _defaultContext = new SynchronizationContext();

void ReceiverRun()
{
    while (true)    // <-- i would replace this with a cancellation token
    {
        var msg = ReadNextMessage();
        TaskWithContext<TResult> task = requests[msg.RequestID];

        // if it wasn't a winforms/wpf thread, it would be null
        // we choose our default context (threadpool)
        var context = task.Context ?? _defaultContext;

        // execute it on the context which was captured where it was added. So it won't get completed on this thread.
        context.Post(state =>
        {
            if (msg.Error == null)
                task.TaskCompletionSource.SetResult(msg);
            else
                task.TaskCompletionSource.SetException(new Exception(msg.Error));
        });
    }
}

public static Task<Response> SendAwaitResponse(string msg)
{
    // The key is here! Save the current synchronization context.
    var t = new TaskWithContext<Response>(SynchronizationContext.Current); 

    requests.Add(GetID(msg), t);
    stream.Write(msg);
    return t.TaskCompletionSource.Task;
}

// class to hold a task and context
public class TaskWithContext<TResult>
{
    public SynchronizationContext Context { get; }

    public TaskCompletionSource<TResult> TaskCompletionSource { get; } = new TaskCompletionSource<Response>();

    public TaskWithContext(SynchronizationContext context)
    {
        Context = context;
    }
}