线程同步与互斥
同步互斥概念
互斥:
互斥就是指某一资源同时只能允许一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问是无序的。
对于线程来说,互斥就是说两个线程之间不可以同时运行,他们之间会相互排斥,必须等一个线程运行完毕之后,另一个才能运行。
同步:
同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所写入资源的情况必定是互斥的,少数情况可以是指允许多个访问者同时访问资源。
对于线程来说,同步也是不能同时运行,但是它必须按照某种次序来运行相应。也就是按照一定的顺序运行线程,这种先后次序依赖于要完成的特定的任务。显然,同步是一种更复杂的互斥,互斥是一种特殊的同步。
线程间的同步互斥
mutex(互斥量)
- 多个线程并发的操作共享变量,会带来一些问题
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int g_count = 0;
void* ThreadEntry1(void* arg){
(void)arg;
while(1){
++g_count;
sleep(1);
}
return NULL;
}
void* ThreadEntry2(void* arg){
(void)arg;
while(1){
printf("g_count = %d\n", g_count);
sleep(1);
}
return NULL;
}
int main(){
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, ThreadEntry1, p);
pthread_create(&tid2, NULL, ThreadEntry2, p);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
一个线程要把某个全局变量增加1另一个线程读出,这个操作并不是原子操作,至少需要三条指令才能完成:
- 将全局变量从内存中加载到寄存器
- 寄存器的值加1
- 将寄存器的值写回内存
由于存在线程安全问题,当线程一进行加一操作时,可能还未完成加一操作线程而便来读取,这将会导致与预期结果不同。
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
此时引入互斥量的概念:
(1)创建锁资源: pthread_mutex_t mutex;
创建锁资源就像创建变量一样,锁的类型是pthread_mutex_t。因为线程是共享数据区和堆区的,所以我们可以创建全局或静态的锁变量或者在堆上创建,这样就可以使得所有线程都可以看见锁,所以说实现线程间通信是非常简单的。
(2)初始化锁: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
如果创建的是全局或静态的锁的话,可以用宏PTHREAD_MUTEX_INITIALIZER初始化。也可以用函数初始化。
attr:变量表示锁的属性。为NULL的话就相当于用宏初始化。
返回值:成功返回0,失败返回错误码。
(3)销毁锁: int pthread_mutex_destroy(pthread_mutex_t* mutex);
成功返回0,失败返回错误码。
注意:
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
(4)加锁: int pthread_mutex_lock(pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误码。
(5)解锁: int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码。
一个线程可以调用pthread_mutex_lock获得锁,如果这时另一个线程先获得锁,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock解锁,当前线程被唤醒,才能获得锁,并继续执行。可以看到,互斥锁就像轻量级的二元信号量一样,只能用在线程间,而信号量既可以用在线程间,也可以用在进程间。
死锁
死锁危害:线程一直等待,可能导致整个服务器崩溃
死锁的两个常见场景:
- 一个线程获取到锁之后,又尝试获取锁,就会出现死锁
- 两个线程A和B,线程A获取了锁1,线程B获取了锁2。然后A尝试获取锁2,B尝试获取锁1。这个时候双方都无法拿到对方的锁,并且会在获取锁的函数中阻塞等待
如果线程数和锁的数目更多了,就会使死锁问题更容易出现,问题场景更复杂
对于这种需要获取多个锁的场景,规定所有的线程都按照固定的顺序来获取锁,能够一定程度上避免死锁。
避免死锁:临界区代码”短平快”
- 短:临界区代码简短明了
- 平:临界区代码逻辑清晰,没有复杂的函数调用,尤其是尽量不要申请其他互斥资源
- 快:临界区代码执行速度快
线程安全与可重入
可重入函数:在多个执行流中被同时调用不会存在逻辑问题
线程安全函数:在多线程中被同时调用不会存在逻辑问题
- 可重入函数一般情况下都是线程安全的
- 线程安全函数不一定是可重入的
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
pthread_mutex_t g_lock;
int g_count = 0;
//Func函数线程安全,不可重入
void Func(){
pthread_mutex_lock(&g_lock);
printf("lock!\n");
++g_count;
sleep(3);
printf("unlock!\n");
pthread_mutex_unlock(&g_lock);
}
void* ThreadEntry(void* arg){
(void)arg;
while(1){
Func();
}
return NULL;
}
void MyHandler(int sig){
(void)sig;
Func();
}
int main(){
signal(SIGINT, MyHandler);
pthread_mutex_init(&g_lock, NULL);
ThreadEntry(&g_count);
pthread_mutex_destroy(&g_lock);
return 0;
}
在lock!打印完后按下Ctrl+c,这时进程收到此信号就会进入到信号处理函数中,也尝试获取锁,信号处理函数就会阻塞在加锁函数中。同时我们知道信号处理函数如果不执行结束,操作系统是不会切换回原有的执行逻辑的,这就意味着主线程再也没有机会释放锁了。因此对于这个场景下, Func函数是线程安全的,但是不可重入。
条件变量
为什么要有条件变量:
线程间同步还有这样一种情况,线程A需要等待某个条件成立才能继续向下执行,现在这个条件不成立,线程A就被阻塞等待,而线程B在执行的过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或唤醒等待这个条件的线程。
条件变量函数:
(1)条件变量的创建: int pthread_cond_t cond;
条件变量的创建和普通变量一样,为了能够实现共享条件变量,所以将条件变量创建为全局或静态变量,或者在堆上创建。
(2)条件变量的初始化:
条件变量和mutex是非常相似的,如果创建成全局或静态的,则可以使用宏来初始化: pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
还可以使用函数来初始化: int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
返回值:成功返回0,失败返回错误码。
cond:要初始化的条件变量
attr:NULL
(3)条件变量的销毁: int pthread_cond_destroy(pthread_cond_t* cond);
返回值:成功返回0,失败返回错误码。
(4)等待条件满足: int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误码。
cond:要在这个条件变量上等待
mutex:互斥量
可以预想到,在wait中肯定是要释放锁的。如果解锁和等待不是原子操作的话,那么在解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过(竞态条件),导致永远阻塞在wait中。所以解锁和等待必须是一个原子操作。
(5)唤醒等待: int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
broadcast可以唤醒所有在这个cond上等待的线程。 int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
singnal可以唤醒某个在条件变量cond上等待的线程。在signal里面肯定是要再次获取锁资源的。
如:蓝球运动员的传球投篮,只有球传到运动员手中,他才可以去投篮
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t lock;
void* Pass(void* arg){
(void)arg;
while(1){
printf("传球!\n");
pthread_cond_signal(&cond);
usleep(789123);
}
return NULL;
}
void* Shot(void* arg){
(void)arg;
while(1){
pthread_cond_wait(&cond, &lock);
printf("投篮!\n");
usleep(123456);
}
return NULL;
}
int main(){
//模拟蓝球训练场景
//球员1,球员2
//球员1负责传球,球员2负责投篮
pthread_t tid1, tid2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&lock, NULL);
pthread_create(&tid1, NULL, Pass, NULL);
pthread_create(&tid2, NULL, Shot, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
读写锁
在一些程序中存在读者写者问题,也就是说,对某些资源的访问会存在两种可能的情况:一种是访问必须是排它的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。这个问题模型是从对文件的读写操作中引申出来的。
通常而言,在读的过程中,往往伴随着查找操作,中间耗时很长,给这段代码加锁的话会极大的降低我们的效率。针对这种多读少写的情况,我们通常采用读写锁。 (写独占,读共享,写锁优先级高)
读写锁是本质上一种特殊的自旋锁,他把对共享资源的访问者划分成读者和写者,读者只对共享资源进行访问,写者只对共享资源进行写操作,一个读写锁同时只能有一个写者或多个读者,但是不能同时既有写者又有读者。 读写锁比起mutex具有更高的适用性,具有更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。
自旋锁的作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:
- 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,就会大大消耗CPU资源。
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
int count = 0;
void* writer(void* arg){
(void)arg;
while(1){
pthread_rwlock_wrlock(&rwlock);
++count;
printf("write:%d\n", count);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
void* reader(void* arg){
(void)arg;
while(1){
pthread_rwlock_rdlock(&rwlock);
printf("read:%d\n", count);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
int main(){
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
int i = 0;
for(; i < 3; ++i){
pthread_create(&tid[i], NULL, writer, NULL);
}
for(i = 0; i < 5; ++i){
pthread_create(&tid[i], NULL, reader, NULL);
}
for(i = 0; i < 8; ++i){
pthread_join(tid[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
上一篇: 多线程-------线程的同步与互斥
下一篇: 线程的同步与互斥