【问题标题】:Safe to access shared resource from concurrent async tasks? (C#)从并发异步任务访问共享资源安全吗? (C#)
【发布时间】:2016-04-22 19:42:30
【问题描述】:

我需要并行运行多个异步方法并等待所有这些方法。 (Task.WhenAll) 然后在这些方法中,我需要访问一个共享资源,如 Dictionary()。我需要它是线程安全的吗?

我尝试运行以下两个示例,两者似乎都暗示它是多线程的(交错的消息/日期)。 http://rextester.com/AEH56431http://rextester.com/YEB50034

这张图有问题吗?任何人都可以确认/否认? 也没有看到 C# 并发大师 Stephen Cleary 专门谈论这个案例。 编辑:实际上 Stephen 在他的 async intro blog 中谈到了线程模型,这可以提供帮助。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Rextester
{
    public class Program
    {
        static public async Task run2() 
        {
            Console.WriteLine("START");

            await Task.WhenAll(steps(1),steps(2),steps(3),steps(4));

            Console.WriteLine("STOP");
        }


        static public async Task steps(int i) {
            await Task.Delay(100*i);
            Console.WriteLine("step"+i+".1");
            Thread.Sleep((5-i)*300);
            Console.WriteLine("step"+i+".2");
            Thread.Sleep((5-i)*300);
            Console.WriteLine("step"+i+".3");
            Thread.Sleep((5-i)*300);
            Console.WriteLine("step"+i+".4");
        }

       public static void Main(string[] args)
        {
           run2().Wait();
        }

    }
}

结果是:

START
step1.1
step2.1
step3.1
step4.1
step4.2
step3.2
step4.3   <-- multithreading? (2 isn't done)
step2.2
step1.2
step4.4
step3.3   <-- multithreading?
step2.3
step3.4
step1.3   <-- multithreading?
step2.4
step1.4
STOP

然后是另一个变体:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace Rextester
{
    public class Program
    {
        static public async Task run() 
        {
            Console.WriteLine("START");

            await Task.WhenAll(steps(1),steps(2),steps(3),steps(4));

            Console.WriteLine("\nSTOP");
        }

        static public async Task steps(int i) {
            var a = new StringBuilder();

            await Task.Delay(100*i); // This is to force to run in Async mode.

            // following is a block of code with (hopefully) no "async waits". Just CPU-bound (Thread.Sleep should be blocking the thread I think?)

            a.Append("\nSTART ["+Thread.CurrentThread.ManagedThreadId+"]");
            a.Append("\nstep "+i+".1 >"+DateTime.Now.ToString("mm:ss.fff"));
            Thread.Sleep((5-i)*400);
            a.Append("\nstep "+i+".2 >"+DateTime.Now.ToString("mm:ss.fff"));
            Thread.Sleep((5-i)*400);
            a.Append("\nstep "+i+".3 >"+DateTime.Now.ToString("mm:ss.fff"));
            Thread.Sleep((5-i)*400);
            a.Append("\nstep "+i+".4 >"+DateTime.Now.ToString("mm:ss.fff"));
            a.Append("\nSTOP");

            Console.WriteLine(a.ToString());
        }

       public static void Main(string[] args)
        {
           run().Wait();
        }

    }
}

输出如下:

START

START [9]
step 4.1 >57:08.485
step 4.2 >57:08.891
step 4.3 >57:09.298
step 4.4 >57:09.704
STOP

START [8]
step 3.1 >57:08.391
step 3.2 >57:09.204
step 3.3 >57:10.017
step 3.4 >57:10.830
STOP

START [7]
step 2.1 >57:08.297
step 2.2 >57:09.501
step 2.3 >57:10.705
step 2.4 >57:11.908
STOP

START [6]
step 1.1 >57:08.203
step 1.2 >57:09.814
step 1.3 >57:11.424
step 1.4 >57:13.034
STOP

STOP

【问题讨论】:

  • 如果你想展示一些东西,打印CurrentThread.ManagedId
  • okidoki。我找到了 Thread.CurrentThread.ManagedThreadId 但没有找到 ManagedId。都有不同的 ManagedThreadId
  • 只是在此处说明问题已发生重大变化。查看编辑历史记录。
  • 上一个问题是:“任务可以从线程中产生并行运行吗?”我试图通过谈论典型的多线程问题来更加“具体”。对我来说这是同一个问题!对困惑感到抱歉。无论如何我接受了你的回答。谢谢!
  • 您的第二个版本没有抓住重点,因为您的所有线程仍然是 Task.Delay 的副作用。对于任何真实事物,这根本不是一个好的模型。

标签: c# .net multithreading concurrency task


【解决方案1】:

暗示它是多线程的(交错的消息/日期)

那些暗示它是并发。而且,事实上,使用Task.WhenAll 的这行代码就是你做异步并发的方式:

await Task.WhenAll(steps(1),steps(2),steps(3),steps(4))

请注意,每个单独的步骤总是连续。因此,steps 的每次执行都将“线性”进行,即使它是异步的。这意味着1.1 将始终在1.2 之前,在1.3 之前等。所以所有1.x 将按顺序排列,2.x 将按顺序排列,等等。但是,有多个并发执行steps,它们可以交错。

还要注意,无论是否存在多线程,这都是正确的。在这种情况下(控制台应用程序),没有SynchronizationContextTaskScheduler,因此每个await 都在线程池线程上恢复。如果在单线程场景(例如 UI 线程)上运行相同的代码,您会在同一个线程上看到相同的顺序和交错行为。

【讨论】:

  • 哇!谢谢回答!所以你说存在多线程的事实纯粹是任务系统的一个实现细节——正如你所说的那样——可能会有所不同。只是为了澄清一下:“UI 线程”案例会像一个窗口应用程序或者我想可能是 Xamarin-on-Android?但不适用于 ASP.NET,对吧?
  • 稍微详细一点,步骤(1.1、1.2、1.3、1.4)不是由 Task.Delay() 而是由 Thread.Sleep() 分隔的。这是一个重要的区别。因此,在“UI 线程”(单线程场景)中,它们永远不会交错。这是正确的吗?
  • @Bernard:不是实现细节;它有据可查。我有一个async intro 博客文章,其中详细介绍了。是的,UI 意味着 GUI 应用程序,而不是 ASP.NET。在 UI 线程场景中,Thread.Sleep 会阻塞 UI 线程。
  • 嗯,是的,不是的。它运行位置的语义可以改变,但仅查看源代码,您无法真正“知道”这一点。你必须知道它在哪里运行——这就是我所说的“实现细节”。很久以前看过这篇文章,忘记了它包含的内容,我应该重新阅读它。再次感谢您为 C# 异步编程所做的所有努力!
  • Thread.sleep 阻塞 UI 线程:我猜你的意思是,与非 UI 线程的区别在于它不会阻塞 其他任务,因为池中有其他线程运行它们。 Thread.sleep 在任何情况下仍然会阻塞线程。
【解决方案2】:

这些“步骤”能够并行运行。异步函数在遇到未完成的可等待对象的第一个等待时返回。这里是await Task.Delay(100*i);Delay 的延续将在线程池上运行。出于这个原因,延迟会将其余代码移动到线程池中。

如果没有Delay,所有这些都将是连续的(包括其他睡眠)。

真的,这段代码的工作方式有点巧合。如果您使所有阻塞异步,它会同时运行就更清楚了。或者,您可以插入Task.Run 调用以强制并发行为,并记录它是有意的。

另外请注意,异步 IO(包括Task.Delay)在运行时不会消耗工作线程。它根本不消耗线程。

也许您可以删除Delay 调用并单步执行代码。这很容易做到,因为现在一切都在主线程上执行。

【讨论】:

  • 对不起,我真的很想并行执行多个任务(并行调用多个 Web 服务是用例)。但是我需要知道这部分 C# 代码是否会同时执行(如 multi-threading)。或不。我将更新我的问题以明确。
  • 也许你不知道 await 不会启动线程?也许这就是问题所在。我会等你的编辑。
  • 异步不启动线程,是的。但是代码可能会运行到另一个线程中。也许有很多这样的线程? (不像在nodejs中只有一个线程供所有人使用)。我故意使用“Thread.Sleep()”来“保持”线程并“查看”它们是否真的同时运行。如果它们同时运行并且没有“等待/任务”,这意味着我假设多线程(?)。我的第二个示例使用 StringBuffer(),因此它受 CPU 限制(不是 I/O 限制)。
  • 我看不出第二个例子有什么实质性的不同。;事实上, Sleep 持有线程,但在 Sleep 运行时,由于延迟,您已经在线程池中。消除延迟以查看差异。延迟并不神奇,任何安排其在 TP 上延续的东西都有效。试试await Task.Yield();await Task.Run(() =&gt; { }) (这个不能保证工作,但极有可能作为测试工作)。
  • 我只是注意到您的问题发生了重大变化。这不是一个好方法。让我们在这里坚持旧的。你的问题得到解答了吗?
猜你喜欢
  • 2021-05-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-05
  • 2015-06-07
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 2013-11-04
相关资源
最近更新 更多