• Linux多线程基础总结


    一、线程参数传递

    线程传递的参数类型为void*,传递方式有值传递、指针传递、引用传递

    1. 值传递:拷贝一份值给新的线程,多线程间不共享

    2. 址传递(指针传递):

      • 连续创建多个线程中如果传入的是同一变量地址,多线程共享这变量,由于哪个线程先运行是不确定的,变量的值是不可控的,并且会造成共享资源竞争问题,因为修改变量的值不是原子操作
      • 如果主线程先结束,其内存空间会被释放,子进程中的指针就成了野指针,可想而知,继续操作这块内存会造成意想不到的后果。
        结果方案:定义的变量使用堆内存分配,可以避免栈内存释放,也可以使用全局变量

    二、线程分离

    线程分离后,线程退出自动释放全部资源:pthread_detach()

    三、线程资源的回收

    非分离状态的线程才可以被join回收资源

    1. 阻塞回收:pthread_join()

    2. 非阻塞回收:pthread_tryjoin_np()

    3. 限时阻塞回收:pthread_timedjoin_np()

    四、线程清理函数

    线程终止的时候,可以调用清理函数释放资源,入栈和出栈函数必须成对的出现

    1. 清理函数入栈:pthread_cleanup_push()

    2. 清理函数出栈:pthread_cleanup_pop()(0:出栈不执行 非零:出栈并执行)

    五、线程取消

    1. 线程在运行过程中可以调用被取消:pthread_cancel()

    2. 线程被取消后,join返回值为PTHREAD_CANCELED 即 -1

    3. 设置线程的取消状态:pthread_setcancelstate()
      宏:PTHREAD_CANCEL_ASYNCHRONOUS: 立即取消
      宏:PTHREAD_CANCEL_DEFERRED: 到达取消点(例如sleep())才取消
      设置线程的取消点:pthread_testcancel()

    六、线程与信号

    1. 向指定线程发送信号:pthread_kill()
    2. 信号屏蔽:进程:sigpromask() 线程:pthread_sigmask()
    3. 阻塞等待信号:sigwait()sigwaitinfo()sigtimedwait()
    4. 在多线程中,外部向进程发送信号(ctl+c)不会中断系统调用(sleep()
    5. 在多线程中,信号的处理是所有线程共享的(即信号注册在任一线程中注册都可以)
    6. 进程中的信号可以送达单个线程,会中断系统调用
    7. 如果某个线程因为信号(SIGTERM默认处理)而终止,整个进程将终止

    七、线程安全

    1. 多个线程访问共享资源(全局和静态变量)的时候会冲突
      例:定义全局变量int a=0; 线程1和线程2同时:循环执行 a++ 一万次 得到a的值小于2万,a并不会自动到两万

    2. 三个概念:原子性、可见性、顺序性

      • 原子性
        一个操作(有可能包含有多个子操作)要么全部执行(生效)要么全部都不执行(都不生效)
        CPU执行指令:读取指令、读取内存、执行指令、写回内存
        例(非原子性):第一不读取指令:i++ 第二步:从内存中读取i的值 第三步:把i+1 第四步:把结果写回内存
      • 可见性
        当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。
        CPU有高速缓存。每个线程读取共享变量时,会将该变量从内存加载到CPU的缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存。此时其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程更新后的数据。

      • 顺序性
        程序执行的顺序按照代码的先后顺序执行。
        CPU为了提高程序整体的执行效率,可能会对代码进行优化,按照更高效的顺序执行代码。
        CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
        例:int a = 1; a = 2; a = 3; 编译器对代码优化为int a = 3;

    3. volatile关键字:保证可见性和禁止代码优化,但不是原子操作

    4. 解决线程安全问题:原子操作(c++原子类)、和线程同步(锁)
      原子操作:本质是总线锁

    八、线程同步

    1. 互斥锁

    等待锁的时候,线程会休眠,不会消耗CPU,适合等待时间可能很长的场景

    声明锁

    pthread_mutex_t mutex;
    
    • 1
    • 普通锁: PTHREAD_MUTEX_TIMED_NP
      当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。
      这种锁策略保证了资源分配的公平性。

    • 嵌套锁PTHREAD_MUTEX_RECURSIVE_NP
      允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。
      如果是不同线程请求,则在加锁线程解锁时重新竞争。

    • 适应锁PTHREAD_MUTEX_ADAPTIVE_NP
      解锁后,请求锁的线程重新竞争。

    	int pthread_mutex_init();     //初始化锁,也可定义时直接使用宏初始化如上
    	int pthread_mutex_lock();     //等待并加锁
    	int pthread_mutex_trylock();  //尝试加锁,不等待
    	int pthread_mutex_timedlock();//带超时机制的加锁
    	int pthread_mutex_unlock()    //解锁
    	int pthread_mutex_destroy();  //销毁锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    2. 自旋锁

    循环的加测锁是否可用,会消耗CPU,适合等待时间很短的场景

    pthread_spinlock_t mutex;   //声明锁
    int pthread_spin_init();    //初始化锁
    int pthread_spin_lock();    //等待并加锁
    int pthread_spin_trylock(); //尝试加锁,不等待
    int pthread_spin_unlock();  //解锁
    int pthread_spin_destroy(); //销毁锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    4. 读写锁

    读时共享,写时单独,适用于读的次数远大于写的场景

    • 定义锁

       pthread_rwlock_t mutex; 		//声明锁
       PTHREAD_RWLOCK_INITIALIZER		//使用宏初始化锁
       int pthread_rwlock_init();		//初始化锁
       int pthread_rwlock_destroy();	//销毁锁
      
      • 1
      • 2
      • 3
      • 4
    • 锁属性

      int pthread_rwlockattr_getpshared();//获取读写锁属性
      int pthread_rwlockattr_setpshared();//设置读写锁属性
      PTHREAD_PROXESS_PRIVATE(单个线程私有)
      PTHREAD_PROCESS_SHARED(多线程共享)
      
      • 1
      • 2
      • 3
      • 4
    • 读锁

      int pthread_rwlock_rdlock();	  //阻塞获取读锁
      int pthread_rwlock_tryrdlock();   //尝试获取读锁,不阻塞
      int pthread_rwlock_timedrdlock(); //获取读锁,带超时机制
      
      • 1
      • 2
      • 3
    • 写锁

      int pthread_rwlock_wrlock();	  //阻塞获取写锁
      int pthread_rwlock_trywrlock();	  //尝试获取写锁,不阻塞
      int pthread_rwlock_timedwrlock(); //获取写锁,带超时机制
      
      • 1
      • 2
      • 3

    注意:只有在不加锁时,才能获取到写锁。linux系统优先考虑获取读锁,获取写锁的线程需要等待所有读锁释放才能获得到锁

    九、条件变量

    pthread_cond_t cond; 	     //声明条件变量
    PTHREAD_COND_INITIALIZER;    //使用宏初始化条件变量
    int pthread_cond_init();     //初始化条件变量
    int pthread_cond_destroy();  //销毁条件变量
    
    int pthread_cond_wait();     //等待被唤醒进行加锁
    int pthread_cond_timedwait();//等待被唤醒进行加锁,带超时机制
    
    int pthread_cond_signal();   //唤醒至少一个等待中的线程
    int pthread_cond_broadcast();//唤醒全部等待中的线程
    
    int pthread_condattr_getpshared();//获取共享属性
    int pthread_condattr_setpshared();//设置共享属性(单个线程私有、多线程共享)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    十、信号量

    多进程的信号量可以用在多线程中,而多线程的信号量只能用于多线程中,多线程的信号量使用比较简单

    sem_t *sem;  	  //声明信号量
    int sem_init();   //初始化信号量
    int sem_destroy();//销毁信号量
    
    int sem_wait(sem_t *sem);   //信号量的P操作
    int sem_trywait(sem_t *sem);//信号量的P操作,不阻塞
    int sem_timedwait();	    //信号量的P操作,带超时机制
    int sem_post(sem_t *sem);   //信号量的V操作
    int sem_getvalue();	    //获取信号量的值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:以上几种同步机制都能形成等待队列,但是不是绝对公平的,当前线程的cpu时间片还未使用完,获取到锁的概率会很大

    十一、生产者消费者模型

    1. 互斥锁+条件变量实现
    2. 信号量实现

    十二、保证多线程程序的稳定性

    1. 主进程只用与监控和调度
    2. 程序的功能由子线程实现,把心跳写入全局变量
    3. 如果心跳超时,取消子线程再重新启动

    十三、多线程实现异步通讯

    主线程创建socket连接,一个子线程负责发送,另一个子线程负责接收

  • 相关阅读:
    C# Kafka重置到最新的偏移量,即从指定的Partition订阅消息使用Assign方法
    4.验证面试高频问题整理(附答案)
    protobuf 黑盒调用 blackboxprotobuf 不用proto文件 application/x-protobuf 超短保姆级教程
    express学习3-捕获错误
    xxl-job-架构及原理
    Linux - 内核 - 安全机制 - 内存页表安全
    (附源码)ssm在线学习网站 毕业设计 080833
    LLM之幻觉(一):大语言模型幻觉解决方案综述
    图的存储 —— 链式前向星
    2022了你还不会『低代码』?数据科学也能玩转Low-Code啦! ⛵
  • 原文地址:https://blog.csdn.net/weixin_54178481/article/details/126567127