【问题标题】:Do I need to synchronize resource access between Tasks with the default TaskScheduler?我是否需要使用默认的 TaskScheduler 同步 Task 之间的资源访问?
【发布时间】:2020-04-16 21:36:00
【问题描述】:

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

【问题讨论】:

  • 你能更具体地说明你想做什么吗?有很多方法可以解决问题,但这很大程度上取决于您拥有的应用程序。一个简短的回答就是你的问题是,是的。
  • 有两种类型的任务。在后台线程中卸载 CPU 密集型工作的任务,以及促进异步性的承诺式任务。 TaskScheduler 仅与第一种类型相关。您打算自己执行什么样的任务?
  • @TolgaEvcimen,我添加了更多细节。
  • @TheodorZoulias,后者。一项任务是网络绑定的,所有其他任务都使用循环中的 Task.Delay 来定期完成工作。
  • 为什么要使用BeginRead/EndRead和TaskCancellationSource,而不是直接使用ReadAsync?这将大大简化您的代码,因为您不再需要 OnRead 回调,也不需要声明和访问 TaskCancellationSource。

标签: c# .net multithreading async-await


【解决方案1】:

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

不完全是。

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2021-01-04
    • 2018-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 2013-12-06
    相关资源
    最近更新 更多