操作系统总结系列之【线程同步】

目录

1.线程同步

what

why

举例

how

互斥量(互斥锁)

读写锁

条件变量

信号量(信号灯)

2.总结

几种线程同步方式的总结:


本文针对linux系统的线程同步机制做出总结,如有理解错误之处或需要优化的地方望请指出,感谢。

1.线程同步

  • what

多线程通过特定的方式来控制线程之间执行的顺序。

  • why

产生原因:多线程访问共享数据时,可能发生冲突。

举例

两个线程操作同一个变量,分为三个步骤:从内存读变量到寄存器、寄存器中赋值、将寄存器值写回内存。

产生冲突的原因就在于这三个步骤不是一个原子操作,中间某一过程中断,切换线程再对同一变量或共享资源操作,就会出现问题。

下图例子供理解参考。

  • how

linux下四种基本的线程同步方式:互斥量、读写锁、条件变量、信号量。


互斥量(互斥锁)

  • what

一种加锁方法来控制对共享资源的原子操作,同一时刻只允许一个线程执行一个关键部分。

相关概念

原子操作:CPU在处理指令时,线程/进程在处理这个指令前不会失去CPU。

临界区:被互斥量锁住的代码段称为临界区。

  • why

为了避免多线程访问共享资源时发生冲突。

  • how

步骤:声明、初始化锁——寻找共享资源——加锁(在操作共享资源前)——解锁

所需头文件 #include <pthread.h>

声明:                 pthread_mutex_t mutex
动态/静态初始化:       pthread_mutex_init() or 直接在声明后赋值
尝试上锁,失败阻塞:    pthread_mutex_lock()    
尝试上锁,失败非阻塞:  pthread_mutex_trylock()
解锁:                 pthread_mutex_unlock()
销毁:                 pthread_mutex_destroy()

函数原型,返回值及其参数:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

返回值,成功:0 出错:-1

注意:

1.加锁时,锁住的代码段(临界区)越小越好;

2.多线程加锁时,所有线程都要使用同一把锁。 

优点:能解决多线程访问共享资源时的冲突问题,且实现起来较为简单。

缺点:多线程访问共享数据时是串行访问的,加锁后执行效率变低。


读写锁

  • what

对临界区加以区分读写操作的一种线程同步方式。

  • why

在多线程中,有时共享资源修改的机会比较少,读的机会却非常多,而读操作不会修改数据,只是做一些查询,如果每次读操作都给代码段加锁,浪费时间和资源,降低程序的效率,所以在读的时候不用加锁,可以共享的访问,只有涉及到写的时候,才互斥访问。

  • how

步骤:声明、初始化锁——寻找共享资源——加锁(在操作共享资源前)——解锁

所需头文件 #include <pthread.h>

声明:                 pthread_rwlock_t rwlock
动态/静态初始化:       pthread_rwlock_init() or 直接在声明后赋值
尝试加读锁,失败阻塞:   pthread_rwlock_rdlock()
尝试加读锁,失败非阻塞: pthread_rwlock_tryrdlock()
尝试加写锁,失败阻塞:   pthread_rwlock_wrlock()
尝试加写锁,失败非阻塞: pthread_rwlock_trywrlock()
解锁:                 pthread_rwlock_unlock()
销毁:                 pthread_rwlock_destroy()

函数原型,返回值及其参数:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthred_rwlockattr_t *restrict attr); //锁的初始化属性
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

返回值,成功:0 出错:返回错误码

特点:读并行,写串行,读共享,写独占,读写不能同时进行,写的优先级较高。

场景分析:线程A持有读锁,线程B请求加读锁,成功。                    (读并行,共享)

                  线程A持有写锁,线程B请求加读锁,失败,线程B阻塞。(写独占)

                  线程A持有写锁,线程B请求加写锁,失败,线程B阻塞。(写串行)

                  线程A持有读锁,线程B请求加写锁,失败,线程B阻塞。(读写不能同时进行,正在读时不会被写中断)

                  线程A持有读锁,线程B请求写锁,线程C请求读锁。线程B、C阻塞。A解锁后,线程B持有写锁,C继续阻塞。B解锁后,C持有读锁。(同上)

                  线程A持有读锁,线程B请求读锁,线程C请求写锁。线程B、C阻塞。A解锁后,线程C持有写锁,B继续阻塞。C解锁后,B持有读锁。(写的优先级高)

优点:在读时,与互斥量相比,是并行读。

缺点:在写时仍是串行写。


条件变量

  • what

条件变量是一种“事件通知机制”,用来阻塞线程等待某种条件发生,通常(必须)与互斥量配合使用。

  • why

当线程正在等待共享资源内某个条件出现,可能需要重复对数据对象加锁和解锁(轮询),非常耗费时间和资源,而且效率非常低,所以加锁不太适合这种情况。

而条件变量能使线程在等待满足某些条件时进入睡眠状态,一旦条件满足,就唤醒线程工作。

  • how

步骤:声明、初始化条件变量——寻找共享资源状态发生变化的位置——加条件变量,阻塞线程——等待条件变量发生变化——通知阻塞线程可以工作——重新判断状态。

分为两个动作,条件不满足,则线程阻塞;条件满足,则通知阻塞的线程开始工作。

所需头文件 #include <pthread.h>

声明:                              pthread_cond_t
动态/静态初始化:                    pthread_cond_init() or 直接在声明后赋值
阻塞等待条件变量:                   pthread_cond_wait()
限时等待条件变量,到时间自动解除阻塞: pthread_cond_timedwait()
唤醒至少一个阻塞在条件变量上的线程:   pthread_cond_signal()
唤醒全部阻塞在条件变量上的线程:       pthread_cond_broadcast()
销毁:                              pthread_cond_destroy()

函数原型,返回值及其参数:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); 
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);  
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);
int pthread_cond_signal(pthread_cond_t *cptr); 
int pthread_cond_broadcast (pthread_cond_t * cptr);

返回值,成功:0 出错:返回错误码

注意1:条件变量+互斥锁,通常用于生产者-消费者模型中,实现生产者与消费者的同步与互斥。

  • 生产者消费者模型(多生产者-多消费者)

https://blog.csdn.net/lvxin15353715790/article/details/89143121

注意2:为什么必须和互斥锁配合?

  • 唤醒丢失问题

https://blog.csdn.net/jinking01/article/details/112547230

注意3:

  • 虚假唤醒问题

https://blog.csdn.net/chengcheng1024/article/details/108335591?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control


信号量(信号灯)

  • what

信号量是一个特殊类型的变量,它可以被增加或减少,其关键访问操作被保证是原子操作。

最简单的信号量是二进制(二值)信号量,它只有0和1两种取值。还有更通用的信号量——计数信号量,可以有更大的取值范围。

当需要用来保护一段代码,使其每次只能被一个执行线程运行,可以使用二进制信号量。

有时,我们希望可以允许有限数目的线程执行一段指定的代码,这就需要计数信号量。

一个例子有助于理解信号量:

信号量就是在一个叫做互斥区的仓库门口放一个钥匙串,上面挂着固定数量的钥匙,每个线程来临时都从上面取走一把钥匙,进入互斥区工作完成后出来了,再把钥匙挂回去。

如果一个线程来到互斥区门口发现没有钥匙,就无法进入,只能站在门口等待一个线程出来放回一把钥匙后,再进入。

由于钥匙数量固定,则互斥区里最大线程数也是固定的,不会出现一下进去太多线程把互斥区挤爆的情况,这是信号量用于并发量限制的应用。 

另一种情况,钥匙是一次性的,线程拿走钥匙进了互斥区,出来后把钥匙扔掉,用着用着钥匙就没了,需要另一些线程(生产者)进来补充钥匙串上的钥匙,这样就可以有新的线程(消费者)

进去了,放一把钥匙,进一个线程,这是信号量用于同步功能的应用。

从本质上来说信号量就是一串钥匙,以及“拿不到钥匙就不让进门”的机制。

  • why

可以实现并发量限制或同步功能。

  • how
所需头文件 #include <semaphore.h>

声明:                              sem_t
初始化:                            sem_init()
加锁,阻塞等待信号量,信号量的值减1:  sem_wait()
加锁,非阻塞等待信号量:              sem_trywait()
加锁,等待一段时间:                 sem_timedwait()
将信号量的值加1:                    sem_post()
销毁:                              sem_destroy()

函数原型,返回值及其参数:
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);  
int sem_trywait(sem_t *sem); 
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem); 

返回值,成功:0 出错:返回错误码

2.总结

几种线程同步方式的总结:

互斥锁与读写锁:在面对读远大于写的应用场景下,尽量应用读写锁。

条件变量与信号量:信号量有一个与之关联的计数值,信号量挂出操作总是被记录。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。

互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯量可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号量,而另一个线程可以挂出该信号量。


问题思考:

为什么现实应用场景中通常都使用互斥量+条件变量,而不是使用信号量?

可以从以下三方面来作答

1.单一职责

   互斥量的作用是加锁,实现多线程的互斥访问共享资源,条件变量的作用是等待,实现多线程的同步访问共享资源。

   而对于信号量,即可用于上锁,也可用于等待,两件事都用一个东西来实现违背了单一职责,会使两个功能过度耦合。

2.互斥量+条件变量能实现的功能信号量难以做到

   加锁的位置和等待的位置可以是两个位置,但使用信号量就不行。

3.难易程度

   互斥量+条件变量的实现容易,便于操作,代码可读性强,而信号量等待和释放的可以不是一个线程,同时可能导致更多的开销和更高的复杂性,操作不当会出现死锁问题。

猜你喜欢

转载自blog.csdn.net/qq_37348221/article/details/113102629