Linux中的线程安全与线程同步详解

 更新时间:2025年04月27日 09:38:43   作者:s_little_monster_  
这篇文章主要介绍了Linux中的线程安全与线程同步,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

一、线程安全

1、概念

我们这里通过理解重入与线程安全的关系来理解线程安全

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

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

2、常见线程情况

常见线程不安全情况

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

常见线程安全情况

  • 每个线程对全局变量或静态变量只有读权限没有写权限
  • 类或接口对于线程来说是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

3、常见重入情况

常见不可重入情况

  • 调用了malloc或new函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准IO库函数,标准IO库中很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入情况

  • 不使用全局和静态变量
  • 不使用malloc或new出来的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都由函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

4、可重入与线程安全

联系

  • 函数可重入就代表着线程安全
  • 函数不可重入,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,这么这个函数既是不可重入的又不是线程安全的

区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,但可重入的一定是线程安全的
  • 如果将对临界资源的访问加上锁,那么这个函数是线程安全的,但如果这个重入函数的锁还没释放则会产生死锁,因此是不可重入的

5、死锁

(一)概念

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

死锁都是人为产生的,我们可以规避掉的

(二)死锁的四个必要条件

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

(三)避免死锁的方法

  • 当有死锁的时候,必然是满足上面这四个条件的,但满足上面四个条件不一定形成死锁,我们只要破坏上面其中任何一条条件就可以避免死锁
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

二、线程同步

1、概念

在纯互斥的场景下,由于我们的锁只有少量个,多个线程同时竞争锁,但是得到锁的只有一小部分线程,剩下的线程就会因为等待,产生 “线程饥饿” 问题,线程饥饿本质上就是抢夺不到锁的线程,即抢夺不到资源的线程在等待锁的释放,为了避免这里的饥饿的问题,我们就通过线程同步来在保证数据安全的前提下,让线程按照顺序访问临界资源

2、条件变量

(一)概念

当一个线程互斥的访问某个变量时,它可能在其他线程改变状态之前什么也做不了,比如一个线程访问队列时,发现队列为空,那么它只能等待,直到其他进程将一个节点添加到队列当中,这个时候我们就可以利用条件变量来规避这种情况

(二)调用函数

(1)初始化条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • 返回值:成功返回0,失败返回非0错误码
  • cond:指向要初始化的条件变量的指针,pthread_cond_t 是一个表示条件变量的数据类型
  • attr:指向条件变量属性对象的指针,传入NULL表示使用默认属性
(2)销毁条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
  • 返回值:成功返回0,失败返回非0错误码
  • cond:指向要销毁的条件变量的指针
(3)等待条件被满足
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • 返回值:成功返回0,失败返回非0错误码
  • cond:指向要操作的条件变量的指针,条件变量用于线程之间的等待和通知机制
  • mutex:指向互斥锁的指针,互斥锁用于保护共享资源,确保线程安全

调用该函数时,线程会自动释放互斥锁mutex,以便其他线程可以获取锁,当收到信号被唤醒后,线程会重新尝试获取互斥锁

(4)唤醒等待线程
#include <pthread.h>
//唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤起所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 返回值:成功返回0,失败返回非0错误码
  • cond:指向要操作的条件变量的指针,条件变量是一种用于线程同步的机制,允许线程在某个条件不满足时阻塞,直到其他线程通知该条件已经满足

如果一个线程执行 pthread_cond_broadcast,它会将所有等待该条件变量的线程全部唤醒,若执行 pthread_cond_signal,则只会唤醒至少一个等待该条件变量的线程,而非只唤醒当前线程

(三)样例

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;

#define NUM 4

int cnt = 0;
//条件变量函数的用法几乎与锁函数的用法完全等同
//定义全局锁和全局条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *Count(void *args)
{
    pthread_detach(pthread_self()); // 线程分离,跑完就不管了,不在乎它的返回值
	// Linux是64位机,指针是8字节,uint是unsigned long long int   
    uint64_t num = (uint64_t)args;
    cout << "Thread " << num << " is creat success" << endl;
    usleep(100000);
    while (true)
    {
        pthread_mutex_lock(&lock);
        
        //这里pthread_cond_wait要在临界区的原因是:
        //因为 pthread_cond_wait 是让线程去等待,等待的原因一定是临界资源不就绪
        //而临界资源是否就绪,是通过判断得来的,判断也是访问临界资源,所以判断必须在加锁之后
        pthread_cond_wait(&cond, &lock); 
        //线程在此处进入等待状态,等待条件变量 cond 发出信号
        
        cout << "Thread " << num << " is running... cnt: " << cnt << endl;
        cnt++;
        usleep(10000);
        pthread_mutex_unlock(&lock);
    }
}

int main()
{
    for (uint64_t i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        //这里的第四个参数,如果想要与新线程共享这个参数的话,可以设为(void*)&i,进行传址调用
        //我们这里要传值调用,不能让它用i
        pthread_create(&tid, nullptr, Count, (void *)i);
        usleep(1000);
    }
	//指定唤醒线程来访问临界资源
    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond); // 唤醒一个线程
        cout << "signal one thread..." << endl;
    }
    return 0;
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Jexus 5.8.2正式发布! 为Asp.Net Core生产环境提供平台支持

    Jexus 5.8.2正式发布! 为Asp.Net Core生产环境提供平台支持

    Jexus 5.8.2正式发布!Jexus支持ASP.NET、PHP为特色的集高安全性和高性能为一体的WEB服务器和反向代理服务器,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • Xshell7远程连接失败(connection failed)的问题解决

    Xshell7远程连接失败(connection failed)的问题解决

    本文主要介绍了Xshell7远程连接失败(connection failed)的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Linux 内核通用链表学习小结

    Linux 内核通用链表学习小结

    本篇文章主要介绍了Linux 内核通用链表学习小结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • Linux进程终止的N种方式详解

    Linux进程终止的N种方式详解

    进程终止是操作系统中,进程的一个重要阶段,他标志着进程生命周期的结束,下面小编为大家整理了一些常见的Linux进程终止方式,大家可以根据需求选择
    2025-03-03
  • Linux使用dd命令来复制和转换数据的操作方法

    Linux使用dd命令来复制和转换数据的操作方法

    Linux 中的 dd 命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的 USB 驱动器、克隆磁盘和生成随机数据等任务,本文给大家介绍了Linux 如何使用dd命令来复制和转换数据,需要的朋友可以参考下
    2025-01-01
  • CentOS7系统增加swap的操作方法实例

    CentOS7系统增加swap的操作方法实例

    这篇文章主要给大家介绍了关于CentOS7系统增加swap的操作方法,文中通过示例代码介绍的非常详细,对大家的学习或者使用CentOS7系统具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-10-10
  • 解析linux或android添加文件系统的属性接口的方法

    解析linux或android添加文件系统的属性接口的方法

    这篇文章主要介绍了linux或android添加文件系统的属性接口的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • linux中pip操作时的超时解决方法

    linux中pip操作时的超时解决方法

    在本文中我们给大家讲述了linux中pip操作时的超时解决方法以及相关注意点,有兴趣的朋友们参考下。
    2018-09-09
  • ubuntu (linux)修改网卡名称命令

    ubuntu (linux)修改网卡名称命令

    这篇文章主要介绍了ubuntu (linux)修改网卡名称命令的相关资料,这里提供了实现的命令代码,需要的朋友可以参考下
    2016-11-11
  • Linux查看服务器硬件信息的方法步骤

    Linux查看服务器硬件信息的方法步骤

    这篇文章主要介绍了Linux查看服务器硬件信息的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12

最新评论