MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言CPU亲和性设置提升多线程性能

2021-11-241.2k 阅读

多线程编程在Linux环境下的性能挑战

在Linux系统的多线程编程中,随着处理器核心数量的不断增加,如何有效地分配线程到不同的CPU核心上,以充分发挥多核处理器的性能优势,成为了一个关键问题。默认情况下,Linux内核的调度器会根据自己的算法动态地将线程分配到各个CPU核心上执行。虽然这种动态调度机制在大多数情况下能够较好地平衡系统负载,但在某些特定场景下,它可能无法满足对性能有极致要求的应用程序。

例如,在一些实时性要求极高的应用中,如音频和视频处理、高性能计算等领域,线程频繁地在不同CPU核心之间迁移会带来额外的开销。这种开销主要体现在缓存一致性维护、上下文切换等方面。当一个线程从一个CPU核心迁移到另一个核心时,之前在原核心缓存中的数据可能在新核心中不可用,需要重新从内存中加载,这大大增加了数据访问的延迟。同时,上下文切换也需要保存和恢复线程的运行状态,消耗一定的CPU时间。

CPU亲和性的概念及原理

CPU亲和性(CPU Affinity)是一种允许用户或程序指定某个线程在特定CPU核心上运行的机制。通过设置CPU亲和性,线程可以固定在某个或某些CPU核心上执行,避免了线程在不同核心之间不必要的迁移,从而提高了性能。

从操作系统内核的角度来看,CPU亲和性的实现依赖于调度器的支持。Linux内核的调度器提供了一组系统调用来设置和获取线程的CPU亲和性掩码。CPU亲和性掩码是一个位掩码,每一位对应一个CPU核心。例如,在一个4核系统中,掩码0x01表示线程只能在第0号核心上运行,掩码0x03表示线程可以在第0号和第1号核心上运行。

Linux C语言中设置CPU亲和性的系统调用

在Linux环境下,C语言可以通过系统调用sys_sched_setaffinitysys_sched_getaffinity来设置和获取线程的CPU亲和性。这两个系统调用定义在<sched.h>头文件中。

sys_sched_setaffinity系统调用

sys_sched_setaffinity函数用于设置指定进程或线程的CPU亲和性掩码。其函数原型如下:

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
  • pid:指定要设置亲和性的进程或线程的ID。如果pid为0,则表示设置调用该函数的当前线程的亲和性。
  • cpusetsizemask参数的大小,以字节为单位。通常使用sizeof(cpu_set_t)来获取正确的大小。
  • mask:指向cpu_set_t类型的结构体指针,该结构体表示CPU亲和性掩码。

sys_sched_getaffinity系统调用

sys_sched_getaffinity函数用于获取指定进程或线程的当前CPU亲和性掩码。其函数原型如下:

int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
  • pid:指定要获取亲和性的进程或线程的ID。如果pid为0,则表示获取调用该函数的当前线程的亲和性。
  • cpusetsizemask参数的大小,以字节为单位。同样使用sizeof(cpu_set_t)
  • mask:指向cpu_set_t类型的结构体指针,用于存储获取到的CPU亲和性掩码。

操作cpu_set_t结构体的宏

cpu_set_t结构体用于表示CPU亲和性掩码,它是一个位集合。为了方便操作cpu_set_t结构体,Linux提供了一组宏:

  • CPU_ZERO(cpuset):清空cpuset中的所有位,即将其初始化为空集合。
  • CPU_SET(cpu, cpuset):将cpuset中对应cpu核心的位置1,表示将该核心添加到集合中。
  • CPU_CLR(cpu, cpuset):将cpuset中对应cpu核心的位置0,表示将该核心从集合中移除。
  • CPU_ISSET(cpu, cpuset):检查cpuset中对应cpu核心的位是否为1,如果是则返回非零值,否则返回0。

代码示例:多线程设置CPU亲和性提升性能

下面通过一个具体的代码示例来展示如何在Linux C语言中设置CPU亲和性以提升多线程性能。这个示例模拟了一个简单的多线程计算任务,每个线程计算一个数组的部分元素之和。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#include <time.h>

#define NUM_THREADS 4
#define ARRAY_SIZE 100000000

// 线程参数结构体
typedef struct {
    int start;
    int end;
    long long sum;
} ThreadArgs;

// 线程执行函数
void* calculate_sum(void* arg) {
    ThreadArgs* args = (ThreadArgs*)arg;
    for (int i = args->start; i < args->end; i++) {
        args->sum += i;
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    ThreadArgs args[NUM_THREADS];
    long long total_sum = 0;
    cpu_set_t cpuset;
    int num_cpus = sysconf(_SC_NPROCESSORS_ONLN);

    // 初始化CPU集合
    CPU_ZERO(&cpuset);

    // 为每个线程分配任务范围
    int step = ARRAY_SIZE / NUM_THREADS;
    for (int i = 0; i < NUM_THREADS; i++) {
        args[i].start = i * step;
        args[i].end = (i == NUM_THREADS - 1)? ARRAY_SIZE : (i + 1) * step;
        args[i].sum = 0;

        // 设置线程的CPU亲和性
        CPU_SET(i % num_cpus, &cpuset);
        if (pthread_setaffinity_np(threads[i], sizeof(cpu_set_t), &cpuset) != 0) {
            perror("pthread_setaffinity_np");
            return 1;
        }

        // 创建线程
        if (pthread_create(&threads[i], NULL, calculate_sum, &args[i]) != 0) {
            perror("pthread_create");
            return 1;
        }
    }

    // 等待所有线程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            return 1;
        }
        total_sum += args[i].sum;
    }

    printf("Total sum: %lld\n", total_sum);

    return 0;
}

在这个示例中:

  1. 定义了一个ThreadArgs结构体,用于传递每个线程的计算范围和计算结果。
  2. calculate_sum函数是线程的执行函数,负责计算分配给自己的数组部分的和。
  3. main函数中,初始化了cpu_set_t结构体,并为每个线程设置了CPU亲和性,确保每个线程在不同的CPU核心上执行。
  4. 创建并启动所有线程,等待它们完成计算,最后将各个线程的计算结果累加得到最终的总和。

性能测试与分析

为了验证设置CPU亲和性对多线程性能的提升,我们可以进行性能测试。通过记录设置和未设置CPU亲和性两种情况下程序的运行时间,来对比性能差异。

测试方法

  1. 未设置CPU亲和性:运行上述代码,注释掉设置CPU亲和性的部分,即移除CPU_SET(i % num_cpus, &cpuset);pthread_setaffinity_np(threads[i], sizeof(cpu_set_t), &cpuset);这两行代码。记录程序的运行时间。
  2. 设置CPU亲和性:运行原始代码,即保留设置CPU亲和性的部分。记录程序的运行时间。

测试结果分析

在大多数多核处理器系统上,设置CPU亲和性后的程序运行时间会明显短于未设置CPU亲和性的情况。这是因为设置CPU亲和性后,线程避免了在不同CPU核心之间的频繁迁移,减少了缓存失效和上下文切换带来的开销,从而提高了整体性能。

然而,需要注意的是,并非所有场景下设置CPU亲和性都能带来性能提升。例如,在CPU核心负载较轻,线程之间通信频繁的情况下,设置CPU亲和性可能会因为限制了调度器的优化能力而导致性能下降。因此,在实际应用中,需要根据具体的业务场景和系统负载情况来合理地设置CPU亲和性。

注意事项及常见问题

  1. CPU核心数量限制:在设置CPU亲和性掩码时,要确保指定的核心编号在系统实际的CPU核心数量范围内。否则,可能会导致设置失败或未定义行为。
  2. 线程与进程的区别sched_setaffinitypthread_setaffinity_np函数在设置对象上有所不同。sched_setaffinity可以设置进程或线程的亲和性,而pthread_setaffinity_np专门用于设置线程的亲和性。在使用时要根据实际需求选择合适的函数。
  3. 调度策略影响:不同的调度策略对CPU亲和性的支持和行为可能有所不同。例如,实时调度策略(如SCHED_FIFO和SCHED_RR)在设置CPU亲和性后,线程的调度行为可能与普通调度策略(如SCHED_OTHER)有所差异。在应用中要根据具体的调度需求来综合考虑CPU亲和性的设置。
  4. 多核异构系统:在一些多核异构系统中,不同类型的CPU核心可能具有不同的性能和功耗特点。在设置CPU亲和性时,需要根据任务的性质和核心的特性来合理分配线程,以达到最佳的性能功耗比。例如,对于计算密集型任务,可以分配到性能较高的核心上;对于功耗敏感型任务,可以分配到功耗较低的核心上。

进一步优化与扩展

  1. 动态调整CPU亲和性:在某些应用场景中,系统的负载情况可能会随时间变化。此时,可以考虑动态调整线程的CPU亲和性,根据系统实时的负载信息,将线程重新分配到负载较轻的CPU核心上,以进一步提高系统的整体性能。这可以通过定期检查系统负载(如使用top命令获取的CPU使用率等信息),并根据预设的策略来动态修改CPU亲和性掩码实现。
  2. 结合其他性能优化技术:CPU亲和性设置只是多线程性能优化的一部分。可以结合其他技术,如缓存优化、内存对齐、锁优化等,进一步提升多线程程序的性能。例如,通过合理地组织数据结构,使线程频繁访问的数据能够更好地利用CPU缓存;在多线程访问共享资源时,采用更高效的锁机制(如读写锁、无锁数据结构等)来减少锁竞争。
  3. 多进程与多线程混合编程:在一些复杂的应用中,可以采用多进程与多线程混合编程的方式,并结合CPU亲和性设置来充分利用系统资源。例如,将不同功能模块划分为不同的进程,每个进程内部再使用多线程进行并行计算。通过合理地设置进程和线程的CPU亲和性,可以避免进程间资源竞争,同时提高线程的执行效率。

应用场景

  1. 高性能计算:在科学计算、数据分析等领域,经常需要处理大规模的数据和复杂的计算任务。多线程编程结合CPU亲和性设置可以充分利用多核处理器的性能,加速计算过程。例如,在矩阵乘法、数值模拟等算法中,将不同的计算任务分配到不同的CPU核心上,避免线程迁移带来的开销,提高计算效率。
  2. 实时系统:在工业控制、自动驾驶等实时系统中,对任务的响应时间和稳定性要求极高。通过设置CPU亲和性,确保关键线程在特定的CPU核心上稳定运行,避免因线程迁移导致的延迟抖动,满足实时性需求。例如,在自动驾驶系统中,负责传感器数据处理和决策控制的线程可以固定在性能较高且稳定的CPU核心上,以保证车辆行驶的安全性。
  3. 网络服务器:在网络服务器应用中,如Web服务器、数据库服务器等,通常需要处理大量的并发请求。多线程技术用于并行处理这些请求,而设置CPU亲和性可以使每个线程在固定的CPU核心上执行,减少上下文切换开销,提高服务器的并发处理能力。例如,在一个高并发的Web服务器中,将处理HTTP请求的线程分别绑定到不同的CPU核心上,能够更高效地处理用户请求,提升服务器的整体性能。