在C++中创建 100 个线程

Creating 100 threads in C++

本文关键字:线程 创建 C++      更新时间:2023-10-16

我正在尝试学习C++中的多线程。我正在尝试使用线程数组创建 100 个带有循环的线程,但出现错误。它给出了此错误:

error: array initializer must be an initializer list
thread threads[i](task1, list[i]);

这是代码:

static int list [100] = {};
thread threads [100] = {};

void task1(int n)
{
for (int i = 0; i < 10; i++)
n = n + 1;
}
int main()
{
for (int i = 0; i < sizeof(list); i++){
thread threads[i](task1, list[i]);
threads[i].join();
}
int total = 0;
for (int i = 0; i < sizeof(list); i++)
total += list[i];
cout << total << endl;
return 0;
}

数组thread threads [100] = {};创建 100 个默认初始化的非活动线程。

您可以通过更改循环来替换这些默认线程,如下所示:

for (int i = 0; i < sizeof(list); i++){
threads[i] = thread(task1, list[i]);    // <---- valid syntax
threads[i].join();
}

话虽如此:

  • 启动线程并立即加入它是一个可怜的问题。 如果您希望一些真正的多线程,最好有第二个循环进行连接。
  • 更好的做法是使用vector<thread>,以便仅在需要时构建线程(请参阅其他答案)
  • 您可能有兴趣std::thread::hardware_concurrency()找出有关硬件支持的实际并发线程数的提示,以免在太多上下文切换中创建太多线程和松散性能。

你应该以不同的方式执行此操作。线程必须使用它将运行的任务和参数进行初始化。当你创建一个包含 100 个线程的数组时,它会将它们全部初始化,没有任何内容,这是一种无用的浪费。此外,您对sizeof的使用是错误的。sizeof会给你数据结构的原始大小(以字节为单位),不会给你数组的元素数。如果你想使用sizeof来获取数组的元素数量,你应该做类似sizeof(array) / sizeof(<element type>)的事情,在你的情况下,这将是sizeof(list) / sizeof(int)。但是,实际上,您可能不应该在C++中使用 C 样式数组,在这种情况下肯定不应该。

您应该在运行时构建一个vector,并使用emplace_back逐个创建线程。此外,您正在以非常C-ish的方式编写代码。你应该写C++,而不是C.(另外,一个宠物烦恼,总是更喜欢前缀++。这里并不重要,但是,有时出于性能原因,它很重要,如果您习惯于始终使用前缀版本,那么您就不会有问题。这是可能的样子:

#include <vector>
#include <array>
#include <thread>
#include <iostream>
using ::std::thread;
using ::std::array;
using ::std::vector;
using ::std::cout;
using ::std::endl;
using ::std::ref;
static array<int, 100> list {};
vector<thread> threads;
void task1(int &n)
{
for (int i = 0; i < 10; ++i)
n = n + 1;
}
int main()
{
threads.reserve(list.size());  // Not needed, an optimization.
for (int &n : list) {  // Use a range-based for loop, not an explicit counting loop
threads.emplace_back(task1, ::std::ref(n));
}
for (auto &thr : threads) {
thr.join();
}
int total = 0;
for (int const &n : list) {
total += n;
}    
cout << total << endl;
return 0;
}

现在,由于这是一个玩具程序,我不会批评您随机创建 100 个线程的决定。实际上,这是一个坏主意。您希望根据您拥有的 CPU 数量定制您创建的线程数,否则操作系统将浪费大量时间在繁忙线程之间切换。以这种方式限制线程将涉及使用::std::thread::hardware_concurrency等函数来查询有多少个内核可用,并使用该信息来决定运行时有多少线程。

当然,这并不总是编写程序的最简单方法,为了简单起见,您可以选择任意数量的线程并坚持下去。但如果你这样做,它应该尽可能少地

逃脱。 但是,您创建线程的漫不经心的方法,以及您在创建线程后立即对每个线程进行join的方式告诉我,您并不真正了解线程的作用。如果立即使用线程join,则该线程不会并发运行。您正在启动它,然后立即等待它完成,然后再开始下一个。

此外,您让线程执行的小任务是对它们的不良使用。创建线程的成本有些高。每次调用时都涉及创建线程的函数调用具有数十、数百甚至数千微秒的开销。这听起来不像很多时间,但你必须记住,典型的函数调用开销大约是 1/50 微秒甚至 1/100 微秒。因此,通过创建线程调用函数的开销是正常调用函数的数万倍。

这意味着您应该在线程中执行相当大的任务。如果任务至少不需要一毫秒的时间,则不应创建线程。理想情况下,您应该创建一个线程,然后使用线程安全队列向其发送要执行的操作。这将大大减少每件事的开销。由于这样做的开销要小得多,因此您可以经济地在线程中执行较小的任务。

当您只是尝试使用一个小程序来创建线程时,所有这些都需要大量内容。但是,写得不好的多线程程序对世界来说是一件可怕的事情,尤其是对你自己。除了学习线程接口的基础知识之外,在使用它们之前,您还应该彻底了解它们。它们是一种非常容易滥用的工具。

运行尽可能多的线程,因为有问题要解决,可能会导致程序进行大量的上下文切换,因此不能尽快解决它。您通常不希望运行的线程数超过硬件支持的线程数(通常少一个线程)。

另一件经常产生世界差异的事情是虚假共享,这会大大降低您的表现。

如果您使用的是支持新的 C++17 执行策略(如 VS2017 或 g++ 9)的编译器,则可以使用并行执行策略std::execution::par执行for_each循环来完成您的工作。

下面的示例(我大大增加了工作量)在合理地确保避免错误共享(使用 alignas(std::hardware_destructive_interference_size))时,在我的计算机上需要 3.2 秒,使用默认对齐需要 21.3 秒。

#include <iostream>
#include <array> // std::array
#include <execution> // std::execution::par
#include <new> // std::hardware_destructive_interference_size
struct bork {
alignas(std::hardware_destructive_interference_size) int n;
// int n; // default alignment
};
std::array<bork, 1000> list{ 0 };
int main() {
std::for_each(std::execution::par, list.begin(), list.end(), [](auto& b) {
for (int i = 0; i < 100000000; i++) b.n = b.n + 1;
}
);
long long total = 0;
for (const auto& b : list) total += b.n;
std::cout << total << "n";
}