Linux C语言多线程并发控制的策略
多线程并发基础概念
线程与进程
在Linux系统中,进程是资源分配的基本单位,而线程是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。相比进程间的切换,线程间的切换开销更小,因此在需要大量并发执行任务的场景下,多线程编程具有更高的效率。
例如,在一个Web服务器程序中,每个请求可以由一个线程来处理,这样可以在同一进程内同时处理多个请求,提高服务器的并发处理能力。
并发与并行
并发(Concurrency)是指在同一时间段内,多个任务交替执行,宏观上看起来像是同时进行。而并行(Parallelism)是指在同一时刻,多个任务真正地同时执行,这需要多核CPU的支持。在多线程编程中,通过合理的调度,可以实现并发执行任务。
多线程并发控制的必要性
资源竞争问题
当多个线程同时访问和修改共享资源时,就会出现资源竞争问题。例如,多个线程同时对一个全局变量进行累加操作,如果没有适当的控制,最终的结果可能是错误的。
下面是一个简单的代码示例,展示资源竞争问题:
#include <stdio.h>
#include <pthread.h>
int count = 0;
void* increment(void* arg) {
int i;
for (i = 0; i < 1000000; i++) {
count++;
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, increment, NULL) != 0) {
printf("\n ERROR creating thread 1");
return 1;
}
if (pthread_create(&tid2, NULL, increment, NULL) != 0) {
printf("\n ERROR creating thread 2");
return 2;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 3;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 4;
}
if (count != 2000000) {
printf("\n BOOM! count is [%d], should be 2000000\n", count);
} else {
printf("\n OK! count is [%d]\n", count);
}
return 0;
}
在这个例子中,两个线程同时对count
变量进行累加操作。由于没有并发控制,每次运行程序,count
的值可能都不等于2000000,这就是资源竞争导致的结果。
死锁问题
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1,这样就形成了死锁。
以下是一个死锁的代码示例:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void* thread1(void* arg) {
pthread_mutex_lock(&mutex1);
printf("Thread 1 has locked mutex 1\n");
sleep(1);
pthread_mutex_lock(&mutex2);
printf("Thread 1 has locked mutex 2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2(void* arg) {
pthread_mutex_lock(&mutex2);
printf("Thread 2 has locked mutex 2\n");
sleep(1);
pthread_mutex_lock(&mutex1);
printf("Thread 2 has locked mutex 1\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main() {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, thread1, NULL) != 0) {
printf("\n ERROR creating thread 1");
return 1;
}
if (pthread_create(&tid2, NULL, thread2, NULL) != 0) {
printf("\n ERROR creating thread 2");
return 2;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 3;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 4;
}
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
在这个例子中,线程1先锁定mutex1
,然后尝试锁定mutex2
,而线程2先锁定mutex2
,然后尝试锁定mutex1
。由于两个线程互相等待对方释放资源,就会导致死锁。
互斥锁(Mutex)实现并发控制
互斥锁的基本原理
互斥锁(Mutex,即Mutual Exclusion的缩写)是一种简单的并发控制机制。它只有两种状态:锁定(locked)和解锁(unlocked)。当一个线程获取到互斥锁(将其锁定)时,其他线程就不能再获取,直到该线程释放互斥锁(将其解锁)。这样就可以保证在同一时刻只有一个线程能够访问共享资源,从而避免资源竞争问题。
互斥锁的使用方法
在Linux C语言中,使用pthread_mutex_t
类型来表示互斥锁。以下是使用互斥锁解决前面资源竞争问题的代码示例:
#include <stdio.h>
#include <pthread.h>
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* increment(void* arg) {
int i;
for (i = 0; i < 1000000; i++) {
pthread_mutex_lock(&mutex);
count++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, increment, NULL) != 0) {
printf("\n ERROR creating thread 1");
return 1;
}
if (pthread_create(&tid2, NULL, increment, NULL) != 0) {
printf("\n ERROR creating thread 2");
return 2;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 3;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 4;
}
if (count != 2000000) {
printf("\n BOOM! count is [%d], should be 2000000\n", count);
} else {
printf("\n OK! count is [%d]\n", count);
}
pthread_mutex_destroy(&mutex);
return 0;
}
在这个例子中,通过在对count
变量进行操作前后分别调用pthread_mutex_lock
和pthread_mutex_unlock
函数,确保了同一时刻只有一个线程能够修改count
,从而解决了资源竞争问题。
互斥锁的注意事项
- 初始化:互斥锁在使用前需要进行初始化,可以使用
PTHREAD_MUTEX_INITIALIZER
进行静态初始化,或者使用pthread_mutex_init
函数进行动态初始化。动态初始化适用于互斥锁的属性需要定制的情况。 - 销毁:当互斥锁不再使用时,需要调用
pthread_mutex_destroy
函数进行销毁,以释放相关资源。 - 死锁风险:虽然互斥锁可以解决资源竞争问题,但如果使用不当,仍然可能导致死锁。例如,在嵌套锁的情况下,如果获取锁的顺序不一致,就容易出现死锁。
读写锁(Read - Write Lock)实现并发控制
读写锁的基本原理
读写锁允许在同一时刻有多个线程进行读操作,但只允许一个线程进行写操作。这是因为读操作不会修改共享资源,多个线程同时读不会产生资源竞争问题,而写操作会修改共享资源,必须保证同一时刻只有一个线程进行写操作。
读写锁的使用方法
在Linux C语言中,使用pthread_rwlock_t
类型来表示读写锁。以下是一个使用读写锁的示例代码:
#include <stdio.h>
#include <pthread.h>
int data = 0;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void* reader(void* arg) {
int i;
for (i = 0; i < 5; i++) {
pthread_rwlock_rdlock(&rwlock);
printf("Reader %ld reads data: %d\n", (long)pthread_self(), data);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
void* writer(void* arg) {
int i;
for (i = 0; i < 3; i++) {
pthread_rwlock_wrlock(&rwlock);
data++;
printf("Writer %ld writes data: %d\n", (long)pthread_self(), data);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid1, tid2, tid3;
if (pthread_create(&tid1, NULL, reader, NULL) != 0) {
printf("\n ERROR creating reader thread 1");
return 1;
}
if (pthread_create(&tid2, NULL, reader, NULL) != 0) {
printf("\n ERROR creating reader thread 2");
return 2;
}
if (pthread_create(&tid3, NULL, writer, NULL) != 0) {
printf("\n ERROR creating writer thread");
return 3;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 4;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 5;
}
if (pthread_join(tid3, NULL) != 0) {
printf("\n ERROR joining thread");
return 6;
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
在这个例子中,读者线程通过调用pthread_rwlock_rdlock
获取读锁,写者线程通过调用pthread_rwlock_wrlock
获取写锁。读锁允许多个读者线程同时进入临界区读取数据,而写锁会阻止其他线程(包括读者和写者)进入临界区,直到写操作完成并释放写锁。
读写锁的注意事项
- 优先级问题:读写锁的实现可能存在读优先或写优先的情况。读优先可能导致写操作长时间等待,而写优先可能导致读操作长时间等待。在实际应用中,需要根据具体需求选择合适的实现或进行优先级调整。
- 死锁风险:与互斥锁类似,读写锁在嵌套使用或获取锁顺序不当的情况下,也可能导致死锁。
条件变量(Condition Variable)实现并发控制
条件变量的基本原理
条件变量是一种线程同步机制,它允许线程等待某个条件满足后再继续执行。条件变量通常与互斥锁配合使用。线程在等待条件变量时,会先释放互斥锁,进入睡眠状态,当条件满足时,由其他线程唤醒等待的线程,唤醒后的线程会重新获取互斥锁,然后继续执行。
条件变量的使用方法
在Linux C语言中,使用pthread_cond_t
类型来表示条件变量。以下是一个使用条件变量的示例代码:
#include <stdio.h>
#include <pthread.h>
int ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* waiter(void* arg) {
pthread_mutex_lock(&mutex);
while (!ready) {
printf("Waiter is waiting...\n");
pthread_cond_wait(&cond, &mutex);
}
printf("Waiter condition met, proceeding...\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
void* signaler(void* arg) {
sleep(3);
pthread_mutex_lock(&mutex);
ready = 1;
printf("Signaler setting ready to 1\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, waiter, NULL) != 0) {
printf("\n ERROR creating waiter thread");
return 1;
}
if (pthread_create(&tid2, NULL, signaler, NULL) != 0) {
printf("\n ERROR creating signaler thread");
return 2;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 3;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 4;
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
在这个例子中,waiter
线程在ready
条件不满足时,调用pthread_cond_wait
等待条件变量,同时释放mutex
互斥锁。signaler
线程在睡眠3秒后,设置ready
为1,并调用pthread_cond_signal
唤醒waiter
线程。waiter
线程被唤醒后,重新获取mutex
互斥锁,然后继续执行。
条件变量的注意事项
- 使用
while
循环等待:在使用pthread_cond_wait
等待条件变量时,应该使用while
循环来检查条件,而不是if
语句。这是因为可能存在虚假唤醒的情况,即条件变量被意外唤醒,而实际条件并未满足。 - 与互斥锁配合使用:条件变量必须与互斥锁配合使用,否则可能会出现竞态条件。在调用
pthread_cond_wait
之前,必须先获取互斥锁,并且pthread_cond_wait
会自动释放互斥锁并进入等待状态,当被唤醒时,会重新获取互斥锁。
信号量(Semaphore)实现并发控制
信号量的基本原理
信号量是一个整型变量,它通过计数器来控制对共享资源的访问。信号量的值表示可用资源的数量。当一个线程获取信号量时,信号量的值减1;当一个线程释放信号量时,信号量的值加1。如果信号量的值为0,那么获取信号量的线程会被阻塞,直到信号量的值大于0。
信号量的使用方法
在Linux C语言中,可以使用sem_t
类型来表示信号量,相关函数有sem_init
(初始化信号量)、sem_wait
(获取信号量)、sem_post
(释放信号量)和sem_destroy
(销毁信号量)。以下是一个使用信号量控制线程访问共享资源数量的示例代码:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
int shared_resource = 0;
void* thread_function(void* arg) {
sem_wait(&sem);
shared_resource++;
printf("Thread %ld entered critical section, shared resource value: %d\n", (long)pthread_self(), shared_resource);
sleep(1);
shared_resource--;
printf("Thread %ld left critical section, shared resource value: %d\n", (long)pthread_self(), shared_resource);
sem_post(&sem);
return NULL;
}
int main() {
pthread_t tid1, tid2, tid3;
if (sem_init(&sem, 0, 2) != 0) {
printf("\n ERROR initializing semaphore");
return 1;
}
if (pthread_create(&tid1, NULL, thread_function, NULL) != 0) {
printf("\n ERROR creating thread 1");
return 2;
}
if (pthread_create(&tid2, NULL, thread_function, NULL) != 0) {
printf("\n ERROR creating thread 2");
return 3;
}
if (pthread_create(&tid3, NULL, thread_function, NULL) != 0) {
printf("\n ERROR creating thread 3");
return 4;
}
if (pthread_join(tid1, NULL) != 0) {
printf("\n ERROR joining thread");
return 5;
}
if (pthread_join(tid2, NULL) != 0) {
printf("\n ERROR joining thread");
return 6;
}
if (pthread_join(tid3, NULL) != 0) {
printf("\n ERROR joining thread");
return 7;
}
sem_destroy(&sem);
return 0;
}
在这个例子中,通过sem_init
将信号量初始化为2,表示最多允许两个线程同时访问共享资源。每个线程在进入临界区(访问共享资源)前调用sem_wait
获取信号量,离开临界区时调用sem_post
释放信号量。
信号量的注意事项
- 初始化值的选择:信号量的初始值决定了同时允许访问共享资源的线程数量。需要根据实际需求合理设置初始值。
- 死锁风险:与其他并发控制机制类似,如果信号量的获取和释放顺序不当,也可能导致死锁。
多线程并发控制策略的选择
根据应用场景选择
- 资源竞争场景:如果主要问题是多个线程对共享资源的竞争,互斥锁是一个简单有效的选择。例如,在对全局变量进行读写操作时,使用互斥锁可以确保数据的一致性。
- 读写频繁场景:当读操作远多于写操作时,读写锁可以提高并发性能。比如在一个数据库查询频繁但更新较少的应用中,读写锁可以让多个查询线程同时进行,而写操作时能保证数据的一致性。
- 条件等待场景:如果线程需要等待某个条件满足后才能继续执行,条件变量是合适的选择。例如,在生产者 - 消费者模型中,消费者线程需要等待生产者线程生产数据后才能消费,这时可以使用条件变量来实现线程间的同步。
- 资源数量控制场景:当需要控制同时访问共享资源的线程数量时,信号量是一个好的选择。比如在连接池的实现中,使用信号量可以控制同时获取连接的线程数量。
综合使用多种策略
在复杂的多线程应用中,往往需要综合使用多种并发控制策略。例如,在一个网络服务器程序中,可能会使用互斥锁来保护共享的连接池资源,使用条件变量来实现线程间的任务同步,同时使用读写锁来优化对配置文件等共享数据的读写操作。
在实际编程中,需要仔细分析应用场景的特点,选择合适的并发控制策略,以实现高效、稳定的多线程程序。同时,要注意避免死锁、资源泄漏等常见问题,确保程序的正确性和可靠性。通过合理运用这些并发控制策略,可以充分发挥多线程编程的优势,提高程序的性能和响应能力。