在Linux多线程编程中,当多个线程同时访问共享资源时,很容易出现数据竞争问题,导致程序运行结果不符合预期,因此需要通过线程同步机制来协调线程的执行顺序,保证共享资源在同一时间只被一个或一组线程访问。
互斥锁
互斥锁是最常用的线程同步方式,它的核心作用是保证同一时间只有一个线程能够持有锁并访问共享资源,其他尝试获取锁的线程会被阻塞,直到锁被释放。
互斥锁的使用步骤通常分为初始化、加锁、访问共享资源、解锁、销毁五个部分,下面是使用POSIX线程库的互斥锁示例:
#include <stdio.h>
#include <pthread.h>
// 定义共享资源
int shared_num = 0;
// 定义互斥锁
pthread_mutex_t mutex;
// 线程执行函数
void* thread_func(void* arg) {
for (int i = 0; i < 10000; i++) {
// 加锁
pthread_mutex_lock(&mutex);
// 访问共享资源
shared_num++;
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建两个线程
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
// 等待线程执行完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 打印共享资源结果
printf("shared_num result: %dn", shared_num);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
条件变量
条件变量通常和互斥锁配合使用,用于在线程之间同步共享数据状态的变化,当某个条件不满足时,线程可以等待条件变量,直到其他线程修改了共享数据并通知条件变量,等待的线程才会被唤醒继续执行。
常见的使用场景是生产者消费者模型,下面是条件变量的使用示例:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 定义共享缓冲区
int buffer = 0;
// 定义互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t cond;
// 生产者线程函数
void* producer(void* arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex);
buffer = i;
printf("生产者生产数据: %dn", buffer);
// 通知等待条件变量的消费者线程
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex);
// 等待条件满足,避免虚假唤醒,使用while循环判断
while (buffer == 0) {
pthread_cond_wait(&cond, &mutex);
}
printf("消费者消费数据: %dn", buffer);
buffer = 0;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t prod, cons;
// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// 创建生产者和消费者线程
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
// 等待线程执行完成
pthread_join(prod, NULL);
pthread_join(cons, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
信号量
信号量是一个计数器,用于控制多个线程对共享资源的访问,它支持同时多个线程访问共享资源,计数器的初始值表示可以同时访问资源的线程数量,线程访问资源前需要对信号量做P操作(减1),访问完成后做V操作(加1),当计数器为0时,尝试P操作的线程会被阻塞。
信号量分为无名信号量和有名信号量,下面是无名信号量的使用示例,同样实现生产者消费者逻辑:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
// 定义共享缓冲区
int buffer = 0;
// 定义信号量,empty表示空缓冲区数量,full表示满缓冲区数量
sem_t empty, full;
pthread_mutex_t mutex;
// 生产者线程函数
void* producer(void* arg) {
for (int i = 0; i < 5; i++) {
// 申请空缓冲区,P操作
sem_wait(&empty);
pthread_mutex_lock(&mutex);
buffer = i;
printf("生产者生产数据: %dn", buffer);
pthread_mutex_unlock(&mutex);
// 释放满缓冲区,V操作
sem_post(&full);
sleep(1);
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 0; i < 5; i++) {
// 申请满缓冲区,P操作
sem_wait(&full);
pthread_mutex_lock(&mutex);
printf("消费者消费数据: %dn", buffer);
buffer = 0;
pthread_mutex_unlock(&mutex);
// 释放空缓冲区,V操作
sem_post(&empty);
}
return NULL;
}
int main() {
pthread_t prod, cons;
// 初始化信号量,empty初始值为1表示有1个空缓冲区,full初始值为0表示没有满缓冲区
sem_init(&empty, 0, 1);
sem_init(&full, 0, 0);
pthread_mutex_init(&mutex, NULL);
// 创建线程
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
// 等待线程执行完成
pthread_join(prod, NULL);
pthread_join(cons, NULL);
// 销毁信号量和互斥锁
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
读写锁
读写锁适用于读操作远多于写操作的场景,它允许多个线程同时持有读锁进行读操作,但是同一时间只能有一个线程持有写锁进行写操作,并且当有线程持有写锁时,其他线程无法获取读锁或写锁,当有线程持有读锁时,其他线程可以获取读锁但无法获取写锁。
下面是读写锁的使用示例:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 定义共享资源
int shared_data = 0;
// 定义读写锁
pthread_rwlock_t rwlock;
// 读线程函数
void* reader(void* arg) {
int thread_id = *(int*)arg;
pthread_rwlock_rdlock(&rwlock);
printf("读线程%d读取数据: %dn", thread_id, shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
// 写线程函数
void* writer(void* arg) {
int thread_id = *(int*)arg;
pthread_rwlock_wrlock(&rwlock);
shared_data += 10;
printf("写线程%d修改数据,当前值: %dn", thread_id, shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
pthread_t readers[3], writers[2];
int reader_ids[3] = {1,2,3};
int writer_ids[2] = {1,2};
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建读线程和写线程
for (int i = 0; i < 3; i++) {
pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
}
for (int i = 0; i < 2; i++) {
pthread_create(&writers[i], NULL, writer, &writer_ids[i]);
}
// 等待所有线程执行完成
for (int i = 0; i < 3; i++) {
pthread_join(readers[i], NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
不同同步方式的适用场景
不同的线程同步方式有不同的适用场景,开发者可以根据实际需求选择:
- 互斥锁适合简单的共享资源互斥访问场景,实现简单,开销较小
- 条件变量适合需要等待某个条件满足的场景,通常和互斥锁配合使用
- 信号量适合控制同时访问共享资源的线程数量,也可以用于线程间的顺序同步
- 读写锁适合读多写少的场景,可以提升程序的并发性能
在实际开发中,需要根据具体的业务场景选择合适的同步方式,同时要注意避免死锁问题,比如保证加锁和解锁成对出现,按照固定的顺序获取多个锁等。