深入理解Linux多线程
深入理解Linux多线程
目录
- Linux线程概念
- 什么是线程
- 二级页表
- 线程的优点
- 线程的缺点
- 线程异常
- 线程用途
- Linux进程VS线程
- Linux线程控制
- POSIX线程库
- 创建线程
- 线程等待
- 线程终止与分离
- 线程ID的本质
- Linux线程互斥
- 进程线程间的互斥相关背景概念
- 互斥量mutex
- 互斥量实现原理探究
- 可重入VS线程安全
- 死锁
- 死锁四个必要条件
- 避免死锁
- Linux线程同步
- 条件变量
- 生产者消费者模型
- 基于BlockingQueue的生产者消费者模型
- POSIX信号量
- 基于环形队列的生产消费模型
Linux线程概念
什么是线程
1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
2、一切进程至少都有一个执行线程
3、线程在进程内部运行,本质是在进程地址空间内运行
4、在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
5、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
6、Linux下,并不存在真正的多线程,而是用进程模拟的,但windows中存在真正的多线程。如果Linux下支持真正的多线程,当线程足够多的时候,OS要管理线程,如果支持真的线程,OS要创建线程,终止线程,调度线程,切换线程,给线程分配资源,释放资源,回收资源,所有的这一套相比较进程都会另起炉灶,再搭一套在进程内部,或与进程平行的另一条线程管理模块,这样的话一定会提高设计OS的复杂程度,所以Linux中线程的 设计直接复用了进程的数据结构,所以Linux下并不存在真正的多线程,是用进程模拟的。
二级页表
二级页表:32个比特位,前10个比特位查页目录对应的二级页表,中10个比特位查页表中的某一位置确定要访问的是哪一页,中间10个比特位能够让我们确定映射到物理内存是哪一个页框,再拿最后12个比特位,能够确定某个起始页内的偏移地址,可以找到具体的哪一个字节。
线程的优点
1、创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3、线程占用的资源要比进程少很多
4、能充分利用多处理器的可并行数量
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(计算密集型:执行流的大部分任务,主要以计算为主,例:加密解密,排序查找)
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(执行流执行的大部分任务,是以IO为主的,刷磁盘、访问数据库、访问网络)
线程的缺点
1、性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程异常
1、单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
1、合理的使用多线程,能提高CPU密集型程序的执行效率
2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
Linux进程VS线程
一、进程是资源分配的基本单位
二、线程是调度的基本单位
三、线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器(线程是调度的基本单位,只要调度就要对各自的数据进行上下文保存)
栈(线程运行时都会产生自己的临时数据,需要将数据进行压栈)
errno
信号屏蔽字
调度优先级
四、进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
进程和线程的关系如下图:
Linux线程控制
POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库,要通过引入头文<pthread.h>链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
创建线程
#include<stdio.h> 2 #include<pthread.h>3 #include<unistd.h>4 #include<sys/types.h>5 #include<stdlib.h>6 7 void *Routinue(void *arg)8 {9 char *msg=(char*)arg;10 while(1)11 {12 printf("%s: pid: %d,ppid: %d\n",msg,getpid(),getppid());13 sleep(1);14 }15 }16 int main()17 {18 pthread_t tid;19 20 // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑21 // "thread 1",给回调函数指定的参数22 pthread_create(&tid,NULL,Routinue,(void*)"thread 1"); //创建一个线程23 while(1)24 {25 printf("main thread: pid: %d,ppid: %d\n",getpid(),getppid());26 sleep(2);27 }28 return 0;29 } 30
创建多线程及获取线程自身id
1 #include<stdio.h> 2 #include<pthread.h>3 #include<unistd.h>4 #include<sys/types.h>5 #include<stdlib.h>6 7 void *Routinue(void *arg)8 {9 char *msg=(char*)arg;10 while(1)11 {12 //pthread_self(),获取线程自身id13 printf("%s: pid: %d,ppid: %d,tid: %lu\n",msg,getpid(),getppid(),pthread_self());14 sleep(1);15 }16 }17 int main()18 {19 pthread_t tid[5]; //创建多个线程20 for(int i=0;i<5;i++)21 {22 char buffer[64];23 sprintf(buffer,"thread %d\n",i);24 // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑25 // "thread 1",给回调函数指定的参数26 pthread_create(&tid[i],NULL,Routinue,(void*)buffer); //创建线程 27 printf("%s tid is: %lu\n",buffer,tid[i]);28 }29 while(1)30 {31 printf("main thread: pid: %d,ppid: %d, tid: %lu\n",getpid(),getppid(),pthread_self());32 sleep(2);33 }34 return 0;35 }
线程等待
1 #include<stdio.h> 2 #include<pthread.h>3 #include<unistd.h>4 #include<sys/types.h>5 #include<stdlib.h>6 7 void *Routinue(void *arg)8 {9 char *msg=(char*)arg;10 int count=0;11 while(count<5)12 {13 //pthread_self(),获取线程自身id14 printf("%s: pid: %d,ppid: %d,tid: %lu\n",msg,getpid(),getppid(),pthread_self());15 sleep(1);16 count++;17 }18 return (void*)10;19 }20 int main()21 {22 pthread_t tid[5]; //创建多个线程23 for(int i=0;i<5;i++)24 {E> 25 char* buffer=(void*)malloc(64);26 sprintf(buffer,"thread %d\n",i);27 // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑28 // "thread 1",给回调函数指定的参数29 pthread_create(&tid[i],NULL,Routinue,(void*)buffer); //创建线程30 printf("%s tid is: %lu\n",buffer,tid[i]);31 }32 printf("main thread: pid: %d,ppid: %d, tid: %lu\n",getpid(),getppid(),pthread_self());33 for(int i=0;i<5;i++)34 {35 void* ret=NULL;36 // pthread_join 线程等待37 pthread_join(tid[i],&ret); //&ret是退出码
E> 38 printf("thread %d[%lu] ...quit!,code: %d\n",i,tid[i],(int)ret); 39 }40 41 return 0;42 }
线程终止与分离
线程ID的本质
pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
所谓的线程id(pthread_t)本质是一个地址,本质是动态库的地址Linux不提供真正的线程,只提供LWP,意味着OS只需要对LWP内核执行流进行管理,那么,供用户使用的接口等其它数据应该由谁来管理呢?-》pthread线程库来管理,在库里面,先描述,在组织。CPU在进行调度的时候,要上下文保存的时候,执行的代码,并不会跑到内核代码完成切换,而是到动态库里进行线程切换,线程切换是把当前线程在CPU的所有临时数据保存到线程的局部存储区,把形成的临时变量保存在栈里面,线程控制块被切走,切到下一个线程。此时,把下一个线程的数据恢复到CPU上。
Linux线程互斥
进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
对原子性理解:在汇编层面上,只有一行代码,可以称之为原子性,那么对一个全局变量进行++,或- -是原子的吗? -》不是
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
// 操作共享变量会有问题的售票系统代码
1 #include<stdio.h> 2 #include<pthread.h>3 #include<unistd.h>4 #include<sys/types.h>5 #include<stdlib.h>int tickets=2000;55 56 void *TicketGrabbing(void *arg)57 {58 const char *name=(char*)arg;59 while(1)60 {61 //票大于0,才可以抢62 if(tickets>0)63 {64 usleep(1000); //usleep()微秒级别 65 printf("[%s] get a ticket : %d\n",name,tickets--); // 抢到票打印出来66 }67 else 68 {69 break; 70 }71 }72 printf("%s quit!\n",name);73 pthread_exit((void*)0);74 }int main()76 {77 pthread_t t1,t2,t3,t4;78 // &t1:线程id;NULL:为默认属性;TicketGrabbing,回调函数,即想让线程执行什么逻辑79 // "thread 1",给回调函数指定的参i数80 pthread_create(&t1,NULL,TicketGrabbing, "thread 1"); 81 pthread_create(&t2,NULL,TicketGrabbing, "thread 2");82 pthread_create(&t3,NULL,TicketGrabbing, "thread 3");83 pthread_create(&t4,NULL,TicketGrabbing, "thread 4");84 85 pthread_join(t1,NULL);86 87 pthread_join(t2,NULL); 88 pthread_join(t3,NULL);89 pthread_join(t4,NULL);90 }
为什么还会有负数呢?票还有负的,因为tickets–,不是原子性的,可能当票还有1张的时候,有几个线程同时抢票,导致减了多次,成为负数
要解决以上问题,需要做到三点:
1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
加入互斥量改进抢票系统
1#include<stdio.h> 2 #include<pthread.h>3 #include<unistd.h>4 #include<stdlib.h>5 #define NUM 20006 7 int tickets=NUM;8 //定义互斥锁9 pthread_mutex_t lock;10 11 void *GetTicket(void *arg)12 {13 int number=(int)arg;14 while(1)15 {16 pthread_mutex_lock(&lock); //加锁17 if(tickets>0)18 {19 //****** 保护起来的临界区 ***********20 usleep(100);21 printf("thread [ %d ] 抢票: %d\n",number,tickets--);22 23 pthread_mutex_unlock(&lock); //解锁24 }25 else 26 {27 pthread_mutex_unlock(&lock); //解锁28 break;29 }30 }31 32 }33 34 int main()35 {36 pthread_t thds[5];37 pthread_mutex_init(&lock,NULL);//锁初始化38 39 for(int i=0;i<5;i++)40 { 41 pthread_create(&thds[i], NULL, GetTicket, (void*)i);42 }43 44 for(int i=0;i<5;i++)24 }25 else 26 {27 pthread_mutex_unlock(&lock); //解锁28 break;29 }30 }31 32 }33 34 int main()35 {36 pthread_t thds[5];37 pthread_mutex_init(&lock,NULL);//锁初始化38 39 for(int i=0;i<5;i++)40 { 41 pthread_create(&thds[i], NULL, GetTicket, (void*)i);42 }43 44 for(int i=0;i<5;i++)45 {46 pthread_join(thds[i],NULL);47 }48 49 pthread_mutex_destroy(&lock); //释放锁50 return 0;51 }
互斥量实现原理探究
可重入VS线程安全
概念
1、线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
2、重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况
不使用全局变量或静态变量
不使用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
如和避免死锁呢?破坏死锁四个必要条件中的一个就可以
一般不会破环互斥条件:因为互斥锁就是用来保护临界资源的
破坏循环等待条件:加锁顺序一致,申请锁要按顺序来,先申请A再申请B
破坏请求与保持条件:我可以要求你的,但我也可以把我的给你,避免锁未释放的场景
资源一次性分配
Linux线程同步
条件变量
条件变量:用来描述某种临界资源是否就绪的一种数据化描述,条件变量通常需要配合mutex互斥锁一起使用。
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
同步概念
同步:在特定的情况下,保证彼此之间能够按照特定的顺序进行资源访问,这就叫做同步,同步的本质就是让多线程协同起来,然后能够有效的避免竞争力比较弱的进程,因为长时间得不到锁资源,而引起的饥饿问题,就叫同步
互斥:互斥一定不会出错,但是可能导致竞争力比较弱的进程,因为长时间得不到锁资源,而引起的饥饿问题
例,用主线程控制其它线程
Makefile文件
1 demo:demo.cc2 g++ -o $@ $^ -std=c++11 -lpthread 3 .PHONY:clean4 clean:5 rm -f demo
~
main.cc文件
1 #include<iostream> 2 #include<pthread.h>3 #include<cstdio>4 pthread_mutex_t lock;5 pthread_cond_t cond;6 7 void* Run(void* arg)8 {9 pthread_detach(pthread_self()); //将当前线程先分离10 std::cout<<(char*)arg<<" run..."<<std::endl;11 while(true)12 {13 pthread_cond_wait(&cond,&lock); //线程阻塞在这,等条件就绪14 std::cout<<"thread: "<<pthread_self()<<" 活动..."<<std::endl;15 }16 }17 int main()18 {19 pthread_mutex_init(&lock,nullptr);20 pthread_cond_init(&cond,nullptr); //初始化21 22 pthread_t t1,t2,t3; // ctrl是控制线程,用来控制其它3个线程23 pthread_create(&t1,nullptr,Run,(void*)"thread 1");pthread_create(&t2,nullptr,Run,(void*)"thread 2");25 pthread_create(&t3,nullptr,Run,(void*)"thread 3");26 27 //********* 主线程控制其它线程**************28 while(true)29 {30 31 getchar();32 //pthread_cond_signal(&cond); //唤醒第一个线程33 pthread_cond_broadcast(&cond); //唤醒全部线程34 }35 36 37 pthread_mutex_destroy(&lock); //释放锁38 pthread_cond_destroy(&cond); //释放条件变量39 40 return 0;41 }
生产者消费者模型
生产以及消费对应的需要完成的功能主要是:
一:互斥功能
二:条件不满足时,让当前执行流具有等待的功能
三:当我的条件满足时,要唤醒对方等待的线程,此时就需要有两种Linux提供的同步互斥机制,分别叫做互斥锁以及条件变量。
生产者消费者模型321原则:
3:3种关系,消费者与消费者(竞争关系/互斥关系),生产者与生产者(竞争关系/互斥关系),
生产者与消费者(互斥关系和同步关系)
2:2种角色,生产者与消费者(特定的线程或进程)
1:1个交易 场所(通常是内存中的一段缓存区,自己通过某种方式组织起来)
为什么要有生产者消费者模型:
实质是代码进行解耦的过程,生产者只负责生产数据,消费者只负责消费数据,支持并发,支持忙闲不均
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
例,生产者生产任务,消费者消费任务
BlockQueue.hpp文件
1 #pragma once 2 #include<iostream>3 #include<queue>4 #include<pthread.h>5 #include<cstdlib>6 #include<ctime>7 #include<unistd.h>8 9 #define NUM 510 template<typename T>11 12 class BlockQueue13 {14 private:15 bool IsFull()16 {17 return q.size()==cap;18 }19 20 bool IsEmpty()21 {22 return q.empty();23 }
24 public:25 BlockQueue(int _cap=NUM)26 :cap(_cap)27 {28 pthread_mutex_init(&lock,nullptr);29 pthread_cond_init(&full,nullptr); //条件变量初始化30 pthread_cond_init(&empty,nullptr);31 }32 33 void Push(const T& in) //供生产者调用34 {35 pthread_mutex_lock(&lock); //加锁36 while(IsFull()) //使用while是为了防止pthread_cond _wait调用失败37 {38 //满了,不能进行生产,等待q有空间可以容纳新的数据39 pthread_cond_wait(&full,&lock); //满了在该条件变量下等待,在等待时,释放互斥锁40 } 41 q.push(in);42 if(q.size()>=cap/2)43 {44 //生产空间的一半就通知消费者45 pthread_cond_signal(&empty); //唤醒消费者46 std::cout<<"数据已经很多了,消费者快来消费吧"<<std::endl;47 }48 pthread_mutex_unlock(&lock); //解锁49 }50 51 void Pop(T& out) //供消费者调用52 {53 pthread_mutex_lock(&lock);54 55 while(IsEmpty())56 {57 //不能进行消费,需要等待q有新的数据58 pthread_cond_wait(&empty,&lock); //等待,等待时,释放锁59 }60 out=q.front();61 q.pop();62 if(q.size()<=cap/2)63 { 64 //消费了一半就唤醒生产者65 pthread_cond_signal(&full); //唤醒生产者66 std::cout<<"空间已经很多了,生产者快来生产吧"<<std::endl;67 }68 pthread_mutex_unlock(&lock);69 }70 71 ~BlockQueue()72 {73 pthread_mutex_destroy(&lock);74 pthread_cond_destroy(&full);75 pthread_cond_destroy(&empty);76 }77 78 private:79 std::queue<T> q; //临界资源80 int cap;81 pthread_mutex_t lock; //定义锁82 pthread_cond_t full;83 pthread_cond_t empty; 84 85 };
main.cc文件
1 #include "BlockQueue.hpp" 2 #include "Task.hpp"3 void* Consumer(void* arg)4 {5 auto bq=(BlockQueue<Task>*)arg;6 while(true)7 {8 sleep(1);9 Task t;10 11 bq->Pop(t); //消费数据12 t.Run();13 14 }15 }16 17 void* Producter(void* arg)18 {19 auto bq=(BlockQueue<Task>*)arg;20 const char *arr="+-*/";21 while(true)22 {23 int x=rand()%100+1;24 int y=rand()%50;25 char op=arr[rand()%4];26 Task t(x,y,op);27 bq->Push(t); //生产数据28 std::cout<<"producter task done"<<std::endl;29 }30 }31 32 int main()33 {34 35 srand((unsigned long)time(nullptr));36 BlockQueue<Task> *bq=new BlockQueue<Task>();37 pthread_t c,p; //创建两个线程38 39 pthread_create(&c,nullptr,Consumer,bq); //消费者40 pthread_create(&c,nullptr,Producter,bq); //生产者41 42 pthread_join(c,nullptr); //等待消费者43 pthread_join(p,nullptr); //等待生产者44 return 0;45 }
Makefile文件
1 cp:main.cc2 g++ -o $@ $^ -std=c++11 -lpthread3 .PHONY:clean4 clean:5 rm -f cp
Task.hpp文件
1 #pragma once 2 #include<iostream>3 class Task4 {5 private:6 int x;7 int y;8 char op;9 public:10 Task(int _x,int _y,char _op)11 :x(_x)12 ,y(_y)13 ,op(_op)14 {}15 Task()16 {} 17 void Run()18 {19 int result=0;20 switch(op)21 {22 case '+':23 result=x+y;24 break;25 case '-':26 result=x-y;27 break;28 case '*':29 result=x*y;30 break;31 case '/':32 if(y==0)33 {34 std::cout<<"Waring : div zero! "<<std::endl;35 result=-1;36 }37 else 38 {39 result=x/y;40 } 41 break;42 default:43 break;44 }45 std::cout<<x<<op<<y<<"="<<result<<std::endl;46 }47 ~Task()48 {}49 50 };
效果实现
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
什么是信号量:信号量本质是一个计数器,描述临界资源中资源数目的计数器
信号量存在的价值:1、同步互斥 2、更细粒度的临界资源的管理
申请到信号量的本质:并不是已经开始使用临界资源中你所申请的那个区域,而是有了使用特定资源的权限。
申请信号量的本质,让计数器–, P操作
释放信号量的本质,让计数器++, V操作
本质上信号量是一种临界资源,信号量的PV操作必须是原子的
信号量是一个计数器,如果是sem的值是1呢? -》基本等价于互斥锁,(二元信号量)
用二元信号量模拟互斥锁实现抢票逻辑
Makefile文件
1 Getticket:Getticket.cc2 g++ -o $@ $^ -lpthread -std=c++11 3 .PHONY:clean4 clean:5 rm -f Getticket
~
main.cc文件
1 #include<iostream> 2 #include<semaphore.h>3 #include<pthread.h>4 #include<unistd.h>5 #include<string>6 7 // **************** 二元信号量模拟互斥锁抢票 ******************8 class Sem9 {10 private:11 sem_t sem;12 public:13 Sem(int num)14 {15 sem_init(&sem,0,num); //0,是指线程共享,num是信号量个数16 }17 void P()18 {19 sem_wait(&sem); //申请信号量20 }21 22 void V()23 {24 sem_post(&sem); //释放信号量25 }26 27 ~Sem()28 {29 sem_destroy(&sem); 30 }31 };32 33 Sem sem(1); //1-》二元信号量34 int tickets=2000;35 36 void* GetTickets(void* arg)37 {38 std::string name=(char*)arg;39 while(true)40 { 41 sem.P(); //相当于申请锁42 if(tickets>0)43 {44 usleep(10000);45 std::cout<<name<< " get ticket: "<<tickets--<<std::endl;46 sem.V(); //相当于释放锁47 }48 else49 {50 sem.V();51 break;52 }53 }54 std::cout<<name <<" quit"<<std::endl;55 pthread_exit((void*)0);56 }57 int main()58 {59 pthread_t tid1,tid2,tid3,tid4,tid5,tid6;60 pthread_create(&tid1,nullptr,GetTickets, (void*)"thread 1");61 pthread_create(&tid2,nullptr,GetTickets, (void*)"thread 2");62 pthread_create(&tid3,nullptr,GetTickets, (void*)"thread 3");63 pthread_create(&tid4,nullptr,GetTickets, (void*)"thread 4"); 64 pthread_create(&tid5,nullptr,GetTickets, (void*)"thread 5");65 pthread_create(&tid6,nullptr,GetTickets, (void*)"thread 6");66 67 pthread_join(tid1,nullptr);68 pthread_join(tid2,nullptr);69 pthread_join(tid3,nullptr);70 pthread_join(tid4,nullptr);71 pthread_join(tid5,nullptr);72 pthread_join(tid6,nullptr);73 74 return 0;75 76 }
基于环形队列的生产消费模型
代码实现
Makefile文件
1 Ring:main.cc 2 g++ -o $@ $^ -lpthread -std=c++113 .PHONY:clean4 clean:5 rm -f Ring
main.cc文件
1 #include "Ring.hpp" 2 #include<stdlib.h>3 #include<unistd.h>4 void *consum(void *arg)5 {6 RingQueue<int> *rq=(RingQueue<int>*)arg;7 while(true)8 {9 sleep(1);10 int x=0;11 rq->Pop(x);12 std::cout<< "consume done" <<x<<std::endl;13 }14 }15 16 void *product(void *arg)17 {18 RingQueue<int> *rq=(RingQueue<int>*)arg;19 while(true)20 {21 int x=rand()%100+1;22 rq->Push(x);23 std::cout<<"product done "<<x<<std::endl;24 }25 }26 int main()27 {28 srand((unsigned long)time(nullptr));29 RingQueue<int> *rq=new RingQueue<int>();30 pthread_t c,p;31 pthread_create(&c,nullptr,consum,rq);32 pthread_create(&p,nullptr,product,rq);33 34 35 pthread_join(c,nullptr);36 pthread_join(p,nullptr);37 return 0;38 }
Ring.hpp文件
1 #pragma once 2 #include<iostream>3 #include<vector>4 #include<pthread.h>5 #include<semaphore.h>6 7 // *********** 创建环形队列 ******************8 #define NUM 59 template<typename T>10 class RingQueue11 {12 private:13 std::vector<T> q;14 int cap;15 int c_pos; //消费位置16 int p_pos; //生产位置17 sem_t blank_sem;18 sem_t data_sem;19 private:20 void P(sem_t &s)21 {22 sem_wait(&s);23 }24 25 void V(sem_t &s)26 {27 sem_post(&s);28 }29 public:30 RingQueue(int _cap=NUM):cap(_cap),c_pos(0),p_pos(0)31 {32 q.resize(cap);33 sem_init(&blank_sem,0,cap);//刚开始时空间资源是cap34 sem_init(&data_sem,0,0); //刚开始时,数据资源是035 }36 37 //生产者调用,生产者关心blank(空间)38 void Push(const T& in)39 {40 P(blank_sem); //申请blank空间 41 q[p_pos]=in;42 V(data_sem); //释放空间43 p_pos++;44 p_pos %=cap;45 }46 //消费者调用,消费数据,关心data数据47 void Pop(T& out)48 {49 P(data_sem);50 out=q[c_pos];51 V(blank_sem);52 c_pos++;53 c_pos %=cap;54 }55 56 ~RingQueue()57 {58 sem_destroy(&blank_sem);59 sem_destroy(&data_sem);60 }61 };
结果演示
深入理解Linux多线程相关推荐
- Linux多线程的同步------读写锁
前面介绍过Linux多线程同步的另外两个方法------互斥锁和信号量 Linux多线程的同步-----信号量和互斥锁_神厨小福贵!的博客-CSDN博客 下面来看一下读写锁: 读写锁和互斥锁都带有一个 ...
- Linux多线程的同步-----信号量和互斥锁
前面两篇给基本概念讲过了,大家有兴趣的可以去看一下: Linux多线程_神厨小福贵!的博客-CSDN博客进程和线程的区别有哪些呢?进程是资源分配的最小单位,线程是CPU调度的最小单位进程有自己的独立地 ...
- linux多线程编写哲学家,Linux系统编程(三) ------ 多线程编程
一.线程的创建和调度 1.线程是程序执行的某一条指令流的映像. 为了进一步减少处理机制的空转时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念--线程.它是进程内独立的一条运行路线, ...
- 10个问题带你全面理解Linux性能优化
10个问题带你全面理解Linux性能优化 • Feiskyhttps://feisky.xyz/posts/2020-06-06-linux-perf/本文整理自极客时间"10个问题带你全面 ...
- ZT 为什么pthread_cond_t要和pthread_mutex_t同时使用 || pthread/Linux多线程编程
为什么线程同步的时候pthread_cond_t要和pthread_mutex_t同时使用 (2009-10-27 11:07:23) 转载▼ 标签: 杂谈 分类: 计算机 举一个例子(http:// ...
- 深入理解Linux进程调度(0.4)
学习方法论 写作原则 标题括号中的数字代表完成度与完善度 0.0-1.0 代表完成度,1.1-1.5 代表完善度 0.0 :还没开始写 0.1 :写了一个简介 0.3 :写了一小部分内容 0.5 :写 ...
- 《Linux多线程服务端编程:使用muduoC++网络库》学习笔记
文章目录 第1章 线程安全的对象生命期管理 1.1 当析构函数遇到多线程 1.1.1 线程安全的定义 1.1.3 线程安全实例 1.2 对象的创建很简单 1.3 销毁很难 1.4 线程安全的Obser ...
- 【Linux】Linux多线程(上)
前言 hi~ 大家好呀,欢迎来到我的Linux学习笔记.本篇笔记将会重点从内核结构引入Linux下的线程,理解Linux下线程和进程的相关性和区别,以及线程相关的操作方法,在到之后的线程互斥和线程同步 ...
- linux多线程同步概览
linux多线程同步概览 临界区 互斥锁 mutex 基本函数 pthread_mutex_destroy 何时调用? 互斥锁类型? 互斥量和自旋锁的区别 条件变量 condition variabl ...
最新文章
- auto自动类型推断
- 第14章WEB14-JDBC案例篇
- P3870-[TJOI2009]开关【分块】
- CS224n笔记3 高级词向量表示
- c语言非线程安全函数引发的BUG一列
- 【运维程序】简单的命令控制器(支持定时命令执行、重复定时任务命令和进程管理,开发这个小程序主要是为了方便管理服务进程)【个人github项目】...
- 使用com.alibaba.fastjson.JSONObject构造简单的JSON数据
- BZOJ4001[TJOI2015]概率论(数学、期望、生成函数、卡特兰数)
- 13、三维图绘制及添加文本
- SQL 连接嵌套查询实验报告
- ios 扫描本地音乐_iOS 获取 媒体资料库里的音乐(本地音乐)
- linux命令行计算器 bc命令用法
- 推杆如妻子,发球木如情人,短铁如父母,球道木如朋友,长铁如兄弟
- android obb权限,解决部分手机读取obb失败的问题
- python电玩城源码_2019最新最全价值2W的微信H5电玩城游戏全套源码+架设教程+配置文档...
- 拼多多item_get_app - 根据ID取商品详情原数据
- 又一巅峰!,Java开发实用必备的几款插件
- python实现ID3
- workbench焊接实例_[转载]Workbench的焊接模拟过程(高斯移动热源)
- 性能监控-软中断出现瓶颈的查看方式