首页 > 解决方案 > 我是否需要使用默认的 TaskScheduler 来同步 Tasks 之间的资源访问?

问题描述

我正在使用任务和等待/异步进行编程。我假设多线程的工作方式就像它在 NodeJS 或 Python 中一样,也就是说,它没有,一切都在同一个线程上运行。但是我一直在尝试了解任务实际上是如何执行的,我的理解是它们是由 TaskScheduler.Default 执行的,它的实现是隐藏的,但可以预期使用 ThreadPool。

我是否应该像所有任务都可以在任何线程中运行一样进行编程?

我的异步编程范围是相当轻量级的 CPU 工作,由几个无限循环组成,这些循环确实有效,然后在 Task.Delay 上等待几秒钟。现在唯一的共享资源是一个 int,每次我写网络消息时都会增加,但将来我希望我的任务将是共享字典和列表。

我还有一个网络任务,它连接到 TCP 服务器并使用我在 BeginRead+EndRead 上实现的任务读取消息。Read 函数由一个无限循环调用,该循环读取一条消息,对其进行处理,然后读取一条新消息。

        void OnRead(IAsyncResult result)
        {
            var pair = (Tuple<TaskCompletionSource<int>, NetworkStream>)result.AsyncState;
            int count = pair.Item2.EndRead(result);
            pair.Item1.SetResult(count);
        }

        async Task<byte[]> Read(NetworkStream stream, uint size)
        {
            var result = new byte[size];
            var count = 0;
            while(count < size)
            {
                var tcs = new TaskCompletionSource<int>();
                stream.BeginRead(result, count, result.Length - (int)count, new AsyncCallback(OnRead), Tuple.Create(tcs, stream));
                count += await tcs.Task;
            }
            return result;
        }

我使用同步写入写入 NetworkStream。

标签: c#.netmultithreadingasync-await

解决方案


我假设多线程的工作方式就像它在 NodeJS 或 Python 中一样,也就是说,它没有,一切都在同一个线程上运行。但是我一直在尝试了解任务实际上是如何执行的,我的理解是它们是由 TaskScheduler.Default 执行的,它的实现是隐藏的,但可以预期使用 ThreadPool。

不完全是。

首先,Task在 .NET 中可以是两个完全不同的东西。委托任务代表可以在某个线程上运行的代码,使用 aTaskScheduler来确定它们运行的​​位置和方式。委托任务是在原始任务并行库中引入的,几乎从未与异步代码一起使用。另一种Task是承诺任务。Promise这些与JavaScript中的更相似:它们可以表示任何东西——它们只是一个“尚未完成”或“已完成结果”或“已完成错误”的对象。这是不同类型任务的不同状态图的对比。

因此,首先要认识到的是,就像您不会在 JavaScript 中“执行 Promise”一样,您也不会在 .NET 中“执行(Promise)任务”。所以询问它在哪个线程上运行是没有意义的,因为它们不会在任何地方运行

但是,JS 和 C# 都有一个async/await语言结构,允许您编写更自然的代码来控制Promise。当async方法完成时,promise 完成;如果该async方法抛出,则 Promise 是错误的。

那么问题就变成了:控制这个承诺的代码在哪里运行?

在 JavaScript 世界中,答案是显而易见的:只有一个线程,所以这是代码运行的地方。在 .NET 世界中,答案有点复杂。我的异步介绍给出了核心概念:每个async方法都开始在调用线程上同步执行,就像任何其他方法一样。当它由于 a 而产生时await,它将捕获其“上下文”。然后,当该async方法准备好在 之后恢复时await,它会在该“上下文”中恢复。

“上下文”是SynchronizationContext.Current,除非它是null,在这种情况下上下文是TaskScheduler.Current。在现代代码中,“上下文”通常是 GUI 线程上下文(始终在 GUI 线程上恢复)或线程池上下文(在任何可用的线程池线程上恢复)。

我是否应该像所有任务都可以在任何线程中运行一样进行编程?

async如果在没有上下文的情况下调用方法中的代码,则可以在线程池线程上恢复。

是否需要在 Tasks 之间同步资源访问

可能不是。和关键字旨在允许轻松编写串行代码asyncawait因此,无需将 a 之前的代码与;await之后的代码同步。await之后的代码await始终在 之前的代码之后运行await,即使它在不同的线程上运行。此外,await注入所有必要的线程屏障,因此不存在乱序读取或类似问题。

但是,如果您的代码同时运行多个async方法,并且这些方法共享数据,则需要同步。我有一篇博客文章涵盖了这种偶然的隐式并行性(在文章的末尾)。一般来说,异步代码鼓励返回结果而不是应用副作用,只要你这样做,隐式并行性就不成问题了。


推荐阅读