网站推广软件免费版可tv,网站错误列表,百度网页pc版登录,开一个设计公司目录 一、互斥#xff08;Mutex#xff09; 1. 不互斥抢票导致数据错误 2. 为什么需要互斥#xff1f; 3. 互斥锁 4. 进程间互斥 5. 加锁后的线程切换 6. RAII 风格的锁#xff08;智能指针风格#xff09; 二、锁的原理 1. 硬件实现 2. 软件实现 三、线程同步…目录一、互斥Mutex1. 不互斥抢票导致数据错误2. 为什么需要互斥3. 互斥锁4. 进程间互斥5. 加锁后的线程切换6. RAII 风格的锁智能指针风格二、锁的原理1. 硬件实现2. 软件实现三、线程同步1. 线程饥饿2. 同步要求3. 线程等待与唤醒条件变量4. 条件变量的用处四、生产者消费者模型1. 现实类比2. 321 原则3. 好处五、阻塞队列模型1. 阻塞队列2. 成员声明3. 伪唤醒4. 生产者插入5. 消费者执行6. 唤醒与解锁的先后顺序7. 效率高的原因六、POSIX 信号量1. 信号量回顾2. 多线程资源场景3. 环形队列4. 数组模拟七、基于环形队列和信号量的生产消费模型1. 信号量封装2. 生产者消费者操作3. 与二元信号量对照八、日志库设计1. 输出策略接口策略模式2. 日志器类管理策略3. 日志信息类logmsg4. 使用流程5. 宏简化使用九、线程池1. 成员声明与构造2. 激活线程池3. 任务处理函数4. 插入任务5. 停止线程池十、单例模式1. 实现方式2. 为线程池应用单例模式十一、死锁1. 可重入与线程安全2. 死锁的四个必要条件缺一不可十二、C 标准库的线程安全一、互斥Mutex1. 不互斥抢票导致数据错误创建多个线程执行抢票逻辑pthread_t t1, t2, t3, t4; pthread_create(t1, NULL, route, (void*)thread 1); pthread_create(t2, NULL, route, (void*)thread 2); pthread_create(t3, NULL, route, (void*)thread 3); pthread_create(t4, NULL, route, (void*)thread 4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL);结果票数被抢到负数如 -2 张。https://media/image1.png使用互斥锁后void* route(void* arg) { char* id (char*)arg; while (1) { pthread_mutex_lock(mutex); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; pthread_mutex_unlock(mutex); } else { pthread_mutex_unlock(mutex); break; } } return nullptr; }结果有序进行数据正确。https://media/image2.png2. 为什么需要互斥ticket--操作非原子--操作在汇编层面需要三步从内存读到寄存器 - 寄存器减1 - 写回内存。当线程 A 执行完两步后被切出线程 B 执行最后写回内存的数字可能被覆盖导致数据不一致。if (ticket 0)判断也是运算多个线程同时判断为真都被切出之后都执行--操作可能将票数减为负数。线程切换线程被切走的原因时间片用完、阻塞 I/O、sleep等会陷入内核。线程被切回从内核态返回到用户态时会进行检查。3. 互斥锁作用保证同一时刻只有一个线程执行临界区代码。锁本身也是临界资源因此锁的获取和释放操作必须是原子的。本质在执行临界区代码时让线程执行由并行改为串行。4. 进程间互斥在进程管道通信时可以将管道头部强转为锁对应的类型将管道变为锁实现进程间互斥。5. 加锁后的线程切换加锁后线程允许切换和调度。但持有锁的线程被切出时因为没有释放锁其他线程即使获得 CPU 也无法进入临界区。因此对于其他线程来说持有锁的线程要么在运行要么就绪它独占临界区这体现了原子性。6. RAII 风格的锁智能指针风格class guard_thread { public: guard_thread(my_thread* th) :_th(th) { pthread_mutex_lock(_th-_m); } ~guard_thread() { pthread_mutex_unlock(_th-_m); } private: my_thread* _th; };在循环中创建对象时自动加锁对象销毁时自动解锁防止忘记解锁。二、锁的原理1. 硬件实现可以通过关闭 CPU 中断时钟中断来实现。但这样做有风险只能在操作系统内部使用。2. 软件实现锁申请流程初始化锁值为1线程值为0- 交换exchange- 验证。因为1只有一个验证时值为1的线程就抢到了锁可以运行程序其他线程挂起。这个流程在任意异步时刻被打断也没关系因为1的个数没变。交换的本质获取锁时交换操作就是将线程的数据0交换到 CPU 的寄存器同时将锁的值1取出来。三、线程同步1. 线程饥饿高频申请锁的线程可能导致其他线程长期申请不到锁产生饥饿。因为刚运行结束的线程不需要被唤醒它还在运行而没运行的线程需要被唤醒。运行结束的线程在竞争锁时有优势可能一直由它运行。2. 同步要求线程不能立即申请第二次锁申请时要按特定次序进行。这样可以让锁的分配更加公平。3. 线程等待与唤醒条件变量使用cond条件变量相关接口。pthread_cond_init/PTHREAD_COND_INITIALIZER初始化。pthread_cond_wait(gcond, glock)在glock锁定的情况下阻塞等待gcond信号并自动释放锁。pthread_cond_signal(gcond)唤醒一个因gcond阻塞的线程。pthread_cond_broadcast(gcond)唤醒所有因gcond阻塞的线程。4. 条件变量的用处当条件不满足时线程会等待并休眠直到被唤醒才继续执行。判断条件是否满足需要使用临界区资源说明线程就在临界区内。因此让线程休眠时必须让它释放锁以便其他线程可以进入临界区修改条件。pthread_cond_wait的设计就是为了实现这一点它原子性地释放锁并进入休眠被唤醒后重新获取锁。四、生产者消费者模型1. 现实类比货物经历工厂 - 超市 - 消费者。对应数据线程 - 内存相关区域交易场所 - 线程。可用于多线程通信。2. 321 原则3种关系消费者之间互斥关系。生产者之间互斥关系竞争资源。消费者与生产者之间互斥与同步关系缓冲区满时生产者等空时消费者等。2个角色消费者、生产者。1个交易场所超市 - 内存空间如队列。3. 好处解耦合生产者和消费者互相不直接影响。支持忙闲不均生产者和消费者的速度可以不同。提高效率并发执行。五、阻塞队列模型1. 阻塞队列队列有内容消费者才能读取否则阻塞。队列不满生产者才能写入否则阻塞。2. 成员声明std::queueT _qu; int _cap; int _csleep; // 等待的消费者数量 int _psleep; // 等待的生产者数量 pthread_mutex_t _mutex; pthread_cond_t _full_cond; // 队列满时生产者等待 pthread_cond_t _empty_cond; // 队列空时消费者等待生产者和消费者竞争同一把锁。_full_cond控制生产者队列不满时才可生产。_empty_cond控制消费者队列不空时才可消费。queue是临界资源需要加锁保护。3. 伪唤醒当异常如被信号中断导致多唤醒了几个生产者后这些生产者从wait返回但此时队列可能又满了。pthread_cond_wait在被唤醒后还需要重新获取锁才能继续执行。因此线程继续执行需要两个条件收到唤醒信号 拿到锁。解决方式将判断条件的if改为while被唤醒后再次检查条件。4. 生产者插入void enque(const T val) { pthread_mutex_lock(_mutex); while (isfull()) { _psleep; std::cout 生产者进入休眠了: _psleep std::endl; pthread_cond_wait(_full_cond, _mutex); _psleep--; } _qu.push(val); if (_csleep) { pthread_cond_signal(_empty_cond); std::cout 唤醒消费者 std::endl; } pthread_mutex_unlock(_mutex); }当多个生产者被唤醒其中一个插入val后队列又满了。此时while循环再次触发发现满了其他被唤醒的生产者会再次阻塞。5. 消费者执行T pop() { pthread_mutex_lock(_mutex); while (isempty()) { _csleep; pthread_cond_wait(_empty_cond, _mutex); _csleep--; } T ret _qu.front(); _qu.pop(); if (_psleep) { pthread_cond_signal(_full_cond); std::cout 唤醒生产者 std::endl; } pthread_mutex_unlock(_mutex); return ret; }6. 唤醒与解锁的先后顺序先后关系都可以。但先唤醒后解锁意味着被唤醒的线程一定拿不到锁因为锁还在当前线程手中只能再次阻塞等待锁。7. 效率高的原因生产者和消费者在访问临界区时虽然是串行的但在现实中任务可能来自另一个模块会有等待。这个模型允许生产和消费并发执行当生产者等待时队列满消费者可以执行任务当消费者等待时队列空生产者可以生产。六、POSIX 信号量1. 信号量回顾类比电影票是一种资源的计数器和预定机制。之前的互斥锁可以看作二元信号量计数为 1同一时刻只有一个线程可进入。2. 多线程资源场景目标资源整体使用使用互斥锁。目标资源分块使用使用信号量。3. 环形队列头指针 (Head)消费者指针。尾指针 (Tail)生产者指针。指针同位置队列可能为空也可能为满。满需要消费者处理。空需要生产者处理。指针不同位置生产者和消费者可以同时处理。4. 数组模拟指针位置通过对数组容量取模% N实现。七、基于环形队列和信号量的生产消费模型1. 信号量封装class sem { public: sem(int sem_value 1) { sem_init(_sem, 0, sem_value); } void P() { // 申请资源wait sem_wait(_sem); } void V() { // 释放资源post sem_post(_sem); } ~sem() { sem_destroy(_sem); } private: sem_t _sem; };P操作等待资源资源数减1原子操作。V操作释放资源资源数加1原子操作。信号量将临界资源是否可用、就绪等操作变为原子的。2. 生产者消费者操作void enqueue(const T val) { _blank_sem.P(); // 申请空格子资源 LockGuard lg(_c_mutex); // 保护临界区环形队列 _rq[_p_step] val; _p_step (_p_step 1) % _cap; _full_sem.V(); // 增加数据资源 } void pop(T* val) { _full_sem.P(); // 申请数据资源 LockGuard lg(_p_mutex); *val _rq[_c_step]; _c_step (_c_step 1) % _cap; _blank_sem.V(); // 增加空格子资源 }申请信号量和加锁的先后顺序先申请信号量再申请锁效率更高。类比买票先买到票再排队如果先排队再买票可能排到了却买不到票白排了。3. 与二元信号量对照资源可以拆分如多个缓冲区单元使用计数信号量。资源不可拆分如一个变量使用互斥锁二元信号量。八、日志库设计1. 输出策略接口策略模式class logmodule { public: virtual void synclog(const std::string msg) 0; virtual ~logmodule() default; };控制台输出策略class consolelogstrategy : public logmodule { public: consolelogstrategy() {} void synclog(const std::string msg) { LockGuard lg(_mu); std::cout msg \r\n; } ~consolelogstrategy() {} private: Mutex _mu; };文件输出策略class filelogstrategy : public logmodule { public: filelogstrategy(const std::string path ./log, const std::string file log.log) : _path(path), _file(file) { LockGuard lg(_mu); if (std::filesystem::exists(path)) return; std::filesystem::create_directories(path); } ~filelogstrategy() {} void synclog(const std::string msg) { LockGuard lg(_mu); std::string filename _path (_path[_path.size() - 1] / ? : /) _file; std::ofstream out(filename, std::ios::app); out msg \r\n; out.close(); } private: Mutex _mu; std::string _path; std::string _file; };2. 日志器类管理策略class log { public: log() { enableconsolelogstrateg(); // 默认控制台输出 } void enableconsolelogstrateg() { _strategy std::make_uniqueconsolelogstrategy(); } void enablefilelogstrategy() { _strategy std::make_uniquefilelogstrategy(); } class logmsg; // 前向声明 logmsg operator()(LogLevel level, std::string file_name, int line) { return logmsg(level, file_name, line, *this); } private: std::unique_ptrlogmodule _strategy; };使用智能指针和基类可以在运行时切换输出策略。3. 日志信息类logmsg目标格式[2026-02-22 16:14:42] [DEBUG] [2955193] [main.cpp] [5] - This is a debug message.class logmsg { public: std::string LevelStr(LogLevel level) { switch (level) { case LogLevel::DEBUG: return DEBUG; case LogLevel::INFO: return INFO; case LogLevel::WARNING: return WARNING; case LogLevel::ERROR: return ERROR; case LogLevel::FATAL: return FATAL; default: return UNKNOWN; } } logmsg(LogLevel level, std::string file_name, int line, log logger) : _level(level), _file_name(file_name), _line(line), _logger(logger) { _pid getpid(); time_t now time(nullptr); struct tm curr_time; localtime_r(now, curr_time); char time_buffer[128]; snprintf(time_buffer, sizeof(time_buffer), %4d-%02d-%02d %02d:%02d:%02d, curr_time.tm_year 1900, curr_time.tm_mon 1, curr_time.tm_mday, curr_time.tm_hour, curr_time.tm_min, curr_time.tm_sec); _curr_time time_buffer; std::stringstream ss; ss [ _curr_time ] [ LevelStr(_level) ] [ _pid ] [ _file_name ] [ _line ] - ; _loginfo ss.str(); } template class T logmsg operator(const T data) { std::stringstream ss; ss data; _loginfo ss.str(); return *this; } ~logmsg() { _logger._strategy-synclog(_loginfo); } private: std::string _curr_time; LogLevel _level; pid_t _pid; std::string _file_name; int _line; std::string _loginfo; // 拼接好的日志信息 log _logger; };关键析构函数中调用synclog输出实现了自动打印。4. 使用流程定义全局或局部log对象程序结束时析构。log对象默认选择控制台输出模式可手动切换。打印日志logger(LogLevel::DEBUG, __FILE__, __LINE__) This is a debug message.;执行过程调用operator()返回一个临时的logmsg对象调用其构造函数。调用operator将消息内容追加到_loginfo。临时对象生命周期结束调用析构函数将完整的日志信息通过策略输出。5. 宏简化使用#define LOG(level) logger(level, __FILE__, __LINE__) // 使用 LOG(LogLevel::DEBUG) This is a debug message.;九、线程池1. 成员声明与构造std::vectormythread _threads; int _num; std::queueT _tasks; Mutex _mu; Cond _co; bool _isrunning; int _sleepercnt; // 休眠线程数量_threads存放线程对象。_tasks存放任务可调用对象包装器。_sleepercnt和_isrunning用于控制线程池状态。threadpool(int num 5) : _num(num), _sleepercnt(0), _isrunning(0) { for (int i 0; i num; i) { _threads.emplace_back([this]() { HandlerTask(); }); } }2. 激活线程池void start() { _isrunning 1; for (int i 0; i _num; i) { _threads[i].start(); } }3. 任务处理函数void HandlerTask() { char name[128]; pthread_getname_np(pthread_self(), name, strlen(name)); while (1) { T t; { LockGuard lg(_mu); // 线程休眠条件没有任务 且 线程池在运行 while (_tasks.empty() _isrunning 1) { _sleepercnt; std::cout _sleepercnt std::endl; _co.Wait(_mu); _sleepercnt--; } // 线程退出条件线程池停止 且 没有任务 if (_isrunning 0 _tasks.empty()) { LOG(LogLevel::DEBUG) name 退出; break; } t _tasks.front(); _tasks.pop(); } t(); // 执行任务 } }唤醒条件有任务到来。线程池被销毁广播唤醒。被唤醒后如果_isrunning 1说明有任务要执行否则说明线程池要销毁线程退出。4. 插入任务bool enque(const T val) { if (_isrunning 1) { LockGuard lg(_mu); _tasks.push(val); if (_threads.size() _sleepercnt) { // 所有线程都在休眠 _co.Signal(); LOG(LogLevel::INFO) 唤醒一个线程; return 1; } } return 0; }当插入任务且所有线程都在休眠时唤醒一个线程来处理。5. 停止线程池void stop() { _isrunning 0; _co.Broadcast(); // 唤醒所有等待的线程 LOG(LogLevel::INFO) 唤醒所有线程; }先设置停止标志然后广播唤醒所有线程让它们根据HandlerTask中的逻辑退出。十、单例模式1. 实现方式饿汉模式template typename T class Singleton { static T data; public: static T* GetInstance() { return data; } };静态T对象在程序一开始就创建。缺点可能延长程序启动时间如果不使用。懒汉模式template typename T class Singleton { static T* inst; public: static T* GetInstance() { if (inst NULL) { inst new T(); } return inst; } };只有首次调用GetInstance时才创建对象。页表的原理和它类似。2. 为线程池应用单例模式禁用拷贝构造和赋值重载threadpool(const threadpoolT tp) delete; threadpoolT operator(const threadpoolT tp) delete;静态指针和静态获取方法static threadpoolT *_tp; // 在类外初始化为 nullptr static threadpoolT *getptr() { if (_tp nullptr) { LockGuard lg(_mutex); if (_tp nullptr) { LOG(LogLevel::INFO) 获取单例; _tp new threadpoolT(); _tp-start(); } } return _tp; }双检锁保证线程安全。十一、死锁1. 可重入与线程安全可重入多个执行流同时执行不出问题同一个函数被多次调用结果正确。线程安全多个线程访问共享资源时能正确执行。关系可重入一定线程安全。线程安全不一定可重入。反例一个线程拿到锁 - 收到信号如CtrlC触发的信号处理函数- 信号处理函数调用需要同一把锁的函数。该线程持有锁信号处理函数在同一线程中执行再次申请锁导致自己锁住自己死锁。但多线程中这个反例不太容易触发因为执行信号处理函数的是接收到信号的线程主线程不是其他线程。在一般情况下可以将可重入和线程安全看作一个硬币的两面。2. 死锁的四个必要条件缺一不可互斥资源一次只能被一个执行流使用。不剥夺执行流不能抢夺其他执行流已持有的资源。请求与保持执行流在等待其他资源时不释放自己已持有的资源。循环等待存在一个执行流等待链如 A 等 BB 等 A。类比两个小孩买糖。互斥两人不能吃同一块糖。不剥夺不能抢对方的钱。请求与保持两人都手拿钱等着对方的糖。循环等待两人都不让。十二、C 标准库的线程安全STL标准模板库大多数容器不是线程安全的。因为加锁会影响性能需要用户自行加锁。智能指针一般使用不涉及线程安全问题因为通常只在当前代码作用域内使用。但库设计时考虑到了线程安全例如shared_ptr的引用计数操作是原子的。