【Linux】Linux线程的同步与互斥(2)

简介: 【Linux】Linux线程的同步与互斥

二、可重入与线程安全

1、概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2、常见的线程不安全的情况

  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数

3、常见的线程安全的情况

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

4、常见不可重入的情况

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构

5、常见可重入的情况

  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6、可重入与线程安全联系

  1. 函数是可重入的,那就是线程安全的
  2. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  3. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

7、可重入与线程安全区别

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

三、死锁问题

1、死锁的概念


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

例如:有一份临界资源需要同时拿到A,B两把锁才能进行访问,线程1拿到了A锁,线程2拿到了B锁,然后线程1,线程2都想访问这份临界资源,于是相互申请对方的锁,但是两方都不释放锁,于是产生了僵持,这就是死锁。

单执行流可能产生死锁吗?

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

例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁,然后使用ps命令查看线程的状态。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 静态分配一把锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main()
{
    cout << "I am thread" << endl;
    // 申请锁
    pthread_mutex_lock(&mutex);
    cout << "I got a lock" << endl;
    // 再次申请锁
    pthread_mutex_lock(&mutex);
    cout << "I got a lock again" << endl;
    // 解锁
    pthread_mutex_unlock(&mutex);
    pthread_mutex_unlock(&mutex);
    return 0;
}

可以看到,线程被死锁了

2、死锁四个必要条件

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
  4. . 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

注意: 这是死锁的四个必要条件,也就是说只要是死锁,就一定同时满足这四个条件。

3、避免死锁

核心是:破坏死锁的四个必要条件,必要条件被破坏,就不可能形成死锁。

  1. 不加锁,不加锁当然不会产生死锁问题,当一个方案可以加锁完成也可以不加锁完成时,优先选择不加锁就能完成的!
  2. 加锁顺序一致,例如A,B,C三把锁,必须依次获取,顺序不能乱。
  3. 避免锁未释放的场景, 锁不释放,再次申请时当然会产生死锁问题。
  4. 主动释放锁,当我们申请锁失败的时候,我们可以主动释放自己的锁,这个可以借助pthread_mutex_trylock(),与pthread_mutex_unlock()函数完成。
  5. 控制线程统一释放锁,利用一个控制线程判断如果产生了死锁问题,就将所有的锁全部释放,重新竞争。(锁的申请与释放锁可以不是同一个线程

避免死锁也有一些其他算法如:死锁检测算法,银行家算法感兴趣的可以了解一下。

四、Linux线程同步

1、同步引入与概念

有了加锁以后我们多线程访问临界资源导致数据不一致性的问题确实得到了解决,但是单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后由于条件不满足于是什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题

为了解决饥饿问题,于是引入了线程同步。

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件: 指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。竞态条件会产生超出预期的情况因此竞态条件是一种需要被避免的情形。

2、条件变量

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

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

  1. 一个线程使用等待条件变量而被挂起。
  2. 另一个线程使条件成立后唤醒挂起的线程。

条件变量的使用总是和一个互斥量结合在一起。

例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中这个线程才被唤醒,这种情况就需要用到条件变量

有了这个条件变量以后该线程也不必不断的申请锁,使用队列里面的数据,结果没有数据,于是释放锁的循环,同时其他线程也能够有机会拿到锁,从而避免了饥饿问题。


①初始化条件变量

动态分配

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

参数说明:

  1. cond:需要初始化的条件变量。
  2. attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

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

静态分配

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

静态分配的条件变量不用我们手动销毁。

②销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

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

③等待条件变量满足

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);
  1. pthread_cond_signal函数用于唤醒等待队列中首个线程。
  2. pthread_cond_broadcast函数用于按顺序唤醒等待队列中的全部线程。

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

下面一份代码,我们假设条件全部都是不满足的,让多个线程在等待条件变量下面挂起,等待3秒以后条件满足,主线程再让所有的线程依次唤醒,继续执行。

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 定义锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* threadRoutine(void* args)
{
    char* s = static_cast<char*> (args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        // 假设要访问临界资源的条件不成立
        pthread_cond_wait(&cond, &mutex);
        cout << s << "active" << endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t tname[3];
    // 创建线程
    for (int i = 0; i < 3; i++)
    {
        char* ps  = new char[32];
        snprintf(ps, 32, "thread-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, ps);
    }
    sleep(3);
    // 3s以后唤醒等待队列里面的线程
    cout << "main thread wake up ..." << endl;
    while (true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
    return 0;
}

运行结果:

我们发现唤醒这三个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行等待,所以我们能够看到一个循环周转的现象。

3、为什么pthread_cond_wait需要互斥量的理解

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
  • 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。
相关文章
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
129 0
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
241 67
|
11月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
656 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
6月前
|
Ubuntu Linux
Linux系统管理:服务器时间与网络时间同步技巧。
以上就是在Linux服务器上设置时间同步的方式。然而,要正确运用这些知识,需要理解其背后的工作原理:服务器根据网络中的其他机器的时间进行校对,逐步地精确自己的系统时间,就像一只犹豫不决的啮齿动物,通过观察其他啮齿动物的行为,逐渐确定自己的行为逻辑,既简单,又有趣。最后希望这个过程既能给你带来乐趣,也能提高你作为系统管理员的专业素养。
1043 20
|
7月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
128 26
|
7月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
132 17
|
11月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
751 61
|
10月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
10月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
141 6