Linux下线程同步(带你了解什么是互斥锁、死锁、读写锁、条件变量、信号量等)

简介: Linux下线程同步(带你了解什么是互斥锁、死锁、读写锁、条件变量、信号量等)

线程同步概念

假设有四个线程A、B、C、D,当前一个线程A对内存中的共享资源进行访问的时候,其他线程B、C、D都不可以对这块内存进行操作,直至A对这块内存访问完毕为止,B、C、D中的一个才能访问这块内存,剩余的俩个需要继续阻塞等待,一次类推,直到所有的线程都对这块内存操作完毕。线程对内存的这种访问方式称为线程同步,所谓的现成的同步并不是多个线程同时对内存访问,而是按照先后顺序依次进行的。


源码:

结果图:


CPU对于寄存器、一级缓存、二级缓存、三级缓存是独占的。用于存储处理的数据和线程的状态信息,数据被处理完需要再次被写入到物理内存中,物理内存数据也可以通过文件IO操作写入到磁盘中。


上述程序中,俩个线程共用全局变量number当线程变成运行态之后开始计数时,从物理内存(Memory)中加载数据,然后让数据放到CPU进行运算,最后将结果跟新到物理内存中。


如果线程A执行这个过程期间就失去了CPU时间片,线程A被挂起了最新的数据没能更新到物理内存中(保存到了寄存器中),线程B变成了运行态之后从物理内存读取数据,这个时候B拿的数据就不是最新数据。只能基于旧的数据往后计数,然后失去时间片。线程A在得到时间片变成运行态时,第一件事就是将上次没更新到内存的数据更新到内存(从寄存器拿上次),但是这样会导致B线程已经跟新到内存的数据被覆盖掉。活白干了。最终导致有些数据会被重复很多次。



同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对于的共享资源也被称之为临界资源。



互斥锁

互斥锁函数

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有线程只能顺序执行(不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程可以并行处理的,现在只能串性处理。


在Linux中互斥锁的类型为pthread_mutex_t,创建一个这样类型的变量就得到了一把互斥锁。ptherad_mutex_t mutex;


在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞,一般情况下,每一个共享资源对应一把互斥锁,锁的个数与线程的个数无关。


初始互斥锁: int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);


释放互斥锁: int pthread_mutex_destroy(pthread_mutex_t *mutex);


restrict:关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址。其他指针不行

mutex:互斥锁变量地址

attr:互斥锁的属性,一般使用默认属性就可以,这个参数指定NULL


修改互斥锁状态

int pthread_mutex_lock(pthread_mutex *mutex);

该函数被调用时,首先会判断参数mutex互斥锁中的状态是不是锁定状态。

• 若没有被锁定,时打开的。这个线程可以加锁成功。该锁会记录那个线程加锁成功了

• 锁被锁定了,其他线程加锁就会失败,这些线程就会阻塞到这把锁上。

• 当把这把锁解开之后,阻塞在这把锁的线程就解除阻塞了,并且这些线程通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞。

例如:你要去上厕所,发现里面有人,你就在外边等着。


尝试加锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

• 如果这把锁没有被锁定,线程加锁成功

• 如果这把锁被锁定后了。调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误码。

例如:你要去上厕所,发现里面有人,你就去干别的事情了,


互斥锁解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

那个线程加的锁,必须那个线程去解锁。


互斥锁的使用

需要先创建一个互斥锁

再使用互斥锁对需要加锁的位置进行加锁和解锁操作。

全部代码如下:

运行图如下:


死锁

当多个线程访问共享资源时,需要加锁,如果锁使用不当,就会造成死锁现象。如果线程死锁造成的后果是:所有线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。

例如:加锁后忘记解锁


再例如:

  1. 当我们加锁后进入if语句后退出(return)了,等其他线程再进来就发现当前已被锁,就阻塞自己。
  2. 重复加锁(多加锁了一次)


多个线程对一个临界资源进行访问我们需要一把锁

一个线程对多个临界资源进行访问我们需要多把锁


如何避免死锁:

• 对共享资源访问完毕之后,一定要解锁。或者在加锁的时候使用tylock

• 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问共享资源,但是有些情况下是做不到的),另外也可以在对其他互斥锁操作前,先释放当前线程拥有的互斥锁。

• 项目程序中可以引入一些专门用于检测死锁的模块


读写锁

读写锁是一把锁,是互斥锁的升级版。在做读操作的时候可以提高程序执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。


锁的类型为pthread_rwlock_t 这把锁既可以锁定读操作,还可以锁定写操作。

记录信息:


1.锁的状态:锁定/打开

2.锁定的是什么操作:读操作/写操作,使用了读写锁锁定了读操作,需要先解锁,再去锁定写操作。

3.哪个线程将这把锁锁上的

特点:

• 使用了读写锁的读锁锁定了临界区,线程对临界区的访问是并行的。读锁是共享的。

• 使用了读写锁的写锁锁定了临界区,线程对临界区的访问时串性的。写锁是独占的。

• 使用了读写锁分别对俩个临界区区加了读锁和写锁,俩个线程要同时访问着俩个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。


如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势。但是说程序中所有的线程对共享资源既有读也有写操作,使用读写锁更有优势。


初始化读写锁

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlock_t *restrict attr);


rwlock:读写锁地址,传出参数

attr:读写锁属性,一般使用默认属性,指定为NULL


释放读写锁占用的系统资源

int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock);


rwlock:读写锁地址,传出参数


加锁读操作

int pthread_rwlock_rdlock(pthread_rwlock_t * );

如果读写锁是打开的,那么加锁成功,

如果读写锁锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的。

如果读写锁锁定了写操作,调用这个函数会阻塞。


如果加读锁失败,不会阻塞当前线程,直接返回错误号 有效避免死锁

int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);

如果读写锁是打开的,那么加锁成功,

如果读写锁锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的。

如果读写锁锁定了写操作,不会阻塞当前线程,函数会返回一个错误码。


加锁写操作

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

如果读写锁是打开的,那么加锁成功,

如果读写锁锁定了读操作,调用这个函数会阻塞。

如果读写锁锁定了写操作,调用这个函数会阻塞。


如果加锁失败,不会阻塞当前线程,直接返回错误号 有效避免死锁

int pthread_rwlock_trywrdlock(pthread_rwlock_t * rwlock);

如果读写锁是打开的,那么加锁成功,

如果读写锁锁定了读操作,不会阻塞当前线程,函数会返回一个错误码。

如果读写锁锁定了写操作,不会阻塞当前线程,函数会返回一个错误码。


解锁 读锁写锁都可以

int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);


读写锁使用

题目要求:8个线程操作同一个全局变量,3个线程不定时写同一全局资源,5个线程不定时读同一线程。

源码如下:

运行效果图:


条件变量

条件变量函数

条件变量的主要作用不是处理线程同步,而是进行线程阻塞。如果多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样得到,二者的区别如下:

• 假设有A-Z26个线程,这26个线程共同访问同一把互斥锁,如果A线程加锁成功,那么其余B-Z线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区。

• 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界区资源,这种情况下还是会出现共享资源中数据混乱。


一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用,条件变量类型对应的类型为pthread_cond_t,


被条件变量阻塞的线程的信息会被记录到这个变量中,以便在解除阻塞的时候使用。


初始化

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

cond:条件变量的地址

attr:条件变量属性,一般使用默认属性,指定为NULL


销毁释放资源

int pthread_cond_destroy(pthread_cond_t *cond);

cond:条件变量的地址


阻塞函数

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex *restrict mutex);

该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。会对互斥锁做俩件事

• 1、在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样避免死锁。

• 2、当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区。


阻塞时长函数

将线程阻塞一定时长,时间到了,线程就解除阻塞。

struct timespec{
time_t tv_sec;   
time_t tv_nsec;
};

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

前俩个参数和前面阻塞哈数一样,第三个参数是阻塞时长,


唤醒阻塞函数(至少唤醒一个)

int pthread_cond_signal(pthread_cond_t *cond);


唤醒阻塞函数(全部唤醒)

int pthread_cond_broadcast(pthread_cond_t *cond);


生产者和消费者

场景描述:使用条件变量实现生产者和消费者模型,生产者有5个,往链表头部添加节点,消费者也有5个,删除链表尾巴的节点

源码如下:

接下里我们该做线程同步问题

• 生产者和消费者区域全部上锁

• 当生产者发现生产数量超过5时,阻塞生产者,唤醒消费者。当生产者生产完毕,也需要唤醒消费者。

• 当消费者发现没有产品时,阻塞消费者,唤醒生产者。当消费者消费完毕,也需要唤醒生产者


信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。流水线


信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。


信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。

信号的类型为 sem_t 对应的头文件为 <semaphore.h>


初始化信号量/信号灯

int sem_init(sem_t *sem, int pshared, unsigned int value);

sem:信号量变量地址

pshared:0线程同步,非0进程同步

value:初始化当前信号量拥有的资源数(>=0),如果资源数位0,线程就会被阻塞了。


资源释放,线程销毁之后调用这个函数

int sem_destroy(sem_t *sem);


消耗函数

int sem_wait(sem_t *sem); 函数调用时sem中的资源就会被消耗一个,资源数-1

当线程调用这个函数,并且sem>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时。资源被耗尽,因此线程也就被阻塞了。


int sem_trywait(sem_t *sem); 函数调用时sem中的资源就会被消耗一个,资源数-1

当线程调用这个函数,并且sem>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时。资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

解除阻塞时间函数

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示

struct timespec {
time_t tv_sec;      /* Seconds */
long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};


// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。

// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);


该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。


增加资源函数

// 调用该函数给sem中的资源数+1

int sem_post(sem_t *sem);

调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。


查看信号量函数

// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中

// sval是一个传出参数

int sem_getvalue(sem_t *sem, int *sval);


生产者消费者(一个资源量)

一个资源量就不需要线程同步。必然是单线程 , 只有资源总数为1时候,才可以不加锁

源代码:


生产者消费者(多个资源)

需要加锁,生产者和消费者共用一个链表,所以用一把锁。

锁要在信号量里面。不然有可能会造成死锁(比如:现在资源被消费完毕。然后一直被消费者抢到,进入阻塞,当全部线程被阻塞在消费者这边时,就造成了死锁)


源代码:


目录
相关文章
|
4月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
182 3
|
3月前
|
Linux
linux内核 —— 读写信号量实验
linux内核 —— 读写信号量实验
|
4月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
74 0
|
4月前
|
运维 监控 Linux
Linux系统读写硬盘慢,如何排查?四步教你定位问题!
【8月更文挑战第24天】在Linux系统运维中,硬盘读写速度慢是一个常见且影响重大的问题。它不仅会导致服务器性能下降,还会影响用户体验。本文将详细介绍四个步骤,帮助你快速定位并解决Linux系统硬盘读写慢的问题。
730 0
|
4月前
|
关系型数据库 Linux 数据库
linux设置信号量系统参数
linux设置信号量系统参数
|
5月前
|
Linux
【Linux】生产者消费者模型——环形队列RingQueue(信号量)
【Linux】生产者消费者模型——环形队列RingQueue(信号量)
51 0
|
5月前
|
安全 算法 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
53 0
|
5月前
|
存储 安全 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
61 0
|
5月前
|
消息中间件 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
74 0
|
5月前
|
消息中间件 存储 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
79 0