性能优化与多线程编程
1. 性能优化相关
在性能优化方面,我们可以从时间测量和数据转换等基础操作入手。
1.1 单次时间测量示例
以下是一个单次时间测量的示例代码:
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
int main(void) {
struct rusage r;
struct timeval start;
struct timeval end;
getrusage(RUSAGE_SELF, &r );
start = r.ru_utime;
for( uint64_t i = 0; i < 100000000; i++ );
getrusage(RUSAGE_SELF, &r );
end = r.ru_utime;
long res = ((end.tv_sec - start.tv_sec) * 1000000L) +
end.tv_usec - start.tv_usec;
printf( "Time elapsed in microseconds: %ld\n", res );
return 0;
}
该代码通过
getrusage
函数获取程序执行前后的用户时间,计算出程序执行的微秒数。
1.2 无符号字符到浮点数的快速转换
可以使用一个表来实现无符号字符到浮点数的快速转换:
float const byte_to_float[] = {
0.0f, 1.0f, 2.0f, ..., 255.0f };
通过这种方式,可以快速地将无符号字符转换为对应的浮点数。
1.3 性能优化相关问题
以下是一些性能优化相关的常见问题:
-
GCC 全局优化选项
:需要了解哪些 GCC 选项可以全局控制优化选项。
-
高收益优化类型
:探讨哪些类型的优化可能带来最大的收益。
-
省略帧指针的影响
:分析省略帧指针带来的好处和缺点。
-
尾递归函数与普通递归函数的区别
:明确尾递归函数和普通递归函数的差异。
-
递归函数的改写
:思考是否任何递归函数都可以在不使用额外数据结构的情况下重写为尾递归。
-
公共子表达式消除
:理解什么是公共子表达式消除以及它对代码编写的影响。
-
常量传播
:了解常量传播的概念。
-
函数静态标记的作用
:解释为什么在可能的情况下标记函数为静态有助于编译器优化。
-
命名返回值优化的好处
:说明命名返回值优化带来的好处。
-
分支预测
:介绍什么是分支预测。
-
动态分支预测及相关表
:解释动态分支预测、全局和局部历史表的概念。
-
执行单元
:说明什么是执行单元以及为什么要关注它们。
-
AVX 指令速度与执行单元数量的关系
:研究 AVX 指令速度和执行单元数量之间的联系。
-
良好的内存访问模式
:探讨哪些类型的内存访问模式是良好的。
-
多级缓存的原因
:解释为什么需要多级缓存。
-
预取带来性能提升的情况
:分析在哪些情况下预取可以带来性能提升以及原因。
-
SSE 指令的用途
:介绍 SSE 指令的用途。
-
SSE 指令对齐操作数的原因
:说明为什么大多数 SSE 指令需要对齐的操作数。
-
数据从通用寄存器复制到 xmm 寄存器的方法
:讲解如何将数据从通用寄存器复制到 xmm 寄存器。
-
使用 SIMD 指令的适用情况
:确定在哪些情况下使用 SIMD 指令是值得的。
2. 多线程编程基础
多线程编程是提高程序性能的重要手段,但也带来了一些挑战。
2.1 进程与线程的区别
在多线程编程中,需要明确进程和线程的区别:
-
进程
:是一个资源容器,收集程序执行所需的各种运行时信息和资源,包括地址空间、打开的文件描述符、寄存器等。具体包含以下内容:
- 部分填充有可执行代码、数据、共享库、其他映射文件等的地址空间,部分可以与其他进程共享。
- 所有其他相关状态,如打开的文件描述符、寄存器等。
- 进程 ID、进程组 ID、用户 ID、组 ID 等信息。
- 用于进程间通信的其他资源,如管道、信号量、消息队列等。
-
线程
:是可以由操作系统调度执行的指令流。操作系统调度的是线程而不是进程。每个线程是进程的一部分,有自己的进程状态,包括:
- 寄存器。
- 栈(技术上由栈指针寄存器定义,但由于所有处理器线程共享相同的地址空间,一个线程可以访问其他线程的栈,但这通常不是一个好主意)。
- 对调度器重要的属性,如优先级。
- 待处理和阻塞的信号。
- 信号掩码。
当进程关闭时,所有相关资源(包括所有线程、打开的文件描述符等)都会被释放。
2.2 多线程编程的难点
多线程编程允许利用多个处理器核心同时执行线程,例如,当一个线程从磁盘读取文件(这是一个非常慢的操作)时,另一个线程可以利用这个间隙进行 CPU 密集型计算,从而更均匀地分配 CPU 负载。然而,当线程需要处理相同的数据时,如果数据被修改,会面临以下问题:
-
数据可见性
:线程 A 何时能看到线程 B 所做的更改?
-
数据修改顺序
:线程以什么顺序修改数据?由于指令可能为了优化目的而重新排序,这个问题变得更加复杂。
-
数据操作的原子性
:如何在其他线程不干扰的情况下对复杂数据进行操作?
如果这些问题处理不当,会出现难以捕捉的错误,因为这些错误只有在不同线程的指令以特定的、不幸的顺序执行时才会出现。
2.3 执行顺序与内存重排序
在单线程编程中,我们通常认为 C 语句的顺序对应于编译后的机器指令的执行顺序。然而,编译器可能会为了提高局部性而在不改变代码语义的情况下重新排序指令。例如:
// 原代码
char x[1000000], y[1000000];
...
x[4] = y[4];
x[10004] = y[10004];
x[5] = y[5];
x[10005] = y[10005];
// 可能的汇编翻译结果 1
mov al,[rsi + 4]
mov [rdi+4],al
mov al,[rsi + 10004]
mov [rdi+10004],al
mov al,[rsi + 5]
mov [rdi+5],al
mov al,[rsi + 10005]
mov [rdi+10005],al
// 优化后的汇编翻译结果 2
mov al,[rsi + 4]
mov [rdi+4],al
mov al,[rsi + 5]
mov [rdi+5],al
mov al,[rsi + 10004]
mov [rdi+10004],al
mov al,[rsi + 10005]
mov [rdi+10005],al
这两个指令序列在单 CPU 的抽象机器中效果相似,但第二个结果通常执行得更快,因此编译器可能会选择它。这就是内存重排序的简单例子,即内存访问顺序与源代码不同。
对于单线程应用程序,只要可观察行为不变,操作顺序通常无关紧要。但在多线程通信中,这种自由就会受到限制。因此,需要了解内存重排序并正确设置它们。
2.4 强内存模型和弱内存模型
内存重排序可以由编译器或处理器本身在微代码中执行。通常,两者都会进行重排序,我们需要关注这两种情况。内存模型可以分为以下四类,从更宽松到更强:
| 内存模型类型 | 描述 |
| — | — |
| 非常弱 | 任何类型的内存重排序都可能发生(当然,只要单线程程序的可观察行为不变)。 |
| 具有数据依赖排序的弱模型(如 ARM v7 的硬件内存模型) | 考虑一种特定类型的数据依赖,即加载之间的依赖。例如,需要从内存中获取地址,然后使用该地址进行另一次获取。非常弱的内存模型不保证数据依赖排序。 |
| 通常强(如 Intel 64 的硬件内存模型) | 保证所有存储操作按提供的顺序执行,但某些加载操作可以移动。 |
| 顺序一致 | 就像在单处理器核心上逐步调试非优化程序时看到的那样,不会发生内存重排序。 |
2.5 重排序示例
以下是一个内存重排序可能导致问题的示例代码:
// 原代码
int x = 0;
int y = 0;
void thread1(void) {
x = 1;
print(y);
}
void thread2(void) {
y = 1;
print(x);
}
// 重排序后的代码
int x = 0;
int y = 0;
void thread1(void) {
print(y);
x = 1;
}
void thread2(void) {
print(x);
y = 1;
}
两个线程共享变量
x
和
y
,由于这些指令是完全独立的,它们可以在每个线程内重新排序而不改变可观察行为。对于每个线程,有存储 + 加载或加载 + 存储两种选择,每种选择又有六种可能的执行顺序,如 1 - 1 - 2 - 2、1 - 2 - 1 - 2 等。如果编译器对这些操作进行了重新排序,或者即使编译器控制良好或禁用了重排序,处理器的内存重排序仍然可能导致
load x
和
load y
先执行,使得它们的值看起来为 0。这个示例表明,这样的程序结果是高度不可预测的。
2.6
volatile
关键字的作用
C 语言的内存模型相当弱,编译器可能会对代码进行指令重排序,甚至删除无用的赋值语句。
volatile
关键字可以解决这个问题,它强制编译器不对该变量的读写进行优化,并且抑制任何可能的指令重排序。但是,它只对单个变量施加这些限制,不能保证不同
volatile
变量的写入顺序。例如:
// 原代码
int x,y; x = 1;
y = 2;
x = 3;
// 使用 volatile 关键字
volatile int x, y;
x = 1;
x = 3;
y = 2;
// 或者
volatile int x, y;
y = 2;
x = 1;
x = 3;
显然,这些保证对于多线程应用程序是不够的,不能使用
volatile
变量来组织对共享数据的访问,因为这些访问仍然可以自由移动。为了安全地访问共享数据,需要两个保证:
- 读写操作实际发生,编译器不会将值缓存在寄存器中而不写回内存。
- 不发生内存重排序。
在后续内容中,我们将介绍两种提供这些保证的机制:内存屏障和 C11 中引入的原子变量。
2.7 内存屏障
内存屏障是一种特殊的指令或语句,用于限制内存重排序的方式。编译器或硬件可以使用各种技巧来提高平均性能,如重排序、延迟内存操作、推测加载或分支预测、将变量缓存在寄存器中。控制这些操作对于确保某些操作已经执行至关重要,因为其他线程的逻辑可能依赖于此。
以下是几种常见的内存屏障:
-
写内存屏障
:保证代码中屏障之前指定的所有存储操作在屏障之后指定的所有存储操作之前发生。GCC 使用
asm volatile(""::: "memory")
作为通用内存屏障,Intel 64 使用
sfence
指令。
-
读内存屏障
:保证代码中屏障之前指定的所有加载操作在屏障之后指定的所有加载操作之前发生。它是一种更强形式的数据依赖屏障。GCC 使用
asm volatile(""::: "memory")
作为通用内存屏障,Intel 64 使用
lfence
指令。
-
数据依赖屏障
:考虑加载之间的依赖,是一种较弱形式的读内存屏障,不提供关于独立加载或任何类型存储的保证。
-
通用内存屏障
:这是最终的屏障,强制代码中屏障之前指定的每个内存更改都被提交,并且防止后续操作以看起来在屏障之前执行的方式重新排序。GCC 使用
asm volatile(""::: "memory")
作为通用内存屏障,Intel 64 使用
mfence
指令。
-
获取操作
:如果一个操作从共享内存中读取数据,并且保证不会与源代码中后续的读写操作重新排序,则称该操作具有获取语义。类似于通用内存屏障,但后续代码不会以在该屏障之前执行的方式重新排序。
-
释放操作
:如果一个操作向共享内存中写入数据,并且保证不会与源代码中之前的读写操作重新排序,则称该操作具有释放语义。类似于通用内存屏障,但允许较新的操作重新排序到释放操作之前的位置。
以下是一个 GCC 内联的
mfence
汇编命令示例,它结合了编译器屏障和完整的内存屏障:
asm volatile("mfence" ::: "memory")
任何定义在当前翻译单元中不可用且不是内建函数(特定汇编指令的跨平台替代)的函数调用都是一个编译器内存屏障。
综上所述,性能优化和多线程编程是复杂而重要的领域,需要深入理解各种概念和技术,才能编写出高效、稳定的程序。在后续内容中,我们将继续探讨如何限制编译器和处理器的重排序,以及如何使用原子变量来确保线程安全。
性能优化与多线程编程
3. 内存屏障的应用与分析
内存屏障在多线程编程中起着至关重要的作用,它能够控制内存操作的顺序,确保程序的正确性。下面我们将详细分析不同类型内存屏障的应用场景及示例。
3.1 写内存屏障的应用
写内存屏障保证了在屏障之前的所有存储操作会在屏障之后的存储操作之前完成。在某些场景下,我们需要确保数据的写入顺序,以避免其他线程读取到不一致的数据。
例如,在一个多线程的配置更新系统中,一个线程负责更新配置数据,另一个线程负责读取配置数据。为了确保更新线程写入的数据能够被读取线程正确看到,我们可以使用写内存屏障。
#include <stdio.h>
#include <pthread.h>
// 模拟配置数据
volatile int config_data = 0;
// 写内存屏障
#define WRITE_MEMORY_BARRIER asm volatile("sfence" ::: "memory")
// 更新配置的线程函数
void* update_config(void* arg) {
// 更新配置数据
config_data = 1;
// 插入写内存屏障
WRITE_MEMORY_BARRIER;
return NULL;
}
// 读取配置的线程函数
void* read_config(void* arg) {
// 等待一段时间,确保更新线程有机会执行
sleep(1);
// 读取配置数据
int value = config_data;
printf("Read config value: %d\n", value);
return NULL;
}
int main() {
pthread_t update_thread, read_thread;
// 创建更新线程
pthread_create(&update_thread, NULL, update_config, NULL);
// 创建读取线程
pthread_create(&read_thread, NULL, read_config, NULL);
// 等待线程结束
pthread_join(update_thread, NULL);
pthread_join(read_thread, NULL);
return 0;
}
在上述代码中,
update_config
线程更新配置数据后,插入了写内存屏障
WRITE_MEMORY_BARRIER
,确保配置数据的写入操作在屏障之后的操作之前完成。这样,
read_config
线程读取到的配置数据就是最新的。
3.2 读内存屏障的应用
读内存屏障保证了在屏障之前的所有加载操作会在屏障之后的加载操作之前完成。在一些需要确保数据依赖顺序的场景中,读内存屏障非常有用。
例如,在一个多线程的链表遍历系统中,一个线程负责更新链表节点,另一个线程负责遍历链表。为了确保遍历线程能够正确读取到更新后的链表节点,我们可以使用读内存屏障。
#include <stdio.h>
#include <pthread.h>
// 链表节点结构
typedef struct Node {
int data;
struct Node* next;
} Node;
// 全局链表头
volatile Node* head = NULL;
// 读内存屏障
#define READ_MEMORY_BARRIER asm volatile("lfence" ::: "memory")
// 更新链表的线程函数
void* update_list(void* arg) {
// 创建新节点
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = 1;
new_node->next = head;
// 更新链表头
head = new_node;
return NULL;
}
// 遍历链表的线程函数
void* traverse_list(void* arg) {
// 等待一段时间,确保更新线程有机会执行
sleep(1);
// 插入读内存屏障
READ_MEMORY_BARRIER;
Node* current = head;
while (current != NULL) {
printf("Node data: %d\n", current->data);
current = current->next;
}
return NULL;
}
int main() {
pthread_t update_thread, traverse_thread;
// 创建更新线程
pthread_create(&update_thread, NULL, update_list, NULL);
// 创建遍历线程
pthread_create(&traverse_thread, NULL, traverse_thread, NULL);
// 等待线程结束
pthread_join(update_thread, NULL);
pthread_join(traverse_thread, NULL);
return 0;
}
在上述代码中,
traverse_list
线程在遍历链表之前插入了读内存屏障
READ_MEMORY_BARRIER
,确保在读取链表节点之前,所有之前的加载操作已经完成,从而避免读取到不一致的数据。
3.3 数据依赖屏障的应用
数据依赖屏障主要考虑加载操作之间的依赖关系,它是一种较弱形式的读内存屏障。在一些对数据依赖顺序有要求,但对独立加载操作和存储操作没有严格顺序要求的场景中,数据依赖屏障可以发挥作用。
例如,在一个多线程的矩阵计算系统中,一个线程负责计算矩阵的一部分元素,另一个线程负责使用这些计算结果进行后续计算。为了确保后续计算线程能够正确使用计算结果,我们可以使用数据依赖屏障。
#include <stdio.h>
#include <pthread.h>
#define MATRIX_SIZE 10
// 矩阵
volatile int matrix[MATRIX_SIZE][MATRIX_SIZE];
// 数据依赖屏障
#define DATA_DEPENDENCY_BARRIER asm volatile("" ::: "memory")
// 计算矩阵元素的线程函数
void* calculate_matrix(void* arg) {
for (int i = 0; i < MATRIX_SIZE; i++) {
for (int j = 0; j < MATRIX_SIZE; j++) {
matrix[i][j] = i + j;
}
}
// 插入数据依赖屏障
DATA_DEPENDENCY_BARRIER;
return NULL;
}
// 使用矩阵元素进行后续计算的线程函数
void* use_matrix(void* arg) {
// 等待一段时间,确保计算线程有机会执行
sleep(1);
// 插入数据依赖屏障
DATA_DEPENDENCY_BARRIER;
int sum = 0;
for (int i = 0; i < MATRIX_SIZE; i++) {
for (int j = 0; j < MATRIX_SIZE; j++) {
sum += matrix[i][j];
}
}
printf("Matrix sum: %d\n", sum);
return NULL;
}
int main() {
pthread_t calculate_thread, use_thread;
// 创建计算线程
pthread_create(&calculate_thread, NULL, calculate_matrix, NULL);
// 创建使用线程
pthread_create(&use_thread, NULL, use_matrix, NULL);
// 等待线程结束
pthread_join(calculate_thread, NULL);
pthread_join(use_thread, NULL);
return 0;
}
在上述代码中,
calculate_matrix
线程计算矩阵元素后插入了数据依赖屏障
DATA_DEPENDENCY_BARRIER
,
use_matrix
线程在使用矩阵元素进行后续计算之前也插入了数据依赖屏障,确保数据依赖顺序的正确性。
3.4 通用内存屏障的应用
通用内存屏障是最严格的内存屏障,它强制所有在屏障之前的内存操作都完成,并且防止后续操作在屏障之前执行。在一些对内存操作顺序要求非常严格的场景中,通用内存屏障是必要的。
例如,在一个多线程的文件写入系统中,一个线程负责写入文件头信息,另一个线程负责写入文件内容。为了确保文件头信息先于文件内容写入,我们可以使用通用内存屏障。
#include <stdio.h>
#include <pthread.h>
// 模拟文件头信息
volatile int file_header = 0;
// 模拟文件内容
volatile int file_content = 0;
// 通用内存屏障
#define GENERAL_MEMORY_BARRIER asm volatile("mfence" ::: "memory")
// 写入文件头的线程函数
void* write_header(void* arg) {
// 写入文件头信息
file_header = 1;
// 插入通用内存屏障
GENERAL_MEMORY_BARRIER;
return NULL;
}
// 写入文件内容的线程函数
void* write_content(void* arg) {
// 等待一段时间,确保写入文件头线程有机会执行
sleep(1);
// 插入通用内存屏障
GENERAL_MEMORY_BARRIER;
// 写入文件内容
file_content = 2;
return NULL;
}
int main() {
pthread_t header_thread, content_thread;
// 创建写入文件头线程
pthread_create(&header_thread, NULL, write_header, NULL);
// 创建写入文件内容线程
pthread_create(&content_thread, NULL, write_content, NULL);
// 等待线程结束
pthread_join(header_thread, NULL);
pthread_join(content_thread, NULL);
return 0;
}
在上述代码中,
write_header
线程写入文件头信息后插入了通用内存屏障
GENERAL_MEMORY_BARRIER
,
write_content
线程在写入文件内容之前也插入了通用内存屏障,确保文件头信息先于文件内容写入。
3.5 获取操作和释放操作的应用
获取操作和释放操作是一种特殊的内存屏障,它们分别保证了操作前后的读写顺序。获取操作确保后续的读写操作不会在该操作之前执行,释放操作确保之前的读写操作不会在该操作之后执行。
例如,在一个多线程的信号量系统中,一个线程负责释放信号量,另一个线程负责获取信号量。为了确保信号量的正确使用,我们可以使用获取操作和释放操作。
#include <stdio.h>
#include <pthread.h>
// 模拟信号量
volatile int semaphore = 0;
// 获取操作
#define ACQUIRE_OPERATION asm volatile("" ::: "memory")
// 释放操作
#define RELEASE_OPERATION asm volatile("" ::: "memory")
// 释放信号量的线程函数
void* release_semaphore(void* arg) {
// 释放信号量
semaphore = 1;
// 插入释放操作
RELEASE_OPERATION;
return NULL;
}
// 获取信号量的线程函数
void* acquire_semaphore(void* arg) {
// 等待一段时间,确保释放信号量线程有机会执行
sleep(1);
// 插入获取操作
ACQUIRE_OPERATION;
if (semaphore == 1) {
printf("Semaphore acquired.\n");
// 重置信号量
semaphore = 0;
} else {
printf("Semaphore not available.\n");
}
return NULL;
}
int main() {
pthread_t release_thread, acquire_thread;
// 创建释放信号量线程
pthread_create(&release_thread, NULL, release_semaphore, NULL);
// 创建获取信号量线程
pthread_create(&acquire_thread, NULL, acquire_semaphore, NULL);
// 等待线程结束
pthread_join(release_thread, NULL);
pthread_join(acquire_thread, NULL);
return 0;
}
在上述代码中,
release_semaphore
线程释放信号量后插入了释放操作
RELEASE_OPERATION
,
acquire_semaphore
线程在获取信号量之前插入了获取操作
ACQUIRE_OPERATION
,确保信号量的正确使用。
4. 多线程编程的最佳实践
多线程编程虽然能够提高程序的性能,但也带来了许多挑战,如数据竞争、死锁等。为了编写出高效、稳定的多线程程序,我们需要遵循一些最佳实践。
4.1 避免数据竞争
数据竞争是多线程编程中最常见的问题之一,它发生在多个线程同时访问和修改共享数据时。为了避免数据竞争,我们可以使用同步机制,如互斥锁、信号量等。
以下是一个使用互斥锁避免数据竞争的示例:
#include <stdio.h>
#include <pthread.h>
// 共享数据
volatile int shared_data = 0;
// 互斥锁
pthread_mutex_t mutex;
// 线程函数
void* increment_data(void* arg) {
// 加锁
pthread_mutex_lock(&mutex);
// 访问和修改共享数据
shared_data++;
// 解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t threads[10];
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建线程
for (int i = 0; i < 10; i++) {
pthread_create(&threads[i], NULL, increment_data, NULL);
}
// 等待线程结束
for (int i = 0; i < 10; i++) {
pthread_join(threads[i], NULL);
}
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Shared data value: %d\n", shared_data);
return 0;
}
在上述代码中,我们使用
pthread_mutex_t
类型的互斥锁来保护共享数据
shared_data
。在访问和修改共享数据之前,线程先加锁,访问和修改完成后再解锁,确保同一时间只有一个线程能够访问和修改共享数据。
4.2 避免死锁
死锁是另一个常见的多线程问题,它发生在多个线程相互等待对方释放资源的情况下。为了避免死锁,我们可以遵循以下原则:
-
按顺序获取锁
:确保所有线程按照相同的顺序获取锁,避免循环等待。
-
限时获取锁
:在获取锁时设置一个超时时间,如果在规定时间内无法获取锁,则放弃获取,避免无限等待。
以下是一个按顺序获取锁避免死锁的示例:
#include <stdio.h>
#include <pthread.h>
// 两个互斥锁
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
// 线程 1 函数
void* thread1_function(void* arg) {
// 按顺序获取锁
pthread_mutex_lock(&mutex1);
printf("Thread 1 acquired mutex 1\n");
pthread_mutex_lock(&mutex2);
printf("Thread 1 acquired mutex 2\n");
// 释放锁
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
// 线程 2 函数
void* thread2_function(void* arg) {
// 按顺序获取锁
pthread_mutex_lock(&mutex1);
printf("Thread 2 acquired mutex 1\n");
pthread_mutex_lock(&mutex2);
printf("Thread 2 acquired mutex 2\n");
// 释放锁
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化互斥锁
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
// 创建线程
pthread_create(&thread1, NULL, thread1_function, NULL);
pthread_create(&thread2, NULL, thread2_function, NULL);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
在上述代码中,线程 1 和线程 2 都按照相同的顺序获取锁,避免了死锁的发生。
4.3 合理使用线程池
线程池是一种管理线程的机制,它可以避免频繁创建和销毁线程带来的开销。在需要处理大量短期任务的场景中,使用线程池可以提高程序的性能。
以下是一个简单的线程池实现示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 任务结构体
typedef struct Task {
void (*function)(void*);
void* argument;
struct Task* next;
} Task;
// 任务队列结构体
typedef struct TaskQueue {
Task* head;
Task* tail;
int size;
} TaskQueue;
// 线程池结构体
typedef struct ThreadPool {
pthread_t* threads;
int thread_count;
TaskQueue task_queue;
pthread_mutex_t queue_mutex;
pthread_cond_t queue_cond;
int shutdown;
} ThreadPool;
// 初始化任务队列
void task_queue_init(TaskQueue* queue) {
queue->head = NULL;
queue->tail = NULL;
queue->size = 0;
}
// 添加任务到任务队列
void task_queue_add(TaskQueue* queue, void (*function)(void*), void* argument) {
Task* new_task = (Task*)malloc(sizeof(Task));
new_task->function = function;
new_task->argument = argument;
new_task->next = NULL;
if (queue->tail == NULL) {
queue->head = new_task;
queue->tail = new_task;
} else {
queue->tail->next = new_task;
queue->tail = new_task;
}
queue->size++;
}
// 从任务队列中取出任务
Task* task_queue_remove(TaskQueue* queue) {
if (queue->head == NULL) {
return NULL;
}
Task* task = queue->head;
queue->head = queue->head->next;
if (queue->head == NULL) {
queue->tail = NULL;
}
queue->size--;
return task;
}
// 线程工作函数
void* thread_worker(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
pthread_mutex_lock(&pool->queue_mutex);
// 等待任务队列中有任务或者线程池关闭
while (pool->task_queue.size == 0 && !pool->shutdown) {
pthread_cond_wait(&pool->queue_cond, &pool->queue_mutex);
}
// 如果线程池关闭且任务队列为空,则退出线程
if (pool->shutdown && pool->task_queue.size == 0) {
pthread_mutex_unlock(&pool->queue_mutex);
break;
}
// 从任务队列中取出任务
Task* task = task_queue_remove(&pool->task_queue);
pthread_mutex_unlock(&pool->queue_mutex);
// 执行任务
if (task != NULL) {
task->function(task->argument);
free(task);
}
}
return NULL;
}
// 初始化线程池
ThreadPool* thread_pool_init(int thread_count) {
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
pool->thread_count = thread_count;
pool->threads = (pthread_t*)malloc(sizeof(pthread_t) * thread_count);
task_queue_init(&pool->task_queue);
pthread_mutex_init(&pool->queue_mutex, NULL);
pthread_cond_init(&pool->queue_cond, NULL);
pool->shutdown = 0;
// 创建线程
for (int i = 0; i < thread_count; i++) {
pthread_create(&pool->threads[i], NULL, thread_worker, (void*)pool);
}
return pool;
}
// 向线程池添加任务
void thread_pool_add_task(ThreadPool* pool, void (*function)(void*), void* argument) {
pthread_mutex_lock(&pool->queue_mutex);
task_queue_add(&pool->task_queue, function, argument);
pthread_cond_signal(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_mutex);
}
// 销毁线程池
void thread_pool_destroy(ThreadPool* pool) {
pthread_mutex_lock(&pool->queue_mutex);
pool->shutdown = 1;
pthread_cond_broadcast(&pool->queue_cond);
pthread_mutex_unlock(&pool->queue_mutex);
// 等待所有线程结束
for (int i = 0; i < pool->thread_count; i++) {
pthread_join(pool->threads[i], NULL);
}
// 释放资源
free(pool->threads);
while (pool->task_queue.head != NULL) {
Task* task = task_queue_remove(&pool->task_queue);
free(task);
}
pthread_mutex_destroy(&pool->queue_mutex);
pthread_cond_destroy(&pool->queue_cond);
free(pool);
}
// 示例任务函数
void example_task(void* arg) {
int* value = (int*)arg;
printf("Task executed with value: %d\n", *value);
}
int main() {
// 初始化线程池
ThreadPool* pool = thread_pool_init(4);
// 添加任务到线程池
for (int i = 0; i < 10; i++) {
int* value = (int*)malloc(sizeof(int));
*value = i;
thread_pool_add_task(pool, example_task, (void*)value);
}
// 等待一段时间,确保任务执行完成
sleep(2);
// 销毁线程池
thread_pool_destroy(pool);
return 0;
}
在上述代码中,我们实现了一个简单的线程池,通过任务队列来管理任务,线程从任务队列中取出任务并执行。使用线程池可以避免频繁创建和销毁线程带来的开销,提高程序的性能。
5. 总结
性能优化和多线程编程是现代软件开发中非常重要的领域。通过合理使用性能优化技术,如时间测量、内存访问优化、编译器优化等,可以提高程序的执行效率。而多线程编程则可以充分利用多核处理器的性能,提高程序的并发处理能力。
在多线程编程中,我们需要注意内存重排序、数据竞争、死锁等问题,并使用同步机制和内存屏障来解决这些问题。同时,遵循多线程编程的最佳实践,如避免数据竞争、避免死锁、合理使用线程池等,可以编写出高效、稳定的多线程程序。
希望本文能够帮助你更好地理解性能优化和多线程编程的相关知识,并在实际开发中应用这些知识,编写出更优秀的程序。
超级会员免费看

被折叠的 条评论
为什么被折叠?



