线程互斥、同步(二)

简介: 线程互斥、同步

二、可重入与线程安全

2.1 概念

线程安全: 多个线程并发同一段代码时,不会出现不同的结果

重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。

注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入


2.2 常见线程不安全的情况

不保护共享变量的函数


函数状态随着被调用,状态发生变化的函数


返回指向静态变量指针的函数


调用线程不安全函数的函数


2.3 常见线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

2.4 常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构

函数体内使用了静态的数据结构

2.5 常见可重入的情况

不使用全局变量或静态变量

不使用malloc或者new开辟出的空间

不调用不可重入函数

不返回静态或全局数据,所有数据都由函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

2.6 可重入与线程安全的关系

线程安全不一定是可重入的,而可重入函数则一定是线程安全的(可重入函数是线程安全函数的一种)

函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

若一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

若对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的

三、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态


单执行流产生死锁


单执行流也可能产生死锁,若某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁时是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有办法释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态

#include <iostream>
#include <pthread.h>
using namespace std;
void *Routine(void *pmtx)
{
    pthread_mutex_lock((pthread_mutex_t*)pmtx);
    pthread_mutex_lock((pthread_mutex_t*)pmtx);
    pthread_mutex_unlock((pthread_mutex_t*)pmtx);//无法执行
    pthread_exit(nullptr);
}
int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void *)&mtx);
    pthread_join(tid, NULL);//等待不到
  pthread_mutex_destroy(&mtx);
    return 0;
}

此时主线程阻塞等待新线程退出,但是线程被阻塞进入死锁状态


5bb002383cb549e98ca2b7f1ddab85ab.png


该进程当前的状态是 sl+ ,其中 l 就是lock的意思,表示该进程当前处于一种死锁的状态


e410ba70d9c84a3baefafef43df334ca.png


多执行流产生死锁


线程A申请锁资源的顺序为:锁1、锁2;线程B申请锁资源的顺序为:锁2、锁1


当线程A申请到锁1准备申请锁2时,线程B已申请到锁2准备申请锁1,这时两个线程都会因为申请锁失败而陷入阻塞,并且无法释放锁,进入死锁状态


产生死锁的条件


互斥条件: 一个资源每次只能被一个执行流使用

请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺

循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁


破坏死锁的四个必要条件

加锁顺序一致

避免锁未释放的场景

资源一次性分配

四、线程同步

4.1 同步概念与竞态条件

同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步

竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件


单纯的加锁是会存在某些问题的,若某个线程的优先级较高或竞争力较强,每次都能够申请到锁,但申请到锁之后什么也不做,那么这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题

单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源

现在增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后

增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,若有十个线程,就能够让这十个线程按照某种次序进行临界资源的访问

譬如,现在有两个线程访问一块临界资源,一个线程往临界资源写入数据,另一个线程从临界资源读取数据。但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界资源被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取。引入同步后该问题就能很好的解决


4.2 条件变量

4.2.1 概念

条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述


条件变量主要包括两个动作:


一个线程等待条件变量的条件成立而被挂起

另一个线程使条件成立后唤醒等待的线程

条件变量通常需要配合互斥锁一起使用


4.2.2 接口

初始化条件变量

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

参数:


cond:需要初始化的条件变量的地址

attr:初始化条件变量的属性,一般设置为NULL即可

返回值:条件变量初始化成功返回0,失败返回错误码


使用pthread_cond_init()函数初始化条件的方式被称为动态分配,还可以使用静态分配进行初始化,即下面这种方式:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

参数cond:需要销毁的条件变量的地址


返回值:条件变量销毁成功返回0,失败返回错误码


注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁


等待条件变量满足

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

参数:


cond:需要等待的条件变量的地址

mutex:当前线程所处临界区对应的互斥锁

返回值:函数调用成功返回0,失败返回错误码


唤醒等待


int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal()函数用于唤醒该条件变量等待队列中首个线程

pthread_cond_broadcast()函数用于唤醒该条件变量等待队列中的全部线程

参数cond:唤醒在cond条件变量下等待的线程


返回值:函数调用成功返回0,失败返回错误码


4.2.3 为什么pthread_cond_wait需要互斥量

条件等待是线程间同步的一种手段。若只有一个线程,并且其条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作改变共享变量,使原先不满足的条件变得满足,并且通知等待在条件变量上的线程

条件不会无缘无故变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据

在调用pthread_cond_wait函数时,将对应的互斥锁传入,当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。让其他线程也可以得到锁,进入条件变量的等待队列,这样就不会发生同一个线程多次抢占锁的情况

当该线程被唤醒时,该线程会接着执行临界区内的代码,并且会自动获得对应的互斥锁

错误的设计


当进入临界区上锁后,若发现条件不满足,先解锁,然后在该条件变量下进行等待

//错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false){
  pthread_mutex_unlock(&mutex);
  //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
  pthread_cond_wait(&cond, &mutex);
  pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

不可行。解锁后、调用pthread_cond_wait()函数前,若此时有其他线程获取到互斥量,发现此时条件满足,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号(已经释放锁,无法在拿到锁了),最终可能会导致线程永远不会被唤醒。调用pthread_cond_wait()的线程必须是持有锁的


4.2.4 使用规范

等待条件变量的代码

pthread_mutex_lock(&mutex);
while (条件为假)
  pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);

唤醒等待线程的代码

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);


目录
相关文章
|
3月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
141 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
2月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
45 6
|
2月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
3月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
62 1
|
3月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
3月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
168 0
|
3月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
5月前
|
Java 程序员
从0到1,手把手教你玩转Java多线程同步!
从0到1,手把手教你玩转Java多线程同步!
51 3
|
5月前
|
Java 测试技术
Java多线程同步实战:从synchronized到Lock的进化之路!
Java多线程同步实战:从synchronized到Lock的进化之路!
110 1
|
5月前
|
开发者 C# UED
WPF与多媒体:解锁音频视频播放新姿势——从界面设计到代码实践,全方位教你如何在WPF应用中集成流畅的多媒体功能
【8月更文挑战第31天】本文以随笔形式介绍了如何在WPF应用中集成音频和视频播放功能。通过使用MediaElement控件,开发者能轻松创建多媒体应用程序。文章详细展示了从创建WPF项目到设计UI及实现媒体控制逻辑的过程,并提供了完整的示例代码。此外,还介绍了如何添加进度条等额外功能以增强用户体验。希望本文能为WPF开发者提供实用的技术指导与灵感。
193 0