当前位置: 首页 > news >正文

Linux的多线程

目录

1、线程同步

1.1 线程同步的概念

1.2、线程同步的主要手段

2、互斥锁

2.1 为什么ticket会<=0

​编辑

2.2 互斥锁的相关操作

2.2.1 创建锁及初始化

2.2.2 加锁与解锁

2.3 保证ticket不会<=0

2.4 互斥锁的原理

2.5 互斥锁的封装

3、条件变量

3.1 条件变量的相关操作

3.1.1 创建条件变量及初始化

3.1.2 等待与唤醒

3.2 条件变量的封装

4、POSIX信号量

4.1 信号量的相关操作

4.1.1 创建信号量及初始化

4.1.2 P/V操作

4.2 信号量的封装

5、死锁

5.1 死锁的定义

5.2 死锁产生的四个必要条件

5.3 避免死锁

6、线程安全


1、线程同步

1.1 线程同步的概念

  • 同步(Synchronization)是一个大目标:它指的是协调多个线程的活动,确保它们能够正确地可控地协作。它的目的是避免混乱(即数据竞争和不一致)。

  • 临界资源需要被保护共享资源
  • 临界区访问临界资源代码
  • 原子性(不可分割):一个或多个操作要么全部执行成功要么全部不执行中间不会被打断

1.2、线程同步的主要手段

  • 互斥锁(Mutex):是实现线程同步最基础、最核心的手段。它通过 “互斥访问” 的逻辑,强制保证同一时间只有一个线程能进入临界区(即操作共享资源的代码段)。
    • 例如多个线程修改同一计数器(读取->修改->写回)时,互斥锁能避免 “两个线程同时读取旧值、各自修改后覆盖” 的错误,从根本上防止数据竞争,实现线程间的基础协调。
    • 注意:互斥锁保护的是代码段(临界区),而非数据本身。任何访问共享资源的代码都必须配合同一个互斥锁使用。
  • 条件变量(Condition Variable):用于解决 “线程需等待特定条件满足后再执行” 的复杂场景常与互斥锁配合使用。其核心逻辑是:当线程检查到目标条件不满足时(如 “队列已空”),会释放持有的互斥锁主动进入等待状态;当其他线程修改资源后使条件可能满足时(如向队列添加数据),可通过条件变量唤醒等待的线程,让其重新获取锁并继续执行
    • 例如生产者 - 消费者模型中,消费者线程可通过条件变量等待 “队列有数据”,生产者线程在生产数据后唤醒消费者。
  • 信号量(Semaphore):是更通用的同步原语,基于 “计数器” 实现,核心作用是控制对 “一种多块的临界资源” 的并发访问次数(即控制同时访问该资源的线程数量)。计数器的初始值代表资源总量,线程获取资源时计数器减 1(若计数器为 0 则等待),释放资源时计数器加 1。
    • 既可以模拟互斥锁(计数器初始值设为 1,确保同一时间只有一个线程获取资源),也可用于资源计数场景(如限制同时访问数据库的线程数量为 5,计数器初始值设为 5)。

2、互斥锁

  • pthread库提供了互斥锁(互斥量),对于一种整块临界资源,只有一把锁(唯一),所以只有一个线程会执行临界区的代码,保证了执行临界区代码原子性。即临界区的代码要么全部执行成功(一个持有锁的线程),要么全部不执行(其他没有申请到锁的线程),中间不会被打断(其他没有申请到锁的线程执行不了临界区的代码)。

2.1 为什么ticket会<=0

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>int ticket = 100; // 1-100号的票void* buyTicket(void* arg)
{char* name = static_cast<char*>(arg);// 临界区beginwhile(ticket > 0){usleep(100); // 睡眠0.1msprintf("%s buy ticket:%d\n",name,ticket);--ticket;}// 临界区endreturn nullptr;
}int main()
{pthread_t tid1,tid2,tid3;pthread_create(&tid1,nullptr,buyTicket,(void*)"tid1");pthread_create(&tid2,nullptr,buyTicket,(void*)"tid2");pthread_create(&tid3,nullptr,buyTicket,(void*)"tid3");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);return 0;
}

  • 最后抢1号票的过程如下:
  • 注意
  1. 如果不写usleep,可能一个线程就直接抢完了。
  2. 实际上,这是抢票混乱其中的一种情况。
  3. 在这个场景中,判断也属于临界区,正是因为判断,所以tid2和tid3都能进入循环,抢最后的1号票。
  4. 因为临界区的代码操作不是原子性的,所以出现问题。其实是线程并发而产生的问题(本质是线程切换),称为线程安全问题

2.2 互斥锁的相关操作

2.2.1 创建锁及初始化
#include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 锁的类型pthread_mutex_t。锁有全局锁和局部锁。
  1. 全局锁,要在编译时完成初始化,所以使用PTHREAD_MUTEX_INITIALIZER初始化。会自动销毁
  2. 局部锁,要在运行时完成初始化,所以使用pthread_mutex_init()初始化第二个参数用于配置锁的属性,一般传nullptr(默认属性)。必须手动pthread_mutex_destroy(pthread_mutex_t *mutex);进行销毁
2.2.2 加锁与解锁
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock(pthread_mutex_t *mutex),申请加锁成功继续向后执行失败阻塞并挂起,等待持有锁的线程释放锁。pthread_mutex_trylock(pthread_mutex_t *mutex),同上,失败,就是非阻塞(本文不考虑)。
  • pthread_mutex_unlock(pthread_mutex_t *mutex),解锁。
  • 注意:
  1. 所有的线程都要遵循锁的规则(加锁,解锁)。
  2. 一般对临界区进行加锁,不要包含太多的非临界区,会影响效率。
  3. 加锁之后,在临界区内部,即使线程被切换了,但是其他线程申请加锁,会失败,就会被阻塞并挂起。

2.3 保证ticket不会<=0

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>int ticket = 100; // 1-100号的票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *buyTicket(void *arg)
{char *name = static_cast<char *>(arg);while (true){pthread_mutex_lock(&lock);if (ticket > 0){usleep(100); // 模拟抢票耗时,睡眠0.1msprintf("%s buy ticket:%d\n", name, ticket);--ticket;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}// 如果这样写,抢到锁后,就一直循环抢票了,其他线程都没有锁,抢不到票
// void* buyTicket(void* arg)
// {
//     char* name = static_cast<char*>(arg);
//     pthread_mutex_lock(&lock);
//     while(ticket > 0)
//     {
//         usleep(100); // 睡眠0.1ms
//         printf("%s buy ticket:%d\n",name,ticket);
//         --ticket;
//     }
//     pthread_mutex_unlock(&lock);//     return nullptr;
// }// 如果这样写,还是会抢票混乱,关键是抢到锁后要再判断
// void* buyTicket(void* arg)
// {
//     char* name = static_cast<char*>(arg);
//     while(ticket > 0)
//     {
//         pthread_mutex_lock(&lock);//         usleep(100); // 睡眠0.1ms
//         printf("%s buy ticket:%d\n",name,ticket);
//         --ticket;//         pthread_mutex_unlock(&lock);
//     }//     return nullptr;
// }int main()
{pthread_t tid1, tid2, tid3;pthread_create(&tid1, nullptr, buyTicket, (void *)"tid1");pthread_create(&tid2, nullptr, buyTicket, (void *)"tid2");pthread_create(&tid3, nullptr, buyTicket, (void *)"tid3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

  • 注意:关键是抢到锁后要再判断
  • 这里发现1-100号的票都是tid1抢的,这并不代表代码有问题,只是多线程调度的正常现象。加锁的目的是保证数据安全,而不是保证线程执行的公平性。如果需要严格的公平性,可以考虑使用条件变量或其他同步机制来实现。

2.4 互斥锁的原理

  • 疑问:线程要竞争锁,那么锁就是共享资源,而锁是为了保护共享资源的,那么谁来保护锁?由申请加锁的原子性操作保证锁的设计原理保证申请加锁的过程是原子的。
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,于只有一条指令保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。如下是lock和unlock的伪代码:
  • 流程:第一个线程lock,%a1 = 0,原子操作swap(%a1,mutex),%a1 = 1,mutex = 0,因为%a1>0,return,执行临界区的代码;后面的线程lock,%a1 = 0,原子操作swap(%a1,mutex),%a1 = 0,mutex = 0,因为%a1<=0,挂起等待;第一个线程unlock,mutex = 1。
  • 注意:
  • %a1每次lock都会置为0,unlock不需要swap(%a1,mutex),直接mutex = 1。
  • 始终只有一个线程的%a1为1(%a1放到线程私有堆栈中)。1本质就是唯一的一把锁

2.5 互斥锁的封装

#include <pthread.h>namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}pthread_mutex_t *Get(){return &_mutex;}~Mutex(){pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex;};// RAII,资源的初始化与释放与对象的生命周期绑定class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}

3、条件变量

  • pthread库提供了条件变量,当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

3.1 条件变量的相关操作

3.1.1 创建条件变量及初始化
#include <pthread.h>pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
  • 条件变量的类型pthread_cond_t。条件变量有全局条件变量和局部条件变量。
  1. 全局条件变量,要在编译时完成初始化,所以使用PTHREAD_COND_INITIALIZER初始化。会自动销毁
  2. 局部条件变量,要在运行时完成初始化,所以使用pthread_cond_init()初始化第二个参数用于配置条件变量的属性,一般传nullptr(默认属性)。必须手动pthread_cond_destroy(pthread_cond_t *cond);进行销毁
3.1.2 等待与唤醒
#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_wait(cond, mutex),线程原子释放互斥锁 mutex 等待条件变量 cond 被触发(即阻塞)。当线程被唤醒时需重新竞争互斥锁再返回

  • pthread_cond_timedwait(cond, mutex, abstime),同上,阻塞,但带有超时时间,如果在指定时间内条件未触发,则返回ETIMEDOUT

  • pthread_cond_signal(cond)唤醒 至少一个 等待在该条件变量上线程

  • pthread_cond_broadcast(cond)唤醒 所有 等待在该条件变量上线程

  • 注意:

  1. 所有的线程都要遵循条件变量的规则(等待,被唤醒)。
  2. 流程:是先申请到加锁,然后判断,条件不满足,原子释放锁等待,当条件满足,被唤醒(可能会错误唤醒 或 唤醒时线程切换,条件再次不满足,所以要while判断)。

3.2 条件变量的封装

#include <pthread.h>
#incude "Mutex.hpp"namespace CondModule
{class Cond{public:// RAII,资源的初始化与释放与对象的生命周期绑定Cond(){pthread_cond_init(_cond,nullptr);}void Wait(MutexModule::Mutex& mutex){pthread_cond_wait(_cond,mutex.Get());}void Signal(){pthread_cond_signal(_cond);}void Broadcast(){pthread_cond_broadcast(_cond);}~Cond(){pthread_cond_destroy(_cond);}private:pthread_cond_t _cond;};
}

4、POSIX信号量

  • 信号量本质是一个计数器,是对一种多块临界资源的预定机制。如果只有一块资源,就称二元信号量,满足互斥。

4.1 信号量的相关操作

4.1.1 创建信号量及初始化
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
  • 信号量的类型sem_t
  • int sem_init(sem_t *sem, int pshared, unsigned int value)pshared:0 表示线程间共享非零表示进程间共享 value 信号量初始值
  • int sem_destroy(sem_t *sem),进行销毁
4.1.2 P/V操作
#include <semaphore.h>int sem_wait(sem_t *sem); // P()
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);int sem_post(sem_t *sem); // V()
  • int sem_wait(sem_t *sem),等待信号量,预定资源。如果sem-1 >=0成功预定了一块临界资源,如果sem-1 < 0失败阻塞并挂起
  • int sem_trywait(sem_t *sem),同上,如果失败就不阻塞
  • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout),同上,阻塞,但带有超时时间,如果在指定时间内条件未触发,则返回ETIMEDOUT
  • int sem_post(sem_t *sem),增加信号量,释放资源。++sem
  • 注意:
  1. 为什么是P(--),V(++)操作。发明者,使用荷兰语,Prolaag尝试减少,Verhoog增加
  2. P/V操作是原子的,所以信号量这个共享资源是线程安全的。

4.2 信号量的封装

#include <semaphore.h>namespace SemModule
{const unsigned int default_value = 1;class Sem{public:// RAII,资源的初始化与释放与对象的生命周期绑定Sem(unsigned int sem_value = default_value): _sem_value(sem_value){sem_init(&_sem, 0, _sem_value);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}~Sem(){sem_destroy(&_sem);}private:unsigned int _sem_value;sem_t _sem;};
}

画外音:const unsigned int传给unsigned int 没关系吗?没关系,是数值拷贝,指针和引用才需要担心权限的问题。

5、死锁

5.1 死锁的定义

  • 死锁是指两个或两个以上并发执行单元(线程或进程),在争夺系统资源时,陷入一种相互等待对方释放资源的状态,若无外力干涉,这些单元都将陷入一种永久等待状态。

简单比喻:两个人在一座独木桥上迎面相遇。
person A 需要从左边去右边,但被 person B 挡住。
person B 需要从右边去左边,但被 person A 挡住。
两人都坚持不让,于是谁也过不去,形成了僵局。这就是死锁。

5.2 死锁产生的四个必要条件

  • 四个条件必须同时满足死锁才可能发生。只要破坏其中任意一个,就能预防死锁
  • 互斥条件 (Mutual Exclusion)
    • 含义:资源一次只能被一个执行单元独占使用。如果另一个单元请求该资源,请求者必须等待,直到资源被释放。
    • 例子:打印机、一个共享变量的写操作、一个互斥锁(Mutex)。
  • 占有并等待条件 (Hold and Wait)
    • 含义:一个执行单元已经持有了至少一个资源,但又提出了新的资源请求,而该新资源恰好被其他单元占有,此时该单元进入等待状态,但并不释放自己已持有的资源
    • 例子:线程A锁住了锁L1,然后试图去锁L2;与此同时,线程B锁住了L2,然后试图去锁L1。
  • 不可抢占条件 (No Preemption)
    • 含义:资源不能被强制地从占有它的执行单元手中抢走只能占有者在使用完毕后主动释放
    • 例子:对于互斥锁,操作系统不能强行从一个线程那里把锁夺过来给另一个线程。线程必须自己调用 unlock()。
  • 循环等待条件 (Circular Wait)
    • 含义:存在一个等待资源的循环链每个执行单元都在等待下一个单元所占有的资源
    • 例子:线程A等待线程B占有的资源R2,线程B又在等待线程A占有的资源R1。这样就形成了一个A->B->A的循环等待。

5.3 避免死锁

  • 核心思想破坏死锁的四个必要条件
  • 我们无法破坏“互斥”(因为锁的本质就是互斥),但可以破坏其他三个条件。
避免方法破坏的条件核心思想优点缺点
固定顺序加锁循环等待定义全局统一的锁获取顺序简单有效,最常用需要全局规划,有时顺序难定
超时与回退占有并等待尝试获取锁,失败则释放已有锁灵活性高,可应对未知顺序实现复杂,可能活锁,性能开销
一次性申请占有并等待原子性地获取所有需要的锁从根本上杜绝“持有等待”资源利用率低,可能提前占锁
锁粒度粗化循环等待减少所需锁的数量简化编程模型,提升性能并发度降低

6、线程安全

  • STL中的容器是否是线程安全的? 不是
  • 原因是,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此STL默认不是线程安全,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全
  • 智能指针是否是线程安全的? 对于unique_ptr所有权独占,有且只有一个 unique_ptr 实例拥有对一个对象的所有权和控制权,通常不跨线程(跨线程用std::move()),无并发问题。对于shared_ptr多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,保证了引用计数的线程安全,但是指向的原始数据默认不是线程安全的
  • 注意:
  • 函数可重入一定线程安全的,但是线程安全函数不一定可重入的,如:发送信号造成死锁。
  • #include <pthread.h>
    #include <signal.h>
    #include <unistd.h>pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;// 一个线程安全的函数(但不可重入)
    void process_data() {pthread_mutex_lock(&global_lock); // (1) 加锁// ... 执行一些操作,访问全局数据 ...sleep(5); // 模拟一个耗时操作,在此期间信号很可能发生// ... 执行更多操作 ...pthread_mutex_unlock(&global_lock); // (3) 解锁
    }// 信号处理函数
    void signal_handler(int sig) {process_data(); // (2) 在信号处理中再次调用!
    }int main() {signal(SIGINT, signal_handler); // 注册信号处理函数process_data(); // 主调用return 0;
    }
  • 死锁的发生流程:主线程 进入 process_data(),成功获取互斥锁 global_lock(执行点(1);然后开始sleep(5);在 sleep 期间,用户按下了 Ctrl+C(发送 SIGINT 信号);主线程被中断,转而执行 信号处理函数 signal_handler;信号处理函数 调用 process_data();process_data() 试图再次获取 global_lock(执行点(2));然而,这把锁正被主线程自己持有(它在被中断前已经拿到了锁);于是,信号处理函数(在主线程的上下文中执行),等待一把永远不可能被释放的锁(因为主线程被阻塞了,无法继续执行到解锁操作(3))会永远阻塞;死锁发生!

文章转载自:

http://LFTsZsl8.bxqpL.cn
http://MpfDcXJs.bxqpL.cn
http://zs1lvHLe.bxqpL.cn
http://qxnvc7MT.bxqpL.cn
http://IVqz5nUp.bxqpL.cn
http://pWknxm3s.bxqpL.cn
http://co1MAu89.bxqpL.cn
http://lIt2oYR6.bxqpL.cn
http://z2PYghQt.bxqpL.cn
http://dW1ptyUE.bxqpL.cn
http://AbkGI6WD.bxqpL.cn
http://nCLKT7xA.bxqpL.cn
http://RmodUEUg.bxqpL.cn
http://2MiLISdC.bxqpL.cn
http://e2lX7irj.bxqpL.cn
http://HQtFfTN9.bxqpL.cn
http://C7PszqwT.bxqpL.cn
http://KbzoAqSK.bxqpL.cn
http://D4cNyqRa.bxqpL.cn
http://uPjDh7PW.bxqpL.cn
http://5IaNfove.bxqpL.cn
http://wDldY9xF.bxqpL.cn
http://zCmsTmoG.bxqpL.cn
http://vIaY0HJ5.bxqpL.cn
http://bHuEJklJ.bxqpL.cn
http://0o9dpjMs.bxqpL.cn
http://Nzqp4LuK.bxqpL.cn
http://DjjbBXZ2.bxqpL.cn
http://eeFQ9lEo.bxqpL.cn
http://1t4epibz.bxqpL.cn
http://www.dtcms.com/a/381493.html

相关文章:

  • 《链式二叉树常用操作全解析》
  • ——贪心算法——
  • IDEA使用Maven和MyBatis简化数据库连接(配置篇)
  • MLLM学习~M3-Agent如何处理视频:视频clip提取、音频提取、抽帧提取和人脸提取
  • video视频标签 响应式写法 pc 手机调用不同视频 亲测
  • CMD简单用法
  • 【iOS】AFNetworking
  • 【Qt】Window环境下搭建Qt6、MSVC2022开发环境(无需提前安装Visual Studio)
  • 惠普打印机驱动下载安装教程?【图文详解】惠普打印机驱动下载官网?电脑连接惠普打印机?
  • 【PHP7内核剖析】-1.1 PHP概述
  • ajax
  • STM32之RTOS移植和使用
  • [VL|RIS] RSRefSeg 2
  • Hadoop伪分布式环境配置
  • Python中的深拷贝与浅拷贝
  • 冒泡排序与选择排序以及单链表与双链表
  • 垂直大模型的“手术刀”时代:从蒙牛MENGNIU.GPT看AI落地的范式革命
  • 【高并发内存池】六、三种缓存的回收内存过程
  • 缓存常见问题与解决方案
  • 【pure-admin】登录页面代码详解
  • 初学鸿蒙笔记-真机调试
  • 反序列化漏洞详解
  • 使用 vue-virtual-scroller 实现高性能传输列表功能总结
  • python 实现 transformer 的 position embeding
  • 003 cargo使用
  • 制作一个简单的vscode插件
  • 【算法详解】:从 模拟 开始打开算法密匙
  • kubeadm搭建生产环境的单master多node的k8s集群
  • RocketMQ存储核心:MappedFile解析
  • 7.k8s四层代理service