ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux之线程进阶(一)

Linux之线程进阶(一) 同进程内多个线程共享地址空间、共享进程资源↓多个线程同时访问同一份共享资源↓典型现象多线程同时打印到显示器stdoutfd1输出乱序、文字穿插干扰↓本质问题并发访问共享资源带来的数据不一致问题一个现象#include iostream #include Thread.hpp #include unistd.h #include string #include vector using namespace std; int tickets 1000; void GetTicket() { char name[64]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (1) { if (tickets 0) { usleep(1000); printf(%s sells ticket:%d\n, name, tickets); tickets--; } else { break; } } } int main() { ThreadModul::Thread t1(GetTicket); ThreadModul::Thread t2(GetTicket); ThreadModul::Thread t3(GetTicket); ThreadModul::Thread t4(GetTicket); t1.Start(); t2.Start(); t3.Start(); t4.Start(); sleep(10); t1.Join(); t2.Join(); t3.Join(); t4.Join(); return 0; }初始tickets 1线程 2if(tickets0)→ 10条件成立进入代码块usleep休眠、让出 CPU线程 1if(tickets0)→ 10条件成立进入代码块usleep休眠重点两个线程都已经通过了if(tickets0)的校验此时票还没减调度唤醒线程 2打印 ticket:1 →tickets--→ tickets0调度唤醒线程 1打印 ticket:0 →tickets--→ tickets-1于是就出现票已经 0 了还能继续卖出出现负数你截图里-1-2就是多轮这种叠加导致的同步VIP 自习室 临界资源同一时间只允许 1 个人用人 线程自习的过程 临界区钥匙 互斥锁pthread_mutex规则错了吗没错问题裸竞争不合理效率太低管理员规则归还钥匙之后不能立即申请如果要再用就必须重新排队外部的人必须排队✅线程同步定义在临界资源安全的前提下让访问临界资源具有一定的顺序性区分互斥保证同一时刻只有一个线程进入临界区解决安全问题最基础同步在互斥基础上控制线程访问的先后顺序解决顺序问题生产者消费者模型这是多线程协同的一种模式,提高协作效率,本质是一种通信工具3 种关系生产者和生产者消费者和消费者生产者和消费者生产者 ↔ 生产者互斥消费者 ↔ 消费者互斥生产者 ↔ 消费者同步 互斥2 种角色生产者线程、消费者线程1 一个交易场所超市缓冲区 / 内存空间一般用队列、环形队列这类数据结构实现生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间 不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔 给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区 平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。✨生产者消费者模型的优势解耦生产者、消费者代码逻辑分离互不依赖易于维护扩展并发生产、消费线程并行执行提升系统吞吐量削峰缓冲瞬时流量先存入缓冲区保护下游消费者不被突发流量打垮速率适配自动协调快慢生产快就缓存、消费快就等待减少无效 CPU 占用理解条件变量条件变量就是让线程「等某个条件满足再干活」不满足就休眠不瞎循环空耗 CPU必须搭配互斥锁一起使用互斥锁管能不能进临界区互斥 条件变量管什么时候可以干活等待 唤醒同步例子宿舍饮水机互斥锁 饮水机钥匙同一时间只能 1 个人接水条件变量 “有水吗” 这个信号你来接水拿到钥匙发现没水→ 不一直反复拧钥匙空等直接休眠释放钥匙pthread_cond_wait自动解锁 休眠送水工另一个线程换水完成发信号唤醒等待的人pthread_cond_signal你被唤醒后自动重新拿到锁再检查有没有水有水再接水代码void* Print(void* args) { string namestatic_castconst char*(args); while(1) { cout我是新线程:nameendl; } return nullptr; } int main() { pthread_t tids[4]; for(int i0;i4;i) { char *namenew char[64]; snprintf(name,64,thread-%d,i1); pthread_create(tidsi,nullptr,Print,name); } for(int i0;i4;i) { void *retval; pthread_join(tids[i],retval); } return 0; }上锁阻塞等待pthread_mutex_t gmutexPTHREAD_MUTEX_INITIALIZER; pthread_cond_t gcondPTHREAD_COND_INITIALIZER; void* Print(void* args) { string namestatic_castconst char*(args); while(1) { pthread_mutex_lock(gmutex); // 1.抢锁 cout我是新线程:nameendl; // 2.打印 pthread_cond_wait(gcond,gmutex); // 3.【重点】释放锁 阻塞休眠等唤醒信号 pthread_mutex_unlock(gmutex); // 4.被唤醒后抢到锁才走到这里解锁 } return nullptr; }唤醒#include iostream #include unistd.h #include string #include vector using namespace std; pthread_mutex_t gmutexPTHREAD_MUTEX_INITIALIZER; pthread_cond_t gcondPTHREAD_COND_INITIALIZER; void* Print(void* args) { string namestatic_castconst char*(args); while(1) { pthread_mutex_lock(gmutex); cout我是新线程:nameendl; pthread_cond_wait(gcond,gmutex); pthread_mutex_unlock(gmutex); sleep(1); } return nullptr; } int main() { pthread_t tids[4]; for(int i0;i4;i) { char *namenew char[64]; snprintf(name,64,thread-%d,i1); pthread_create(tidsi,nullptr,Print,name); } while(1) pthread_cond_signal(gcond); for(int i0;i4;i) { void *retval; pthread_join(tids[i],retval); } return 0; }阻塞整体结构说明BlockQueueT模板类阻塞队列内部封装pthread_mutex_tpthread_cond_tProdurorRoutine生产者线程入口函数不断往阻塞队列放数据ConsumerRoutine消费者线程入口函数不断从阻塞队列取数据空队列时阻塞等待main创建阻塞队列、生产者 / 消费者子线程pthread_join等待回收ProdurorRoutineConsumerRoutine接下来我们来实现封装构造和析构Pop线程为什么会在临界区内部等待疑惑临界区是要快速执行、尽快解锁怎么还在里面阻塞等待 核心一句话判断临界资源是否就绪这件事本身就属于临界区操作我们要判断阻塞队列_bq是否为空队列是多线程共享的临界资源读它也要受互斥锁保护所以这个判断逻辑必须放在pthread_mutex_lock之后、持有锁的临界区内。 如果我们先解锁、再判断队列状态就会出现时序竞态生产者修改队列的同时消费者在读队列造成数据错乱、信号丢失。顺序不能乱加锁 → 判断资源状态 → 资源不就绪就 wait 等待为什么pthread_cond_wait一定要传入互斥锁pthread_cond_wait不是单纯休眠它和传入的 mutex 绑定内部自动完成一套原子操作分为 3 步原子释放我们已经持有的_mutex互斥锁释放锁的目的让生产者线程有机会拿到锁、往队列里生产数据之后再发信号唤醒当前消费者线程阻塞休眠挂在条件变量_consumer_cond的等待队列上等待 signal/broadcast 唤醒收到唤醒信号、函数返回之前自动去竞争、重新获取这把_mutex锁Enqueue和pop类似,但是面对的是productor问题但是当队列满了,如何唤醒consumer队列快要空了,如何唤醒productor目前我们先让这个生产一个,消费一个生产者较快消费者更快细节问题伪唤醒可能会有过量唤醒信息,函数调用失败等等POSIX 标准允许没有任何线程调用 signalcond_wait 也可能莫名唤醒。如果用if(_bq.empty())只判断一次。一旦伪唤醒不会二次检查直接执行 pop空队列取数据程序异常如果用while(_bq.empty())唤醒后循环复检条件队列依旧为空就继续 wait保证安全规范写法永远 while带高低水位线的有界阻塞队列void Enqueue(T in) { pthread_mutex_lock(_mutex); while (_bq.size() _cap) { pthread_cond_wait(_productor_cond, _mutex); } _bq.push(in); if (_bq.size() _highline) { pthread_cond_signal(_consumer_cond); } pthread_mutex_unlock(_mutex); } void Pop(T *out) { pthread_mutex_lock(_mutex); while (_bq.empty()) { pthread_cond_wait(_consumer_cond, _mutex); } *out _bq.front(); _bq.pop(); if (_bq.size() _lowline) { pthread_cond_signal(_productor_cond); } pthread_mutex_unlock(_mutex); }统计休眠线程数量的唤醒策略void Enqueue(T in) { pthread_mutex_lock(_mutex); while (_bq.size() _cap) { _sleep_pro_num; pthread_cond_wait(_productor_cond, _mutex); _sleep_pro_num--; } _bq.push(in); // if (_bq.size() _highline) // { // pthread_cond_signal(_consumer_cond); // } if (_sleep_con_num 0) { pthread_cond_signal(_consumer_cond); } pthread_mutex_unlock(_mutex); } void Pop(T *out)多线程多任务以上都是单线程单任务,那我们能不能实现多线程多任务???#include iostream #include unistd.h #include string #include vector using namespace std; #include BlockQueue.hpp int num 0; pthread_mutex_t mutex; int GetNumber() { pthread_mutex_lock(mutex); num; pthread_mutex_unlock(mutex); return num; } void *ConsumerRoutine(void *args) { int number GetNumber(); BlockQueueint *bp static_castBlockQueueint *(args); while (1) { int data; bp-Pop(data); cout con-- number ; cout 我是消费者,消费了-- data endl; } } void *ProdutorRoutine(void *args) { BlockQueueint *bp static_castBlockQueueint *(args); int data 10; int number GetNumber(); while (1) { sleep(1); bp-Enqueue(data); cout pro-- number ; cout 我是生产者,生产了-- data endl; } } int main() { BlockQueueint *bp new BlockQueueint(); pthread_t c[3], p[2]; pthread_create(c, nullptr, ConsumerRoutine, bp); pthread_create(c 1, nullptr, ConsumerRoutine, bp); pthread_create(c 2, nullptr, ConsumerRoutine, bp); pthread_create(p, nullptr, ProdutorRoutine, bp); pthread_create(p 1, nullptr, ProdutorRoutine, bp); pthread_join(c[0], nullptr); pthread_join(c[1], nullptr); pthread_join(c[2], nullptr); pthread_join(p[0], nullptr); pthread_join(p[1], nullptr); return 0; }
返回列表