线程1. 线程概述1.1 从进程到线程1.2 用户级线程和内核级线程1.3 线程的创建1.3.1 pthread_create函数定义代码示例注意事项A: 主线程的消亡,引发的一些问题B: 链接 -lpthread C: 了解1.3.2 线程函数错误处理1.4 线程和数据共享1.4.1 共享全局变量 (数据段)1.4.2 共享堆空间1.4.3 访问栈数据1..4.4 其它注意: long作为8字节参数注意: 栈数据共享的释放1.5 获取线程的退出状态1.5.0 补充: ps -elLf1.5.1 pthread_join函数定义代码示例注意事项A. 注意: 阻塞线程B. 注意: 捕获和被捕获线程的关系C. 注意: 局部变量问题1.6 线程的主动退出1.6.1 线程的return退出1.6.2 通过pthread_exit退出函数定义代码示例其它注意A. 注意B. 了解C. 一个stdout的特殊情况D. 注意1.7 线程的被动退出1.7.1 线程的取消函数原理函数定义代码示例注意事项: 了解, 不重要1.7.2 手打取消点函数定义代码示例1.7.3 总结补充1.8 资源清理1.8.1 pthread_cleanup函数原理函数定义代码示例注意事项2. 线程的同步和互斥2.1 互斥锁2.1.1 锁的基本情况2.1.2 锁的使用函数原理和定义代码示例注意事项A gettimeofday: 了解B pthread_mutex_trylockC 其它锁1:了解D 其它锁2: 了解2.1.3 锁的类型锁的类型函数定义代码示例代码练习2.2 条件变量2.2.1 pthread_cond_wait函数原理函数定义代码示例注意事项2.2.2 pthread_cond_timedwait函数特点函数定义代码示例2.2.3 pthread_cond_broadcast函数说明代码示例2.2.4 代码练习2.3 其它: 了解2.3.1 线程的属性2.3.2 线程安全2.3.3 可重入性
在经过之前课程的学习之后,我们已经了解进程的基本概念:进程是正在执行的程序,并且是系统资源分配的基本单位。
当用户需要在一台计算机上去完成多个独立的工作任务时,可以使用多进程的方式,为每个独立的工作任务分配一个进程。多进程的管理则由操作系统负责——操作系统调度进程,合理地在多个进程之间分配资源,包括CPU资源、内存、文件等等。除此以外,即便是一个单独的应用,采用多进程的设计也可以提升其吞吐量,改善其响应时间。假如在处理任务的过程中,其中一个进程因为死循环或者等待IO之类的原因无法完成任务时,操作系统可以调度另一个进程来完成任务或者响应用户的请求。比如在文本处理程序当中,可以并发地处理用户输入和保存已完成文件的任务。随着进程进入大规模使用,程序员在分析性能的时候发现计算机花费了大量的时间在切换不同的进程上面。当一个进程在执行过程中,CPU的寄存器当中需要保存一些必要的信息,比如堆栈、代码段等,这些状态称作上下文。上下文切换这里涉及到大量的寄存器和内存之间的保存和载入工作。除此以外,Linux操作系统还支持虚拟内存,所以每个用户进程都拥有自己独立的地址空间,这在实现上就要求每个进程有自己独立的页目录表(页目录表: 映射虚拟地址到物理地址)(TLB是是一个页目录表的缓存,来存储最近使用的物理和虚拟地址映射)。所以,具体到Linux操作系统,进程切换包括两步:首先是切换页目录表,以支持一个新的地址空间;然后是进入内核态,将硬件上下文,即CPU寄存器的内容以及内核态栈切换到新的进程。
为了缩小切换进程的开销,线程的概念便诞生了。线程又被称为轻量级进程 (Light Weight Process,LWP),我们将一个进程分解成多个线程,每个线程是独立的运行中程序实体,线程之间并发运行,这样线程就取代进程成为CPU时分配和调度的最小单位,在Linux操作系统当中,每个线程都拥有自己独立的 task_struct 结构体,当然,在属于同一个进程多个线程中, task_struct 中大量的字段是相同的或者是共享的。注意到在Linux操作系统中,线程并没有脱离进程而存在。而计算机的内存、文件、IO设备等资源依然还是按进程为单位进行分配,属于同一个进程的多个线程会共享进程地址空间,每个线程在执行过程会在地址空间中有自己独立的栈,而堆、数据段、代码段、文件描述符和信号屏蔽字等资源则是共享的。
因为属于同一个进程的多个线程是共享地址空间的,所以线程切换的时候不需要切换页目录表,而且上下文的内容中需要切换的部分也很少,只需要切换栈和PC指针以及其他少量控制信息即可,数据段、代码段等信息可以保持不变。因此,切换线程的花费要小于切换进程的。
同一个进程的多个线程各自拥有自己的栈,这些栈虽然是独立的,但是都位于同一个地址空间中,所以一个线程可以通过地址去访问另一个线程的栈区。
了解:
在Linux文件系统中,路径 /proc 挂载了一个伪文件系统,通过这个伪文件系统用户可以采用访问普通文件的方式(使用read系统调用等),来访问操作系统内核的数据结构。其中,在 /proc/[num] 目录中,就包含了pid为num的进程的大量的内核数据。而在 /proc/[num]/task 就包含本进程中所有线程的内核数据。我们之前的编写进程都可以看成是单线程进程。
xxxxxxxxxx31man ps2ps -elf3ps aux
在Linux2.4以及以前版本,因为还没有线程的概念,Linux内核不知道什么是线程。而后来随着技术发展,因为创建进程开销的资源更多,且进程间的切换相比线程更慢,线程带来的好处被大家所认识。于是我们希望Linux能实现多线程编程,然而要修改一个操作系统并不是件容易的事情,于是采用的办法是写函数来实现,而不是去修改操作系统的内核。而这些函数也就是最初的线程库,由于在Linux内核中没有线程的概念,因此这种线程是用进程来模拟的。因此这种线程也被称为用户级线程。
虽然当时的线程库已经和POSIX的标准非常接近了,但是在Linux的线程实现版本和POSIX标准之间还是存在着细微的差别,最明显的是关于信号处理部分,这些差别中的大部分都受底层Linux内核的限制,而不是函数所能改变的。许多项目都在研究如何才能改善Linux对线程的支持,这其中大部分工作集中在了如何将用户级的线程映射到内核级的线程。
其中IBM公司的NGPT(Next Generation POSIX Threads),和Redhat公司的NPTL(Native POSIX Thread Library)通过修改Linux内核来支持新的线程库,两者都极大地提升了性能。在2002年,NGPT项目组宣布,由于不希望分化团队,所以停止为NGPT添新功能。也因此NPTL成为了Linux线程的新标准。在NPTL中, 内核会处理每个线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理。
目前使用最广泛的线程库名为NPTL (Native POSIX Threads Library),在Linux内核2.6版本之后,它取代了传统的LinuxThreads线程库。NPTL支持了POSIX的线程标准库,在早期,NPTL只支持用户级线程,随着内核版本的迭代,现在每一个用户级线程都对应一个内核态线程,这样NPTL线程本质就成为了内核级线程,可被操作系统调度
xxxxxxxxxx41man 7 pthreads2(Linux implementations of POSIX threads3LinuxThreads: 并不完全兼容POSIX标准4NPTL: Native POSIX Thread Library)
用户级线程和内核级线程在实现方式、运行状态、切换方式、调度方式、对系统资源的访问、创建和销毁等方面存在明显的差异
用户级线程和内核级线程的主要区别体现在以下方面:
实现方式:用户级线程是在用户程序中实现的,而内核级线程是由操作系统内核创建和管理的。
运行状态:用户级线程运行在用户态下,而内核级线程可以访问操作系统的所有资源。
切换方式:用户级线程的切换由应用程序自己控制,而内核级线程的切换需要操作系统的支持。
调度方式:用户级线程的调度由应用程序自己控制,而内核级线程的调度由操作系统内核负责。
等...
xxxxxxxxxx21闲聊: (线程源码参考)2/usr/src/linux-source-4.15.0/linux-source-4.15.0/arch/h8300/include/asm/thread_info.h使用线程的思路其实和使用进程的思路类似,用户需要去关心线程的创建、退出和资源回收等行为,初学者可以参考之前学习进程的方式对比学习线程对应的库函数。下面是常用的线程库函数和之前的进程中的对应关系。

线程创建使用的函数是 pthread_create ,这个函数的函数原型如下( man pthread_create )
xxxxxxxxxx812int pthread_create(3pthread_t *thread, // 不同线程独立唯一的线程id4const pthread_attr_t *attr, // 线程的属性 (attr属性), 暂时使用NULL填充5void *(*start_routine) (void *), // 线程的启动函数6void *arg // 线程的传参7);8// 返回值: 0成功, 其它失败可暂时忽略or参见1.3.2错误处理xxxxxxxxxx41ps: 在使用线程相关的函数之后,在链接时需要加上 -lpthread 选项以显式链接线程库2ps: 在一个进程当中的不同线程都拥有自己独立唯一的线程id(thread id),NPTL使用pthread_t类型来保存线程id,在不同的操作系统中,pthread_t底层实现不同,具体到Linux是一个无符号整型数 (也可以使用函数 pthread_self 获取本线程的id)。3ps: 在操作系统中,当一个新进程被创建时(例如,通过执行一个程序),操作系统会自动创建一个线程(作为这个进程的主线程)。4ps: 主线程终止时(例如通过返回main()或调用exit()),这会导致整个进程终止,包括它创建的所有子线程也会随着进程的终止而消亡。(除非主线程是通过调用pthread_exit()来终止的, 这不会导致进程终止,这种情况下进程内的其他线程可以继续依赖进程存活)。
xxxxxxxxxx151//下面是一个不传递参数的版本2void * threadFunc(void *arg){3printf("I am child thread, \n" );4return NULL;5}6int main()7{8pthread_t tid;9pthread_create(&tid,NULL,threadFunc,NULL);1011printf("I am main thread \n")12sleep(1);13//usleep(20);14return 0;15}
在上面的例子当中,如果主线程不sleep, 或者将 sleep(1) 改成 usleep(20)或者别的 (设个时间也取决于系统的cpu核心和执行效率),线程并发执行的特点会导致一些看上去非常奇怪的结果:在某次执行的时候,标准输出上有一定的几率显示两条相同的语句。
产生这种问题的原因是这样的,stdout缓冲区在多个线程之间是共享,当执行 printf 时,会首先将stdout的内容拷贝到内核态的文件对象中,再清空缓冲区,当主线程终止导致所有线程终止时,可能子线程已经将数据拷贝到了内核态(此时第一份语句已经打印了),但是stdout的内容并未清空,此时进程终止,会把所有缓冲区清空,清空缓冲区的行为会将留存在缓冲区的内容直接清空并输出到标准输出中,此时就会出现内容的重复打印了。
在使用线程相关的函数之后,在链接时需要加上 -lpthread 选项以显式链接线程库
在一个进程当中的不同线程都拥有自己独立唯一的线程id(thread id),NPTL使用pthread_t类型来保存线程id,在不同的操作系统中,pthread_t底层实现不同,具体到Linux是一个无符号整型数 (也可以使用函数 pthread_self 获取本线程的id)。
之前的POSIX系统调用和库函数在调用出错的时候,通常会把全局变量 errno (按进程隔离)设置为一个特别的数值以指示报错的类型,这样就可以调用 perror 以显示符合人阅读需求的报错信息。但是在多线程编程之中,全局变量是各个线程的共享资源,很容易被并发地读写,所以pthread系列的函数优化了检错方式,它会根据pthread系列的函数不同的返回值, 让strerror 函数根据这个错误返回值,使用 strerror 函数可以根据返回值显示报错字符串。(man strerror)
xxxxxxxxxx31char *strerror(int errnum);23xxxxxxxxxx141void * threadFunc(void *arg){2printf("I am child thread, \n" );3return NULL;4}5int main()6{7pthread_t tid;8int ret = pthread_create(&tid,NULL,threadFunc,NULL);9THREAD_ERROR_CHECK(ret,"pthread_create");10printf("I am main thread \n")11sleep(1);12//usleep(20);13return 0;14}xxxxxxxxxx231// 创建最大的线程数2void * threadFunc(void *arg){3sleep(3);4return NULL;5}6int main()7{8int count = 1;9whlie(1){1011pthread_t tid;12int ret = pthread_create(&tid,NULL,threadFunc,NULL);13THREAD_ERROR_CHECK(ret,"pthread_create");1415count++;16if(ret != 0){17printf("sum thread count=%d \n", count);18break;19}20}2122return 0;23}
多个线程是共享一片地址空间的,所以各个线程可以并发地访问的同一个数据段、堆空间和其他位置。
下面是一个共享数据段的例子:
xxxxxxxxxx221//多线程共享数据段23int global= 100;45void * threadFunc(void *arg){6printf("I am child thread \n" );7printf("child thread, global = %d\n", global);8return NULL;9}10int main()11{12pthread_t tid;13int ret = pthread_create(&tid,NULL,threadFunc,NULL);14THREAD_ERROR_CHECK(ret,"pthread_create");15printf("I am main thread \n" );1617global = 200;18printf("main thread, global = %d\n", global);1920sleep(1);21return 0;22}
堆空间自然也是可以共享的,我们通过会将堆空间的首地址作为参数在创建线程的时候进行传递,无论是子线程还是主线程访问的区域都是同一片空间。
eg:
xxxxxxxxxx291//多线程共享堆空间2void * threadFunc(void *arg){34char *pHeap = (char *)arg;5printf("I am child thread \n" );67strcpy(pHeap,"world"); // 复制字符串8printf("child thread, %s\n",pHeap);910return NULL;11}12int main()13{1415char *pHeap = (char *)malloc(20);16strcpy(pHeap,"hello");1718pthread_t tid;19int ret = pthread_create(&tid,NULL,threadFunc,(void *)pHeap);20THREAD_ERROR_CHECK(ret,"pthread_create");2122printf("I am main thread \n" );2324sleep(1);25printf("parent thread, %s\n",pHeap);26free(pHeap);2728return 0;29}
需要注意的是,虽然各个线程执行的过程中拥有自己独立的栈区,但是这些所有的栈区都是在同一个地址空间当中,所以一个线程完全可以访问到另一个线程栈帧内部的数据。(如果将主线程栈帧数据的地址作为参数传递给各个子线程,就一定要注意并发访问的情况,有可能另一个线程的执行会修改掉原本想要传递数据的内容)
xxxxxxxxxx231void * threadFunc(void *arg){2printf("I am child thread \n" );34int * pval = (int *)arg;5*pval = 1002;6printf("child, val = %d\n",*pval);78return NULL;9}10int main(){1112pthread_t tid;13int val = 1001;14int ret = pthread_create(&tid,NULL,threadFunc,(void *)&val);15THREAD_ERROR_CHECK(ret,"pthread_create");1617printf("I am main thread \n" );1819sleep(1);20//虽然栈帧是私有的,但是其他线程依然有权限读写21printf("main, val = %d\n",val);22return 0;23}
虽然说 arg 是一个 void* 类型的参数,这暗示着用户可以使用该参数来传递一个数据的地址,但是有些情况下我们只需要传递一个整型数据,在这种情况,除了传递参数地址以外, 用户可以直接把 void* 类型的参数当成是一个8字节的普通数据(比如long)进行传递。
传递地址
xxxxxxxxxx271void * threadFunc(void *arg){23int *intnum = (int *)arg;4printf("I am child thread \n" );5//把参数当成是普通的8字节整型6printf("child, val = %d\n", *intnum);7return NULL;8}9int main(){1011int val = 1001;1213pthread_t tid;14int ret = pthread_create(&tid,NULL,threadFunc,&val);15THREAD_ERROR_CHECK(ret,"pthread_create");1617//sleep(1);1819val = 1002;20ret = pthread_create(&tid,NULL,threadFunc, &val);21THREAD_ERROR_CHECK(ret,"pthread_create");2223printf("I am main thread \n" );24sleep(1);2526return 0;27}传递8字节数据
xxxxxxxxxx261void * threadFunc(void *arg){23printf("I am child thread \n" );4//把参数当成是普通的8字节整型5printf("child, val = %ld\n",(long)arg);6return NULL;7}8int main(){910long val = 1001;1112pthread_t tid;13int ret = pthread_create(&tid,NULL,threadFunc,(void *)val);14THREAD_ERROR_CHECK(ret,"pthread_create");1516//sleep(1);1718val = 1002;19ret = pthread_create(&tid,NULL,threadFunc,(void *)val);20THREAD_ERROR_CHECK(ret,"pthread_create");2122printf("I am main thread \n" );23sleep(1);2425return 0;26}
共享栈数据,一定要注意内存的释放顺序
EgCode:
xxxxxxxxxx251void * threadFunc(void *arg){2sleep(1);34int *intnum = (int *)arg;5printf("I am child thread \n" );6//把参数当成是普通的8字节整型7printf("child, val = %d\n", *intnum);8return NULL;9}10void func(){11int val = 1001;1213pthread_t tid;14int ret = pthread_create(&tid,NULL,threadFunc,&val);15THREAD_ERROR_CHECK(ret,"pthread_create");1617printf("function over \n" );18}19int main(){20func(); // 调用函数2122printf("I am main thread \n" );23sleep(2);24return 0;25}
我们可以通过pthread_join()方法捕获线程的退出和退出状态。
补充
xxxxxxxxxx21ps -elLf2# 我们可以在在shell中使用ps -elLf命令, 列出所有线程运行如下代码,在代码运行中,并通过ps -elLf命令查看所有线程状态,观察线程变化
EgCode:
xxxxxxxxxx1312void * fun1(void * arg){3// while(1){};4sleep(10);5}6int main()7{8pthread_t pid;9int ret = pthread_create(&pid, NULL, fun1, NULL);10THREAD_ERROR_CHECK(ret,"pthread_create");11sleep(20);12return 0;13}
使用 pthread_join() 捕获线程的退出和退出状态。
xxxxxxxxxx1312// join with a terminated thread3int pthread_join(4pthread_t thread, // 要捕获线程目标的线程id5void **retval // 目标线程的退出状态(指向线程的退出时携带参数)6);7// 返回值int标识捕获状态: 0代表成功 (别的仅作了解: 死锁,不可捕获,被别的线程捕获, 不识别的线程id)8910ps1: pthread_join可以使本线程处于等待状态/阻塞,直到指定的thread终止, 就结束等待/结束阻塞,并且捕获到的线程终止状态存入retval指针所指向的内存空间中。11ps2: pthread_join可以捕获一个线程,一个线程的退出也可以由另一个线程使用pthread_join捕获,需要注意的是,另一个线程的选择是任意的,不需要是被捕获线程创建者。12ps3: 一个线程的退出,可以被多个线程通过pthread_join"试图"捕获(多个线程调用该函数获取同一个线程的执行结果),但是只有一个pthread_join能捕获成功。别的都将失败。13ps4: 指针retval捕获线程函数内部的局部数据, 可能会存在问题。
xxxxxxxxxx17123void *funThread(void *arg){4return 0;5}6int main()7{8pthread_t threadId;9int ret = pthread_create(&threadId, NULL, funThread, NULL);10THREAD_ERROR_CHECK(ret, "pthread_create");1112int ret_join = pthread_join(threadId, NULL );// NULL代表不接收join线程返回状态13THREAD_ERROR_CHECK(ret_join, "pthread_join");1415printf("child thread join status = %d \n", ret_join);16return 0;17}
pthread_join 可以使本线程处于等待/阻塞状态,直到指定的 thread 终止, 就结束等待,并且捕获到的线程终止状态(返回参数)存入 retval 指针所指向的内存空间中。
EgCode:
xxxxxxxxxx22123void *funThread(void *arg){4sleep(10);56// return 100;7return (void *)100;8}910int main()11{12pthread_t threadId;13int ret = pthread_create(&threadId, NULL, funThread, NULL);14THREAD_ERROR_CHECK(ret, "pthread_create");1516void * retThread;17int ret_join = pthread_join(threadId, &retThread );18THREAD_ERROR_CHECK(ret_join, "pthread_join");1920printf("child thread retval = %ld \n", (long)retThread);21return 0;22}
ps2: pthread_join可以捕获一个线程,一个线程的退出也可以由另一个线程使用pthread_join捕获,需要注意的是,另一个线程的选择是任意的,不需要是被捕获线程创建者。 ps3: 一个线程的退出,可以被多个线程通过pthread_join试图捕获(多个线程调用该函数获取同一个线程的执行结果),但是只有一个pthread_join能捕获成功。别的都将失败。
指针retval捕获线程函数内部的局部数据, 可能会存在问题(比如局部变量)。
xxxxxxxxxx241// 如下代码报错: Segmentation fault (core dumped),2// 原因是: funThread 函数中定义了一个局部变量 num。 函数返回时,它返回了一个指向 num 的指针。一旦 funThread 函数执行完毕,num 所占用的栈内存将不再有效,因此返回的指针指向一个已经被回收的内存区域。当主线程尝试通过这个指针访问 num 的值时,它访问了一个无效的内存地址,从而引发了 "Segmentation fault"345void *funThread(void *arg){6sleep(5);78int num = 10;9return #10}1112int main()13{14pthread_t threadId;15int ret = pthread_create(&threadId, NULL, funThread, NULL);16THREAD_ERROR_CHECK(ret, "pthread_create");1718void *retThread;19int ret_join = pthread_join(threadId, &retThread );20THREAD_ERROR_CHECK(ret_join, "pthread_join");2122printf("child thread retval = %d \n", *(int *)retThread);23return 0;24}xxxxxxxxxx321// 假设返回的堆上数据没有问题234void *funcThread(void *arg){5sleep(5);67int *num = malloc(sizeof(int));8*num = 10;910//return num;11return (void *)num;12}13int main()14{15pthread_t pid;16int ret = pthread_create(&pid, NULL, funcThread, NULL);17THREAD_ERROR_CHECK(ret, "pthread_create");1819void *retThread;20int ret_join = pthread_join(pid, &retThread);21THREAD_ERROR_CHECK(ret_join, "pthread_join");22printf("child thread retval = %d \n", *(int *)retThread);23free(retThread);2425//void **retThread;26//int ret_join = pthread_join(pid, retThread);27//THREAD_ERROR_CHECK(ret_join, "pthread_join");28//printf("child thread retval = %d \n", *(int *)(*retThread));29//free(*retThread);3031return 0;32}
1, 当存在一个线程, 这个线程正常运行结束, 其入口调用函数通过return运行结束。 此线程将正常退出。
2, 我们可以通过pthread_join捕获到线程return退出, 以及退出时返回数据.
EgCode: (可配合 ps -elLf命令, 查看子线程的结束时机。)
xxxxxxxxxx2312void *fun2(){3sleep(5);4printf("fun2 sleep over \n");5return NULL;6}7void * fun1(void * arg){8fun2();9sleep(5);10printf("fun1 sleep over \n");11// 需要注意的是,要通过return结束线程12// 是要在线程的入口的函数中return(类似主线程的main方法return)13// 而非是在更深层次的非线程的入口函数(调用函数)中return14return NULL;15}16int main()17{18pthread_t pid;19int ret = pthread_create(&pid, NULL, fun1, NULL);20THREAD_ERROR_CHECK(ret,"pthread_create");21sleep(20);22return 0;23}
通过pthread_join捕获正常return线程
xxxxxxxxxx19123void * fun1(void * arg){4sleep(5);5return (void *)11;6}7int main()8{9pthread_t pid;10int ret = pthread_create(&pid, NULL, fun1, NULL);11THREAD_ERROR_CHECK(ret, "pthread_cetate");1213void *numP;14int ret_join = pthread_join(pid, &numP);15THREAD_ERROR_CHECK(ret_join, "pthread_join");1617printf("catch from child thread return value = %ld \n", (long)numP);18return 0;19}
使用 pthread_exit 函数可以主动退出线程。
无论这个函数是否是在 start_routine(主调函数/线程入口函数) 中被调用(其行为类似于进程退出的 exit), 都将退出该线程 。
xxxxxxxxxx1212// terminate calling thread3void pthread_exit(4void *retval // 该参数描述了线程的退出状态5// ( 即它指向的数据将作为线程退出时的返回值。6// 如果线程不需要返回任何数据,将retval参数置为NULL即可。)7);89ps: 指针retval建议不要指向该退出线程函数内部的局部数据。10ps: 在正常return结束的线程中, 其start_routine的最终返回值, 等价于pthread_exit退出线程的携带retval,等待捕获程结束的pthread_join()函数都可以接收到线程的返回值, 对于pthread_join来说,并不认为两者有什么异同11ps: 如果子线程通过return 0, 或者 pthread_exit(NULL) 或者 pthread_exit(0) 结束, 那么pthread_join()捕获的线程退出状态可以表示为012ps: pthread_exit()可以自动调用线程清理程序(参考pthread_cleanup_push()),return 则不具备这个能力。
EgCode: pthread_exit 函数可以主动退出线程
xxxxxxxxxx21123void *funThread(void *arg){4// 退出线程的参数要求是void*类型5pthread_exit((void *) 11);6printf("this scene well not be print \n");7}89int main()10{11pthread_t threadId;12int ret = pthread_create(&threadId, NULL, funThread, NULL);13THREAD_ERROR_CHECK(ret, "pthread_create");1415void *numP;16int ret_join = pthread_join(threadId, &numP);17THREAD_ERROR_CHECK(ret_join, "pthread_join");1819printf("catch child thread by pthread_exit value = %ld \n", (long)numP);20return 0;21}EgCode: 无论这个函数是否是在start_routine函数中被调用, 都将退出该线程 。
xxxxxxxxxx21123void funTHreadDeep(){4printf("this is deep funaction \n");5// 次数调用退出线程, 依旧会导致该方法所在线程退出6// 即使当前方法不是线程的入口函数7pthread_exit(NULL);8printf("this scene2 well not be print \n");9}10void *funThread(void *arg){11funTHreadDeep();// 调用另一个方法12printf("this scene1 well not be print \n");13}14int main()15{16pthread_t threadId;17int ret = pthread_create(&threadId, NULL, funThread, NULL);18THREAD_ERROR_CHECK(ret, "pthread_create");19sleep(20);20return 0;21}
ps: 指针retval建议不要指向该退出线程函数内部的局部数据。
ps: 在正常return结束的线程中, 其start_routine的最终返回值, 等价于pthread_exit退出线程的携带retval,等待捕获程结束的pthread_join()函数都可以接收到线程的返回值, 对于pthread_join来说,并不认为两者有什么异同。
ps: pthread_join()函数的第二个参数线程的返回值的位置, 可以用NULL填充, 表示不捕获线程返回值。
ps: 如果子线程通过pthread_exit(NULL) 退出, 那么pthread_join()捕获的线程退出状态和 return 0以及pthread_exit(0) 结束线程一样,捕获返回结果为0。
xxxxxxxxxx20123void *funThread(void *arg){4//pthread_exit(NULL);5//pthread_exit(0);6return 0;7}8int main()9{10pthread_t threadId;11int ret = pthread_create(&threadId, NULL, funThread, NULL);12THREAD_ERROR_CHECK(ret, "pthread_create");1314void *numP;15int ret_join = pthread_join(threadId, &numP);16THREAD_ERROR_CHECK(ret_join, "pthread_join");1718printf("catch child thread by pthread_exit value = %ld \n", (long)numP);19return 0;20}
每个线程开辟了一个独属于自己的栈, 而堆/数据段/代码段/..是共享的。 同时也共享一个进程的stdout(标准输出)。
xxxxxxxxxx18123void *funThread(void *arg){4printf("i am child thread"); // 这句话并未立即输出,而是等了一会 才输出5// 退出线程的参数要求是void*类型 or NULL6pthread_exit((void *) 11);7printf("this scene well not be print \n");8}910int main()11{12pthread_t threadId;13int ret = pthread_create(&threadId, NULL, funThread, NULL);14THREAD_ERROR_CHECK(ret, "pthread_create");15sleep(10);16printf("i am main thread \n");17return 0;18}
pthread_exit()可以自动调用线程清理程序(参考pthread_cleanup_push()),return 则不具备这个能力。
我们在上面分别介绍了: 通过代码正常运行return结束, 以及通过执行pthread_exit函数退出线程; 而这两种方式都是由执行线程触发的主动退出方式。
除了上述线程自己主动退出的情况, 那线程有没有可能被另一个线程通过某些方式强制终止(即被动退出)的情况那?
xxxxxxxxxx11ps: 不过首先值得注意的是,和进程的'被动退出'情况不同, 我们可以通过'信号'让进程被动退出/终止, 但是我们不能轻易地在多线程程序使用信号,因为多线程是共享代码段的,从而'信号处理的回调函数'也是共享的,当产生一个信号到进程时,进程中用于递送信号的线程是随机的,很有可能会出现主线程因递送信号而终止从而导致所有线程异常退出的情况
而多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送
终止执行的信息, 从而实现在一个线程中去终止另一个线程的操作, 从而使被通知的线程被动退出。
线程的被动取消流程, 如图所示:
需要注意的是, 当线程A向线程B发送取消线程执行信息的时候, 线程B并不是立即终止执行, 线程B是要先运行到一个叫
取消点的位置才终止线程。
取消点
xxxxxxxxxx71// 什么是取消点?2当一个线程调用 pthread_cancel 去取消另一个线程的时候,另一个线程会将本线程的取消标志位设置为真,当这个线程执行一些函数之时,线程就会退出。这些会导致已取消未终止的线程终止的函数称为取消点。3// 那些常见的取消点函数?4a. 几乎所有会引发阻塞的函数: eg:sleep(), select(), wait() ....5b. I/O操作: open(), close(), read(), write(), fopen(), fclose(), printf()....67ps: 可以通过(man 7 pthreads)查看取消点函数(cancellation points)
通过pthread_cancel() 取消一个执行中的线程
xxxxxxxxxx612// send a cancellation request to a thread3int pthread_cancel(4pthread_t thread, // 不同线程的线程id5);6// 返回值int标识捕获状态: 0代表成功, ESRCH代表参数错误(没找到这个线程id)
EgCode: 没有取消点无法取消示例
xxxxxxxxxx22123void * func(void *arg){4while(1){5}6return NULL;7}8int main()9{10pthread_t pid;11int res = pthread_create(&pid, NULL, func, NULL);12THREAD_ERROR_CHECK(res, "pthread_create");1314int res_cancel = pthread_cancel(pid);15THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");1617printf("main use cancel \n");18int res_join = pthread_join(pid, NULL);19THREAD_ERROR_CHECK(res_join, "pthread_join");2021return 0;22}EgCode: 正常取消示例
xxxxxxxxxx25123void * func(void *arg){4while(1){5printf("child thread printf \n");6}7return NULL;8}9int main()10{11pthread_t pid;12int res = pthread_create(&pid, NULL, func, NULL);13THREAD_ERROR_CHECK(res, "pthread_create");1415sleep(10);1617printf("main use cancel \n");18int res_cancel = pthread_cancel(pid);19THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");2021int res_join = pthread_join(pid, NULL);22THREAD_ERROR_CHECK(res_join, "pthread_join");2324return 0;25}
当线程被动取消, 通过pthread_join捕获的结束状态: PTHREAD_CANCELED (参考 man pthread_join 的 description)
(了解:PTHREAD_CANCELED 是一个由pthread.h定义的, 本质值为-1 )
(了解:当线程主动退出:pthread_exit(PTHREAD_CANCELED)退出,pthread_join捕获的结束状态也是PTHREAD_CANCELED )
xxxxxxxxxx32123void * func(void *arg){4while(1){5sleep(1);6printf("child thread printf \n");7}8return NULL;9}10int main()11{12pthread_t pid;13int res = pthread_create(&pid, NULL, func, NULL);14THREAD_ERROR_CHECK(res, "pthread_create");1516sleep(10);1718int res_cancel = pthread_cancel(pid);19THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");2021void *ret_catch ;22int res_join = pthread_join(pid, &ret_catch);23THREAD_ERROR_CHECK(res_join, "pthread_join");2425if(ret_catch == PTHREAD_CANCELED){26printf("catch value = %ld \n", (long)ret_catch); // -127}else{28printf("other value = %ld \n", (long)ret_catch);29}3031return 0;32}
从上述的的被动取消过程中, 我们知道pthread_cancel函数是依赖于被取消线程的
取消点这一关键标志位; 但是有一种可能,要被取消线程代码中(试图被动取消的代码片段位置)没有取消点, 那么我们该怎么办那?我们可以通过pthread_testcancel方法, 手动添加一个取消点。
通过pthread_testcancel方法, 手动添加一个取消点。
xxxxxxxxxx412// request delivery of any pending cancellation request3void pthread_testcancel(void);4// 无参数和返回值
EgCode:
xxxxxxxxxx23123void * func(void *arg){4while(1){5pthread_testcancel();// 没有这句代码, 将一直 循环无法取消6}7return NULL;8}9int main()10{11pthread_t pid;12int res = pthread_create(&pid, NULL, func, NULL);13THREAD_ERROR_CHECK(res, "pthread_create");1415printf("main use cancel \n");16int res_cancel = pthread_cancel(pid);17THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");1819int res_join = pthread_join(pid, NULL);20THREAD_ERROR_CHECK(res_join, "pthread_join");2122return 0;23}
了解线程取消的先后顺序流程是很重要的,尤其是在复杂的多线程应用中。当调用
pthread_cancel来取消一个线程时,以下是线程取消的大致步骤和流程:xxxxxxxxxx51// 1, 首先在某个线程中调用'pthread_cancel', 并传入要取消的目标线程ID。系统根据线程ID找到要取消的目标线程, 给这个线程的内部取消标记设置为真。2// 2, 被取消线程在程序的执行的时候, 当执行到取消点函数, 则去检测标志位。3// 3, 取消点函数一旦检测到线程为取消状态时,在终止线程之前, 将执行清理函数: pthread_cleanup_push, 用于释放占用资源、解锁互斥锁等。。。4// 4, 完成数据清理后, 线程执行退出操作。而操作系统和线程库负责销毁thread local数据, 比如栈空间5// 5, pthread_join被返回, 接受线程取消状态, eg:PTHREAD_CANCELED。
在引入线程取消之后,程序员在管理资源回收的难度上会急剧提升。
EgCode:
xxxxxxxxxx28123void * func(void *arg){4void *p1 = malloc(4);5void *p2 = malloc(4);67int fd = open("file1", O_RDWR);8// ... 其它代码9close(fd); // 有没有可能代码在这个, 被取消了?这决定了p1, p2的空间会不会被释放?1011free(p2);12free(p1);13return NULL;14}15int main()16{17pthread_t pid;18int res = pthread_create(&pid, NULL, func, NULL);19THREAD_ERROR_CHECK(res, "pthread_create");2021int res_cancel = pthread_cancel(pid);// 取消线程22THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");2324int res_join = pthread_join(pid, NULL);25THREAD_ERROR_CHECK(res_join, "pthread_join");2627return 0;28}在上述代码中, 我们遇到了什么问题?
xxxxxxxxxx11和我们前面通过return或者pthread_exit主动退出线程不同(是可控行为), 当我们通过pthread_cancel被动退出线程, 有可能导致被退出的线程中有些资源没有回收, 从而导致内存泄漏。为了解决这个问题, 线程库引入了
pthread_cleanup_push和pthread_cleanup_pop函数来管理线程主动或者被动终止时所申请资源(比如文件、堆空间、锁等等)。
当线程开始执行时,会创建一个清理栈(thread's stack of thread-cancellation),用于存储注册的清理函数和参数。
而我们可以通过
pthread_cleanup_push和pthread_cleanup_pop通过向这个清理栈中添加清理函数,以及pop出清理函数执行。从而确保线程的正确终止和资源回收。
xxxxxxxxxx612// push thread cancellation clean-up handlers3void pthread_cleanup_push(4void (*routine)(void *),// 指向清理函数的指针,该函数将在线程取消时被调用5void *arg // 传递给清理函数的参数6);xxxxxxxxxx512// pop thread cancellation clean-up handlers3void pthread_cleanup_pop(4int execute // 设置参数0代表弹出栈顶函数并且'不执行'这个函数, 非0代表代表弹出栈顶函数并且'执行'这个函数5);xxxxxxxxxx81ps:pthread_cleanup_push()和pthread_cleanup_pop()必须在同一个作用域中要成对出现 。2(此处可参考: /usr/include/pthread.h 对这两个方法的定义3-> 可以发现push和pop的宏定义不是语义完全的,它们必须在同一作用域中成对出现才能使花括号成功匹配。)4ps:线程运行到pthread_cleanup_pop()方法,当execute参数0代表弹出栈顶函数并且'不执行'这个函数,5当execute参数非0代表代表弹出栈顶函数并且'执行'这个函数。6ps: 通过pthread_cancel取消线程, 线程取消,所有入栈的清理函数将按照顺序依次弹栈并执行。7ps: 调用pthread_exit主动退出线程, 所有入栈的清理函数将按照顺序依次弹栈并执行。8ps: 当线程因为在start_routine中因为return结束线程, 清理函数栈将不会弹栈。(这取决于操作系统)
EgCode:
xxxxxxxxxx42123void cleanheap(void *argp){4printf("cleanheap is runing \n");5free(argp);6}7void cleanfile(void *filep){8printf("cleanfile is runing \n");9int *fdP = (int *)filep;10close(*fdP);11}12void * func(void *arg){13void *p1 = malloc(4);14pthread_cleanup_push(cleanheap, p1);15void *p2 = malloc(4);16pthread_cleanup_push(cleanheap, p2);1718int fd = open("file1", O_RDWR); // ps: 注意,代码未必能执行到这个位置19pthread_cleanup_push(cleanfile, &fd);2021// sleep(2);2223pthread_cleanup_pop(1);24pthread_cleanup_pop(1);25pthread_cleanup_pop(1);26}27int main()28{29pthread_t pid;30int res = pthread_create(&pid, NULL, func, NULL);31THREAD_ERROR_CHECK(res, "pthread_create");3233// sleep(1);34int res_cancel = pthread_cancel(pid);35THREAD_ERROR_CHECK(res_cancel, "pthread_cancel");3637int res_join = pthread_join(pid, NULL);38THREAD_ERROR_CHECK(res_join, "pthread_join");3940return 0;41}42
ps:pthread_cleanup_push()和pthread_cleanup_pop()必须在同一个作用域中要成对出现 。 (此处可参考: /usr/include/pthread.h 对这两个方法的定义 -> 可以发现push和pop的宏定义不是语义完全的,它们必须在同一作用域中成对出现才能使花括号成功匹配。)
ps:线程运行到pthread_cleanup_pop()方法: 当execute参数0代表弹出栈顶函数并且'不执行'这个函数, 当execute参数非0代表代表弹出栈顶函数并且'执行'这个函数。
ps: 通过pthread_cancel取消线程, 线程取消,所有入栈的清理函数将按照顺序依次弹栈并执行。
ps: 调用pthread_exit主动退出线程, 所有入栈的清理函数将按照顺序依次弹栈并执行。
ps: 当线程因为在start_routine中因为return结束线程, 清理函数栈将不会弹栈。(这取决于操作系统)
由于多线程之间不存在隔离,共享同一个地址在提高运行效率的同时也给用户带来了巨大的困扰。在并发执行的情况下,大量的共享资源成为
竞争条件(竞争条件是指多个线程并发访问和修改共享资源时可能出现的问题),导致程序执行的结果往往和预期的内容大相径庭。EgCode: 两个线程操作一个共享变量, "每个线程每次加一, 加一百万次"
xxxxxxxxxx2712// 两个线程对同一个共享变量分别加一百万次34int global = 0;56void * func(void *arg){7printf("child thread is runing \n");8for(int i = 0; i<TIMES; i++){9global++;10}11printf("child thread stop \n");12}13int main(int argc,char*argv[])14{15pthread_t pid;16pthread_create(&pid,NULL,func,NULL);1718printf("main thread is runing \n");19for(int i = 0; i<TIMES; i++){20global++;21}22printf("main thread stop \n");2324pthread_join(pid, NULL);25printf("all over, global = %d \n", global);26return 0;27}我们发现结果并不是我们期望的二百万。原因是什么那?
xxxxxxxxxx41// 以某次逻辑为例21, 当主线程在读取共享变量时,假设它的值为x, 那么主线程把它从内存读到寄存器, 把x自增变成x+1再写回内存。32, 在主线程完成上述操作的过程中,假设在写回内存之前, 子线程可能也已经读取了共享变量的值, 即未被主线程写回更新之前的x。 子线程的运行也是把自己读取到寄存器的x自增变成x+1再写回内存。4// 综上, 虽然两个线程都进行了一次逻辑自增执行,但是实际结果产生了覆盖, 最终只增加了1.
如果一个程序的结果是不正确的,那么再高的效率也毫无意义。在基于之前进程对并发的研究之上,线程库也提供了专门用于正确地访问共享资源的机制--> 锁。
在多线程编程中,用来控制共享资源的最简单有效也是最广泛使用的机制就是 mutex(MUTual EXclusion),即互斥锁。锁的本质是一个全局的标志位,线程可以对锁进行原子性修改,即所谓的加锁/解锁。当一个线程持有锁的时候,其余线程再尝试加锁时(包括自己再次加锁),都会使自己陷入阻塞状态,直到这把锁 -> 被持有该锁的线程解锁, 其它线程才能拿到这个锁, 拿到锁之后并且解除之前的阻塞状态恢复运行。所以锁在某个时刻永远不能被两个线程同时持有。
锁
xxxxxxxxxx21互斥锁(Mutex,全称为Mutual Exclusion)是一种用于多线程编程的同步机制。2它可以保证在任意时刻只有一个线程能够访问共享资源,从而避免了多个线程同时对共享资源进行操作, 确保线程之间的互斥执行。锁的状态
xxxxxxxxxx51(锁的本身是一个, 通过原子性操作的全局的标志位.)2// 未锁3此时没有线程拥有该锁, 任意线程都可以试图来占有/获取该锁。4// 已锁5该锁被某一个线程持有, 其它线程在获取该锁的时候阻塞。锁的行为
xxxxxxxxxx51// 加锁(原子操作)2查看锁的状态, 如果未锁, 则加锁/占有锁;3查看锁的状态, 如果已锁, 则阻塞;4// 解锁(原子操作)5让锁处于未锁状态;锁的要求
xxxxxxxxxx211, 同时只有一个线程获取锁22, 谁加锁, 谁解锁 (锁的使用纷繁复杂,如果不按照规范行事,很容易出现错误,而且随意解锁会导致代码的无可读性)临界区/临界资源
xxxxxxxxxx21加锁和解锁是成对出现的操作, 在加锁之后和解锁之前的这一段代码, 我们称为临界区.2在临界区中的这段代码,我们可以对共享资源进行操作。因为将临界区的代码块用互斥锁包裹,由于锁的互斥性, 则可以确保同一时间内只有一个线程能够进入该代码块,从而保证了共享资源的安全性。饥饿
xxxxxxxxxx31饥饿是指线程由于某种原因无法获得所需的锁,导致该线程长时间无法执行。2当多个线程竞争同一把锁时,如果某个线程经常无法获得所需的锁,那么它可能会长时间处于阻塞状态,即饥饿状态; 另外, 临界区范围过大, 也可能导致饥饿。3饥饿现象可能会导致程序性能下降等问题死锁: 重要
xxxxxxxxxx21死锁是指线程在执行过程中,由于竞争资源使用不当造成的一种永久阻塞的现象,若无外力作用,某些逻辑将再阻塞状态下无法向下推进。2使用互斥锁的时候必须小心谨慎,如果是需要持续多个锁的情况,加锁和解锁之间必须要维持一定的顺序。即使是只有一把锁,如果使用不当,也会导致死锁。xxxxxxxxxx111常见的死锁的情况21, 情况一3// 存在线程1: 先持有锁A, 再持有锁B4// 存在线程2: 先持有锁B, 再持有锁A56// 如果上述两个线程, 1先持有了A锁, 2持有了B锁; 在这种状态下1又试图去获取B锁, 2又试图获取A锁都会产生死锁72, 情况二8// 持有锁的线程在未释放锁的情况下, 线程终止了, 造成锁未释放;9// 而另外的线程试图获取这个未释放的锁103, 情况三11// 某个线程在对A锁进行加锁, 当持有了A锁之后, 在未释A放锁的情况下, 重复对A进行加锁
定义锁
xxxxxxxxxx21// 需要先定义一个pthread_mutex_t类型的锁(eg:)2pthread_mutex_t mLock;初始化锁
xxxxxxxxxx91对锁的初始化,有两种方式: (man pthread_mutex_destroy)2// 1, 一种是调用pthread_mutex_init函数3int pthread_mutex_init(4pthread_mutex_t *mutex, // 要初始化的锁5const pthread_mutexattr_t *attr // 锁的属性类型6);78// 2, 另一种是直接将锁变量初始化为PTHREAD_MUTEX_INITIALIZER9pthread_mutex_t mLock = PTHREAD_MUTEX_INITIALIZER;加锁
xxxxxxxxxx61使用pthread_mutex_lock加锁: (加锁后进入临界区)23// lock a mutex4int pthread_mutex_lock(5pthread_mutex_t *mutex // 锁6);解锁
xxxxxxxxxx61使用pthread_mutex_unlock把锁置为未锁: (临界区代码执行结束,解锁)23// unlock a mutex4int pthread_mutex_unlock(5pthread_mutex_t *mutex // 锁6);销毁锁
xxxxxxxxxx61在程序结束前回收锁,释放其占用的资源:23// destroy a mutex4int pthread_mutex_destroy(5pthread_mutex_t *mutex // 锁6);
EgCode: 锁的基本使用
xxxxxxxxxx3712// 两个线程对同一个共享变量分别加一百万次: 使用锁34int global = 0;56void * func(void *arg){7pthread_mutex_t *pmLock = (pthread_mutex_t *)arg; // 接收主线程传参, 获得锁8printf("child thread is runing \n");9for(int i = 0; i<TIMES; i++){10pthread_mutex_lock(pmLock); // 加锁11global++;12pthread_mutex_unlock(pmLock); // 解锁13}14printf("child thread stop \n");15}16int main(int argc,char*argv[])17{18pthread_mutex_t mLock; // 定义锁19pthread_mutex_init(&mLock, NULL);// 对锁初始化2021pthread_t pid;22pthread_create(&pid,NULL,func, &mLock); // 把锁传给子线程2324printf("main thread is runing \n");25for(int i = 0; i<TIMES; i++){26pthread_mutex_lock(&mLock); // 加锁27global++;28pthread_mutex_unlock(&mLock); // 解锁29}30printf("main thread stop \n");3132pthread_join(pid, NULL);33printf("all over, global = %d \n", global);3435pthread_mutex_destroy(&mLock); // 销毁锁36return 0;37}EgCode: 锁的基本使用
xxxxxxxxxx4212// 两个线程对同一个共享变量分别加一百万次: 使用锁34typedef struct share_value{5int sum;6pthread_mutex_t mLock;7} share_value_t;89void * func(void *arg){10share_value_t *pshareValue = (share_value_t *)arg;11printf("child thread is runing \n");12for(int i = 0; i<TIMES; i++){13pthread_mutex_lock(&pshareValue->mLock);14pshareValue->sum ++;15pthread_mutex_unlock(&pshareValue->mLock);16}17printf("child thread stop \n");18}1920int main(int argc,char*argv[])21{22share_value_t shareValue;23shareValue.sum = 0;24pthread_mutex_init(&shareValue.mLock, NULL);2526pthread_t pid;27pthread_create(&pid,NULL,func, &shareValue);2829printf("main thread is runing \n");30for(int i = 0; i<TIMES; i++){31pthread_mutex_lock(&shareValue.mLock);32shareValue.sum++;33pthread_mutex_unlock(&shareValue.mLock);34}35printf("main thread stop \n");3637pthread_join(pid, NULL);38printf("all over, global = %d \n", shareValue.sum);3940pthread_mutex_destroy(&shareValue.mLock);41return 0;42}
gettimeofday是一个在C语言中常用的函数,是POSIX标准的一部分,用于获取当前的时间和日期。xxxxxxxxxx612// get time3int gettimeofday(4struct timeval *tv, // 是一个指向timeval结构体的指针,用于存储获取的时间和日期5struct timezone *tz // 是一个指向timezone结构体的指针,用于指定时区信息,通常设置为NULL。6)xxxxxxxxxx41struct timeval{2time_t tv_sec; // seconds 秒3suseconds_t tv_usec; // microseconds 微妙4}EgCode: 消耗时间
xxxxxxxxxx4912// 两个线程对同一个共享变量分别加一百万次: 使用锁: 计算时间34typedef struct share_value{5int sum;6pthread_mutex_t mLock;7} share_value_t;89void * func(void *arg){1011share_value_t *pshareValue = (share_value_t *)arg;12printf("child thread is runing \n");13for(int i = 0; i<TIMES; i++){14pthread_mutex_lock(&pshareValue->mLock);15pshareValue->sum ++;16pthread_mutex_unlock(&pshareValue->mLock);17}18printf("child thread stop \n");19}2021int main(int argc,char*argv[])22{23struct timeval beginTime, endTime;24gettimeofday(&beginTime, NULL);2526share_value_t shareValue;27shareValue.sum = 0;28pthread_mutex_init(&shareValue.mLock, NULL);2930pthread_t pid;31pthread_create(&pid,NULL,func, &shareValue);3233printf("main thread is runing \n");34for(int i = 0; i<TIMES; i++){35pthread_mutex_lock(&shareValue.mLock);36shareValue.sum++;37pthread_mutex_unlock(&shareValue.mLock);38}39printf("main thread stop \n");4041pthread_join(pid, NULL);42printf("all over, global = %d \n", shareValue.sum);4344pthread_mutex_destroy(&shareValue.mLock);45gettimeofday(&endTime, NULL);4647printf("time: %ld us \n", endTime.tv_usec - beginTime.tv_usec + (endTime.tv_sec - beginTime.tv_sec)*1000000);48return 0;49}
pthread_mutex_trylock在获取互斥锁和加锁的时候,它是一种非阻塞锁, 即: 如果锁已经被其他线程持有,则该函数会立即返回一个错误代码,而不是阻塞当前线程; 如果这个锁没有被别人持有, 拿到这个锁xxxxxxxxxx812// lock a mutex3int pthread_mutex_trylock(4pthread_mutex_t *mutex5)6// int返回值, 获取到锁则返回0; 没有获取到锁则返回错误码78ps: pthread_mutex_trylock配合循环可解锁第一种死锁问题EgCode: trylock解决死锁问题
xxxxxxxxxx471// 代码一: 线程A先获得锁一再获得锁二, 线程B先获得锁二再获得锁一; 这是一个死锁逻辑234typedef struct share_value{5int sum;6pthread_mutex_t mLock1;7pthread_mutex_t mLock2;8} share_value_t;910void * func(void *arg){11share_value_t *pshareValue = (share_value_t *)arg;1213pthread_mutex_lock(&pshareValue->mLock2);14sleep(1);15pthread_mutex_lock(&pshareValue->mLock1);1617printf("child thread runing \n");1819pthread_mutex_unlock(&pshareValue->mLock1);20pthread_mutex_unlock(&pshareValue->mLock2);2122return NULL;23}24int main(int argc,char*argv[])25{26share_value_t shareValue;27shareValue.sum = 0;28pthread_mutex_init(&shareValue.mLock1, NULL);29pthread_mutex_init(&shareValue.mLock2, NULL);3031pthread_t pid;32pthread_create(&pid,NULL,func, &shareValue);3334pthread_mutex_lock(&shareValue.mLock1);35sleep(1);36pthread_mutex_lock(&shareValue.mLock2);3738printf("main thread runing \n");3940pthread_mutex_unlock(&shareValue.mLock2);41pthread_mutex_unlock(&shareValue.mLock1);4243pthread_join(pid, NULL);44pthread_mutex_destroy(&shareValue.mLock1);45pthread_mutex_destroy(&shareValue.mLock2);46return 0;47}xxxxxxxxxx531// 使用pthread_mutex_trylock处理上述死锁问题234typedef struct share_value{5int sum;6pthread_mutex_t mLock1;7pthread_mutex_t mLock2;8} share_value_t;910void * func(void *arg){11share_value_t *pshareValue = (share_value_t *)arg;1213while(1){ // 通过while循环 (这其实是自旋锁机制), 反复的试图获取所有锁, 获取不到所有的锁, 则不持有任何锁14pthread_mutex_lock(&pshareValue->mLock2);15sleep(1);16int res_trylock = pthread_mutex_trylock(&pshareValue->mLock1);17if(res_trylock != 0){18pthread_mutex_unlock(&pshareValue->mLock2);19continue;20}2122printf("child thread runing \n");2324pthread_mutex_unlock(&pshareValue->mLock1);25pthread_mutex_unlock(&pshareValue->mLock2);26break;27}28return NULL;29}30int main(int argc,char*argv[])31{32share_value_t shareValue;33shareValue.sum = 0;34pthread_mutex_init(&shareValue.mLock1, NULL);35pthread_mutex_init(&shareValue.mLock2, NULL);3637pthread_t pid;38pthread_create(&pid,NULL,func, &shareValue);3940pthread_mutex_lock(&shareValue.mLock1);41sleep(1);42pthread_mutex_lock(&shareValue.mLock2);4344printf("main thread runing \n");4546pthread_mutex_unlock(&shareValue.mLock2);47pthread_mutex_unlock(&shareValue.mLock1);4849pthread_join(pid, NULL);50pthread_mutex_destroy(&shareValue.mLock1);51pthread_mutex_destroy(&shareValue.mLock2);52return 0;53}
我们目前所使用的
pthread_mutex_*锁是一种睡眠锁,即不满足条件的线程会陷入睡眠状态(进入阻塞态,不占用CPU)。还存在另一个锁称作自旋锁,当线程不满足条件时,线程会一直循环(一直占用CPU), 直到条件成立。而我们上面代码: pthread_mutex_trylock配合while循环使用, 也是一种自旋锁的思想.
xxxxxxxxxx21ps1: 自旋锁一直占用CPU效率低, 一般适合用于条件很快就会被满足的情况。2ps2: 实际上有些操作系统内核实现互斥锁的底层就用了自旋锁。
在pthread下还提供了一种标准的自旋锁: pthread_spin_lock
xxxxxxxxxx1312// spin lock: spin->旋转34pthread_spinlock_t mLock; // 锁类型5int pthread_spin_init(pthread_spinlock_t *lock int pshared);// 初始化锁6//PTHREAD_PROCESS_PRIVATE: 表示自旋锁仅用于同一进程的不同线程之间的同步。7//PTHREAD_PROCESS_SHARED: 表示自旋锁可以用于不同进程之间的同步(前提是这个锁变量位于某种共享内存区域中)89int pthread_spin_lock(pthread_spinlock_t *lock);// 自旋获取锁10int pthread_spin_trylock(pthread_spinlock_t *lock);// 无法获得锁直接返回11int pthread_spin_unlock(pthread_spinlock_t *lock);// 解锁1213int pthread_spin_destroy(pthread_spinlock_t *lock);// 销毁锁EgCode
xxxxxxxxxx2712// 自旋锁3pthread_spinlock_t mSpinLock;4void * func(void *arg){5sleep(1);6pthread_spin_lock(&mSpinLock);7printf("child get lock \n");8pthread_spin_unlock(&mSpinLock);910return NULL;11}12int main(int argc,char*argv[])13{14pthread_spin_init(&mSpinLock, PTHREAD_PROCESS_PRIVATE);1516pthread_t pid;17pthread_create(&pid, NULL, func, NULL);1819pthread_spin_lock(&mSpinLock);20printf("main get lock \n");21sleep(10);22pthread_spin_unlock(&mSpinLock);2324pthread_join(pid, NULL);25pthread_spin_destroy(&mSpinLock);26return 0;27}
读锁: 它允许多个线程同时以读模式访问共享资源。当一个线程持有读锁时,其他线程可以同时获取读锁来读取数据,但只能有一个线程可以获取写锁来写入数据。
xxxxxxxxxx11pthread_rwlock_t my_lock; // 读写锁: 可以充当读锁, 也可以充当写锁, 读写锁xxxxxxxxxx212int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);写锁: 它只允许一个线程以写模式访问共享资源。当一个线程持有写锁时,其他线程无法获取读锁或写锁,必须等待该锁被释放后才能访问共享资源。
xxxxxxxxxx212int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
xxxxxxxxxx2612// read lock ex3int num = 10;4pthread_rwlock_t rLock = PTHREAD_RWLOCK_INITIALIZER;5void *func(void *arg){67pthread_rwlock_rdlock(&rLock);8printf("i am child thread %ld, num = %d \n", pthread_self(), num);9sleep(10);10printf("%ld free lock \n", pthread_self());11pthread_rwlock_unlock(&rLock);12}13int main(int argc,char*argv[])14{15pthread_t pid1, pid2, pid3;16pthread_create(&pid1, NULL, func, NULL);17pthread_create(&pid2, NULL, func, NULL);18pthread_create(&pid3, NULL, func, NULL);1920pthread_join(pid1, NULL);21pthread_join(pid2, NULL);22pthread_join(pid3, NULL);2324pthread_rwlock_destroy(&rLock);25return 0;26}
我们在初始化锁的时候, 调用
pthread_mutex_init(锁, 类型), 可以给锁设置不同的类型 (可man pthread_mutexattr_gettype查看)xxxxxxxxxx41PTHREAD_MUTEX_NORMAL // 普通锁.2PTHREAD_MUTEX_DEFAULT // 默认锁,和普通锁表现等价.3PTHREAD_MUTEX_ERRORCHECK // 检错锁. 同一线程中, 对某个锁重复上锁, 会返回错误, 去解锁未锁的锁,也会返回错误.是一种锁的错误检查机制4PTHREAD_MUTEX_RECURSIVE // 递归锁/可重入锁. 一个线程中可对该锁重复上锁, 通过计数标记上锁次数, 每次上锁计数+1,每次解锁技术-1; 当计数为0, 其它线程才能获取该锁.
定义锁类型
xxxxxxxxxx21// 需要先定义一个pthread_mutexattr_t的类型(eg:)2pthread_mutexattr_t mutexattr;初始化类型
xxxxxxxxxx512// initialze the mutex attributes object3int pthread_mutexattr_init(4pthread_mutexattr_t *attr // 类型变量5)设置类型
xxxxxxxxxx612// set mutex type attribute3int pthread_mutexattr_settype(4pthread_mutexattr_t *attr, // 类型变量5int type // 锁的具体类型6)使用
xxxxxxxxxx612// 初始化锁的时候, 给锁传入类型3int pthread_mutex_init(4pthread_mutex_t *mutex, // 要初始化的锁5const pthread_mutexattr_t *attr // 锁的类型6);销毁
xxxxxxxxxx512// destroy the mutex attributes object3int pthread_mutexattr_destroy(4pthread_mutexattr_t *attr // 类型变量5)
EgCode: 正常锁
xxxxxxxxxx2112// 使用正常锁3int main(int argc,char*argv[])4{5pthread_mutexattr_t attr;6pthread_mutexattr_init(&attr);7pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);89pthread_mutex_t mLock;10pthread_mutex_init(&mLock, &attr);1112pthread_mutex_lock(&mLock);13int res_errorcheck_lock = pthread_mutex_lock(&mLock);14THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition");1516pthread_mutex_unlock(&mLock);1718pthread_mutex_destroy(&mLock);19pthread_mutexattr_destroy(&attr);20return 0;21}EgCode: 检错锁
xxxxxxxxxx2112// 使用检错锁3int main(int argc,char*argv[])4{5pthread_mutexattr_t attr;6pthread_mutexattr_init(&attr);7pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);89pthread_mutex_t mLock;10pthread_mutex_init(&mLock, &attr);1112pthread_mutex_lock(&mLock);13int res_errorcheck_lock = pthread_mutex_lock(&mLock);14THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition");1516pthread_mutex_unlock(&mLock);1718pthread_mutex_destroy(&mLock);19pthread_mutexattr_destroy(&attr);20return 0;21}EgCode: 递归锁/可重复锁
xxxxxxxxxx2212// 使用重入锁3int main(int argc,char*argv[])4{5pthread_mutexattr_t attr;6pthread_mutexattr_init(&attr);7pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);89pthread_mutex_t mLock;10pthread_mutex_init(&mLock, &attr);1112pthread_mutex_lock(&mLock);13int res_errorcheck_lock = pthread_mutex_lock(&mLock);14THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition");1516pthread_mutex_unlock(&mLock);17pthread_mutex_unlock(&mLock);1819pthread_mutex_destroy(&mLock);20pthread_mutexattr_destroy(&attr);21return 0;22}
xxxxxxxxxx51以一个卖票的逻辑为例:2// 一个人卖票: 票未必每一次都能卖掉, 每一次买票的人在随机的状态下选择是否买票3// 另一个人加票4// 在初始20张票的情况下:每次卖一张5// 当第一次票小于5张的时候再追加一次票: 10张票xxxxxxxxxx601// 失败示例23typedef struct share_state{4int ticketNum;5pthread_mutex_t mLock;6} share_state_t;78void *sellFun(void *arg){9share_state_t *pShareState = (share_state_t *)arg;1011while(1){12pthread_mutex_lock(&pShareState->mLock);1314if(pShareState->ticketNum <= 0){15pthread_mutex_unlock(&pShareState->mLock);16break;17}1819struct timeval nowTime;20gettimeofday(&nowTime, NULL);21srand((unsigned int) nowTime.tv_usec );22double rand_num = (double)rand()/RAND_MAX;2324if(pShareState->ticketNum > 0 && rand_num < 0.1){25pShareState->ticketNum--;26printf("ticketNum = %d \n", pShareState->ticketNum);27pthread_mutex_unlock(&pShareState->mLock);28}2930pthread_mutex_unlock(&pShareState->mLock);31}32}33void *purchaseFun(void *arg){34share_state_t *pShareState = (share_state_t *)arg;3536while(1){37pthread_mutex_lock(&pShareState->mLock);38printf("----");39if(pShareState->ticketNum <= 5){40pShareState->ticketNum = pShareState->ticketNum + 10;41pthread_mutex_unlock(&pShareState->mLock);42break;43}44pthread_mutex_unlock(&pShareState->mLock);45}46}47int main(int argc,char*argv[])48{49share_state_t shareState;50shareState.ticketNum = 20;51pthread_mutex_init(&shareState.mLock, NULL);5253pthread_t pid1, pid2;54pthread_create(&pid2,NULL,purchaseFun,&shareState);55pthread_create(&pid1,NULL,sellFun,&shareState);5657pthread_join(pid1, NULL);58pthread_join(pid2, NULL);59return 0;60}xxxxxxxxxx6112typedef struct share_state{3int flag; // 0: 未加票, 1已加票4int ticketNum;5pthread_mutex_t mLock;6} share_state_t;78void *sellFun(void *arg){9share_state_t *pShareState = (share_state_t *)arg;1011while(1){12pthread_mutex_lock(&pShareState->mLock);1314if(pShareState->ticketNum <= 0 && pShareState->flag == 1){15pthread_mutex_unlock(&pShareState->mLock);16break;17}1819struct timeval nowTime;20gettimeofday(&nowTime, NULL);21srand((unsigned int) nowTime.tv_usec );22double rand_num = (double)rand()/RAND_MAX;2324if(pShareState->ticketNum > 0 && rand_num < 0.1){25pShareState->ticketNum--;26printf("ticketNum = %d \n", pShareState->ticketNum);27}28pthread_mutex_unlock(&pShareState->mLock);29}30}31void *purchaseFun(void *arg){32share_state_t *pShareState = (share_state_t *)arg;3334while(1){35pthread_mutex_lock(&pShareState->mLock);3637if(pShareState->ticketNum <= 5){38pShareState->ticketNum = pShareState->ticketNum + 10;39pShareState->flag = 1;40pthread_mutex_unlock(&pShareState->mLock);41break;42}4344pthread_mutex_unlock(&pShareState->mLock);45}46}47int main(int argc,char*argv[])48{49share_state_t shareState;50shareState.ticketNum = 20;51shareState.flag = 0;52pthread_mutex_init(&shareState.mLock, NULL);5354pthread_t pid1, pid2;55pthread_create(&pid2,NULL,purchaseFun,&shareState);56pthread_create(&pid1,NULL,sellFun,&shareState);5758pthread_join(pid1, NULL);59pthread_join(pid2, NULL);60return 0;61}严格上讲上述逻辑中, 追加票的线程, 是一种以轮询方式反复询问票数是否小于5张的方式进行; 这种轮询的方式比较消耗资源(因为其在一直抢占cpu).
我们接下来可以试图以一种更高效和减少资源消耗的方式, 来实现上述逻辑, 即利用线程的等待/唤醒机制, 来模拟的
异步通知方式. --> 条件变量
理论上来说,利用互斥锁可以解决所有的同步问题,但是生产实践之中往往会出现这样的问题:一个线程能够执行取决于一个共享资源的具体数值,而该共享资源的数值会随着程序的运行不断地变化,线程也经常在可运行和不可运行之间动态切换,假如纯粹使用互斥锁来解决问题的话,就会出现大量的重复的“加锁-检查条件不满足-解锁”的行为,这样的话,不满足条件的线程会经常试图占用CPU资源,上下文切换也会非常频繁。
对于这样
依赖于共享资源作为条件来控制线程之间的同步的问题,我们希望采用一种无竞争的方式让多个线程在共享资源处会和——这就是条件变量。xxxxxxxxxx81条件变量:2// 基本原理是, 线程可以在不满足`共享资源的某个条件`时等待/挂起,直到另一个线程发出通知,告诉它条件已经满足, 进一步唤醒这个等待。3Eg:4// 当A线程持有锁的时候,A认为自己做某些操作的条件还不够成熟,A可以主动让自己阻塞并且释放锁(陷入阻塞和解锁是原子的)5// 当锁被A释放,其他的线程比如B线程可以持有锁(有些情况下可以不持有锁)去修改条件的内容6// B进行逻辑操作的过程中, 一旦B认为现在是一个合适的时机唤醒A时,B可以通过唤醒操作通知到A线程。7// A线程收到唤醒通知之后,会首先恢复运行并加锁,再继续执行后续的指令。8这其中涉及到两个动作: A主动阻塞, B唤醒A
我们可以在线程运行过程中, 通过调用
pthread_cond_wait让不满足条件的线程主动阻塞, 等待被唤醒.当通过
pthread_cond_wait陷入阻塞的时候, 会先释放锁.当
pthread_cond_wait被从阻塞状态唤醒的时候, 会先加锁, 然后继续执行其后的代码逻辑.
定义条件变量
xxxxxxxxxx11pthread_cond_t cond;初始化条件变量
xxxxxxxxxx612// initialize condition variables3int pthread_cond_init(4pthread_cond_t *cond, // 条件变量的指针5pthread_condattr_t *attr // 条件变量属性对象 (默认NULL)6)// 返回值: 初始化成功返回0;否则,返回一个错误码陷入阻塞并释放锁
xxxxxxxxxx612// wait on a condition3int pthread_cond_wait(4pthread_cond_t *cond, // 条件变量的指针5pthread_mutex_t *muten // 要操作(释放和获取)的锁6)// 返回值: 成功返回0;否则,返回一个错误码唤醒以指定条件变量阻塞的线程, 并使其重新获取锁
xxxxxxxxxx512// signal a condition3int pthread_cond_signal(4pthread_cond_t *cond // 条件变量的指针5)// 返回值: 成功返回0;否则,返回一个错误码销毁条件变量
xxxxxxxxxx512// destroy a condition variables3int pthread_cond_destroy(4pthread_cond_t *cond // 条件变量的指针5)// 返回值: 成功返回0;否则,返回一个错误码
重写上面的卖票程序
xxxxxxxxxx72123typedef struct share_state{4int ticketNum;5int flag; // 0未加票, 1已经加票6pthread_mutex_t mLock;7pthread_cond_t cond;8} share_state_t;910void *sellFun(void *arg){11share_state_t *pShareState = (share_state_t *)arg;1213while(1){14pthread_mutex_lock(&pShareState->mLock);1516if(pShareState->ticketNum <= 0 && pShareState->flag != 0){17pthread_mutex_unlock(&pShareState->mLock);18break;19}2021struct timeval nowTime;22gettimeofday(&nowTime, NULL);23srand((unsigned int) nowTime.tv_usec );24double rand_num = (double)rand()/RAND_MAX;2526if(pShareState->ticketNum > 0 && rand_num < 0.1){27pShareState->ticketNum--;28printf("ticketNum = %d \n", pShareState->ticketNum);29}3031if(pShareState->ticketNum <= 5 && pShareState->flag == 0){32pthread_cond_signal(&pShareState->cond);33pthread_cond_wait(&pShareState->cond, &pShareState->mLock );34}3536pthread_mutex_unlock(&pShareState->mLock);37}38return NULL;39}40void *purchaseFun(void *arg){41share_state_t *pShareState = (share_state_t *)arg;4243pthread_mutex_lock(&pShareState->mLock);4445if(pShareState->ticketNum > 5){46pthread_cond_wait(&pShareState->cond, &pShareState->mLock);47pShareState->ticketNum = pShareState->ticketNum + 10;48pShareState->flag = 1;49}else{50pShareState->ticketNum = pShareState->ticketNum + 10;51pShareState->flag = 1;52}53pthread_cond_signal(&pShareState->cond);54pthread_mutex_unlock(&pShareState->mLock);55return NULL;56}57int main(int argc,char*argv[])58{59share_state_t shareState;60shareState.ticketNum = 20;61shareState.flag = 0;62pthread_mutex_init(&shareState.mLock, NULL);63pthread_cond_init(&shareState.cond, NULL);6465pthread_t pid1, pid2;66pthread_create(&pid2,NULL,purchaseFun,&shareState);67pthread_create(&pid1,NULL,sellFun,&shareState);6869pthread_join(pid1, NULL);70pthread_join(pid2, NULL);71return 0;72}
A, pthread_cond_signal一次只能唤醒一个被pthread_cond_wait放进唤醒队列中线程
B, 通过pthread_cond_signal唤醒的线程, 如果无法获取锁, 会立即进入阻塞, 直到锁空闲, 再自动唤醒(无需pthread_cond_signal)并持有锁( 第一个意思: 即使被pthread_cond_signal唤醒也不一定能拿到锁; 第二意思: 拿不到等一会重新拿 )
pthread_cond_timedwait是一个可设置超时的pthread_cond_waitxxxxxxxxxx71pthread_cond_timedwait和pthread_cond_wait区别:2// pthread_cond_timedwait允许设置一个超时时间,当线程等待条件变量超过这个时间后,函数会自动返回。这避免了线程无限期地等待下去,有助于防止死锁和资源占用。3// pthread_cond_timedwait返回有两种可能的情况。如果在设置的等待时间内条件变量被其他线程唤醒并满足条件,函数会返回0。如果等待超时,函数会返回ETIMEDOUT。4// 对于pthread_cond_timedwait,条件变量被唤醒或超时时,互斥锁会被重新锁定,以便线程可以安全地继续执行。56ps: 虽然pthread_cond_timedwait提供了超时机制,但在实际应用中,死锁和活锁问题仍然可能发生。仔细设计和测试代码,才是确保代码正常运行的重要条件7ps: 活锁指的是任务OR代码逻辑没有被阻塞,但是由于某些条件没有满足,导致一直重复尝试,失败,再尝试,再失败的过程。
条件变量的声明/初始化/唤醒/销毁, 皆同
pthread_cond_waitxxxxxxxxxx712// wait on a condition3int pthread_cond_timedwait(4pthread_cond_t *cond, // 条件变量的指针5pthread_mutex_t *muten, // 要操作(释放和获取)的锁6timespec *abstime // 超时时间7)// 返回值: 成功返回0;否则,返回一个错误码xxxxxxxxxx91// timespec类型2struct timespec3{4__time_t tv_sec;//秒5__syscall_slong_t tv_nsec; //纳秒6};78// __time_t -> typedef long int __time_t;9// __syscall_slong_t -> typedef long int __syscall_slong_t;
xxxxxxxxxx24123int main(int argc,char*argv[])4{56pthread_mutex_t mLock;7pthread_cond_t cond;8pthread_mutex_init(&mLock, NULL);9pthread_cond_init(&cond, NULL);1011pthread_mutex_lock(&mLock);1213time_t now = time(NULL);14struct timespec end;15end.tv_sec = now+10;16end.tv_nsec = 0;1718int res = pthread_cond_timedwait(&cond, &mLock, &end);19THREAD_ERROR_CHECK(res, "timedwait");2021pthread_mutex_unlock(&mLock);2223return 0;24}
pthread_cond_broadcast 用于唤醒以指定条件变量阻塞的线程, 并使其重新获取锁, 但是和pthread_cond_signal不同的是, 虽然都是唤醒阻塞线程, pthread_cond_signal 每次从阻塞队列中取出一个唤醒, 而pthread_cond_broadcast 是以广播的方式把指定条件变量的阻塞队列线程全部唤醒.
ps: 要注意虚假唤醒问题(eg: 以一个生产者唤醒多个消费者为例)
xxxxxxxxxx111// 虚假唤醒: pthread_cond_broadcast 唤醒了多个线程, 每一个进程一定会立即获得锁? 不会2eg: 广播: 唤醒了5个线程, 5个线程使用的还是同一把锁, 这个醒的5个线程, 都要去试图获取那个锁, 1个成功, 4个等待锁31个成功获得锁的线程做了针对性的事情, 导致条件变化了;4--> 其余线程喊醒了, 不满足条件, 做不了事, -> 虚假唤醒5--> 一种良好的解决办法6while(num >= 5){7重新wait;8}91011--> pthread_cond_broadcast 唤醒了多个线程: 要获取的不是同一把锁, 分别去试图获取自己设置的锁
xxxxxxxxxx52123typedef struct share_value{4int num;5pthread_mutex_t mLock;6pthread_cond_t cond;7} share_value_t;89void *fun(void *arg){10share_value_t *pShareValue = (share_value_t *)arg;1112pthread_mutex_lock(&pShareValue->mLock);13pShareValue->num++;14int childNum = pShareValue->num;15pthread_mutex_unlock(&pShareValue->mLock);1617sleep(childNum);18printf("i am %d child thread \n", childNum);192021pthread_mutex_lock(&pShareValue->mLock);22printf("i am %d child thread before \n", childNum);23pthread_cond_wait(&pShareValue->cond,&pShareValue->mLock);24printf("i am %d child thread after \n", childNum);25pthread_mutex_unlock(&pShareValue->mLock);2627}28int main(int argc,char*argv[])29{30share_value_t shareValue;31shareValue.num = 0;32pthread_mutex_init(&shareValue.mLock, NULL);33pthread_cond_init(&shareValue.cond, NULL);3435pthread_t pid1, pid2;36pthread_create(&pid1, NULL, fun, &shareValue);37pthread_create(&pid2, NULL, fun, &shareValue);3839sleep(5);40pthread_mutex_lock(&shareValue.mLock);41pthread_cond_broadcast(&shareValue.cond);42// pthread_cond_signal(&shareValue.cond);43pthread_mutex_unlock(&shareValue.mLock);4445pthread_join(pid1, NULL);46pthread_join(pid2, NULL);4748pthread_mutex_destroy(&shareValue.mLock);49pthread_cond_destroy(&shareValue.cond);5051return 0;52}
生产者-消费者:
xxxxxxxxxx41// 存在两个生产者, 三个消费者2// 生产者每隔3秒生产一个商品, 容器满则等待3// 消费者,在前5秒不消费, 5秒之后, 每隔一秒消费一个商品, 无商品则等待4// 容器初始10个商品, 最大上限20个商品xxxxxxxxxx122123typedef struct node_s{4int value;5struct node_s *p_next;6}node_t;78typedef struct queue_s{9node_t *pHead;10node_t *pEnd;11int size;12}queue_t;1314void offer(queue_t *pQueue, int value){15node_t *pNewNode = (node_t *)calloc(1, sizeof(node_t));16pNewNode->value = value;1718if(pQueue->size == 0){19pQueue->pHead = pNewNode;20pQueue->pEnd = pNewNode;21}else{22pQueue->pEnd->p_next = pNewNode;23pQueue->pEnd = pNewNode;24}25pQueue->size++;26}27int poll(queue_t *pQueue){28if(pQueue == NULL || pQueue->size == 0){29return 0;30}31node_t *pOldHead = pQueue->pHead;32pQueue->pHead = pOldHead->p_next;33pQueue->size--;34if(pQueue->size == 0){35pQueue->pEnd = NULL;36}37int pollValue = pOldHead->value;38free(pOldHead);39return pollValue;40}41void toString(queue_t *pQueue){42node_t *mid = pQueue->pHead;43if(mid == NULL){44return;45}46printf("== ");47while(mid){48printf("%3d ", mid->value);49mid = mid->p_next;50}51printf("== \n");52}53typedef struct share_value_s{54queue_t queue;55pthread_mutex_t mLock;56pthread_cond_t cond;57}share_value_t;58void *produce(void *arg){59share_value_t *pShareValue = (share_value_t *)arg;60while(1){61pthread_mutex_lock(&pShareValue->mLock);62while(pShareValue->queue.size >= 20){63pthread_cond_wait(&pShareValue->cond, &pShareValue->mLock);64}65int value = rand()%1000;66offer(&pShareValue->queue, value);67printf("add: %d \n", value);68toString(&pShareValue->queue);6970pthread_cond_broadcast(&pShareValue->cond);7172pthread_mutex_unlock(&pShareValue->mLock);73sleep(3);74}7576}77void *consume(void *arg){78share_value_t *pShareValue = (share_value_t *)arg;79sleep(5);80while(1){81pthread_mutex_lock(&pShareValue->mLock);82while(pShareValue->queue.size <= 0 ){83pthread_cond_wait(&pShareValue->cond, &pShareValue->mLock);84}85int value = poll(&pShareValue->queue);86printf("take out: %d \n", value);87toString(&pShareValue->queue);8889pthread_cond_broadcast(&pShareValue->cond);9091pthread_mutex_unlock(&pShareValue->mLock);92sleep(1);93}94}95int main(){96share_value_t shareValue;9798memset(&shareValue.queue, 0 , sizeof(queue_t));99for(int i=0; i<10; i++){100int data = rand()%1000;101offer(&shareValue.queue, data);102}103toString(&shareValue.queue);104pthread_mutex_init(&shareValue.mLock, NULL);105pthread_cond_init(&shareValue.cond, NULL);106107pthread_t tid1, tid2, tid3, tid4, tid5;108pthread_create(&tid1, NULL, produce, &shareValue);109pthread_create(&tid2, NULL, produce, &shareValue);110pthread_create(&tid3, NULL, consume, &shareValue);111pthread_create(&tid4, NULL, consume, &shareValue);112pthread_create(&tid5, NULL, consume, &shareValue);113114pthread_join(tid1, NULL);115pthread_join(tid2, NULL);116pthread_join(tid3, NULL);117pthread_join(tid4, NULL);118pthread_join(tid5, NULL);119120pthread_mutex_destroy(&shareValue.mLock);121pthread_cond_destroy(&shareValue.cond);122}
了解
在线程创建的时候,用户可以给线程指定一些属性,用来控制线程的调度情况、CPU绑定情况、屏障、线程调用栈和线程分离等属性。这些属性可以通过一个 pthread_attr_t 类型的变量来控制,可以使用pthread_attr_set 系列设置属性,然后可以传入 pthread_create 函数,从控制新建线程的属性。(man -k pthread_attr_set)
xxxxxxxxxx201// 设置线程的CPU2pthread_attr_setaffinity_np (3) - set/get CPU affinity attribute in thread attributes object3// 设置线程的脱离状态4pthread_attr_setdetachstate (3) - set/get detach state attribute in thread attributes object5// 设置线程的扩展内存的大小6pthread_attr_setguardsize (3) - set/get guard size attribute in thread attributes object7// 设置线程的调度属性继承方式8pthread_attr_setinheritsched (3) - set/get inherit-scheduler attribute in thread attributes object9// 线程的调度参数(包括线程的优先级、调度策略和调度参数)10pthread_attr_setschedparam (3) - set/get scheduling parameter attributes in thread attributes object11// 设置线程的调度策略(先进先出(SCHED_FIFO)、轮转法(SCHED_RR),或其它(SCHED_OTHER))12pthread_attr_setschedpolicy (3) - set/get scheduling policy attribute in thread attributes object13// 设置线程优先级的有效范围14pthread_attr_setscope (3) - set/get contention scope attribute in thread attributes object15// 设置线程栈的地址和大小。16pthread_attr_setstack (3) - set/get stack attributes in thread attributes object17// 设置线程的堆栈地址(确保线程的堆栈在特定的内存区域中创建)18pthread_attr_setstackaddr (3) - set/get stack address attribute in thread attributes object19// 设置线程的堆栈大小20pthread_attr_setstacksize (3) - set/get stack size attribute in thread attributes object
在这里,我们以 pthread_attr_setdetachstate 为例子演示如何设置线程的属性。分离/脱离属性影响一个线程的终止状态是否能被其他线程使用 pthread_join 函数捕获终止状态。如果一个线程设置了分离/脱离属性,那么另一个线程使用 pthread_join 时会返回一个报错
xxxxxxxxxx1912// 给线程设置属性3void *func(void *arg){4}5int main(int argc,char*argv[])6{7pthread_attr_t attr;8pthread_attr_init(&attr);9pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);1011pthread_t tid;12pthread_create(&tid,&attr, func, NULL);1314int res = pthread_join(tid, NULL);15THREAD_ERROR_CHECK(res, "join");1617pthread_attr_destroy(&attr);18return 0;19}
由于多线程之间是共享同一个进程地址空间,所以多线程在访问共享数据的时候会出现竞争问题,这个问题不只会发生在用户自定义函数中,在一些库函数执行中也可能会出现竞争问题。
有些库函数在设计的时候会申请额外的内存,或者会在静态区域分配数据结构——一个典型的库函数就是 ctime 。 ctime函数会把日历时间字符串存储在静态区域。
xxxxxxxxxx2712// ctime的线程安全问题3void *func(void *arg){45time_t childTime;6time(&childTime);7char *str = ctime(&childTime);89printf("child time = %s \n", str);10sleep(5);11printf("child time = %s \n", str);12}13int main(int argc,char*argv[])14{15pthread_t tid;16pthread_create(&tid,NULL,func, NULL);1718sleep(3);19time_t mainTime;20time(&mainTime);2122printf("main time = %s \n", ctime(&mainTime));2324pthread_join(tid, NULL);25return 0;26}27// ps: 22行输出时间和11行输出时间相同在上述例子中的,子线程中的 str 是指向的区域是静态的,所以即使子线程没有作任何修改,但是因为主线程会调用ctime 修改静态区域的字符串,子线程两次输出的结构会有不同。
使用 ctime_r 可以避免这个问题,ctime_r 函数会增加一个额外指针参数,这个指针可以指向一个线程私有的数据,比如函数栈帧内,从而避免发生竞争问题
xxxxxxxxxx2912// ctime的线程安全3void *func(void *arg){45time_t childTime;6time(&childTime);78char buff[100];9char *str = ctime_r(&childTime, buff);1011printf("child time = %s \n", str);12sleep(5);13printf("child time = %s \n", str);14}15int main(int argc,char*argv[])16{17pthread_t tid;18pthread_create(&tid,NULL,func, NULL);1920sleep(3);21time_t mainTime;22time(&mainTime);2324char buff[100];25printf("main time = %s \n", ctime_r(&mainTime, buff));2627pthread_join(tid, NULL);28return 0;29}类似于 ctime_r 这种函数是线程安全的,如果额外数据是分配在线程私有区域的情况下,在多线程的情况下并发地使用这些库函数是不会出现并发问题的。
在帮助手册中,库函数作者会说明线程的安全属性。
在信号/多线程情况下,一个函数异步地被重新调用过程中,如果重复的函数调用有可能会导致错乱的结果,那么这些函数就是不可重入的。
xxxxxxxxxx2012char *fun(){3static char p[20] = {0};4for(int i=0; i<20; i++){5if(p[i] == 0){6p[i] = 'a';7break;8}9}10return p;11}12int main(int argc,char*argv[])13{14char *p1 = fun();15printf("p = %s \n", p1);1617char *p2 = fun();18printf("p = %s \n", p2);19return 0;20}理论上来说,实现可重入函数有下列需求:
访问静态或者全局数据必须采取同步手段。
不能调用非可重入函数。
xxxxxxxxxx41// 如果在一个函数中, 这个函数运行, 依赖于静态数据2或者3// 一个函数中, 它本身又调用了, 另一个不可重入函数4--> 大概率也是不可重入的函数xxxxxxxxxx11
一个比较典型的不可重入函数例子就是 malloc 函数, malloc 函数必然是要修改静态数据的,为了保证线程安全性, malloc 函数的实现当中会存在加锁和解锁的过程,假如 malloc 执行到加锁之后,解锁之前的时候,此时有信号产生并且递送的话,线程会转向执行信号处理回调函数,假如信号处理函数当中又调用了 malloc 函数,此时就会导致死锁—>这就是 malloc 的不可重入性。
xxxxxxxxxx11