C 语言多线程编程实践指南
一、C 语言多线程编程基础
1.1 多线程概念
在传统的单线程程序中,程序按照顺序依次执行各个指令。而多线程编程允许一个程序内同时存在多个执行流,这些执行流被称为线程。每个线程都可以独立地执行代码片段,它们共享进程的资源,如内存空间、文件描述符等。
多线程编程带来了诸多优势。首先,它能提高程序的响应性,例如在图形界面应用程序中,一个线程可以负责处理用户输入,另一个线程进行复杂的计算,这样用户在进行操作时不会感觉到程序卡顿。其次,多线程可以充分利用多核 CPU 的优势,将不同的任务分配到不同的核心上并行执行,从而加速程序的运行。
1.2 C 语言中的多线程库
在 C 语言中,常用的多线程库有 POSIX Threads(简称 Pthreads),它是 Unix 和类 Unix 系统上的标准线程库。在 Windows 系统上,可以使用 Windows 线程库,不过为了实现跨平台,也可以通过 MinGW 等工具来使用 Pthreads。
Pthreads 提供了一系列函数来创建、管理和同步线程。例如,pthread_create
函数用于创建一个新线程,pthread_join
函数用于等待一个线程结束。
二、创建与管理线程
2.1 创建线程
使用 Pthreads 创建线程需要调用 pthread_create
函数,其原型如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread
:指向一个pthread_t
类型变量的指针,用于存储新创建线程的标识符。attr
:线程属性,通常可以设置为NULL
,表示使用默认属性。start_routine
:一个函数指针,指向新线程要执行的函数。arg
:传递给start_routine
函数的参数。
下面是一个简单的创建线程的示例:
#include <stdio.h>
#include <pthread.h>
// 线程执行函数
void* thread_function(void* arg) {
printf("This is a thread. Argument passed: %d\n", *(int*)arg);
return NULL;
}
int main() {
pthread_t my_thread;
int arg = 42;
// 创建线程
int result = pthread_create(&my_thread, NULL, thread_function, &arg);
if (result != 0) {
printf("Error creating thread: %d\n", result);
return 1;
}
printf("Main thread continues execution.\n");
// 等待线程结束
pthread_join(my_thread, NULL);
printf("Thread has finished. Main thread exits.\n");
return 0;
}
在上述代码中,pthread_create
创建了一个新线程,该线程执行 thread_function
函数,并将 arg
作为参数传递给它。pthread_join
函数确保主线程等待新创建的线程完成后再继续执行。
2.2 线程终止
线程可以通过以下几种方式终止:
- 线程函数返回:如上述示例中,
thread_function
函数执行完毕后返回,线程随之终止。 - 调用
pthread_exit
函数:线程可以调用pthread_exit
函数来主动终止自身。其原型为void pthread_exit(void *retval);
,retval
可以用来传递线程的返回值。 - 其他线程调用
pthread_cancel
函数:一个线程可以调用pthread_cancel
函数来请求取消另一个线程。被取消的线程可以通过pthread_setcancelstate
和pthread_setcanceltype
函数来设置自己对取消请求的处理方式。
下面是一个使用 pthread_exit
的示例:
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
printf("Thread is running. Exiting now.\n");
pthread_exit(NULL);
}
int main() {
pthread_t my_thread;
int result = pthread_create(&my_thread, NULL, thread_function, NULL);
if (result != 0) {
printf("Error creating thread: %d\n", result);
return 1;
}
printf("Main thread continues execution.\n");
pthread_join(my_thread, NULL);
printf("Thread has finished. Main thread exits.\n");
return 0;
}
在这个示例中,线程在执行了一段打印语句后,调用 pthread_exit
主动终止自身。
2.3 线程属性
线程属性可以通过 pthread_attr_t
结构体来设置。常用的属性包括栈大小、调度策略等。
例如,要设置线程的栈大小,可以使用以下代码:
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
printf("Thread is running.\n");
return NULL;
}
int main() {
pthread_t my_thread;
pthread_attr_t attr;
// 初始化属性对象
pthread_attr_init(&attr);
// 设置栈大小为 8192 字节
pthread_attr_setstacksize(&attr, 8192);
int result = pthread_create(&my_thread, &attr, thread_function, NULL);
if (result != 0) {
printf("Error creating thread: %d\n", result);
return 1;
}
// 销毁属性对象
pthread_attr_destroy(&attr);
printf("Main thread continues execution.\n");
pthread_join(my_thread, NULL);
printf("Thread has finished. Main thread exits.\n");
return 0;
}
在上述代码中,首先使用 pthread_attr_init
初始化属性对象,然后使用 pthread_attr_setstacksize
设置栈大小,最后使用 pthread_attr_destroy
销毁属性对象。
三、线程同步
3.1 共享资源与竞态条件
当多个线程共享相同的资源(如全局变量、文件描述符等)时,如果没有适当的同步机制,就可能出现竞态条件。竞态条件是指多个线程同时访问和修改共享资源,导致程序的执行结果依赖于线程执行的相对顺序,从而产生不可预测的行为。
例如,考虑以下代码:
#include <stdio.h>
#include <pthread.h>
int shared_variable = 0;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
shared_variable++;
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
int result1 = pthread_create(&thread1, NULL, increment, NULL);
int result2 = pthread_create(&thread2, NULL, increment, NULL);
if (result1 != 0 || result2 != 0) {
printf("Error creating threads.\n");
return 1;
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Shared variable value: %d\n", shared_variable);
return 0;
}
在这段代码中,两个线程同时对 shared_variable
进行递增操作。由于现代 CPU 的优化以及线程调度的不确定性,可能会出现一个线程在读取 shared_variable
的值后,还未进行递增操作时,另一个线程也读取了相同的值,导致最终的结果小于预期的 2000000。
3.2 互斥锁(Mutex)
互斥锁是一种最基本的线程同步机制,用于保证在同一时刻只有一个线程能够访问共享资源。
在 Pthreads 中,使用 pthread_mutex_t
类型来表示互斥锁,相关函数有 pthread_mutex_init
用于初始化互斥锁,pthread_mutex_lock
用于加锁,pthread_mutex_unlock
用于解锁,pthread_mutex_destroy
用于销毁互斥锁。
下面是使用互斥锁解决上述竞态条件问题的示例:
#include <stdio.h>
#include <pthread.h>
int shared_variable = 0;
pthread_mutex_t mutex;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
// 加锁
pthread_mutex_lock(&mutex);
shared_variable++;
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
int result1 = pthread_create(&thread1, NULL, increment, NULL);
int result2 = pthread_create(&thread2, NULL, increment, NULL);
if (result1 != 0 || result2 != 0) {
printf("Error creating threads.\n");
return 1;
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Shared variable value: %d\n", shared_variable);
return 0;
}
在这个示例中,通过在对 shared_variable
进行操作前后分别加锁和解锁,确保了同一时刻只有一个线程能够修改 shared_variable
,从而避免了竞态条件。
3.3 条件变量(Condition Variable)
条件变量用于线程之间的同步,它允许线程在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用。
在 Pthreads 中,使用 pthread_cond_t
类型表示条件变量,相关函数有 pthread_cond_init
用于初始化条件变量,pthread_cond_wait
用于等待条件变量,pthread_cond_signal
用于唤醒一个等待该条件变量的线程,pthread_cond_broadcast
用于唤醒所有等待该条件变量的线程,pthread_cond_destroy
用于销毁条件变量。
下面是一个使用条件变量的示例:
#include <stdio.h>
#include <pthread.h>
int shared_variable = 0;
pthread_mutex_t mutex;
pthread_cond_t cond_var;
void* producer(void* arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex);
shared_variable++;
printf("Producer: incremented shared variable to %d\n", shared_variable);
pthread_cond_signal(&cond_var);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (shared_variable < 3) {
printf("Consumer: waiting for shared variable to reach 3\n");
pthread_cond_wait(&cond_var, &mutex);
}
printf("Consumer: shared variable has reached 3. Consuming...\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_var, NULL);
int result1 = pthread_create(&producer_thread, NULL, producer, NULL);
int result2 = pthread_create(&consumer_thread, NULL, consumer, NULL);
if (result1 != 0 || result2 != 0) {
printf("Error creating threads.\n");
return 1;
}
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_var);
return 0;
}
在这个示例中,生产者线程不断递增 shared_variable
,并在每次递增后发送一个条件信号。消费者线程在 shared_variable
小于 3 时等待条件变量,当接收到信号且 shared_variable
达到 3 时,消费者线程继续执行。
3.4 读写锁(Read - Write Lock)
读写锁用于解决读多写少的场景下的同步问题。它允许多个线程同时进行读操作,但只允许一个线程进行写操作。
在 Pthreads 中,使用 pthread_rwlock_t
类型表示读写锁,相关函数有 pthread_rwlock_init
用于初始化读写锁,pthread_rwlock_rdlock
用于加读锁,pthread_rwlock_wrlock
用于加写锁,pthread_rwlock_unlock
用于解锁,pthread_rwlock_destroy
用于销毁读写锁。
下面是一个使用读写锁的示例:
#include <stdio.h>
#include <pthread.h>
int shared_data = 0;
pthread_rwlock_t rw_lock;
void* reader(void* arg) {
pthread_rwlock_rdlock(&rw_lock);
printf("Reader: reading shared data: %d\n", shared_data);
pthread_rwlock_unlock(&rw_lock);
return NULL;
}
void* writer(void* arg) {
pthread_rwlock_wrlock(&rw_lock);
shared_data++;
printf("Writer: incremented shared data to %d\n", shared_data);
pthread_rwlock_unlock(&rw_lock);
return NULL;
}
int main() {
pthread_t reader_thread1, reader_thread2, writer_thread;
pthread_rwlock_init(&rw_lock, NULL);
int result1 = pthread_create(&reader_thread1, NULL, reader, NULL);
int result2 = pthread_create(&reader_thread2, NULL, reader, NULL);
int result3 = pthread_create(&writer_thread, NULL, writer, NULL);
if (result1 != 0 || result2 != 0 || result3 != 0) {
printf("Error creating threads.\n");
return 1;
}
pthread_join(reader_thread1, NULL);
pthread_join(reader_thread2, NULL);
pthread_join(writer_thread, NULL);
pthread_rwlock_destroy(&rw_lock);
return 0;
}
在这个示例中,读线程可以同时获取读锁进行读取操作,而写线程在获取写锁时会独占资源,确保写操作的原子性。
四、多线程编程的性能与调试
4.1 多线程性能优化
- 减少锁竞争:尽量缩短持有锁的时间,避免不必要的锁嵌套。例如,在上述互斥锁的示例中,如果对
shared_variable
的操作可以拆分成多个步骤,尽量将不需要锁保护的步骤放在锁外执行。 - 合理分配任务:根据 CPU 核心数和任务的特性,合理分配任务到不同的线程。例如,对于计算密集型任务,可以平均分配到多个线程利用多核优势;对于 I/O 密集型任务,可以根据 I/O 设备的数量来确定线程数量。
- 避免过度线程化:创建过多的线程会增加系统开销,包括线程的创建、销毁以及上下文切换的开销。要根据实际情况权衡线程数量与性能的关系。
4.2 多线程调试
- 打印调试信息:在关键代码处添加打印语句,输出线程的执行状态和共享资源的值。但要注意在多线程环境下,打印操作本身也可能存在竞争条件,需要适当同步。
- 使用调试工具:例如 GDB 调试器,它支持多线程调试。可以使用
info threads
命令查看当前所有线程的状态,使用thread <thread - id>
命令切换到指定线程进行调试。 - 静态分析工具:如 Valgrind,它可以检测内存泄漏、竞态条件等问题。Valgrind 的
helgrind
工具专门用于检测多线程程序中的竞态条件。
下面是一个使用 GDB 调试多线程程序的简单步骤:
- 编译程序:使用
-g
选项编译程序,以便在可执行文件中包含调试信息。例如:gcc -g -pthread -o my_program my_program.c
。 - 启动 GDB:运行
gdb my_program
。 - 设置断点:使用
break <function - name>
或break <line - number>
命令在关键函数或代码行设置断点。 - 运行程序:使用
run
命令运行程序。 - 调试线程:使用
info threads
查看线程信息,使用thread <thread - id>
切换线程,使用next
、step
等命令单步执行代码。
五、多线程编程的应用场景
5.1 服务器端编程
在服务器程序中,多线程可以用于处理多个客户端的连接请求。例如,一个 Web 服务器可以为每个客户端连接创建一个新线程,这样可以同时处理多个用户的请求,提高服务器的并发处理能力。
下面是一个简单的多线程服务器示例框架(基于 TCP 套接字):
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define BACKLOG 10
void* handle_client(void* arg) {
int client_socket = *((int*)arg);
char buffer[1024] = {0};
int valread = read(client_socket, buffer, 1024);
printf("Received: %s\n", buffer);
const char* hello = "Hello from server";
send(client_socket, hello, strlen(hello), 0);
printf("Message sent\n");
close(client_socket);
return NULL;
}
int main(int argc, char const *argv[]) {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
pthread_t tid;
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(server_fd, BACKLOG) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
while (1) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
continue;
}
// 为每个客户端连接创建新线程
if (pthread_create(&tid, NULL, handle_client, &new_socket) != 0) {
perror("pthread_create");
close(new_socket);
}
}
close(server_fd);
return 0;
}
在这个示例中,主线程负责监听客户端连接,每当有新的连接到来时,创建一个新线程来处理该客户端的请求。
5.2 多媒体处理
在多媒体处理中,多线程可以用于音频和视频的编码、解码、播放等操作。例如,一个视频播放程序可以使用一个线程解码视频帧,另一个线程渲染视频帧,再用一个线程播放音频,从而实现流畅的播放体验。
5.3 科学计算
在科学计算领域,多线程可以加速复杂的数值计算。例如,矩阵运算、模拟仿真等任务可以将数据分割成多个部分,分配到不同的线程中并行计算,最后将结果合并。
例如,下面是一个简单的矩阵乘法多线程实现示例:
#include <stdio.h>
#include <pthread.h>
#define ROWS_A 2
#define COLS_A 3
#define COLS_B 2
int matrix_A[ROWS_A][COLS_A] = {
{1, 2, 3},
{4, 5, 6}
};
int matrix_B[COLS_A][COLS_B] = {
{7, 8},
{9, 10},
{11, 12}
};
int result[ROWS_A][COLS_B];
typedef struct {
int row;
} ThreadArgs;
void* multiply_row(void* arg) {
ThreadArgs* args = (ThreadArgs*)arg;
int row = args->row;
for (int j = 0; j < COLS_B; j++) {
result[row][j] = 0;
for (int k = 0; k < COLS_A; k++) {
result[row][j] += matrix_A[row][k] * matrix_B[k][j];
}
}
return NULL;
}
int main() {
pthread_t threads[ROWS_A];
ThreadArgs args[ROWS_A];
for (int i = 0; i < ROWS_A; i++) {
args[i].row = i;
if (pthread_create(&threads[i], NULL, multiply_row, &args[i]) != 0) {
printf("Error creating thread\n");
return 1;
}
}
for (int i = 0; i < ROWS_A; i++) {
pthread_join(threads[i], NULL);
}
for (int i = 0; i < ROWS_A; i++) {
for (int j = 0; j < COLS_B; j++) {
printf("%d ", result[i][j]);
}
printf("\n");
}
return 0;
}
在这个示例中,每个线程负责计算结果矩阵的一行,通过多线程并行计算加速了矩阵乘法的运算。