热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux多线程(2)

线程的知识点太多,太重要,所以分成三部分进行总结学习线程安全多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

线程的知识点太多,太重要,所以分成三部分进行总结学习

线程安全

多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

多个线程对临界资源进行竞争操作时若不会造成数据二义性时则线程安全;否则,此时就是不安全的

如何实现线程安全

常见的线程安全的情况
  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见的线程不安全的情况
  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

在网上调研过程中看到一个总结:减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护

所以当对临界资源使用时,尽量在必须的地方使用锁的保护

对临界资源又有两种访问,分别是同步访问和互斥访问

同步:临界资源的合理访问

异步:临界资源同一时间的唯一访问

互斥锁

互斥锁的操作就是1/0的操作

一个0或者1的计数器。1可以表示加锁,加锁就是计数-1;操作完毕之后要解锁,解锁就是计数+1;

0表示不可以加锁,不能加锁则等待

//互斥锁的接口
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//函数应销毁mutex引用的mutex对象
//注意!!!
//销毁已解锁的已初始化互斥体应是安全的。试图销毁锁定的互斥体会导致未定义的行为。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//mutex:互斥锁变量
//attr:属性,通常为NULL 
//应使用attr指定的属性初始化mutex引用的mutex

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//宏PTHREAD_MUTEX_INITIALIZER来静态的初始化锁
//互斥锁变量不一定非要全局变量--只要保证要互斥的线程都能访问到就行

int pthread_mutex_lock(pthread_mutex_t *mutex);
//锁定mutex引用的mutex对象。如果互斥体已被锁定,则调用线程应阻塞,直到互斥体可用。此操作将返回互斥对象引用的互斥对象处于锁定状态,调用线程作为其所有者。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//函数应等同于pthread_mutex_lock(),但如果mutex引用的mutex对象当前被锁定(由任何线程,包括当前线程),则调用应立即返回。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//函数应释放mutex引用的mutex对象。互斥体的释放方式取决于互斥体的type属性。如果在调用pthread_mutex_unlock()时,mutex引用的mutex对象上有线程被阻塞,导致mutex可用,调度策略应确定哪个线程应获取mutex。

互斥锁的操作步骤

  1. 定义互斥锁变量
  2. 初始化互斥锁变量
  3. 加锁
  4. 解锁
  5. 销毁互斥锁

通过一个互斥锁Demo来感受一下锁的使用

//模拟黄牛抢票,100张票,共有四个黄牛在抢票
#include 
#include 
#include 
#include 
#include 

int ticket = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//定义初始化锁

void* thr_start(void* arg){
    while(1){
        pthread_mutex_lock(&mutex);
        if(ticket > 0){
            usleep(1000);
            printf("yellow bull : %d----get ticket : %d\n",(int)arg,ticket);
            ticket--;
        }else{
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(int argc, char* argv[]){
    pthread_t tid[4];
    int i = 0,ret;
    
    pthread_mutex_init(&mutex,NULL);
    for(;i <4; ++i){
        ret = pthread_create(&tid[i],NULL,thr_start,(void*)i);
        if(ret != 0){
            printf("yellow bull no exit!");
            return -1;
        }
    }
    for(i = 0;i <4;++i){
        pthread_join(tid[i],NULL);
    }
    pthread_mutex_destroy(&mutex);
	return 0;
}

Linux多线程(2)

这种情况黄牛抢票是比较容易的,一般只有一个黄牛能全抢到票。

但是如果把锁去掉

Linux多线程(2)

这样抢票就很混乱,因为没有了保护。所以锁的使用是在共享资源对它进行保护,换句话说加锁是为了保护资源,所以在这个代码中就将抢票的操作进行加锁保护。这样就只有一个黄牛可以抢到票。

死锁

在进行加锁的过程中很有可能发生死锁的情况下。

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

死锁的四个条件(重点)

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

我操作的时候别人不能操作

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

拿着手里的,但是请求其他的,其他的请求不到,手里拿着的也不放开

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

我的锁,别人不能释放

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

指在发生死锁时,必然存在一个进程资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,Pn正在等待已被P0占用的资源

死锁的产生与处理

当加锁或者解锁顺序不同时会发生死锁的情况;对锁资源的竞争以及进程/线程的加锁的推进顺序b不当

当以上四种条件被破坏时,可以预防死锁的产生

避免死锁的方法可以通过:死锁检测算法,银行家算法(推荐王道视频学习)

同步的实现

条件变量是线程同步的一种手段,条件变量用来自动阻塞一个线程,直到条件满足被触发为止。通常情况下条件变量和互斥锁同时使用

条件变量使我们可以睡眠等待某种条件出现。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

1、一个/多个线程等待“条件变量的条件成立”而挂起;线程1如果操作条件满足,则操作,否则进行等待。

2、另一个线程使“条件成立”信号;线程2促使条件满足,唤醒等待的线程。

如果没有资源则等待(死等),生产资源后唤醒等待。

//条件变量的接口
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//条件变量初始化,一般attr默认为NULL
//使用attr引用的属性初始化cond引用的条件变量。如果attr为空,则使用默认条件变量属性;效果与传递默认条件变量属性对象的地址相同。初始化成功后,条件变量的状态将被初始化。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//静态初始化条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
//销毁由cond指定的给定的条件变量
//销毁当前未阻塞线程的初始化条件变量是安全的。
//试图销毁当前阻止其他线程的条件变量会导致未定义的行为。

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
              const struct timespec *restrict abstime);
//abstime:限时等待时长,限时等待时长,超时后则返回

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//解锁后的挂起操作(原子操作),有可能还没来得及挂起就已经有人唤醒--白唤醒--导致了死锁

int pthread_cond_signal(pthread_cond_t *cond);
//唤醒至少一个等待的

int pthread_cond_boardcast(pthread_cond_t *cond);
//广播唤醒,唤醒所有等待的人

条件变量的步骤:

1、定义条件变量

2、初始化条件变量

3、等待\唤醒定义的条件变量

4、销毁条件变量

 //模拟一个skr与cxk使用比赛舞台的Demo                                  
  #include     
  #include     
  #include     
  #include     
  #include     
      
  int have_stage = 1;    
      
  pthread_cond_t skr;    
  pthread_cond_t cxk;    
  pthread_mutex_t mutex;    
      
  void* thr_skr(void* arg){//skr此时要上台dancing    
    while(1){    
      pthread_mutex_lock(&mutex);    
      //若此时舞台有人用,那么skr进行等待    
      while(have_stage == 1){    
        pthread_cond_wait(&skr,&mutex);    
      }    
      //舞台被人使用了,此时0;因为之前1,代表可以使用    
      printf("skr~~ is freestyle!!!\n");    
      sleep(1);    
      //跳完舞后舞台空了出来    
      have_stage += 1;    
      //有舞台了,叫cxk来使用    
      pthread_cond_signal(&cxk);    
      pthread_mutex_unlock(&mutex);
    }
    return NULL;
  }
  
  void* thr_cxk(void* arg){
    while(1){
      pthread_mutex_lock(&mutex);
      //没有舞台,那么在这里等待
      while(have_stage == 0){
        pthread_cond_wait(&cxk,&mutex);
      }
      //有了舞台就是可以唱跳rap篮球了。。
      printf("cxk~~ is singing,dancing,playing rapping and basketball!!\n");
      sleep(1);
      have_stage -= 1;
      //跳完还想跳。。因此叫skr快跳完换他跳。。
      pthread_mutex_unlock(&mutex);
      pthread_cond_signal(&skr);//唤醒skr,
    }
    return NULL;
  }                                                                                               int main(int argc,char * argv[]){
    pthread_t tid1,tid2;
    int ret;
      
    pthread_cond_init(&skr,NULL);
    pthread_cond_init(&cxk,NULL);
    pthread_mutex_init(&mutex,NULL);
    int i = 0;
    for(i = 0;i < 2;i++){
      ret = pthread_create(&tid1,NULL,thr_skr,NULL);
      if(ret != 0){
        printf("skr error");
        return -1;
      }
    }
    for(i = 0;i < 2; i++){
      ret = pthread_create(&tid2,NULL,thr_cxk,NULL);
      if(ret != 0){
        printf("cxk error");
        return -1;
      }
    }
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
  
    pthread_cond_destroy(&skr);
    pthread_cond_destroy(&cxk);
    pthread_mutex_destroy(&mutex);
    return 0;
  } 

Linux多线程(2)

运行结果可以看到cxk和skr交替互斥的表演节目。。。

那么互斥量(mutex)保护的是什么?其实保护的是变量条件(have_stage),当互斥量被成功lock后我们就可以放心的去读取变量条件,这样就不用在担心在这期间变量条件会被其他线程修改。如果变量条件不满足条件,当前线程阻塞,等待其他线程释放条件成立信号,并释放已经lock的mutex。这样一来其他线程就有了修改变量条件的机会。当其他线程释放条件成立信号后,pthread_cond_wait函数返回,并再次lock

pthread_cond_wait的工作流程可以总结为:unlock mutex,start waiting -> lock mutex。

while的作用

在变量条件处为什么不用if做判断而是用while,这是因为pthread_cond_wait的返回不一定意味着其他线程释放了条件成立信号。也可能意外返回。这种被称为假唤醒,在Linux中带阻塞功能的system call都会在进程中收到了一个signal后返回。这就是为什么使用while来检查的原因。因为不能保证wait函数返回的一定就是条件满足,如果条件不满足,那么我们还需要继续等待

signal条件变量的考虑

解锁互斥量mutex和发出唤醒信号是两个单独的操作,所以就存在一个顺序的问题

(1) 按照 unlock(mutex); condition_signal()顺序,当等待线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程(skr)很容易就锁住了mutex然后从conditon_wait()中返回了。

(2) 按照 condition_signal(); unlock(mutex)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。


推荐阅读
  • Linux环境下进程间通信:深入解析信号机制
    本文详细探讨了Linux系统中信号的生命周期,从信号生成到处理函数执行完毕的全过程,并介绍了信号编程中的注意事项和常见应用实例。通过分析信号在进程中的注册、注销及处理过程,帮助读者理解如何高效利用信号进行进程间通信。 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • Linux环境下C语言实现定时向文件写入当前时间
    本文介绍如何在Linux系统中使用C语言编程,实现在每秒钟向指定文件中写入当前时间戳。通过此示例,读者可以了解基本的文件操作、时间处理以及循环控制。 ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • 本文深入探讨了MySQL中常见的面试问题,包括事务隔离级别、存储引擎选择、索引结构及优化等关键知识点。通过详细解析,帮助读者在面对BAT等大厂面试时更加从容。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • 本文深入探讨了UNIX/Linux系统中的进程间通信(IPC)机制,包括消息传递、同步和共享内存等。详细介绍了管道(Pipe)、有名管道(FIFO)、Posix和System V消息队列、互斥锁与条件变量、读写锁、信号量以及共享内存的使用方法和应用场景。 ... [详细]
  • 本文深入探讨了 Delphi 中类对象成员的核心概念,包括 System 单元的基础知识、TObject 类的定义及其方法、TClass 的作用以及对象的消息处理机制。文章不仅解释了这些概念的基本原理,还提供了丰富的补充和专业解答,帮助读者全面理解 Delphi 的面向对象编程。 ... [详细]
  • 嵌入式开发环境搭建与文件传输指南
    本文详细介绍了如何为嵌入式应用开发搭建必要的软硬件环境,并提供了通过串口和网线两种方式将文件传输到开发板的具体步骤。适合Linux开发初学者参考。 ... [详细]
  • 使用PHP实现网站访客计数器的完整指南
    本文详细介绍了如何利用PHP构建一个简易的网站访客统计系统。通过具体的代码示例和详细的解释,帮助开发者理解和实现这一功能,适用于初学者和有一定经验的开发人员。 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
  • 本章详细介绍SP框架中的数据操作方法,包括数据查找、记录查询、新增、删除、更新、计数及字段增减等核心功能。通过具体示例和详细解析,帮助开发者更好地理解和使用这些方法。 ... [详细]
  • 对于许多初学者而言,遇到总线错误(bus error)或段错误(segmentation fault/core dump)是极其令人困扰的。本文详细探讨了这两种错误的成因、表现形式及解决方法,并提供了实用的调试技巧。 ... [详细]
  • 主调|大侠_重温C++ ... [详细]
  • Spring Cloud学习指南:深入理解微服务架构
    本文介绍了微服务架构的基本概念及其在Spring Cloud中的实现。讨论了微服务架构的主要优势,如简化开发和维护、快速启动、灵活的技术栈选择以及按需扩展的能力。同时,也探讨了微服务架构面临的挑战,包括较高的运维要求、分布式系统的复杂性、接口调整的成本等问题。最后,文章提出了实施微服务时应遵循的设计原则。 ... [详细]
author-avatar
linxin66063
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有