您当前的位置: 首页 >  嵌入式

正点原子

暂无认证

  • 1浏览

    0关注

    382博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【正点原子Linux连载】第十一章 线程-摘自【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.1

正点原子 发布时间:2021-08-14 11:09:10 ,浏览量:1

1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html 3)对正点原子Linux感兴趣的同学可以加群讨论:935446741 4)关注正点原子公众号,获取最新资料更新 在这里插入图片描述

第十一章线程

上一章,学习了进程相关的知识内容,对进程有了一个比较全面的认识和理解;本章开始,将学习Linux应用编程中非常重要的编程技巧—线程(Thread);与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元是线程、而并非进程。虽然线程的概念比较简单,但是其所涉及到的内容比较多,所以本章篇幅会相对比较长,大家加油! 本章将会讨论如下主题内容。

线程的基本概念,线程VS进程; 线程标识; 线程创建与回收; 线程取消; 线程终止; 线程分离; 线程同步技术; 线程安全。

12.1线程概述 12.1.1线程概念 什么是线程? 线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务task1和task2,可将两个不同的任务分别放置在两个线程中。 线程是如何创建起来的? 当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以main()做为入口开始运行的,所以main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。 所以由此可知,任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,譬如前面章节内容中所编写的所有应用程序都是单线程程序,它们只有主线程;既然有单线程进程,那自然就存在多线程进程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create创建一个新的线程),那么创建的新线程就是主线程的子线程。 主线程的重要性体现在两方面: 其它新的线程(也就是子线程)是由主线程创建的; 主线程通常会在最后结束运行,执行各种清理工作,譬如回收各个子线程。 线程的特点? 线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。 同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。 在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被CPU执行,线程具有以下一些特点: 线程不单独存在、而是包含在进程中; 线程是参与系统调度的基本单位; 可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果; 共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。 线程与进程? 进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程)并发处理多任务的需求,那我们究竟选择哪种处理方式呢?首先我们就需要来分析下多进程和多线程两种编程模型的优势和劣势。 多进程编程的劣势: 进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。 进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦,在上一章节给大家有所介绍。 解决方案便是使用多线程编程,多线程能够弥补上面的问题: 同一进程的多个线程间切换开销比较小。 同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易。 线程创建的速度远大于进程创建的速度。 多线程在多核处理器上更有优势! 终上所述,多线程编程相比于多进程编程的优势是比较明显的,在实际的应用当中多线程远比多进程应用更为广泛。那既然如此,为何还存在多进程编程模型呢?难道多线程编程就不存在缺点吗?当然不是,多线程也有它的缺点、劣势,譬如多线程编程难度高,对程序员的编程功底要求比较高,因为在多线程环境下需要考虑很多的问题,例如线程安全问题、信号处理的问题等,编写与调试一个多线程程序比单线程程序困难得多。 当然除此之外,还有一些其它的缺点,这里就不再一一列举了。多进程编程通常会用在一些大型应用程序项目中,譬如网络服务器应用程序,在中小型应用程序中用的比较少。 12.1.2并发和并行 在前面的内容中,曾多次提到了并发这个概念,与此相类似的概念还有并行、串行,这里和大家聊一聊这些概念含义的区别。 对于串行比较容易理解,它指的是一种顺序执行,譬如先完成task1,接着做task2、直到完成task2,然后做task3、直到完成task3……依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。 在这里插入图片描述

图 12.1.1 串行运行示意图 并行与串行则截然不同,并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,譬如并行运行task1、task2、task3。 在这里插入图片描述

图 12.1.2 并行运行示意图1 并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着,譬如: 在这里插入图片描述

图 12.1.3 并行运行示意图2 相比于串行和并行,并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行。如下图所示: 在这里插入图片描述

图 12.1.4 并发运行示例图 笔者在网络上看到了很多比较有意思、形象生动的比喻,用来说明串行、并行以及并发这三个概念的区别,这里笔者截取其中的一个: 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并行,仅仅只是串行。 你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 这里再次进行总结: 串行:一件事、一件事接着做 并发:交替做不同的事; 并行:同时做不同的事。 需要注意的是,并行运行情况下的多个执行单元,每一个执行单元同样也可以以并发方式运行。 从通用角度上介绍完这三个概念之后,类比到计算机系统中,首先我们需要知道两个前提条件: 多核处理器和单核处理器:对于单核处理器来说,只有一个执行单元,同时只能执行一条指令;而对于多核处理起来说,有多个执行单元,可以并行执行多条指令,譬如8核处理器,那么可以并行执行8条不同的指令。 计算机操作系统中,通常同时运行着几十上百个不同的线程,在单核或多核处理系统中都是如此! 对于单核处理器系统来说,它只有一个执行单元(譬如正点原子的阿尔法I.MX6U平台,单核Cortex-A7 SoC),只能采用并发运行系统中的线程,而肯定不可能是串行,而事实上确实如此。内核实现了调度算法,用于控制系统中所有线程的调度,简单点来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。在前面章节内容中也给大家有简单地提到过系统调用的问题,关于更加详细的内容,这里便不再介绍了,我们只需有个大概的认识、了解即可! 对于多核处理器系统来说,它拥有多个执行单元,在操作系统中,多个执行单元以并行方式运行多个线程,同时每一个执行单元以并发方式运行系统中的多个线程。 同时运行 计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替/交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程。 这就好比现实生活中所看到的一些事情,它所给带来的视角效果,譬如一辆车在高速上行驶,有时你会感觉到车的轮毂没有转动,一种视角暂留现象,因为车轮转动速度太快了,人眼是看不清的,会感觉车轮好像是静止的,事实上,车轮肯定是在转动着。 本小节的内容到这里就结束了,理解了本小节的内容,对于后面内容的将会有很大的帮助、也可以帮助大家快速理解后面的内容,大家加油! 12.2线程ID 就像每个进程都有一个进程ID一样,每个线程也有其对应的标识,称为线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。 进程ID使用pid_t数据类型来表示,它是一个非负整数。而线程ID使用pthread_t数据类型来表示,一个线程可通过库函数pthread_self()来获取自己的线程ID,其函数原型如下所示: #include

pthread_t pthread_self(void); 使用该函数需要包含头文件。 该函数调用总是成功,返回当前线程的线程ID。 可以使用pthread_equal()函数来检查两个线程ID是否相等,其函数原型如下所示: #include

int pthread_equal(pthread_t t1, pthread_t t2); 如果两个线程ID t1和t2相等,则pthread_equal()返回一个非零值;否则返回0。在Linux系统中,使用无符号长整型(unsigned long int)来表示pthread_t数据类型,但是在其它系统当中,则不一定是无符号长整型,所以我们必须将pthread_t作为一种不透明的数据类型加以对待,所以pthread_equal()函数用于比较两个线程ID是否相等是有用的。 线程ID在应用程序中非常有用,原因如下: 很多线程相关函数,譬如后面将要学习的pthread_cancel()、pthread_detach()、pthread_join()等,它们都是利用线程ID来标识要操作的目标线程; 在一些应用程序中,以特定线程的线程ID作为动态数据结构的标签,这某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。 12.3创建线程 启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程,本小节我们讨论如何创建一个新的线程。 主线程可以使用库函数pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程,其函数原型如下所示: #include

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 使用该函数需要包含头文件。 函数参数和返回值含义如下: thread:pthread_t类型指针,当pthread_create()成功返回时,新创建的线程的线程ID会保存在参数thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。 attr:pthread_attr_t类型指针,指向pthread_attr_t类型的缓冲区,pthread_attr_t数据类型定义了线程的各种属性,关于线程属性将会在11.7小节介绍。如果将参数attr设置为NULL,那么表示将线程的所有属性设置为默认值,以此创建新线程。 start_routine:参数start_routine是一个函数指针,指向一个函数,新创建的线程从start_routine()函数开始运行,该函数返回值类型为void *,并且该函数的参数只有一个void *,其实这个参数就是pthread_create()函数的第四个参数arg。如果需要向start_routine()传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为arg参数传入。 arg:传递给start_routine()函数的参数。一般情况下,需要将arg指向一个全局或堆变量,意思就是说在线程的生命周期中,该arg指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数arg设置为NULL,表示不需要传入参数给start_routine()函数。 返回值:成功返回0;失败时将返回一个错误号,并且参数thread指向的内容是不确定的。 注意pthread_create()在调用失败时通常会返回错误码,它并不像其它库函数或系统调用一样设置errno,每个线程都提供了全局变量errno的副本,这只是为了与使用errno到的函数进行兼容,在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误的范围限制在引起出错的函数中。 线程创建成功,新线程就会加入到系统调度队列中,获取到CPU之后就会立马从start_routine()函数开始运行该线程的任务;调用pthread_create()函数后,通常我们无法确定系统接着会调度哪一个线程来使用CPU资源,先调度主线程还是新创建的线程呢(而在多核CPU或多CPU系统中,多核线程可能会在不同的核心上同时执行)?如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现。这与前面学习父、子进程时也出现了这个问题,无法确定父进程、子进程谁先被系统调度。 使用示例 使用pthread_create()函数创建一个除主线程之外的新线程,示例代码如下所示: 示例代码 12.3.1 pthread_create()创建线程使用示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程: 进程ID  线程ID\n", getpid(), pthread_self());
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }

    printf("主线程: 进程ID  线程ID\n", getpid(), pthread_self());
    sleep(1);
    exit(0);
}

应该将pthread_t作为一种不透明的数据类型加以对待,但是在示例代码中需要打印线程ID,所以要明确其数据类型,示例代码中使用了printf()函数打印线程ID时,将其作为unsigned long int数据类型,在Linux系统下,确实是使用unsigned long int来表示pthread_t,所以这样做没有问题! 主线程休眠了1秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创建的线程还没有机会运行,整个进程就结束了。 在主线程和新线程中,分别通过getpid()和pthread_self()来获取进程ID和线程ID,将结果打印出来,运行结果如下所示: 在这里插入图片描述

图 12.3.1 编译报错 编译时出现了错误,提示“对‘pthread_create’未定义的引用”,示例代码确实已经包含了头文件,但为什么会出现这样的报错,仔细看,这个报错是出现在程序代码链接时、而并非是编译过程,所以可知这是链接库的文件,如何解决呢? gcc -o testApp testApp.c -lpthread 使用-l选项指定链接库pthread,原因在于pthread不在gcc的默认链接库中,所以需要手动指定。再次编译便不会有问题了,如下: 在这里插入图片描述

图 12.3.2 测试结果 从打印信息可知,正如前面所介绍那样,两个线程的进程ID相同,说明新创建的线程与主线程本来就属于同一个进程,但是它们的线程ID不同。从打印结果可知,Linux系统下线程ID数值非常大,看起来像是一个指针。 12.4终止线程 在示例代码 11.3.1中,我们在新线程的启动函数(线程start函数)new_thread_start()通过return返回之后,意味着该线程已经终止了,除了在线程start函数中执行return语句终止线程外,终止线程的方式还有多种,可以通过如下方式终止线程的运行: 线程的start函数执行return语句并返回指定值,返回值就是线程的退出码; 线程调用pthread_exit()函数; 调用pthread_cancel()取消线程(将在11.6小节介绍); 如果进程中的任意线程调用exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意! pthread_exit()函数将终止调用它的线程,其函数原型如下所示: #include

void pthread_exit(void *retval); 使用该函数需要包含头文件。 参数retval的数据类型为void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用pthread_join()来获取;同理,如果线程是在start函数中执行return语句终止,那么return的返回值也是可以通过pthread_join()来获取的。 参数retval所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效;出于同样的理由,也不应在线程栈中分配线程start函数的返回值。 调用pthread_exit()相当于在线程的start函数中执行return语句,不同之处在于,可在线程start函数所调用的任意函数中调用pthread_exit()来终止线程。如果主线程调用了pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止。 使用示例 示例代码 12.4.1 pthread_exit()终止线程使用示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程start\n");
    sleep(1);
    printf("新线程end\n");
    pthread_exit(NULL);
}

int main(void)
{
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }

    printf("主线程end\n");
    pthread_exit(NULL);
    exit(0);
}

新线程中调用sleep()休眠,保证主线程先调用pthread_exit()终止,休眠结束之后新线程也调用pthread_exit()终止,编译测试看看打印结果: 在这里插入图片描述

图 12.4.1 测试结果 正如上面介绍到,主线程调用pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。 12.5回收线程 在父、子进程当中,父进程可通过wait()函数(或其变体waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;pthread_join()函数原型如下所示: #include

int pthread_join(pthread_t thread, void **retval); 使用该函数需要包含头文件。 函数参数和返回值含义如下: thread:pthread_join()等待指定线程的终止,通过参数thread(线程ID)指定需要等待的线程; retval:如果参数retval不为NULL,则pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程start函数中执行return语句对应的返回值)复制到retval所指向的内存区域;如果目标线程被pthread_cancel()取消,则将PTHREAD_CANCELED放在retval中。如果对目标线程的终止状态不感兴趣,则可将参数retval设置为NULL。 返回值:成功返回0;失败将返回错误码。 调用pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则pthread_join()立刻返回。如果多个线程同时尝试调用pthread_join()等待指定线程的终止,那么结果将是不确定的。 若线程并未分离(detached,将在11.7小节介绍),则必须使用pthread_join()来等待线程终止,回收线程资源;如果线程终止后,其它线程没有调用pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。 当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。 所以,通过上面的介绍可知,pthread_join()执行的功能类似于针对进程的waitpid()调用,不过二者之间存在一些显著差别: 线程之间关系是对等的。进程中的任意线程均可调用pthread_join()函数来等待另一个线程的终止。譬如,如果线程A创建了线程B,线程B再创建线程C,那么线程A可以调用pthread_join()等待线程C的终止,线程C也可以调用pthread_join()等待线程A的终止;这与进程间层次关系不同,父进程如果使用fork()创建了子进程,那么它也是唯一能够对子进程调用wait()的进程,线程之间不存在这样的关系。 不能以非阻塞的方式调用pthread_join()。对于进程,调用waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。 使用示例 示例代码 12.5.1 pthread_join()等待线程终止

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程start\n");
    sleep(2);
    printf("新线程end\n");
    pthread_exit((void *)10);
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

主线程调用pthread_create()创建新线程之后,新线程执行new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止,新线程终止后,pthread_join()返回,将目标线程的退出码保存在*tret所指向的内存中。测试结果如下: 在这里插入图片描述

图 12.5.1 测试结果 12.6取消线程 在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用pthread_exit()退出,或在线程start函数执行return语句退出。 有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。 本小节就来讨论Linux系统下的线程取消机制。 12.6.1取消一个线程 通过调用pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示: #include

int pthread_cancel(pthread_t thread); 使用该函数需要包含头文件,参数thread指定需要取消的目标线程;成功返回0,失败将返回错误码。 发出取消请求之后,函数pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为PTHREAD_CANCELED(其实就是(void *)-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消(11.6.2小节介绍),所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。 使用示例 示例代码 12.6.1 pthread_cancel()取消线程使用示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程--running\n");
    for ( ; ; )
        sleep(1);
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

主线程创建新线程,新线程new_thread_start()函数直接运行for死循环;主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。测试结果如下: 在这里插入图片描述

图 12.6.1 测试结果 由打印结果可知,当主线程发送取消请求之后,新线程便退出了,而且退出码为-1,也就是PTHREAD_CANCELED。 12.6.2取消状态以及类型 默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消,通过pthread_setcancelstate()和pthread_setcanceltype()来设置线程的取消性状态和类型。 #include

int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype); 使用这些函数需要包含头文件,pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数state中给定的值,并将线程之前的取消性状态保存在参数oldstate指向的缓冲区中,如果对之前的状态不感兴趣,Linux允许将参数oldstate设置为NULL;pthread_setcancelstate()调用成功将返回0,失败返回非0值的错误码。 pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。 参数state必须是以下值之一: PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。 PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE。 使用示例 修改示例代码 11.6.1,在新线程的new_thread_start()函数中调用pthread_setcancelstate()函数将线程的取消性状态设置为PTHREAD_CANCEL_DISABLE,我们来试试,此时主线程还能不能取消新线程,示例代码如下所示: 示例代码 12.6.2 pthread_setcancelstate()使用示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    /* 设置为不可被取消 */
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);

    for ( ; ; ) {
        printf("新线程--running\n");
        sleep(2);
    }
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

新线程new_thread_start()函数中调用pthread_setcancelstate()将自己设置为不可被取消,主线程延时1秒钟之后调用pthread_cancel()向新线程发送取消请求,那么此时新线程是不会终止的,pthread_cancel()立刻返回之后进入到pthread_join()函数,那么此时会被阻塞等待新线程终止,接下来运行测试看看,结果会不会是这样: 在这里插入图片描述

图 12.6.2 测试结果 测试结果确实如此,将一直重复打印"新线程–running",因为新线程是一个死循环(测试完成按Ctrl+C退出)。 pthread_setcanceltype()函数 如果线程的取消性状态为PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用pthread_setcanceltype()函数来设置,它的参数type指定了需要设置的类型,而线程之前的取消性类型则会保存在参数oldtype所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux下允许将参数oldtype设置为NULL。同样pthread_setcanceltype()函数调用成功将返回0,失败返回非0值的错误码。 pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。 参数type必须是以下值之一: PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point,将在11.6.3小节介绍)为止,这是所有新建线程包括主线程默认的取消性类型。 PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少,不再介绍! 当某个线程调用fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型,而当某线程调用exec函数时,会将新程序主线程的取消性状态和类型重置为默认值,也就是PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED。 12.6.3取消点 若将线程的取消性类型设置为PTHREAD_CANCEL_DEFERRED时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。 那什么是取消点呢?所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。 取消点函数包括哪些呢?下表给大家简单地列出了一些: 表 11.6.1 可作为取消点的函数 accept() mq_timedsend() pthread_join() sendto() aio_suspend() msgrcv() pthread_testcancel() sigsuspend() clock_nanosleep() msgsnd() pwrite() sigtimedwait() close() msync() read() sigwait() connect() nanosleep() readv() sigwaitinfo() creat() open() recv() sleep() fcntl() openat() recvfrom() system() fdatasync() pause() recvmsg() tcdrain() fsync() poll() select() wait() lockf() pread() sem_timedwait() waitid() mq_receive() pselect() sem_wait() waitpid() mq_send() pthread_cond_timedwait() send() write() mq_timedreceive() pthread_cond_wait() sendmsg() writev() 除了表 11.6.1所列函数之外,还有大量的函数,系统实现可以将其作为取消点,这里便不再一一列举出来了,大家也可以通过man手册进行查询,命令为"man 7 pthreads",如下所示: 在这里插入图片描述

图 12.6.3 查看可作为取消点的函数 线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)。 示例代码 11.6.1中,新线程处于for循环之中,调用sleep()休眠,由表 11.6.1可知,sleep()函数可以作为取消点(printf可能也是),当新线程接收到取消请求之后,便会立马退出,当如果将其修改为如下: static void *new_thread_start(void *arg) { printf(“新线程–running\n”); for ( ; ; ) { } return (void *)0; } 那么线程将永远无法被取消,因为这里不存在取消点。大家可以将代码进行修改测试,看结果是不是如此! 12.6.4线程可取消性的检测 假设线程执行的是一个不含取消点的循环(譬如for循环、while循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它,就如上小节最后给大家列举的例子。 在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。其函数原型如下所示: #include

void pthread_testcancel(void); 功能测试 接下来进行一个测试,主线程创建一个新的进程,新进程的取消性状态和类型置为默认,新进程最终执行的是一个不含取消点的循环;主线程向新线程发送取消请求,示例代码如下所示: 示例代码 12.6.3 不含取消点的循环

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    for ( ; ; ) {

    }
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

新线程的new_thread_start()函数中是一个for死循环,没有执行任何函数,所以是一个没有取消点的循环体,主线程调用pthread_cancel()是无法将其终止的,接下来测试下结果是否如此: 在这里插入图片描述

图 12.6.4 测试结果 执行完之后,程序一直会没有退出,说明主线程确实无法终止新线程。接下来再做一个测试,在new_thread_start函数的for循环体中执行pthread_testcancel()函数,如下所示: 示例代码 12.6.4 使用pthread_testcancel()产生取消点

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    for ( ; ; ) {
        pthread_testcancel();
    }
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    /* 向新线程发送取消请求 */
    ret = pthread_cancel(tid);
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

如果pthread_testcancel()可以产生取消点,那么主线程便可以终止新线程,测试结果如下: 在这里插入图片描述

图 12.6.5 测试结果 从打印结果可知,确实如上面介绍那样,pthread_testcancel()函数就是取消点。 12.7分离线程 默认情况下,当线程终止时,其它线程可以通过调用pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用pthread_detach()将指定线程进行分离,也就是分离线程,pthread_detach()函数原型如下所示: #include

int pthread_detach(pthread_t thread); 使用该函数需要包含头文件,参数thread指定需要分离的线程,函数pthread_detach()调用成功将返回0;失败将返回一个错误码。 一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如: pthread_detach(pthread_self()); 一旦线程处于分离状态,就不能再使用pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。 使用示例 示例代码 12.7.1 pthread_detach()分离线程使用示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    int ret;

    /* 分离自行分离 */
    ret = pthread_detach(pthread_self());
    if (ret) {
        fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
        return NULL;
    }

    printf("新线程start\n");
    sleep(2);   //休眠2秒钟
    printf("新线程end\n");
    pthread_exit(NULL);
}

int main(void)
{
    pthread_t tid;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);   //休眠1秒钟

    /* 等待新线程终止 */
    ret = pthread_join(tid, NULL);
    if (ret)
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));

    pthread_exit(NULL);
}

示例代码中,主线程创建新的线程之后,休眠1秒钟,调用pthread_join()等待新线程终止;新线程调用pthread_detach(pthread_self())将自己分离,休眠2秒钟之后pthread_exit()退出线程;主线程休眠1秒钟是能够确保调用pthread_join()函数时新线程已经将自己分离了,所以按照上面的介绍可知,此时主线程调用pthread_join()必然会失败,测试结果如下: 在这里插入图片描述

图 12.7.1 测试结果 打印结果正如我们所料,主线程调用pthread_join()确实会出错,错误提示为“Invalid argument”。 12.8注册线程清理处理函数 9.1.2小节学习了atexit()函数,使用atexit()函数注册进程终止处理函数,当进程调用exit()退出时就会执行进程终止处理函数;其实,当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数,我们把这个称为线程清理函数(thread cleanup handler)。 与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构,也就是说它们的执行顺序与注册(添加)顺序相反,当执行完所有清理函数后,线程终止。 线程通过函数pthread_cleanup_push()和pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数,函数原型如下所示: #include

void pthread_cleanup_push(void (*routine)(void *), void *arg); void pthread_cleanup_pop(int execute); 使用这些函数需要包含头文件。 调用pthread_cleanup_push()向清理函数栈中添加一个清理函数,第一个参数routine是一个函数指针,指向一个需要添加的清理函数,routine()函数无返回值,只有一个void *类型参数;第二个参数arg,当调用清理函数routine()时,将arg作为routine()函数的参数。 既然有添加,自然就会伴随着删除,就好比对应入栈和出栈,调用函数pthread_cleanup_pop()可以将清理函数栈中最顶层(也就是最后添加的函数,最后入栈)的函数移除。 当线程执行以下动作时,清理函数栈中的清理函数才会被执行: 线程调用pthread_exit()退出时; 线程响应取消请求时; 用非0参数调用pthread_cleanup_pop() 除了以上三种情况之外,其它方式终止线程将不会执行线程清理函数,譬如在线程start函数中执行return语句退出时不会执行清理函数。 函数pthread_cleanup_pop()的execute参数,可以取值为0,也可以为非0;如果为0,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除;如果参数execute为非0,则除了将清理函数栈中最顶层的函数移除之外,还会该清理函数。 尽管上面我们将pthread_cleanup_push()和pthread_cleanup_pop()称之为函数,但它们是通过宏来实现,可展开为分别由{和}所包裹的语句序列,所以必须在与线程相同的作用域中以匹配对的形式使用,必须一一对应着来使用,譬如: pthread_cleanup_push(cleanup, NULL); pthread_cleanup_push(cleanup, NULL); pthread_cleanup_push(cleanup, NULL); … pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_cleanup_pop(0); 否则会编译报错,如下所示: 在这里插入图片描述

图 12.8.1 编译报错 使用示例 示例代码 11.8.1给出了一个使用线程清理函数的例子,虽然例子并没有什么实际作用,当它描述了其中所涉及到的清理机制。 示例代码 12.8.1 pthread_cleanup_push()注册线程清理函数

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}

static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    pthread_cleanup_push(cleanup, "第1次调用");
    pthread_cleanup_push(cleanup, "第2次调用");
    pthread_cleanup_push(cleanup, "第3次调用");

    sleep(2);
    pthread_exit((void *)0);    //线程终止

    /* 为了与pthread_cleanup_push配对,不添加程序编译会通不过 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

主线程创建新线程之后,调用pthread_join()等待新线程终止;新线程调用pthread_cleanup_push()函数添加线程清理函数,调用了三次,但每次添加的都是同一个函数,只是传入的参数不同;清理函数添加完成,休眠一段时间之后,调用pthread_exit()退出。之后还调用了3次pthread_cleanup_pop(),在这里的目的仅仅只是为了与pthread_cleanup_push()配对使用,否则编译不通过。接下来编译运行: 在这里插入图片描述

图 12.8.2 测试结果 从打印结果可知,先添加到线程清理函数栈中的函数会后被执行,添加顺序与执行顺序相反。 将新线程中调用的pthread_exit()替换为return,在进行测试,发现并不会执行清理函数。 有时在线程功能设计中,线程清理函数并不一定需要在线程退出时才执行,譬如当完成某一个步骤之后,就需要执行线程清理函数,此时我们可以调用pthread_cleanup_pop()并传入非0参数,来手动执行线程清理函数,示例代码如下所示: 示例代码 12.8.2 手动执行线程清理函数

#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}

static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    pthread_cleanup_push(cleanup, "第1次调用");
    pthread_cleanup_push(cleanup, "第2次调用");
    pthread_cleanup_push(cleanup, "第3次调用");

    pthread_cleanup_pop(1);     //执行最顶层的清理函数
    printf("~~~~~~~~~~~~~~~~~\n");
    sleep(2);
    pthread_exit((void *)0);    //线程终止

    /* 为了与pthread_cleanup_push配对 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

上述代码中,在新线程调用pthread_exit()之前,先调用pthread_cleanup_pop(1)手动运行了最顶层的清理函数,并将其从栈中移除,测试结果: 在这里插入图片描述

图 12.8.3 测试结果 从打印结果可知,调用pthread_cleanup_pop(1)执行了最后一次注册的清理函数,调用pthread_exit()退出线程时执行了2次清理函数,因为前面调用pthread_cleanup_pop()已经将顶层的清理函数移除栈中了,自然在退出时就不会再执行了。 12.9线程属性 如前所述,调用pthread_create()创建线程,可对新建线程的各种属性进行设置。在Linux下,使用pthread_attr_t数据类型定义线程的所有属性,本书并不打算详细讨论这些属性,以介绍为主,简单地了解下线程属性。 调用pthread_create()创建线程时,参数attr设置为NULL,表示使用属性的默认值创建线程。如果不使用默认值,参数attr必须要指向一个pthread_attr_t对象,而不能使用NULL。当定义pthread_attr_t对象之后,需要使用pthread_attr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用pthread_attr_destroy()函数将其销毁,函数原型如下所示: #include

int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr); 使用这些函数需要包含头文件,参数attr指向一个pthread_attr_t对象,即需要进行初始化的线程属性对象。在调用成功时返回0,失败将返回一个非0值的错误码。 调用pthread_attr_init()函数会将指定的pthread_attr_t对象中定义的各种线程属性初始化为它们各自对应的默认值。 pthread_attr_t数据结构中包含的属性比较多,本小节并不会一一点出,可能比较关注属性包括:线程栈的位置和大小、线程调度策略和优先级,以及线程的分离状态属性等。Linux为pthread_attr_t对象的每种属性提供了设置属性的接口以及获取属性的接口。 12.9.1线程栈属性 每个线程都有自己的栈空间,pthread_attr_t数据结构中定义了栈的起始地址以及栈大小,调用函数pthread_attr_getstack()可以获取这些信息,函数pthread_attr_setstack()对栈起始地址和栈大小进行设置,其函数原型如下所示: #include

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t stacksize); 使用这些函数需要包含头文件,函数pthread_attr_getstack(),参数和返回值含义如下: attr:参数attr指向线程属性对象。 stackaddr:调用pthread_attr_getstack()可获取栈起始地址,并将起始地址信息保存在stackaddr中; stacksize:调用pthread_attr_getstack()可获取栈大小,并将栈大小信息保存在参数stacksize所指向的内存中; 返回值:成功返回0,失败将返回一个非0值的错误码。 函数pthread_attr_setstack(),参数和返回值含义如下: attr:参数attr指向线程属性对象。 stackaddr:设置栈起始地址为指定值。 stacksize:设置栈大小为指定值; 返回值:成功返回0,失败将返回一个非0值的错误码。 如果想单独获取或设置栈大小、栈起始地址,可以使用下面这些函数: #include

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize); int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr); 使用示例 创建新的线程,将线程的栈大小设置为4Kbyte。 示例代码 12.9.1 设置线程栈大小pthread_attr_getstack()

#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    puts("Hello World!");
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_attr_t attr;
    size_t stacksize;
    pthread_t tid;
    int ret;

    /* 对attr对象进行初始化 */
    pthread_attr_init(&attr);

    /* 设置栈大小为4K */
    pthread_attr_setstacksize(&attr, 4096);

    /* 创建新线程 */
    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待新线程终止 */
    ret = pthread_join(tid, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 销毁attr对象 */
    pthread_attr_destroy(&attr);
    exit(0);
}

12.9.2分离状态属性 前面介绍了线程分离的概念,如果对现已创建的某个线程的终止状态不感兴趣,可以使用pthread_detach()函数将其分离,那么该线程在退出时,操作系统会自动回收它所占用的资源。 如果我们在创建线程时就确定要将该线程分离,可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始运行就处于分离状态。调用函数pthread_attr_setdetachstate()设置detachstate线程属性,调用pthread_attr_getdetachstate()获取detachstate线程属性,其函数原型如下所示: #include

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 需要包含头文件,参数attr指向pthread_attr_t对象;调用pthread_attr_setdetachstate()函数将detachstate线程属性设置为参数detachstate所指定的值,参数detachstate取值如下: PTHREAD_CREATE_DETACHED:新建线程一开始运行便处于分离状态,以分离状态启动线程,无法被其它线程调用pthread_join()回收,线程结束后由操作系统收回其所占用的资源; PTHREAD_CREATE_JOINABLE:这是detachstate线程属性的默认值,正常启动线程,可以被其它线程获取终止状态信息。 函数pthread_attr_getdetachstate()用于获取detachstate线程属性,将detachstate线程属性保存在参数detachstate所指定的内存中。 使用示例 示例代码 11.9.2给出了以分离状态启动线程的示例。 示例代码 12.9.2 以分离状态启动线程

#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
    puts("Hello World!");
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_attr_t attr;
    pthread_t tid;
    int ret;

    /* 对attr对象进行初始化 */
    pthread_attr_init(&attr);

    /* 设置以分离状态启动线程 */
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    /* 创建新线程 */
    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    /* 销毁attr对象 */
    pthread_attr_destroy(&attr);
    exit(0);
}

12.10线程安全 当我们编写的程序是一个多线程应用程序时,就不得不考虑到线程安全的问题,确保我们编写的程序是一个线程安全(thread-safe)的多线程应用程序,什么是线程安全以及如何保证线程安全?带着这些问题,本小节将讨论线程安全相关的话题。 Tips:在阅读本小节内容之前,建议先阅读第十二章内容,这章内容原本计划是放在本小节内容之前的,但由于排版问题,不得不将其单独列为一章。 12.10.1线程栈 进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。譬如主线程调用pthread_create()创建了一个新的线程,那么这个新的线程有它自己独立的栈地址空间、而主线程也有它自己独立的栈地址空间。通过11.9.1小节可知,在创建一个新的线程时,可以配置线程栈的大小以及起始地址,当然在大部分情况下,保持默认即可! 既然每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,它们不会相互干扰。在示例代码 11.10.1中,主线程创建了5个新的线程,这5个线程使用同一个start函数new_thread,该函数中定义了局部变量number和tid以及arg参数,意味着这5个线程的线程栈中都各自为这些变量分配了内存空间,任何一个线程修改了number或tid都不会影响其它线程。 示例代码 12.10.1 线程栈示例

#include 
#include 
#include 

static void *new_thread(void *arg)
{
    int number = *((int *)arg);
    unsigned long int tid = pthread_self();
    printf("当前为号线程, 线程ID\n", number, tid);
    return (void *)0;
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(int argc, char *argv[])
{
    pthread_t tid[5];
    int j;

    /* 创建5个线程 */
    for (j = 0; j             
关注
打赏
1665308814
查看更多评论
0.0461s