资讯详情

资讯详情

Linux --线程同步与互斥

为什么需要线程互斥一个翻车的售票系统先看一段错误代码int ticket 100; void *route(void *arg) { char *id (char*)arg; while (1) { if (ticket 0) { usleep(1000); // 模拟漫长业务 printf(%s sells ticket:%d\n, id, ticket); ticket--; } else { break; } } return NULL; } int main() { pthread_t t1, t2, t3, t4; pthread_create(t1, NULL, route, thread 1); pthread_create(t2, NULL, route, thread 2); pthread_create(t3, NULL, route, thread 3); pthread_create(t4, NULL, route, thread 4); // join... }一次真实执行结果thread 4 sells ticket:100 thread 4 sells ticket:1 thread 2 sells ticket:0 thread 1 sells ticket:-1 thread 3 sells ticket:-2票卖成了负数这就是典型的线程安全问题。为什么会出现这个问题表面上看是if (ticket 0)判断之后、ticket--之前线程可能被切走更深层的原因是ticket--并不是一个原子操作。用objdump -d a.out反汇编后可以看到ticket--对应三条汇编指令mov 0x2004e3(%rip),%eax # load从内存读到寄存器 sub $0x1,%eax # update寄存器减 1 mov %eax,0x2004da(%rip) # store写回内存三条指令之间都可能被打断于是两个线程可能同时读到ticket 1都判断0成立都执行ticket--最终导致超卖。核心背景概念概念含义共享资源多个线程都能访问的资源临界资源被保护起来的共享资源临界区每个线程中访问临界资源的那段代码互斥任何时刻保证有且仅有一个执行流进入临界区原子性不会被任何调度机制打断的操作要么完成要么未完成要解决售票问题需要做到三点进入临界区执行时不允许其他线程进入临界区空时只允许一个线程进入不在临界区中的线程不能阻止其他线程进入。本质上就是需要一把锁。Linux 上提供的这把锁叫互斥量mutex。互斥量 mutex互斥量的接口初始化两种方式// 方式一静态分配 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 方式二动态分配 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); // attr 传 NULL 表示默认属性销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);销毁时的注意事项用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量已经销毁的互斥量要确保后面不会有线程再尝试加锁。加锁 / 解锁int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); // 成功返回 0失败返回错误号pthread_mutex_lock的两种情况互斥量未被锁加锁成功立即返回互斥量已被其他线程锁定调用线程阻塞挂起等待互斥量解锁。改进后的售票系统int ticket 100; pthread_mutex_t mutex; 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 NULL; }注意break之前也必须解锁否则其他线程永远等不到锁。互斥量的实现原理大多数体系结构都提供了swap或exchange指令作用是把寄存器和内存单元的数据相交换。由于只有一条指令保证了原子性——即便是多处理器平台访问内存的总线周期也有先后一个处理器的交换指令执行时另一个处理器的交换指令只能等待总线周期。伪代码lock: movb $0, %al xchgb %al, mutex if (al寄存器的内容 0) { return 0; } else { 挂起等待; goto lock; } unlock: movb $1, mutex 唤醒等待Mutex的线程; return 0;理解要点锁的本质就是一个内存中的整数。xchg把寄存器的 0 和内存中的值交换如果换回来的是 1说明锁原本空闲我拿到了锁如果换回来的是 0说明别人持有锁我只能挂起等待。用 RAII 封装互斥量手写 lock/unlock 容易忘记解锁尤其是抛异常或提前 return 时。我们可以用 C 的 RAII资源获取即初始化思想封装// Lock.hpp namespace LockModule { class Mutex { public: Mutex() { pthread_mutex_init(_mutex, nullptr); } void Lock() { pthread_mutex_lock(_mutex); } void Unlock() { pthread_mutex_unlock(_mutex); } pthread_mutex_t *GetMutexOriginal() { 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; }; }使用Mutex mutex; void *route(void *arg) { while (1) { LockGuard lockguard(mutex); // 构造加锁析构解锁 if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; } else { break; // 无需手动解锁 } } }C11 也提供了std::lock_guardstd::mutex原理一致。线程同步与条件变量为什么需要同步互斥解决了数据不被破坏的问题但没有解决执行顺序的问题。例子一个线程访问队列发现队列为空它只能等待另一个线程往队列里加了节点后怎么通知它这就需要条件变量condition variable。同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题。竞态条件因为时序问题导致程序异常。条件变量接口// 初始化 / 销毁 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); int pthread_cond_destroy(pthread_cond_t *cond); // 等待条件满足 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_broadcast(pthread_cond_t *cond); // 唤醒全部一个简单的例子pthread_cond_t cond PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; void *active(void *arg) { std::string name static_castconst char*(arg); while (true) { pthread_mutex_lock(mutex); pthread_cond_wait(cond, mutex); // 等待 std::cout name 活动... std::endl; pthread_mutex_unlock(mutex); } } int main() { pthread_t t1, t2; pthread_create(t1, NULL, active, (void*)thread-1); pthread_create(t2, NULL, active, (void*)thread-2); sleep(3); while (true) { // pthread_cond_signal(cond); // 唤醒一个 pthread_cond_broadcast(cond); // 唤醒全部 sleep(1); } }输出可能是thread-1 活动... thread-2 活动... thread-1 活动... thread-1 活动... thread-2 活动...broadcast会唤醒所有等待的线程让它们重新竞争锁谁抢到谁执行。为什么 pthread_cond_wait 需要互斥量这是最容易迷惑的点。看一个错误设计pthread_mutex_lock(mutex); while (condition_is_false) { pthread_mutex_unlock(mutex); // 解锁 // 就在这里解锁之后、wait 之前条件可能已经满足信号已经发出 pthread_cond_wait(cond, mutex); // 但信号被错过了 pthread_mutex_lock(mutex); } pthread_mutex_unlock(mutex);由于解锁和等待不是原子操作中间可能错过信号导致线程永远阻塞。正确做法pthread_cond_wait内部会原子地完成把调用线程放到条件变量的等待队列自动释放 mutex阻塞等待被唤醒被唤醒后重新竞争 mutex竞争成功才返回。用一句话概括pthread_cond_wait 排队 释放锁 挂起 被唤醒后重新抢锁。条件变量使用规范等待方pthread_mutex_lock(mutex); while (条件为假) // 一定是 while不是 if pthread_cond_wait(cond, mutex); 修改条件 pthread_mutex_unlock(mutex);通知方pthread_mutex_lock(mutex); 设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(mutex);为什么用 while 而不是 if因为存在伪唤醒线程可能在没有 signal 的情况下被唤醒。被唤醒后必须重新判断条件是否真的满足不满足就继续 wait。条件变量的封装// Cond.hpp namespace CondModule { using namespace LockModule; class Cond { public: Cond() { pthread_cond_init(_cond, nullptr); } ~Cond() { pthread_cond_destroy(_cond); } void Wait(Mutex mutex) { pthread_cond_wait(_cond, mutex.GetMutexOriginal()); } void Notify() { pthread_cond_signal(_cond); } void NotifyAll() { pthread_cond_broadcast(_cond); } private: pthread_cond_t _cond; }; }建议Cond 类内不要持有具体的 Mutex 对象避免耦合。使用时通过参数传入。生产者消费者模型321 原则3 种关系生产者与生产者互斥消费者与消费者互斥生产者与消费者互斥 同步2 种角色生产者、消费者1 个交易场所阻塞队列缓冲区为什么要用生产者消费者模型解耦生产者和消费者不直接通信通过缓冲区通信支持并发生产与消费可以同时进行支持忙闲不均缓冲区平衡两者处理能力的差异。基于阻塞队列BlockingQueue的实现核心思想用std::queue 互斥量 两个条件变量。template typename T class BlockQueue { private: bool IsFull() { return _block_queue.size() _cap; } bool IsEmpty() { return _block_queue.empty(); } public: BlockQueue(int cap) : _cap(cap) { _productor_wait_num 0; _consumer_wait_num 0; pthread_mutex_init(_mutex, nullptr); pthread_cond_init(_product_cond, nullptr); pthread_cond_init(_consum_cond, nullptr); } // 生产者接口 void Enqueue(T in) { pthread_mutex_lock(_mutex); while (IsFull()) { // while防伪唤醒 _productor_wait_num; pthread_cond_wait(_product_cond, _mutex); _productor_wait_num--; } _block_queue.push(in); if (_consumer_wait_num 0) pthread_cond_signal(_consum_cond); pthread_mutex_unlock(_mutex); } // 消费者接口 void Pop(T *out) { pthread_mutex_lock(_mutex); while (IsEmpty()) { _consumer_wait_num; pthread_cond_wait(_consum_cond, _mutex); _consumer_wait_num--; } *out _block_queue.front(); _block_queue.pop(); if (_productor_wait_num 0) pthread_cond_signal(_product_cond); pthread_mutex_unlock(_mutex); } ~BlockQueue() { pthread_mutex_destroy(_mutex); pthread_cond_destroy(_product_cond); pthread_cond_destroy(_consum_cond); } private: std::queueT _block_queue; int _cap; pthread_mutex_t _mutex; pthread_cond_t _product_cond; // 专门给生产者 pthread_cond_t _consum_cond; // 专门给消费者 int _productor_wait_num; int _consumer_wait_num; };关键点用两个条件变量分别管理生产者和消费者避免互相唤醒_wait_num计数用于判断是否真的需要 signal减少无效唤醒while而非if防止伪唤醒导致越界。基于环形队列 信号量的实现信号量Semaphore是另一种同步工具#include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem); int sem_wait(sem_t *sem); // P 操作值 -1为 0 时阻塞 int sem_post(sem_t *sem); // V 操作值 1pshared 0表示线程间共享非零表示进程间共享。信号量的本质是把资源的可用性抽象成一个可被原子增减的量并在资源不足时自动让线程睡去。操作语义本质动作P(S)申请一个资源value--若 0则阻塞自己进等待队列V(S)释放一个资源value若≤ 0则唤醒队列中一个线程注意这两个动作是对称的且都先改值再判断是否要阻塞/唤醒其中value 为负数的绝对值 正在等待的线程数这是理解信号量的一把钥匙。封装class Sem { public: Sem(int n) { sem_init(_sem, 0, n); } void P() { sem_wait(_sem); } void V() { sem_post(_sem); } ~Sem() { sem_destroy(_sem); } private: sem_t _sem; };环形队列template typename T class RingQueue { public: RingQueue(int cap) : _ring_queue(cap), _cap(cap), _room_sem(cap), _data_sem(0), _productor_step(0), _consumer_step(0) { pthread_mutex_init(_productor_mutex, nullptr); pthread_mutex_init(_consumer_mutex, nullptr); } void Enqueue(const T in) { _room_sem.P(); // 先申请空间 Lock(_productor_mutex); _ring_queue[_productor_step] in; _productor_step % _cap; Unlock(_productor_mutex); _data_sem.V(); // 通知消费者有数据了 } void Pop(T *out) { _data_sem.P(); // 先申请数据 Lock(_consumer_mutex); *out _ring_queue[_consumer_step]; _consumer_step % _cap; Unlock(_consumer_mutex); _room_sem.V(); // 通知生产者有空位了 } private: std::vectorT _ring_queue; int _cap; int _productor_step, _consumer_step; Sem _room_sem; // 生产者关心剩余空间 Sem _data_sem; // 消费者关心已有数据 pthread_mutex_t _productor_mutex; // 生产者之间互斥 pthread_mutex_t _consumer_mutex; // 消费者之间互斥 };环形缓冲区是一块资源但它有两种状态量从不同角度看是不同的信号量初值代表什么由谁 P等由谁 V通知_room_semcap空位数量还能放几个生产者消费者_data_sem0数据数量还能取几个消费者生产者关键点这两个量是互补的任何时刻都满足_room_sem.value _data_sem.value _cap放进去一个数据空位 -1数据 1 → 和不变取出一个数据空位 1数据 -1 → 和不变所以它们不是两块资源而是同一块资源的两个视角生产者只关心还有没有地方放→ 看_room_sem消费者只关心还有没有东西拿→ 看_data_sem对比两种实现维度BlockingQueue 条件变量RingQueue 信号量底层数据结构std::queue动态数组固定同步方式互斥量 条件变量信号量 互斥量空间判断显式判断size cap信号量计数优势灵活、容量动态高效、无锁判断计数线程池为什么需要线程池问题频繁创建/销毁线程的开销大且难以控制并发量。方案预先创建一批线程循环从任务队列取任务执行。应用场景大量短小任务如 Web 服务器处理网页请求对响应性能要求高的场景突发大量请求避免短时间内线程爆炸。种类固定线程数线程池浮动线程池日志与策略模式在实现线程池前先准备一个好用的日志模块。日志必须包含时间戳必须日志等级必须日志内容必须文件名、行号可选进程/线程 ID可选期望格式[2024-08-04 12:27:03][DEBUG][202938][main.cc][16] - hello world策略模式让日志的刷新方式可切换——输出到控制台还是文件class LogStrategy { public: virtual ~LogStrategy() default; virtual void SyncLog(const std::string message) 0; }; class ConsoleLogStrategy : public LogStrategy { /* 输出到 cerr */ }; class FileLogStrategy : public LogStrategy { /* 追加到文件 */ };Logger 类 内部 LogMessage 类RAIIclass Logger { public: void UseConsoleStrategy() { _strategy std::make_uniqueConsoleLogStrategy(); } void UseFileStrategy() { _strategy std::make_uniqueFileLogStrategy(); } class LogMessage { public: LogMessage(LogLevel type, std::string filename, int line, Logger logger) : _type(type), _curr_time(GetCurrTime()), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { std::stringstream ss; ss [ _curr_time ][ LogLevelToString(type) ] [ _pid ][ _filename ][ _line ] - ; _loginfo ss.str(); } template typename T LogMessage operator(const T info) { std::stringstream ss; ss info; _loginfo ss.str(); return *this; } ~LogMessage() { if (_logger._strategy) _logger._strategy-SyncLog(_loginfo); } private: LogLevel _type; std::string _curr_time; pid_t _pid; std::string _filename; int _line; Logger _logger; std::string _loginfo; }; LogMessage operator()(LogLevel type, std::string filename, int line) { return LogMessage(type, filename, line, *this); } private: std::unique_ptrLogStrategy _strategy; }; Logger logger; #define LOG(type) logger(type, __FILE__, __LINE__) #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy() #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()LOG(DEBUG) ...中的LOG(DEBUG)会构造一个临时LogMessage对象后续的都是往这个对象里追加内容语句结束时临时对象析构触发日志刷新__FILE__/__LINE__宏自动填入文件名和行号省去手写策略模式让切换输出位置只需一行代码。线程池实现template typename T class ThreadPool { private: ThreadPool(int threadnum gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false) {} void InitThreadPool() { for (int i 0; i _threadnum; i) _threads.emplace_back(std::bind(ThreadPool::HandlerTask, this)); } void Start() { _isrunning true; for (auto t : _threads) t.Start(); } void HandlerTask() { std::string name GetThreadNameFromNptl(); while (true) { _mutex.Lock(); while (_task_queue.empty() _isrunning) { _waitnum; _cond.Wait(_mutex); _waitnum--; } if (_task_queue.empty() !_isrunning) { _mutex.Unlock(); break; } T t _task_queue.front(); _task_queue.pop(); _mutex.Unlock(); t(); // 执行任务 } } public: static ThreadPoolT *GetInstance() { // 双重判定 锁保证线程安全的单例 if (nullptr _instance) { LockGuard lockguard(_lock); if (nullptr _instance) { _instance new ThreadPoolT(); _instance-InitThreadPool(); _instance-Start(); } } return _instance; } bool Enqueue(const T t) { bool ret false; _mutex.Lock(); if (_isrunning) { _task_queue.push(t); if (_waitnum 0) _cond.Notify(); ret true; } _mutex.Unlock(); return ret; } void Stop() { _mutex.Lock(); _isrunning false; _cond.NotifyAll(); _mutex.Unlock(); } void Wait() { for (auto t : _threads) t.Join(); } private: int _threadnum; std::vectorThread _threads; std::queueT _task_queue; Mutex _mutex; Cond _cond; int _waitnum; bool _isrunning; static ThreadPoolT *_instance; static Mutex _lock; }; template typename T ThreadPoolT *ThreadPoolT::_instance nullptr; template typename T Mutex ThreadPoolT::_lock;任务类型using task_t std::functionvoid();使用void DownLoad() { std::cout this is a task std::endl; } int main() { ENABLE_CONSOLE_LOG_STRATEGY(); int cnt 10; while (cnt--) { ThreadPooltask_t::GetInstance()-Enqueue(DownLoad); sleep(1); } ThreadPooltask_t::GetInstance()-Stop(); sleep(5); ThreadPooltask_t::GetInstance()-Wait(); }线程安全的单例模式什么是单例模式某些类只应该具有一个对象实例。比如服务器加载几百 GB 数据到内存需要一个单例类来管理。饿汉 vs 懒汉饿汉吃完饭立刻洗碗下一顿直接吃。程序启动就创建实例。懒汉吃完饭先放碗下一顿用时再洗。延时加载优化启动速度。饿汉实现template typename T class Singleton { static T data; public: static T* GetInstance() { return data; } };只要通过 Singleton 使用 T 对象进程中就只有一个 T 实例。懒汉实现线程不安全版template typename T class Singleton { static T* inst; public: static T* GetInstance() { if (inst NULL) inst new T(); // 线程不安全 return inst; } };两个线程同时判断inst NULL为真可能创建两份实例。懒汉实现线程安全版template typename T class Singleton { volatile static T* inst; static std::mutex lock; public: static T* GetInstance() { if (inst NULL) { // 第一次判断减少锁竞争 lock.lock(); if (inst NULL) { // 第二次判断保证只创建一次 inst new T(); } lock.unlock(); } return inst; } };三个要点双重 if 判定第一次避免每次都加锁第二次保证只创建一次加锁位置只包住new那一段volatile 关键字防止编译器过度优化。线程池设计成单例模式的目的把线程池设计成单例模式核心目的可以归结为一句话保证整个进程中只有一个线程池实例从而统一管理全局的线程资源和任务调度。线程安全与可重入概念线程安全多个线程访问共享资源时能够正确执行互不干扰。可重入同一个函数被不同执行流调用前一个还没执行完结果不会有问题。重入的两种情形多线程重入函数信号处理导致一个执行流重复进入函数。常见情况分类类别常见情况线程不安全不保护共享变量的函数状态随调用变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数线程安全每个线程对全局/静态变量只有读权限类或接口是原子操作多线程切换不导致二义性不可重入调用 malloc/free全局链表管理堆调用标准 I/O 库很多实现使用全局数据结构函数体内使用了静态数据结构可重入不使用全局/静态变量不使用 malloc/new不调用不可重入函数数据由调用者提供使用本地数据或全局数据的拷贝联系与区别联系函数可重入 → 是线程安全的函数不可重入 → 不能由多个线程使用可能引发线程安全问题函数中有全局变量 → 既不是线程安全也不是可重入的。区别可重入函数是线程安全函数的一种线程安全不一定可重入可重入一定线程安全加锁保护后的函数是线程安全的但若锁未释放不可重入会死锁。常见锁概念死锁定义一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所占用、不会释放的资源而处于永久等待状态。经典场景线程 A 持有锁 1想申请锁 2线程 B 持有锁 2想申请锁 1。两者互不相让永久阻塞。注意申请一把锁是原子的申请两把锁就不一定了。死锁的四个必要条件互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件已获得的资源在未使用完之前不能强行剥夺循环等待条件若干执行流之间形成头尾相接的循环等待资源关系。四个条件同时满足才会死锁破坏任意一个即可避免。避免死锁的方法破坏循环等待加锁顺序一致所有线程都先申请锁 1再申请锁 2资源一次性分配用std::lock(lock1, lock2)一次申请两把锁不成功就不占用任何锁使用超时机制申请锁时设置超时超时释放已持有锁并重试避免锁未释放的场景用 RAII 风格管理锁。示例代码Cstd::unique_lockstd::mutex lock1(mtx1, std::defer_lock); std::unique_lockstd::mutex lock2(mtx2, std::defer_lock); std::lock(lock1, lock2); // 一次锁定两个避免死锁STL、智能指针与线程安全STL 容器是线程安全的吗不是。原因STL 设计初衷是性能极致加锁会严重影响性能不同容器加锁方式不同如哈希表锁表 vs 锁桶性能差异大因此 STL 默认线程不安全多线程环境下需调用者自行保证。智能指针是线程安全的吗unique_ptr只在当前代码块范围内生效不涉及线程安全shared_ptr引用计数是线程安全的标准库用原子操作 CAS 保证但指向的对象本身不是线程安全的仍需调用者保护。附加部分回调函数怎么理解——从线程封装中的Routine说起static void *Routine(void *args) // 属于类内的成员函数 { Thread *self static_castThread *(args); // 还原 this self-EnableRunning(); // 标记运行状态 if (self-_isdetach) self-Detach(); // 是否需要分离 pthread_setname_np(self-_tid, self-_name.c_str()); // 设置线程名 self-_func(); // 执行用户传入的任务 ← 回调 return nullptr; }回调函数Callback Function通俗讲就是你不直接调用这个函数而是把它交给别人让别人在合适的时机替你调用。线程封装里的回调结构用户视角用户写代码时是这样的void DownLoad() { std::cout this is a task std::endl; } int main() { Thread t(DownLoad); // 把 DownLoad 交给线程 t.Start(); t.Join(); }用户没有直接调用DownLoad()只是把它交给了 Thread 类。真正执行DownLoad()的时机是线程启动之后。Thread 类视角Thread类的构造class Thread { public: using func_t std::functionvoid(); // 回调函数类型 Thread(func_t func) : _func(func), _isdetach(false) {} void Start() { pthread_create(_tid, nullptr, Routine, this); // ↑ // 把 this 传给 Routine而不是直接传 _func } private: func_t _func; // ← 用户传入的回调就存在这里 pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; };关键点用户传进来的DownLoad存在_func里pthread_create的第三个参数必须是void*(*)(void*)类型的静态函数但静态函数没有 this 指针怎么访问成员_func呢答案就是把this作为参数传进去。为什么Routine必须是static这是理解回调的关键难点。成员函数的特殊性普通成员函数非静态有一个隐藏参数thisvoid Thread::Start(); // 编译器眼中实际是 void Start(Thread* this);pthread_create要求传的函数指针类型是void *(*start_routine)(void *);它不接受this参数所以pthread_create(_tid, nullptr, Start, this); // 编译错误 // 因为 Start 实际类型是 void (Thread::*)(void*)不是 void*(*)(void*)static 去掉 this把Routine声明为static它就没有this指针类型变成普通的void *(*)(void *); // 符合 pthread_create 要求但代价是静态函数无法直接访问非静态成员如_func、_name。用参数把 this 传进来既然Routine没有this那就手动把this传进去pthread_create(_tid, nullptr, Routine, this); // ↑ // pthread_create 的第四个参数 // 会作为 Routine 的 args 传入在Routine里再还原回来Thread *self static_castThread *(args); // args 就是 this于是就能访问self-_func、self-_name等成员了。回调的调用链完整走一遍以Thread t(DownLoad); t.Start();为例① main 里构造 Thread 对象 t把 DownLoad 存进 t._func ↓ ② main 里调用 t.Start() ↓ ③ Start() 里pthread_create(_tid, nullptr, Routine, this); 系统创建新线程新线程入口是 Routine ↓ ④ 新线程里Routine(this) 被系统调用 ↓ ⑤ Routine 里self static_castThread*(this) // 还原 ↓ ⑥ Routine 里self-_func(); ← 回调发生在这里 ↓ ⑦ 用户写的 DownLoad() 真正执行关键观察用户从没写过DownLoad()这行调用但DownLoad却执行了执行它的位置是新线程中触发点是self-_func();这一句这就是把函数交给框架框架在合适时机替你调用——回调。线程池里也用到了回调线程池中_threads.emplace_back(std::bind(ThreadPool::HandlerTask, this));这里的std::bind(ThreadPool::HandlerTask, this)就是一个回调把成员函数HandlerTask和this绑定成一个无参可调用对象传给Thread存进_func新线程启动后Routine调用_func()实际执行this-HandlerTask()。链路ThreadPool::ThreadPool → Thread(std::bind(HandlerTask, this)) → Thread::_func 绑定后的可调用对象 → pthread_create(..., Routine, this) → Routine: self-_func() ← 回调 → this-HandlerTask() 真正执行同一套回调机制被复用到了更高层。总结本文从售票 bug出发层层递进地梳理了多线程编程的核心知识线程互斥用 mutex 保护临界区理解ticket--非原子的本质线程同步用条件变量 while 判断实现线程间的顺序协作生产者消费者模型用阻塞队列/环形队列 信号量解耦生产与消费线程池用日志模块 单例 任务队列实现高效的线程复用线程安全与可重入理解两者的联系与区别死锁掌握四个必要条件和避免方法STL 与智能指针明确标准库的线程安全边界。核心心法互斥是同一时刻只允许一个同步是按顺序协作RAII是优雅管理资源双重判定 锁是线程安全单例321 原则是生产者消费者模型。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →