C# Task.Run() 与 C++ std::async()

Posted

技术标签:

【中文标题】C# Task.Run() 与 C++ std::async()【英文标题】:C# Task.Run() vs. C++ std::async() 【发布时间】:2016-05-15 18:57:48 【问题描述】:

我在我的 Mac 2 上运行类似的代码示例,一个在 C++ 中,另一个在 C# 中。 2 个并行执行的简单任务(或者至少我希望它们执行),一个在循环中打印“+”,另一个在循环中打印“-”。我原以为这 2 个样本的输出非常相似,但出乎我的意料,它们却大不相同。

C++ 似乎真正并行运行任务。我可以看到 +- 在每次运行时都很好地交替,但 C# 似乎运行一个任务一段时间,然后切换到另一个任务并运行一段时间。像这样的:

C++: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
C# : ++++++++++---------++++++------

我知道不能对并行线程的运行方式做出假设,我很好奇 C++ 始终如一地产生如此好的结果。

感谢您的宝贵时间!

C#:

using System;
using System.Threading.Tasks;

public class ConcurrentTasks

    public static void Main(String[] args)
    
        var task1 = Task.Run(()=>DemoTask("+"));
        var task2 = Task.Run(()=>DemoTask("-"));
        var res1 = task1.Result;
        var res2 = task2.Result;
        Console.WriteLine("\nResults:");
        Console.WriteLine(res1);
        Console.WriteLine(res2);
    

    private static String DemoTask(String label)
    
        for (int i = 0; i < 1000; i++)
        
            Console.Write(label);
        

        return label + " result";
    


// mcs ConcurrentTasks.cs
// mono ConcurrentTasks.exe

C++:

#include <iostream>
#include <sstream>
#include <future>
using namespace std;

string demoTask(char label)

    for (int i = 0; i < 1000; i++)
    
        cout << label;
    

    stringstream ss;
    ss << label;
    ss << " result";
    return ss.str();


int main()

    auto task1 = async(demoTask, '+');
    auto task2 = async(demoTask, '-');
    auto res1 = task1.get();
    auto res2 = task2.get();
    cout << endl << "Results:" << endl;
    cout << res1 << endl;
    cout << res2 << endl;
    return 0;


// g++ --std=c++14 -Wall ConcurrentTasks.cpp -o ConcurrentTasks.exe
// ./ConcurrentTasks.exe

编辑:我已将 C# 示例更改为使用裸线程,结果相同。

带有线程的 C#:

using System;
using System.Threading;

public class ConcurrentTasks

    public static void Main(String[] args)
    
        var t1 = new Thread(() => DemoTask("+"));
        var t2 = new Thread(() => DemoTask("-"));
        t1.Start();
        t2.Start();
    

    private static String DemoTask(String label)
    
        for (int i = 0; i < 1000; i++)
        
            Console.Write(label);
        

        return label + " result";
    

【问题讨论】:

Task.Run 在线程池线程上运行任务。通常与多线程一样,除非使用显式同步机制,否则不能对两个线程的执行顺序做出任何假设。 是的,根据文档,两者都应该使用线程池。无论如何,我已经修改了 C# 示例以使用 Thread。结果相同。 线程是否来自线程池无关紧要,重要的是有单独的线程。当有单独的线程时,你不能期望它们以任何特定的顺序执行,除非你明确地同步它们。 我知道这一点,我只是好奇语言之间的结果是如此不同,并希望有人对此给出解释,语言之间关于此示例的差异. 不同的语言。线程、锁、调度、线程池、IO 机制等的不同实现。不同的结果。让我感到惊讶。 【参考方案1】:

粒度可能是原因之一。 TPL 可能具有调度任务的 粒度,但 C++ async 实现可能具有 粒度。这实质上意味着 C++ 将花费更多时间,因为只是为了处理 +- 它正在调度另一个任务。

另一个原因可能是coutConsole 如何实现输出流。如何使用锁在屏幕上呈现某些内容?正在实施任何缓冲吗?你看,Console.Write 可能正在缓冲,然后在一段时间后打印,但cout 可能只是立即打印。

因此,您应该做其他事情,而不是依赖于语言的底层 I/O - 可以将字符放在共享静态大小的数组中,共享的int 作为共享数组的索引(他们是原子的,不需要锁 用最轻的同步原语锁定它,比如读写器锁)。不,不要使用 vectorArray - 因为在这种情况下,你又依赖你不知道的东西!

在这两种情况下都使用发布版本,并尽可能使用相同的优化选项。另外,请确保您在同一台机器上运行它们。

【讨论】:

共享 int 索引 可能 需要显式使用原子语义,因为没有它们,像 globalIndex++ 这样的代码可能会将当前值(原子读取)放入当前线程的寄存器中,增加它然后写出来(原子地);而另一个线程可能在幕后对其进行了更新。除非 CPU 具有 increment variable at memory address [x] 形式的指令 并且 编译器总是将它用于未明确表示需要原子性的 int,否则您提到的行为是不可信的。 @JavierMartín,你是对的。 volatile 也可能有同样的问题。最好使用最轻的锁定机制【参考方案2】:

所以看起来 C# 的控制台影响最大。

我再次修改了 C# 示例,在执行 2 个任务时不使用控制台。相反,我使用共享输出字符数组。结果大不相同,非常接近 C++,尽管没有那么完美和一致。

令人惊奇的是,C++ 程序在每次运行时都能生成完美的 +- 对,即使在使用 cout 时也是如此。 C# 输出每次都略有不同,我经常看到一个线程在下一个开始之前完成。我不得不显着增加迭代次数,因为迭代次数很少,任务总是按顺序运行。

这是更新后的 C# 示例(注意互锁增量!):

using System;
using System.Threading;
using System.Threading.Tasks;

public class ConcurrentTasks

    private static char[] m_out;
    private static int m_index = -1;

    public static void Main(String[] args)
    
        var iterations = 5000;
        m_out = new char[iterations * 2];
        var task1 = Task.Run(()=>DemoTask(iterations, '+'));
        var task2 = Task.Run(()=>DemoTask(iterations, '-'));
        var res1 = task1.Result;
        var res2 = task2.Result;

        for (int i = 0; i < m_out.Length; i++)
        
            Console.Write(m_out[i]);
        

        Console.WriteLine("\nResults:");
        Console.WriteLine(res1);
        Console.WriteLine(res2);
    

    private static String DemoTask(int iterations, char label)
    
        for (int i = 0; i < iterations; i++)
        
            int index = Interlocked.Increment(ref m_index);
            m_out[index] = label;
        

        return label + " result";
    

【讨论】:

以上是关于C# Task.Run() 与 C++ std::async()的主要内容,如果未能解决你的问题,请参考以下文章

C#:后台工作/通信的设计模式,不滥用Task.Run

C# 中的 C++ std::async 与异步/等待

避免 Task.Run 导致主线程死锁(C#)

C# Task.Run()运行“含参数和返回值的方法”的用法

C# -Task.Run() 中线程池中的索引超出范围异常

c# string与c++ std::string的互相转换