简单的多线程混淆C++

Simple multi-threading confusion for C++

本文关键字:C++ 多线程 简单      更新时间:2023-10-16

我正在开发一个Qt C++应用程序。我有一个非常基本的疑问,如果这太愚蠢了,请原谅我......

我应该创建多少个线程才能在最短时间内在它们之间分配任务?

我问这个是因为我的笔记本电脑是第三代 i5 处理器 (3210m)。因此,由于它是双核NO_OF_PROCESSORS环境变量向我显示4。我在一篇文章中读到,应用程序的动态内存仅适用于启动该应用程序的处理器。那么我应该只创建 1 个线程(因为 env 变量说 4 个处理器)还是 2 个线程(因为我的处理器是双核的,env 变量可能暗示内核数)还是 4 个线程(如果那篇文章是错误的)?请原谅我,因为我是一个试图学习Qt的初学者。谢谢:)

虽然超线程有点谎言(你被告知你有 4 个内核,但你实际上只有 2 个内核,另外两个只在前两个不使用的资源上运行,如果有这样的事情),正确的做法仍然是使用NO_OF_PROCESSORS告诉你的尽可能多的线程

请注意,英特尔并不是唯一一个对你撒谎的人,在最近的AMD处理器上更糟,你有6个所谓的"真实"内核,但实际上只有其中的4个,资源在它们之间共享。

然而,大多数时候,它或多或少地奏效了。即使没有显式阻塞线程(在等待函数或阻塞读取上),也总会有一个内核停滞的点,例如由于缓存未命中而访问内存,这会泄露超线程内核可以使用的资源。

因此,如果你有很多工作要做,并且你可以很好地并行化它,你真的应该拥有与广告化核心一样多的工人(无论它们是"真正的"还是"超级的")。这样,您可以最大限度地利用可用的处理器资源。

理想情况下,人们会在应用程序启动的早期创建工作线程,并有一个任务队列将任务交给工作线程。由于同步通常是不可忽略的,因此任务队列应该相当"粗糙"。在最大核心使用率和同步开销方面存在权衡。

例如,如果数组中有 1000 万个元素要处理,则可以推送引用 100,000

或 200,000 个连续元素的任务(您不希望推送 1000 万个任务!这样,您可以确保平均没有内核处于空闲状态(如果一个内核提前完成,它会拉取另一个任务而不是什么都不做),并且您只有一百个左右的同步,其开销或多或少可以忽略不计。

如果任务涉及文件/套接字读取或其他可以无限期阻塞的事情,那么生成另外 1-2 个线程通常没有错误(需要一些实验)。

这完全取决于您的工作负载,如果您的工作负载非常占用 cpu,您应该更接近 CPU 的线程数(在您的情况下为 4 - 2 核 * 2 用于超线程)。小额超额订阅也可能是可以的,因为这可以补偿您的一个线程等待锁定或其他内容的时间。
另一方面,如果您的应用程序不依赖于 CPU 并且大部分时间都在等待,您甚至可以创建比 CPU 计数更多的线程。但是,您应该注意到线程创建可能会产生相当大的开销。唯一的解决方案是衡量您的瓶颈是什么,并朝这个方向进行优化。

另请注意,如果您使用的是 c++11,则可以使用 std::thread::hardware_concurrency 来获取一种可移植的方法来确定您拥有的 CPU 内核数量。

关于你

关于动态内存的问题,你一定误解了那里的一些东西。通常,您创建的所有线程都可以访问您在应用程序中创建的内存。此外,这与C++无关,超出了C++标准的范围。

NO_OF_PROCESSORS显示 4,因为您的 CPU 具有超线程。超线程是英特尔的技术商标,它使单个内核能够或多或少地同时执行同一应用程序的 2 个线程。例如,只要一个线程正在获取数据,另一个线程正在访问 ALU,它就可以工作。如果两者都需要相同的资源,并且指令无法重新排序,则一个线程将停止。这就是您看到 4 个内核的原因,即使您有 2 个内核。

该动态内存仅适用于其中一个内核是IMO不太正确的,但是寄存器内容,有时缓存内容是。RAM 中的所有内容都应可供所有 CPU 使用。

比 CPU 更多的线程可以提供帮助,具体取决于操作系统调度程序的工作方式/访问数据的方式等。要找到这一点,您必须对代码进行基准测试。其他一切都只是猜测。

除此之外,如果你想学习Qt,这可能不是正确的担心......

编辑:

回答您的问题:如果您增加线程数,我们无法真正告诉您程序的运行速度会减多少/快。根据您正在执行的操作,这将发生变化。例如,如果您正在等待来自网络的响应,则可以增加线程数。如果您的线程都使用相同的硬件,则 4 个线程的性能可能不会优于 1 个。最好的方法是简单地对代码进行基准测试。

理想的世界中,如果你"只是"处理数字应该不会有什么不同,如果你有 4 或 8 个线程正在运行,净时间应该是相同的(忽略上下文切换的时间等),只是响应时间会有所不同。问题是没有什么是理想的,我们有缓存,您的 CPU 都通过同一总线访问相同的内存,因此最终它们会争夺对资源的访问权。然后,您还有一个操作系统,它可能在给定时间调度线程/进程,也可能不调度。

您还要求提供同步开销的说明:如果所有线程都访问相同的数据结构,则必须执行一些锁定等操作,以便在更新数据时没有线程访问处于无效状态的数据。

假设您有两个线程,它们都执行相同的操作:

int sum = 0; // global variable
thread() {
    int i = sum;
    i += 1;
    sum = i;
}

如果同时启动两个线程执行此操作,则无法可靠地预测输出: 它可能会像这样发生:

THREAD A : i = sum; // i = 0
           i += 1;  // i = 1
**context switch**
THREAD B : i = sum; // i = 0
           i += 1;  // i = 1
           sum = i; // sum = 1
**context switch**
THREAD A : sum = i; // sum = 1

最后sum1,即使你启动了线程两次,也不会2。为了避免这种情况,您必须同步对sum共享数据的访问。通常,您会根据需要阻止对sum的访问来执行此操作。同步开销是线程等待资源再次解锁而不执行任何操作的时间。

如果每个线程都有离散的工作包,并且没有共享资源,则不应有同步开销。

Qt中开始在线程之间划分工作的最简单方法是使用Qt并发框架。 示例:您有一些要对 QList 中的每个项目执行的操作(很常见)。

void operation( ItemType & item )
{
  // do work on item, changing it in place
}
QList<ItemType> seq;  // populate your list
// apply operation to every member of seq
QFuture<void> future = QtConcurrent::map( seq, operation );
// if you want to wait until all operations are complete before you move on...
future.waitForFinished();

Qt自动处理线程...无需担心。 QFuture 文档描述了如果需要,如何使用信号和插槽不对称地处理map完成。