网站源码在线查看科站网站

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

网站源码在线查看,科站网站,商城网站哪个公司做的好处,张掖建设局网站总言 多线程#xff1a;进程线程基本概念、线程控制、互斥与同步。 文章目录 总言1、基本概念1.1、补充知识1.1.1、堆区细粒度划分1.1.2、虚拟地址到物理空间的转化 1.2、如何理解线程、进程1.2.1、如何理解线程#xff1f;1.2.2、如何理解进程#xff1f; 1.3、实践操作1.…总言 多线程进程线程基本概念、线程控制、互斥与同步。 文章目录 总言1、基本概念1.1、补充知识1.1.1、堆区细粒度划分1.1.2、虚拟地址到物理空间的转化 1.2、如何理解线程、进程1.2.1、如何理解线程1.2.2、如何理解进程 1.3、实践操作1.3.1、基本演示线程创建1.3.2、线程如何看待进程内部的资源1.3.3、进程VS线程(调度层面上) 2、线程控制2.1、线程创建2.1.1、函数介绍2.1.2、演示 2.2、线程等待2.2.1、函数介绍2.2.2、演示一验证退出有序2.2.3、演示二线程返回值2.2.4、演示三线程返回值2.0 2.3、线程终止2.3.1、exit终止进程2.3.2、pthread_exit2.3.3、pthread_cancel 2.4、线程ID探索pthread_self()2.5、其它验证2.5.1、验证全局区的数据能被多进程共享__thread介绍2.5.2、如果在线程中使用了execl系列进程替换函数会发生什么 2.6、线程分离 3、线程互斥与同步3.1、线程互斥3.1.1、问题引入与概念介绍3.1.2、互斥锁3.1.2.1、相关涉及函数3.1.2.2、使用一静态、全局方式3.1.2.3、使用二动态、局部方式3.1.2.4、问题说明(由实践到理论理解) 3.1.3、锁的原理3.1.4、死锁3.1.5、可重入VS线程安全 3.2、线程同步3.2.1、问题引入与概念介绍3.2.2、方案一条件变量3.2.2.1、方案说明与函数介绍3.2.2.2、方案演示1.0 1、基本概念 1.1、补充知识 1.1.1、堆区细粒度划分 问题堆区里有很多申请到的小空间那么如何知道哪块区域是一个整体以及如何找到对应堆区申请的空间      回答struct_vm_area_sturct结构体。每次在堆区申请空间会生成这样一个结构体vm_start、vm_end能记录所申请空间的首尾位置。将这些结构体以双链表的形式链接起来就可通过vm_next、vm_prev找到每个空间位置。 struct vm_area_struct {unsigned long vm_start; // Our start address within vm_mm. unsigned long vm_end; // The first byte after our end address within vm_mm.// linked list of VM areas per task, sorted by address struct vm_area_struct *vm_next, *vm_prev;//…………//其它内容 }说明OS是可以做到让进程进行资源的细粒度划分。          1.1.2、虚拟地址到物理空间的转化 ①我们的可执行程序在编译阶段就已经以4KB为单位按照虚拟地址的区域被划分。页帧   ②物理内存也是以4KB为单位划分为一个个小块并以struct page{ }结构体来管理。页框 Linux内核将整个物理内存按照页对齐方式划分成千上万个页进行管理。由于一个物理页用一个struct page表示那么系统会有成千上万个struct page结构体这些结构体也会占用实际的物理内存因此内核选择用union联合体来减少内存的使用。 ③IO的基本单位是4KB。相当于把页帧装进页框里。 1.2、如何理解线程、进程 1.2.1、如何理解线程 在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“一个进程内部的控制序列”。 说明   1、通过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。   2、因此线程在进程内部运行的线程在进程地址空间内运行是OS调度的基本单位CPU进行调度时不关心执行流是进程还是线程只关心PCB。   3、一切进程至少都有一个执行线程。      4、不同操作系统下Linux、windows等线程的实现方案不同只要满足设定的条件规则即可。上述图示的是Linux的线程方案实际上Linux没有真正意义上的线程结构是用进程PCB模拟的。也因此Linux并不能直接给我们提供线程相关的接口只能提供轻量级进程的接口Linux下PCB其它OS的PCB故将Linux进程称之为轻量级进程。   5、pthread线程库Linux系统自带的原生线程库在用户层实现了一套用户层多线程方案以库的方式提供给用户进行使用。相当于省去一定的学习线程库实现的成本只需要会调用该线程库即可。             1.2.2、如何理解进程 用户视角进程 内核数据结构可存在多个PCB 该进程对应的代码和数据。   内核视角进程是承担分配系统资源的基本实体。进程向操作系统申请系统资源此后线程的资源分配就由进程来执行即OS角度这些PCB、虚拟地址、页表等是以进程为单位申请的。      如何理解曾经我们所写的代码   以前我们所写的可执行程序属于内部只有一个执行流的进程。引入线程后可以有内部有多个执行流的进程。             1.3、实践操作 1.3.1、基本演示线程创建 1、相关函数介绍和使用说明   man pthread_create创建一个新的线程。 NAMEpthread_create - create a new threadSYNOPSIS#include pthread.hint pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.参数 thread:返回线程ID attr:设置线程的属性attr为NULL表示使用默认属性 start_routine:是个函数地址线程启动后要执行的函数 arg:传给线程启动函数(start_routine)的参数返回值成功返回0失败返回错误码 RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contentsof *thread are undefined.其它由于调用的是操作系统库因此 使用gcc/g时需要加上选项表示对应库的名称-lpthread。 PS关于动静态库如何使用的相关细节说明见博文Linux || 基础IO二。 2、演示一基本使用演示 int sprintf(char *str, const char *format, …);int snprintf(char *str, size_t size, const char format, …);相关代码如下 #includeiostream #includepthread.h #includestdio.h #includeunistd.h #includestring using namespace std;void threadRun(void * args) {string name (char)args;//字符串首元素地址、赋值运算符while(true){cout name , pid: getpid() \n endl;sleep(1);} }int main() {pthread_t tid[5];//创建5个线程char threadname[64];//用于arg参数传递线程名称以便区分for(int i 0; i 5; i){//snprintf每次都会向threadname数组中写入字符串。thread-1、thread-2、thread-3、……snprintf(threadname, sizeof(threadname) ,%s-%d,thread,i);pthread_create(tidi, nullptr, threadRun, (void)threadname);sleep(1);//此处是缓解传参BUG}while(true){cout main thread , pid: getpid() endl;sleep(3);}return 0; } 以下为演示结果 相关说明   1、ps -aL:可查看线程。   2、主线程和新线程运行顺序是不确定的取决于调度器和父子进程顺序不定一样   3、由上图可知CPU调度时看的是LWP而非PID。因为当有多个线程时LWP唯一但PID可以对应多个线程。PS对于单线程的进程其LWP和PID一样故CPU看的仍旧是LWP。   4、kill -9 PID 用于杀掉一个进程需要注意内部所有线程都被杀掉。 main thread , pid:28571 thread-1 , pid:28571thread-0 , pid:28571thread-3 , pid:28571thread-2 , pid:28571thread-4 , pid:28571Killed #kill -9 28571 杀掉进程对于进程内所有线程都被杀掉 [wjVM-4-3-centos T0927]$ 1.3.2、线程如何看待进程内部的资源 进程是资源分配的基本单位线程是调度的基本单位。 1、线程共享进程数据但也拥有自己的一部分数据如     ①线程ID     ②一组寄存器     ③栈一般认为独自占用     ④errno错误码     ⑤信号屏蔽字     ⑥调度优先级。      2、除了上述全局变量在各线程中都可以访问到各线程还共享以下进程资源和环境     ①文件描述符表     ②每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)     ③当前工作目录     ④用户id和组id     ⑤代码区、全局数据区(已初始化/未初始化)、堆区、共享区。 1.3.3、进程VS线程(调度层面上) 1、为什么说线程切换的成本更低 1、地址空间、页表不需要被切换。假如调度的是另外的进程PCB则上下文、页表、地址空间等都需要切换故而比线程切换成本更高。 2、对于线程CPU内部有L1~L3 cache缓存对内存的代码和数据根据局部性原理预读到CPU内部。若是进程切换cache会失效新进程只能重新缓存。                2、线程控制 1、总览      2、POSIX线程库   1、与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头。   2、要使用这些函数库要通过引入头文pthread.h。   3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项。       2.1、线程创建 2.1.1、函数介绍 1、函数介绍   pthread_create函数在上述小节中已经演示过此处只做补充说明。 NAMEpthread_create - create a new threadSYNOPSIS#include pthread.hint pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.参数 thread:返回线程ID attr:设置线程的属性attr为NULL表示使用默认属性 start_routine:是个函数地址线程启动后要执行的函数 arg:传给线程启动函数(start_routine)的参数返回值成功返回0失败返回错误码 RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contentsof thread are undefined.2.1.2、演示 2、演示线程异常   演示代码如下   PS当创建线程成功新线程执行对应的threadRoutine参数start_routine内部内容主线程继续执行ptread_create后面的代码。 void threadRoutine(void * args) {while(true){sleep(2);cout 新线程 (char*)args , is runing. endl;int a 10;a / 0 ;//error} }int main() {fflush(stdout);pthread_t tid; // 创建一个线程pthread_create(tid, nullptr, threadRoutine, (void *)new thread);while (true){cout main thread , pid: getpid() endl;sleep(1);}return 0; }演示结果如下 线程异常说明:   1、单个线程如果出现除零、野指针等问题导致线程崩溃进程也会随着崩溃   2、线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。                2.2、线程等待 2.2.1、函数介绍 1、 为什么需要线程等待   回答对于已经退出的线程其空间没有被释放仍然在进程的地址空间内。若主线程不等待资源回收那么新创建的线程不会复用刚才退出线程的地址空间。如此就会造成类似于僵尸进程的问题。      2、函数介绍   man pthread_join:等待线程结束。调用该函数的线程将挂起等待直到id为thread的线程终止。 NAMEpthread_join - join with a terminated threadSYNOPSIS#include pthread.hint pthread_join(pthread_t thread, void **retval);Compile and link with -pthread.DESCRIPTIONThe pthread_join() function waits for the thread specified by thread to terminate. If thatthread has already terminated, then pthread_join() returns immediately. The thread specifiedby thread must be joinable. 参数 thread:线程ID retval:它指向一个指针后者通常是线程thread运行结束后的返回值返回值成功返回0失败返回错误码 RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.说明thread线程以不同的方法终止通过pthread_join得到的终止状态是不同的总结如下:

  1. 如果thread线程通过return返回retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉retval所指向的单元里存放的是常数PTHREAD_ CANCELED。宏-1
  3. 如果thread线程是自己调用pthread_exit终止的retval所指向的单元存放的是pthread_exit传递的参数。
  4. 如果对thread线程的终止状态不感兴趣可以将retval设置为NULL。2.2.2、演示一验证退出有序 演示代码如下 void* pthreadRoutine(void * args) {cout (char)args : runing. endl;int count 5;do{cout new thread: count endl;sleep(1);}while(count–);cout new thread: quit. endl;return nullptr; }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);cout main thread: create succeed. endl;pthread_join(tid, nullptr);cout main thread: wait succeed, main quit. endl;return 0; }演示结果如下 可用脚本来观察 while :; do ps -aL | head -1 ps -aL | grep thread; sleep 1; done2.2.3、演示二线程返回值 问题 在上述演示代码中void* pthreadRoutine(void * args) 执行函数会返回一个(void)该返回值是给谁      回答 谁来等待就给谁。一般是给主线程主线程可通过线程等待pthread_join来知道结果即该函数的第二参数 void value_ptr。   PS注意其参数类型是void这里属于输出型参数获取pthread_create的返回值(void)改变了实参value_ptr那么需要二级指针变量。      演示代码如下 void* pthreadRoutine(void * args) {cout (char)args : runing. endl;int count 5;do{cout new thread: count endl;sleep(1);}while(count–);cout new thread: quit. endl;return (void)22;//注意点1从整形转变为void类型相当于将地址数据为22处返回。 }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);cout main thread: create succeed. endl;void* ret nullptr;//注意点2用于接收pthreadRoutine新线程返回值pthread_join(tid, ret);//注意点2要想实参被修改则需要传址void的地址类型为void**cout ret: ret , ret: (long long)ret endl;//注意点3Linux下指针为8字节故此处强转int类型(4字节)不适用(会出现截断问题)cout main thread: wait succeed, main quit. endl;return 0; }演示结果如下 2.2.4、演示三线程返回值2.0 除了上述返回一个值外线程的返回值具有可玩性运用恰当可以做一些有意义的操作。以下为一个代码举例我们可以让新线程做一些运算并将结果存储在堆中或以其它方式返回给主线程。 演示代码如下虽然是新线程申请的动态空间但堆区在线程间能够共享所以主线程也能看到。 void pthreadRoutine(void * args) {cout (char)args : runing. endl;int data new int[5];for(int count 0 ; count 5; count){cout new thread: count endl;data[count] count;sleep(1);}cout new thread: quit. endl;return (void)data;//返回了堆上申请的空间新线程 }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);cout main thread: create succeed. endl;int* ret nullptr;//用于接收pthreadRoutine新线程返回值pthread_join(tid, (void)ret);cout main thread: wait succeed. endl;for(int i 0; i 5; i){cout ret[i] ;}cout endl;return 0; }演示结果如下 2.3、线程终止 2.3.1、exit终止进程 说明exit是用于终止进程的调用它不仅仅当前线程会被终止整个进程都会终止。      演示代码如下 void* pthreadRoutine(void * args) {cout (char)args : runing. endl;int data new int[5];for(int count 0 ; count 5; count){cout new thread: count endl;data[count] count;sleep(1);}cout now, exit the new thread. endl;exit(22);//使用exit终止新线程cout new thread: quit. endl;return nullptr; }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void*)new thread);cout main thread: create succeed. endl;pthread_join(tid, nullptr);//阻塞式等待cout main thread: wait succeed. endl;while(true){cout main still runing. endl;sleep(1);}return 0; }演示结果如下             2.3.2、pthread_exit 1、函数介绍   man pthread_exit线程可以调用该函数终止自己。 NAMEpthread_exit - terminate calling threadSYNOPSIS#include pthread.hvoid pthread_exit(void retval);Compile and link with -pthread.DESCRIPTIONThe pthread_exit() function terminates the calling thread and returns a value viaretval that (if the thread is joinable) is available to another thread in the sameprocess that calls pthread_join(3). 参数 retval:返回指针用于存储退出线程的返回数据注意不要指向一个局部变量。返回值无返回值跟进程一样线程结束的时候无法返回到它的调用者自身。 RETURN VALUEThis function does not return to the caller.2、使用演示   演示代码如下根据之前所学如果thread线程是调用pthread_exit终止的retval参数所指向的单元将会传递给pthread_join。 void pthreadRoutine(void * args) {cout (char)args : runing. endl;int data new int[5];for(int count 0 ; count 5; count){cout new thread: count endl;data[count] count;sleep(1);}cout now, exit the new thread. endl;pthread_exit((void)11);//使用exit终止新线程cout new thread: quit. endl;return nullptr; }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);cout main thread: create succeed. endl;int* ret nullptr;pthread_join(tid, (void)ret);//阻塞式等待,接收来自pthread_exit的参数值cout main thread: wait succeed, the return value: (long long)ret endl;while(true){cout main still runing. endl;sleep(1);}return 0; }演示结果如下 2.3.3、pthread_cancel 1、函数介绍   man pthread_cancel取消一个执行中的线程。 NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include pthread.hint pthread_cancel(pthread_t thread);Compile and link with -pthread.DESCRIPTIONThe pthread_cancel() function sends a cancellation request to the thread thread.Whether and when the target thread reacts to the cancellation request depends ontwo attributes that are under the control of that thread: its cancelability stateand type.参数 thread:线程ID返回值成功返回0失败返回错误码 RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error num‐ber.2、使用演示   演示代码如下 void* pthreadRoutine(void * args) {cout (char)args : runing. endl;size_t count 0;while(true)//让新线程一直循环运行{cout new thread: count endl;sleep(1);}cout new thread: quit. endl;return nullptr; }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);cout main thread: create succeed. endl;sleep(6);//让新线程运行6s后取消新线程。pthread_cancel(tid);int* ret nullptr;pthread_join(tid, (void)ret);//阻塞式等待,接收来自pthread_cancel的返回值cout main thread: wait succeed, the return value: (long long)ret endl;int count 5;//让主线程在新线程退出后再运行一段时间while(count–){cout main thread: runing. endl;sleep(1);}cout main quit. endl;return 0; }演示结果如下 PS不要在随意位置使用终止函数按照场景需求正常使用即可。一般是主线程中使用取消新线程虽然没说不可以在新线程中取消主线程但有可能会引起一些奇怪问题。             2.4、线程ID探索pthread_self() 1、问题引入   此处使用2.3.3中演示代码对main函数稍加修改打印tid值 pthread_cancel(tid);int* ret nullptr;pthread_join(tid, (void)ret);//阻塞式等待,接收来自pthread_cancel的返回值cout main thread: wait succeed, the return value: (long long)ret ,tid: tid endl;如下tid:140604466824960是一个很大的数字。这似乎与我们之前学习到的进程ID文件描述符fd等都不同且也并非我们用ps -aL 指令查看到的LWP值。 [wjVM-4-3-centos T0927]\( ./thread.out main thread: create succeed. new thread: runing. new thread: 0 new thread: 1 new thread: 2 new thread: 3 new thread: 4 new thread: 5 main thread: wait succeed, the return value:-1 ,tid:140604466824960main thread: runing.main thread: runing.main thread: runing.main thread: runing.main thread: runing. main quit. [wjVM-4-3-centos T0927]\) 那么这里的线程ID究竟什么            2、解释说明   结论对于Linux目前实现的实现而言pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址。 主线程用的是虚拟地址的栈结构新线程用的是库里提供的私有栈结构。      pthread_self()可以获得线程自身的ID在哪个线程中使用获取的就是哪个线程的ID。   演示代码如下: void* pthreadRoutine(void * args) {size_t count 3;while(count–) {cout (char)args : runing. tid: pthread_self() endl;sleep(1);}cout new thread: quit. endl;return nullptr; }int main() {pthread_t tid;pthread_create(tid, nullptr, pthreadRoutine, (void)new thread);//创建新线程pthread_join(tid, nullptr);//阻塞式等待新线程int count 3;//让主线程在新线程退出后再运行一段时间while(count–){cout main thread: runing. tid: pthread_self() endl;sleep(1);}cout main quit. endl;return 0; }演示结果如下 [wjVM-4-3-centos T0927]\( make g -o thread.out mythread.cc -stdc11 -lpthread [wjVM-4-3-centos T0927]\) ls makefile mythread.cc thread.out [wjVM-4-3-centos T0927]\( ./thread.out new thread: runing. tid:140510573528832 new thread: runing. tid:140510573528832 new thread: runing. tid:140510573528832 new thread: quit. main thread: runing. tid:140510591952704 main thread: runing. tid:140510591952704 main thread: runing. tid:140510591952704 main quit. [wjVM-4-3-centos T0927]\) 2.5、其它验证 2.5.1、验证全局区的数据能被多进程共享__thread介绍 1、相关演示   演示代码 int val 0;void* Routine(void* args) {while(true){//cout new thread, , val: val , val: val endl;printf(new thread, val:%d, val:%p\n,val,val);val;sleep(1);} }int main() {pthread_t tid 0;pthread_create(tid, nullptr, Routine, (void)new thread);while(true){//cout main thread , val: val , val: val endl;printf(main thread, val:%d, val:%p\n,val,val);sleep(1);}pthreadjoin(tid ,nullptr);return 0; }演示结果         2、若想让全局变量不被共享如何操作 thread修饰全局变量可以让该全局变量被每一个线程独自占有线程的局部存储。 注意这里的的是两个。使用如下__thread int val 0; 2.5.2、如果在线程中使用了execl系列进程替换函数会发生什么 演示代码 void Routine(void* args) {while(true){sleep(3);execl(/bin/ls,ls,nullptr);//进程替换printf(new thread,tid:%u\n,pthread_self());sleep(1);} }int main() {pthread_t tid 0;pthread_create(tid, nullptr, Routine, (void)new thread);while(true){printf(main thread,tid:%u\n,pthread_self());sleep(1);}pthread_join(tid ,nullptr);return 0; }演示结果如下      需要注意语言级别的线程如C中也提供了线程无论再怎么支持其底层还是使用的是原生系统的线程库。             2.6、线程分离 1、函数介绍   man pthread_detach默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。   NAMEpthread_detach - detach a threadSYNOPSIS#include pthread.hint pthread_detach(pthread_t thread);Compile and link with -pthread.DESCRIPTIONThe pthread_detach() function marks the thread identified by thread as detached. When a detachedthread terminates, its resources are automatically released back to the system without the need foranother thread to join with the terminated thread.Attempting to detach an already detached thread results in unspecified behavior.RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number. 1、可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离。   2、线程分离和线程等待是冲突的一个线程不能既是joinable又是分离的。               2、演示一   演示代码如下 void Routine(void* args) {//可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离pthread_detach(pthread_self());int count 5;while(count–){printf(new thread,tid:%u, count:%d\n,pthread_self(),count);sleep(1);}return nullptr; }int main() {pthread_t tid 0;pthread_create(tid, nullptr, Routine, (void)new thread);int count 5;while(count–){printf(main thread, tid:%u, count:%d\n,pthread_self(),count);sleep(1);}cout thr result strerror(pthread_join(tid ,nullptr)) endl;return 0; }演示结果如下 3、演示二线程分离若该线程异常是否会影响主线程   回答:会同一个进程中资源还是共享的。 3、线程互斥与同步 3.1、线程互斥 3.1.1、问题引入与概念介绍 1、问题引入不加保护的情况下多线程抢票逻辑   问题说明如果多线程访问同一个全局变量并对它进行数据计算多线程会相互影响吗      演示代码 int tickets 1000;void getTickets(void* args) {(void)args;while(true){if(tickets 0 ){usleep(1000);//休眠printf(%p: %d\n, pthread_self(), tickets–);}else break;}return nullptr; }int main() {pthread_t tid1, tid2, tid3;//一次创建多个线程pthread_create(tid1, nullptr, getTickets, nullptr);pthread_create(tid2, nullptr, getTickets, nullptr);pthread_create(tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0; }演示结果根据上述代码printf只会打印出tickets 0的数可运行程序我们发现最后总会有ticket -1。为什么会出现此现象 2、原因解释1.0   分析上述代码tickets0是逻辑运算会在CPU中进行同理tickets–也是在CPU中进行的。这就需要将物理地址中存储的tickets变量载入进程(当前执行流)的上下文在线程被调度时CPU执行计算操作。   但需要注意的是tickets–实则为三步操作①读取数据到CPU寄存器②CPU内部进行数据计算③将结果写回内存。若在此期间线程因为CPU调度被切换那么对于tickets的相关操作无论执行到哪一步骤都会随PCB上下文被切换走直到下次再被调度。    由此在不加保护的情况下多个线程对同一数据不具有实时同步性会导致并发访问时数据不一致。 3、一些概念   临界资源 多线程执行流共享的资源就叫做临界资源。   临界区 每个线程内部访问临界资源的代码就叫做临界区实际还有很大一部分代码段属于普通代码。   互斥 任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用。   原子性不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成。                3.1.2、互斥锁 针对上述问题一个避免方法是加锁保护。 3.1.2.1、相关涉及函数 1、对锁初始化初始化互斥量 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);mutex要初始化的互斥量attrNULLpthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;2、加锁、解锁 int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);3、销毁 int pthread_mutex_destroy(pthread_mutext *mutex);①使用 PTHREAD MUTEX_ INITIALIZER 初始化的互斥量不需要销毁   ②不要销毁一个已经加锁的互斥量   ③已经销毁的互斥量要确保后面不会有线程再尝试加锁             3.1.2.2、使用一静态、全局方式 1、代码演示1.0   如下 int tickets 1000;//1、定义一个锁并对其初始化 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;void* getTickets(void* args) {(void)args;while(true){//2、加锁pthread_mutex_lock(mutex);if(tickets 0 ){usleep(1000);//休眠printf(%p: %d\n, pthread_self(), tickets–);pthread_mutex_unlock(mutex);//3、解锁}else {pthread_mutex_unlock(mutex);//3、解锁break;}}return nullptr; }int main() {pthread_t tid1, tid2, tid3;//一次创建多个线程pthread_create(tid1, nullptr, getTickets, nullptr);pthread_create(tid2, nullptr, getTickets, nullptr);pthread_create(tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0; }演示结果 解释说明:   1、if…else语句中break退出前需要解锁 2、能明显看出与不加锁时相比运行速度变慢。可以获取一个时间来验证time、gettimeofday。         3、可加入随机数让持锁进程更加随机。实则我们演示时多个线程都有参与 //int main()srand((unsigned int)time(nullptr)^getpid());//种子用于让持锁线程更加随机//void* getTickets(void* args)usleep(rand() % 1500);//休眠:让休眠随机一点4、加锁时容易影响效率为了保证加锁粒度加锁区域越小越好。             3.1.2.3、使用二动态、局部方式 int tickets 10000; #define THREAD_NUM 5 //待创建线程数目class ThreadData // 用于创建线程时args传参线程名、锁 { public:ThreadData(const string name, pthread_mutex_t pmutex): _name(name), _pmutex(pmutex){}string _name;pthread_mutex_t _pmutex; };void *getTickets(void args) {ThreadData td (ThreadData*)args;while(true){//3、加锁pthread_mutex_lock(td-_pmutex);if(tickets 0 ){usleep(rand() % 1500);//休眠:让休眠随机一点printf(%s: %d\n, td-_name.c_str(), tickets–);pthread_mutex_unlock(td-_pmutex);//3、解锁}else {pthread_mutex_unlock(td-_pmutex);//3、解锁break;}}delete td;//销毁new出来的空间return nullptr; }int main() {srand((unsigned int)time(nullptr) ^ getpid()); // 种子用于让持锁线程更加随机clock_t t1 clock(); // 测试时间pthread_mutex_t mutex; // 1、定义一个锁pthread_mutex_init(mutex, nullptr); // 2、对锁初始化// 创建线程pthread_t tid[THREAD_NUM]; // 线程IDfor (int i 0; i THREAD_NUM; i){string name thread; // 线程名name std::to_string(i 1); // 线程名ThreadData *td new ThreadData(name, mutex);pthread_create(tid i, nullptr, getTickets, (void *)td);}// 等待线程for (int i 0; i THREAD_NUM; i){pthread_join(tid[i], nullptr);}//4、销毁锁pthread_mutex_destroy(mutex);clock_t t2 clock();cout time: (t2 - t1) endl;return 0; }3.1.2.4、问题说明(由实践到理论理解) 1、加锁之后线程在临界区中是否切换是否会有问题   回答会切换但不会有问题。   第一次理解当前线程虽然被切换了但其是持有锁被切换的。而其他抢票线程要执行临界区代码也必须先申请锁此时锁无法申请成功的所以也不会让其他线程进入临界区由此保证了临界区中数据一致性         2、原子性体现   回答设线程1持有锁在未持有锁的线程看来对其最有意义的情况只有两种1. 线程1没有持有锁(什么都没做) 2. 线程1释放锁(做完)此时我可以申请锁。            3、加锁就是串行执行了吗   回答是的执行临界区代码一定是串行的。             3.1.3、锁的原理 1、问题引入   要访问临界资源每一个线程都必须先申请锁每一个线程都必须先看到同把一锁并能够访问它。这就味着锁本身就是一种共享资源。所以为了保证锁的安全申请和释放锁必须是原子的。      那么谁来保证如何保证锁是如何实现的            2、原理解释   为了实现互斥锁操作大多数体系结构都提供了swap或exchange指令该指令的作用是把寄存器和内存单元的数据相交换。在汇编角度若只有一条汇编语句则认为该汇编指令是原子性。 即使是多处理器平台访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。   PS实际底层还有过多概念原理这里只是一层理解。         1、谁来保证锁的安全   回答锁自身。加锁、解锁这步动作都只涉及一行汇编。             3.1.4、死锁 1、概念   死锁是指在一组进程中的各个进程均占有不会释放的资源因互相申请被其他进程所站用的不会释放的资源使得彼此处于一种永久等待的状态。      以下为一种线程自己把自己弄成死锁的场景举例 void *getTickets(void args) {ThreadData td (ThreadData*)args;while(true){//此处已经加锁pthread_mutex_lock(td-_pmutex);if(tickets 0 ){usleep(rand() % 1500);printf(%s: %d\n, td-_name.c_str(), tickets–);pthread_mutex_lock(td-_pmutex);//在加锁后尚未解锁前再次加锁那么即使是线程本身也是申请失败的。pthread_mutex_unlock(td-_pmutex);//解锁}else {pthread_mutex_unlock(td-_pmutex);//解锁break;}}delete td;//销毁new出来的空间return nullptr; }2、死锁的必要条件   互斥条件一个资源每次只能被一个执行流使用。产生死锁正是因为牵扯到互斥   请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放。类似于吃着碗里的还看着锅里的   不剥夺条件一个执行流已获得的资源在末使用完之前不能强行剥夺。   循环等待条件若干执行流之间形成一种头尾相接的循环等待资源的关系。   PS只要产生死锁必然是这四个条件都被满足。反之若破坏其中某个条件就无法达成死锁。            3、如何避免死锁   破坏死锁的四个必要条件   加锁顺序一致   避免锁未释放的场景   资源一次性分配             3.1.5、可重入VS线程安全 1、概念   线程安全多个线程并发同一段代码时不会出现不同的结果则说明线程是安全的。常见对全局变量或者静态变量进行操作时在没有锁保护的情况下会出现并发问题。 重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则称为不可重入函数。            2、常见的线程不安全和线程安全的情况      线程不安全 不保护共享变量的函数 函数状态随着被调用状态发生变化的函数 返回指向静态变量指针的函数 调用线程不安全函数的函数线程安全 每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的 类或者接口对于线程来说都是原子操作 多个线程之间的切换不会导致该接口的执行结果存在二义性3、常见的可重入和不可重入的情况      不可重入 调用了malloc/free函数因为malloc函数是用全局链表来管理堆的 调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构 可重入函数体内使用了静态的数据结构可重入 不使用全局变量或静态变量 不使用用malloc或者new开辟出的空间 不调用不可重入函数 不返回静态或全局数据所有数据都有函数的调用者提供 使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据4、联系与区别   联系 函数是可重入的那就是线程安全的 函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题 如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。区别 可重入函数是线程安全函数的一种 线程安全不一定是可重入的而可重入函数则一定是线程安全的。 如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。3.2、线程同步 3.2.1、问题引入与概念介绍 1、引入上述互斥锁是否存在什么问题   回答存在以下两种不合理的行为虽然没有错误但不合适   1、在拥有资源时某个线程频繁的申请到资源导致其它线程处于饥饿状态(长时间得不到资源)。   2、在资源短缺时某个线程频繁申请失败浪费彼此时间。      为了解决上述访问临界资源合理性的问题我们引入同步的概念。            12、什么叫做同步   说明在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步线程同步。      那么如何实现同步       3.2.2、方案一条件变量 3.2.2.1、方案说明与函数介绍 1、在使用条件变量前的一些理解说明   申请临界资源前要先对临界资源是否存在做出检测而检测本身也是在访问临界资源也需要对其进行加锁解锁。常规方式下 线程检测条件是否就绪就需要频繁申请和释放锁。此时若临界资源不就绪线程申请锁失败相当于其在频繁地加锁解锁做无意义的耗时行为 void *getTickets(void args) {ThreadData td (ThreadData*)args;while(true){pthread_mutex_lock(td-_pmutex);//加锁if(tickets 0 )//如此处在申请临界资源前先对临界资源是否存在做了检测。而该检测是在加锁之后进行的。{usleep(rand() % 1500);printf(%s: %d\n, td-_name.c_str(), tickets–);pthread_mutex_lock(td-_pmutex);pthread_mutex_unlock(td-_pmutex);//解锁}else {pthread_mutex_unlock(td-_pmutex);//解锁break;}}delete td;return nullptr; }考虑到此我们设置出方案让线程在(首次)检测到资源不就绪时①不再频繁地重复进行资源检测而是处于等待状态②当资源就绪时能接收到相应通知随后再进行资源申请和访问。          2、条件变量涉及函数             3.2.2.2、方案演示1.0 演示代码 #includeiostream #includepthread.h #includestring #includeunistd.h using namespace std;#define TNUM 4 volatile bool quit false;//用于让线程结束循环退出typedef void(func_t)(const string name, pthread_mutex_t pmx, pthread_cond_t* pcd);//函数指针class ThreadData//用于create线程是args传参 { public:ThreadData(const string name, func_t func, pthread_mutex_t* pmx, pthread_cond_t* pcd):_name(name),_func(func),_pmx(pmx),_pcd(pcd){}string _name;//线程名func_t _func;//函数指针pthread_mutex_t* _pmx;//锁pthread_cond_t* _pcd;//条件变量 };void func1(const string name, pthread_mutex_t *pmx, pthread_cond_t *pcd) {while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout name.c_str() is running, action : F1–帮助键 endl;cout endl;pthread_mutex_unlock(pmx); // 解锁} }void func2(const string name, pthread_mutex_t *pmx, pthread_cond_t *pcd) {while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout name.c_str() is running, action : F2–重命名 endl;cout endl;pthread_mutex_unlock(pmx); // 解锁} }void func3(const string name, pthread_mutex_t *pmx, pthread_cond_t *pcd) {while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout name.c_str() is running, action : F3–搜索按钮 endl;cout endl;pthread_mutex_unlock(pmx); // 解锁} }void func4(const string name, pthread_mutex_t *pmx, pthread_cond_t pcd) {while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout name.c_str() is running, action : F4–浏览器网址列表 endl;cout endl;pthread_mutex_unlock(pmx); // 解锁} }void Entry(void* args) {//所有新线程都会执行Entry函数在Entry函数中每个线程又会执行其对应的funcThreadData* td (ThreadData) args;td-_func(td-_name, td-_pmx, td-_pcd);sleep(1);cout td-_name.c_str() : pthread_self() endl;delete td;//新线程有自己独立的栈结构每一个td变量都在各自私有栈中保存最后新线程运行结束时记得释放掉申请出来的堆return nullptr; }int main() {//创建并初始化锁、条件变量pthread_cond_t cond;pthread_mutex_t mutex;pthread_cond_init(cond, nullptr);pthread_mutex_init(mutex, nullptr);//创建新线程pthread_t tid[TNUM];func_t funcs[TNUM] {func1, func2, func3, func4};for(int i 0; i TNUM; i){string name thread;name to_string(i);ThreadData td new ThreadData(name, funcs[i], mutex, cond);pthread_create(tidi, nullptr, Entry, (void*)td);}//主线程唤醒新线程int count 10;//执行10s退出while(count–){ cout awake thread: endl;pthread_cond_signal(cond);//任意唤醒一个线程并不关心具体是哪一个sleep(1);}quit true;//此时func函数不满足条件线程回到Entrycout endl quit - true. endl;pthread_cond_broadcast(cond);//虽然quit退出函数但有线程处于等待条件状态此处再统一唤醒。//只是为了演示两种唤醒函数。sleep(3);cout endl;//等待新线程for(int i 0; i TNUM; i){pthread_join(tid[i],nullptr);cout join thread: tid[i] endl;}//销毁锁、条件变量pthread_cond_destroy(cond);pthread_mutex_destroy(mutex);return 0; }演示结果