Linux C语言多线程编程基础
一、线程基础概念
在Linux环境下使用C语言进行多线程编程,首先要理解线程是什么。线程,有时被称为轻量级进程(LWP, Light Weight Process),是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。与进程不同,线程之间共享进程的地址空间,包括代码段、数据段和堆空间,但每个线程都有自己独立的栈空间。
1.1 线程与进程的区别
进程是资源分配的最小单位,而线程是程序执行的最小单位。进程有自己独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响;而线程只是一个进程中的不同执行路径,它们共享进程的资源。如果一个线程崩溃,很可能导致整个进程崩溃。
例如,一个浏览器进程可能包含多个线程,一个线程负责页面渲染,一个线程负责网络请求等。这些线程共享浏览器进程的内存空间等资源,提高了资源利用率和执行效率。
1.2 多线程编程的优势
- 提高程序响应性:例如在一个图形界面应用程序中,主线程负责处理用户界面交互,而可以开启一个子线程进行数据的后台加载。这样在数据加载过程中,用户仍然可以操作界面,不会出现界面假死的情况。
- 充分利用多核处理器:现代计算机大多是多核处理器,多线程程序可以将不同的任务分配到不同的核心上并行执行,从而提高程序的整体执行效率。
- 简化程序结构:对于一些复杂的任务,可以将其分解为多个相对简单的子任务,每个子任务由一个线程负责执行,使程序结构更加清晰。
二、Linux下C语言线程库
在Linux系统中,使用POSIX线程库(通常称为pthread库)来进行多线程编程。这个库提供了一系列的函数来创建、管理和同步线程。要使用pthread库,需要在编译时链接该库,通常使用-lpthread
选项。
2.1 线程创建
使用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
类型变量的指针,该变量用于存储新创建线程的ID。attr
:用于设置线程的属性,通常可以设置为NULL
,表示使用默认属性。start_routine
:是一个函数指针,指向线程要执行的函数。arg
:是传递给start_routine
函数的参数。
下面是一个简单的示例代码,创建一个新线程并输出一条消息:
#include <stdio.h>
#include <pthread.h>
// 线程执行函数
void *print_message(void *ptr) {
char *message;
message = (char *) ptr;
printf("%s\n", message);
return NULL;
}
int main() {
pthread_t thread;
char *message = "Hello, from thread!";
int ret;
// 创建线程
ret = pthread_create(&thread, NULL, print_message, (void *) message);
if (ret != 0) {
printf("Error creating thread: %d\n", ret);
return 1;
}
printf("Thread created successfully\n");
// 等待线程结束
pthread_join(thread, NULL);
return 0;
}
在上述代码中,print_message
函数是新线程要执行的函数,通过pthread_create
创建线程,并将message
字符串作为参数传递给该函数。pthread_join
函数用于等待线程结束,防止主线程提前退出。
2.2 线程ID与线程终止
每个线程都有一个唯一的ID,类型为pthread_t
。可以使用pthread_self
函数获取当前线程的ID,函数原型为:
pthread_t pthread_self(void);
线程的终止有几种方式:
- 线程函数返回:如上述示例中,
print_message
函数执行完毕后返回,线程也就自然终止。 - 调用
pthread_exit
函数:在线程内部可以调用pthread_exit
函数来主动终止线程,其函数原型为:
void pthread_exit(void *retval);
其中retval
是线程的返回值,可以被pthread_join
函数获取。
3. 其他线程调用pthread_cancel
函数取消该线程:函数原型为:
int pthread_cancel(pthread_t thread);
被取消的线程可以通过设置取消点来决定何时响应取消请求。
下面是一个使用pthread_exit
的示例:
#include <stdio.h>
#include <pthread.h>
void *thread_function(void *arg) {
printf("Thread is running\n");
pthread_exit((void *) 1);
}
int main() {
pthread_t thread;
int ret;
void *thread_result;
ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0) {
printf("Error creating thread: %d\n", ret);
return 1;
}
printf("Thread created successfully\n");
// 等待线程结束并获取返回值
ret = pthread_join(thread, &thread_result);
if (ret != 0) {
printf("Error joining thread: %d\n", ret);
return 1;
}
printf("Thread returned: %ld\n", (long) thread_result);
return 0;
}
在这个示例中,thread_function
函数调用pthread_exit
并传递返回值1,主线程通过pthread_join
获取该返回值并输出。
三、线程同步
由于多个线程共享进程的资源,可能会出现竞态条件(Race Condition)。例如,多个线程同时访问和修改同一个共享变量,导致结果不可预测。为了避免这种情况,需要使用线程同步机制。
3.1 互斥锁(Mutex)
互斥锁是一种简单的同步工具,用于保证在同一时间只有一个线程可以访问共享资源。在pthread库中,使用pthread_mutex_t
类型来表示互斥锁。
- 初始化互斥锁:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
通常attr
参数设置为NULL
,表示使用默认属性。也可以使用静态初始化方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 加锁与解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当一个线程调用pthread_mutex_lock
时,如果互斥锁处于未锁定状态,该线程将锁定互斥锁并继续执行;如果互斥锁已被其他线程锁定,该线程将被阻塞,直到互斥锁被解锁。
下面是一个使用互斥锁保护共享变量的示例:
#include <stdio.h>
#include <pthread.h>
// 共享变量
int counter = 0;
// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *increment_counter(void *arg) {
for (int i = 0; i < 1000000; i++) {
// 加锁
pthread_mutex_lock(&mutex);
counter++;
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
int ret;
// 创建两个线程
ret = pthread_create(&thread1, NULL, increment_counter, NULL);
if (ret != 0) {
printf("Error creating thread1: %d\n", ret);
return 1;
}
ret = pthread_create(&thread2, NULL, increment_counter, NULL);
if (ret != 0) {
printf("Error creating thread2: %d\n", ret);
return 1;
}
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Final counter value: %d\n", counter);
return 0;
}
在这个示例中,两个线程同时对counter
变量进行递增操作。通过互斥锁,保证了在同一时间只有一个线程可以修改counter
,从而避免了竞态条件。
3.2 条件变量(Condition Variable)
条件变量用于线程间的同步,它允许线程在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用。在pthread库中,使用pthread_cond_t
类型来表示条件变量。
- 初始化条件变量:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
同样,attr
参数通常设置为NULL
。也有静态初始化方式:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 等待条件变量与唤醒:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_wait
函数会自动解锁互斥锁并使当前线程阻塞,直到条件变量被pthread_cond_signal
或pthread_cond_broadcast
唤醒。唤醒后,线程会重新获取互斥锁。pthread_cond_signal
唤醒一个等待在条件变量上的线程,而pthread_cond_broadcast
唤醒所有等待在条件变量上的线程。
下面是一个生产者 - 消费者模型的示例,使用条件变量和互斥锁实现:
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
int item = 1;
while (1) {
pthread_mutex_lock(&mutex);
while ((in + 1) % BUFFER_SIZE == out) {
// 缓冲区满,等待
pthread_cond_wait(¬_full, &mutex);
}
buffer[in] = item;
printf("Produced: %d\n", item);
in = (in + 1) % BUFFER_SIZE;
item++;
// 通知缓冲区有数据
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (in == out) {
// 缓冲区空,等待
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[out];
printf("Consumed: %d\n", item);
out = (out + 1) % BUFFER_SIZE;
// 通知缓冲区有空间
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t producer_thread, consumer_thread;
int ret;
ret = pthread_create(&producer_thread, NULL, producer, NULL);
if (ret != 0) {
printf("Error creating producer thread: %d\n", ret);
return 1;
}
ret = pthread_create(&consumer_thread, NULL, consumer, NULL);
if (ret != 0) {
printf("Error creating consumer thread: %d\n", ret);
return 1;
}
// 等待线程结束(实际这里可以使用其他方式停止线程,例如信号)
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
在这个示例中,生产者线程向缓冲区中写入数据,消费者线程从缓冲区中读取数据。通过条件变量not_full
和not_empty
以及互斥锁,实现了线程间的同步,确保缓冲区既不会溢出也不会下溢。
3.3 读写锁(Read - Write Lock)
读写锁允许同一时间有多个线程进行读操作,但只允许一个线程进行写操作。在pthread库中,使用pthread_rwlock_t
类型来表示读写锁。
- 初始化读写锁:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
attr
参数通常为NULL
。也有静态初始化方式:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
- 加锁与解锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock
用于读锁定,pthread_rwlock_wrlock
用于写锁定。
下面是一个简单的示例,展示读写锁的使用:
#include <stdio.h>
#include <pthread.h>
// 共享数据
int shared_data = 0;
// 读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void *reader(void *arg) {
pthread_rwlock_rdlock(&rwlock);
printf("Reader %ld reads: %d\n", (long) pthread_self(), shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void *writer(void *arg) {
pthread_rwlock_wrlock(&rwlock);
shared_data++;
printf("Writer %ld writes: %d\n", (long) pthread_self(), shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
pthread_t reader1, reader2, writer1;
int ret;
ret = pthread_create(&reader1, NULL, reader, NULL);
if (ret != 0) {
printf("Error creating reader1: %d\n", ret);
return 1;
}
ret = pthread_create(&reader2, NULL, reader, NULL);
if (ret != 0) {
printf("Error creating reader2: %d\n", ret);
return 1;
}
ret = pthread_create(&writer1, NULL, writer, NULL);
if (ret != 0) {
printf("Error creating writer1: %d\n", ret);
return 1;
}
// 等待线程结束
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(writer1, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
在这个示例中,多个读线程可以同时读取shared_data
,而写线程在写入时会独占shared_data
,保证数据的一致性。
四、线程属性
线程属性可以在创建线程时进行设置,通过pthread_attr_t
类型来表示。可以设置的属性包括线程的栈大小、调度策略等。
4.1 栈大小设置
可以使用pthread_attr_setstacksize
函数来设置线程的栈大小,函数原型为:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
例如,下面的代码设置线程栈大小为8192字节:
#include <stdio.h>
#include <pthread.h>
void *thread_function(void *arg) {
printf("Thread is running\n");
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
int ret;
// 初始化线程属性
pthread_attr_init(&attr);
// 设置栈大小
ret = pthread_attr_setstacksize(&attr, 8192);
if (ret != 0) {
printf("Error setting stack size: %d\n", ret);
return 1;
}
// 创建线程
ret = pthread_create(&thread, &attr, thread_function, NULL);
if (ret != 0) {
printf("Error creating thread: %d\n", ret);
return 1;
}
// 等待线程结束
pthread_join(thread, NULL);
// 销毁线程属性
pthread_attr_destroy(&attr);
return 0;
}
4.2 调度策略设置
线程的调度策略决定了线程在系统中的执行优先级。在pthread库中,可以使用pthread_attr_setschedpolicy
函数来设置调度策略,函数原型为:
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
其中policy
可以取值为SCHED_OTHER
(普通调度策略)、SCHED_FIFO
(先进先出调度策略)或SCHED_RR
(时间片轮转调度策略)。
下面是一个设置调度策略为SCHED_RR
的示例:
#include <stdio.h>
#include <pthread.h>
#include <sched.h>
void *thread_function(void *arg) {
printf("Thread is running\n");
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
struct sched_param param;
int ret;
// 初始化线程属性
pthread_attr_init(&attr);
// 设置调度策略为SCHED_RR
ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
if (ret != 0) {
printf("Error setting scheduling policy: %d\n", ret);
return 1;
}
// 设置调度参数(优先级)
param.sched_priority = sched_get_priority_max(SCHED_RR);
ret = pthread_attr_setschedparam(&attr, ¶m);
if (ret != 0) {
printf("Error setting scheduling parameters: %d\n", ret);
return 1;
}
// 创建线程
ret = pthread_create(&thread, &attr, thread_function, NULL);
if (ret != 0) {
printf("Error creating thread: %d\n", ret);
return 1;
}
// 等待线程结束
pthread_join(thread, NULL);
// 销毁线程属性
pthread_attr_destroy(&attr);
return 0;
}
在这个示例中,首先设置线程的调度策略为SCHED_RR
,然后设置调度参数中的优先级为SCHED_RR
策略下的最大优先级。
五、多线程编程的注意事项
- 死锁问题:死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时,就会发生死锁。例如,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1,这样就形成了死锁。为了避免死锁,需要遵循一些原则,如按照相同的顺序获取锁,避免嵌套锁等。
- 资源泄漏:在线程中动态分配的资源,如果没有正确释放,可能会导致资源泄漏。例如,在线程中使用
malloc
分配了内存,但线程提前终止而没有调用free
释放内存。要确保所有动态分配的资源在适当的时候被释放。 - 线程安全函数:并非所有的C标准库函数都是线程安全的。例如,
asctime
函数就不是线程安全的,因为它使用了静态缓冲区。在多线程程序中,应尽量使用线程安全的函数,或者自行实现同步机制来保证对非线程安全函数的正确调用。 - 调试困难:多线程程序的调试比单线程程序更加困难,因为线程的执行顺序是不确定的,可能会导致一些难以重现的问题。可以使用调试工具如
gdb
的多线程调试功能,或者添加日志输出来辅助调试。
通过深入理解Linux下C语言多线程编程的基础知识、同步机制、线程属性以及注意事项,开发者可以编写出高效、稳定的多线程程序,充分利用多核处理器的优势,提升程序的性能和响应性。在实际应用中,需要根据具体的需求和场景,合理选择线程同步方式和设置线程属性,以实现最优的多线程解决方案。