linux_线程同步
在Linux系统中,多线程编程能充分利用多核资源提升程序效率,但多个线程共享进程资源时,若不加控制就可能导致数据混乱、结果不一致等问题。线程同步正是解决这类问题的关键技术,它通过一系列机制协调线程间的执行顺序,确保共享资源的安全访问。
为什么需要线程同步?
想象这样一个场景:两个线程同时操作同一个银行账户,线程A要存款100元,线程B要取款100元,初始余额为200元。正常逻辑下,最终余额应为200元,但实际执行中可能出现以下情况:
- 线程A读取余额200元,准备计算新余额(200 + 100 = 300)- 线程B此时也读取余额200元,计算新余额(200 - 100 = 100)
- 线程A先写入300元,随后线程B写入100元,最终余额为100元,显然错误
这种因多线程无序访问共享资源导致的问题,称为竞态条件。线程同步的核心目标就是避免竞态条件,保证共享资源操作的原子性(即操作要么完全执行,要么完全不执行,中间不被打断)。
Linux常用线程同步机制
Linux提供了多种线程同步工具,适用于不同场景,以下是最常用的几种:
1.互斥锁(Mutex)
互斥锁是最基础的同步机制,它通过\"加锁 - 操作 - 解锁\"的流程控制共享资源的访问:
- 线程访问共享资源前,必须先获取锁(lock),若锁已被其他线程持有,则当前线程阻塞等待
- 线程完成操作后,释放锁(unlock),让其他等待的线程有机会获取锁
特点:同一时间只允许一个线程持有锁,适合保护短时间的临界区(共享资源操作代码段)。
常用函数: pthread_mutex_init (初始化锁)、 pthread_mutex_lock (加锁)、 pthread_mutex_unlock (解锁)、 pthread_mutex_destroy (销毁锁)。
2. 条件变量(Condition Variable)
定义与作用
条件变量用于线程间的\"等待 - 通知\"通信,解决线程间因特定条件未满足而需要等待的问题。它允许线程在某个条件不满足时阻塞,直到其他线程通知该条件已满足。与互斥锁不同,条件变量本身并不用于保护共享资源,而是用于线程之间的协调。
同步概念与竞态条件
在多线程环境中,当一个线程需要等待某个条件满足才能继续执行时,如果仅使用互斥锁,可能会导致竞态条件。例如,线程A需要等待某个数据准备好才能处理,若线程A在检查数据是否准备好时持有锁,而线程B负责准备数据,那么线程B在准备数据时就无法获取锁,从而无法更新数据,导致线程A一直等待。条件变量通过允许线程在等待时释放锁,避免了这种竞态条件。
常用函数
- 初始化: pthread_cond_init 函数用于初始化条件变量。它为条件变量分配必要的资源,并将其初始化为默认状态。
- 销毁: pthread_cond_destroy 函数用于销毁条件变量,释放其占用的资源。在不再使用条件变量时,应调用该函数进行清理。
- 等待: pthread_cond_wait 函数用于让线程等待条件变量被通知。当线程调用该函数时,它会自动释放关联的互斥锁,并进入阻塞状态,直到其他线程通过 pthread_cond_signal 或 pthread_cond_broadcast 通知该条件变量。一旦收到通知,线程会重新获取互斥锁,并继续执行。- 唤醒: pthread_cond_signal 函数用于唤醒一个等待在该条件变量上的线程。如果有多个线程在等待,它会选择其中一个线程唤醒。 pthread_cond_broadcast 函数则用于唤醒所有等待在该条件变量上的线程。
简单案例代码
以下是一个简单的生产者 - 消费者模型的案例代码,展示了条件变量的使用:
#include #include #define BUFFER_SIZE 5int buffer[BUFFER_SIZE];int count = 0;int in = 0;int out = 0;pthread_mutex_t mutex;pthread_cond_t not_full;pthread_cond_t not_empty;void *producer(void *arg) { for (int i = 0; i < 10; i++) { pthread_mutex_lock(&mutex); while (count == BUFFER_SIZE) { pthread_cond_wait(¬_full, &mutex); } buffer[in] = i; in = (in + 1) % BUFFER_SIZE; count++; pthread_cond_signal(¬_empty); pthread_mutex_unlock(&mutex); } return NULL;}void *consumer(void *arg) { for (int i = 0; i < 10; i++) { pthread_mutex_lock(&mutex); while (count == 0) { pthread_cond_wait(¬_empty, &mutex); } int item = buffer[out]; out = (out + 1) % BUFFER_SIZE; count--; pthread_cond_signal(¬_full); pthread_mutex_unlock(&mutex); printf(\"Consumed: %d\\n\", item); } return NULL;}int main() { pthread_t producer_thread, consumer_thread; pthread_mutex_init(&mutex, NULL); pthread_cond_init(¬_full, NULL); pthread_cond_init(¬_empty, NULL); pthread_create(&producer_thread, NULL, producer, NULL); pthread_create(&consumer_thread, NULL, consumer, NULL); 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. 信号量(Semaphore)
什么是信号量?
简单来说,它就像一个“计数器”,通过控制这个计数器的值来协调多个进程对共享资源的访问。
信号量的值通常代表当前可用资源的数量:
- 当信号量的值大于 0 时,表示有可用资源,进程可以获取资源并继续执行,同时信号量的值减 1。
- 当信号量的值等于 0 时,表示没有可用资源,进程会被阻塞,直到有其他进程释放资源使信号量的值大于 0。
信号量的分类
在 Linux 中,信号量主要分为两类:
- 无名信号量:也叫本地信号量,通常用于同一进程内的线程同步,或者通过共享内存实现的同一台机器上的不同进程间同步。它没有名字,存在于内存中,生命周期随进程或线程结束而结束。
- 有名信号量:可以用于不同进程间的同步,即使这些进程没有共享内存。它有一个名字,存在于文件系统中(通常在 /dev/shm 目录下),生命周期不依赖于具体的进程,只有被显式删除时才会消失。
信号量的基本操作
信号量的核心操作主要有三个:
1. 初始化(sem_init 或 sem_open):设置信号量的初始值,以及它的作用范围(是进程内还是进程间)。
2. P 操作(sem_wait):也叫等待操作,检查信号量的值。如果大于 0,则将其减 1 并继续;如果等于 0,则阻塞进程。
3. V 操作(sem_post):也叫释放操作,将信号量的值加 1。如果有进程因等待该信号量而被阻塞,会唤醒其中一个进程。
此外,还有用于销毁信号量的操作(sem_destroy 或 sem_close、sem_unlink),用于释放信号量所占用的资源。
信号量的应用场景
信号量在并发编程中应用广泛,典型场景包括:
- 控制资源访问:比如限制同时访问某个文件的进程数量,或控制对数据库连接池的使用。
- 进程间同步:让多个进程按照特定的顺序执行,比如进程 A 完成某操作后,进程 B 才能开始。
- 避免死锁:通过合理设置信号量的值和操作顺序,避免多个进程因相互等待资源而陷入死锁。
#4.自旋锁
5. 读写锁(Reader - Writer Lock)
读写有着自己的一个对应关系及读写关系
他与消费关系类似,都遵守321原则,即三个关系,两个角色,一个交易场所
三个关系分别为写写,写读,读读。
两个角色,即读者,写者,由线程承担,
一个交易场所,即数据交换的地点
写写是互斥竞争关系。写读是互斥,同步。读读是共享。
在读者与读者和消费者与消费者的关系上,有不同点的原因在于,读者和读者之间是信息互相共享交流的,而消费者与消费者之间它对产品是一种竞争关系,是会将资源拿走的
读写锁针对\"读多写少\"的场景优化,区分读操作和写操作:
读者优先
- 多个线程可同时获取读锁(共享),不阻塞彼此- 写锁是排他的,获取写锁时会阻塞所有读锁和其他写锁
特点:提高读操作的并发效率,适合日志读取、配置加载等场景,常用函数有 pthread_rwlock_rdlock (读锁)
锁会导致死锁,需通过固定锁顺序、设置超时等方式避免。
5. 减少锁粒度:锁保护的范围越小,并发效
、 pthread_rwlock_wrlock (写锁)等。
释放锁资源
线程同步的注意事项
4. 避免死锁:多个线程互相等待对方释放率越高,避免将无关操作放入临界区。
6. 选择合适机制:根据场景选择工具(如读多写少用读写锁,有限资源用信号量),避免过度同步。
总结
线程同步是Linux多线程编程的核心,通过互斥锁、条件变量、信号量等机制,能有效解决竞态条件,保证共享资源的安全访问。实际开发中,需结合业务场景选择合适的同步工具,在安全性和效率之间找到平衡,才能写出高效、稳定的多线程程序。