Linux_应用篇(11) 线程

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

线程概述

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

并发和并行

在前面的内容中,曾多次提到了并发这个概念,与此相类似的概念还有并行、串行,这里和大家聊一聊这些概念含义的区别。对于串行比较容易理解,它指的是一种顺序执行,譬如先完成 task1,接着做 task2、直到完成 task2,然后做 task3、直到完成 task3……依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。

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

并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着,譬如:

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

这里再次进行总结:
⚫ 串行:一件事、一件事接着做
⚫ 并发:交替做不同的事;
⚫ 并行:同时做不同的事。
 

需要注意的是,并行运行情况下的多个执行单元,每一个执行单元同样也可以以并发方式运行。从通用角度上介绍完这三个概念之后, 类比到计算机系统中, 首先我们需要知道两个前提条件:
⚫ 多核处理器和单核处理器:对于单核处理器来说,只有一个执行单元,同时只能执行一条指令;而对于多核处理起来说,有多个执行单元,可以并行执行多条指令,譬如 8 核处理器,那么可以并行执行 8 条不同的指令。
⚫ 计算机操作系统中,通常同时运行着几十上百个不同的线程,在单核或多核处理系统中都是如此!

对于单核处理器系统来说,它只有一个执行单元(譬如 I.MX6U 硬件平台,单核 Cortex-A7 SoC) ,只能采用并发运行系统中的线程, 而肯定不可能是串行, 而事实上确实如此。内核实现了调度算法,用于控制系统中所有线程的调度,简单点来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。 在前面章节内容中也给大家有简单地提到过系统调用的问题, 关于更加详细的内容,这里便不再介绍了,我们只需有个大概的认识、了解即可!对于多核处理器系统来说, 它拥有多个执行单元, 在操作系统中,多个执行单元以并行方式运行多个线程,同时每一个执行单元以并发方式运行系统中的多个线程。
计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替/交叉方式运行不同的线程) , 但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮询一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程。这就好比现实生活中所看到的一些事情,它所给带来的视角效果,譬如一辆车在高速上行驶,有时你会感觉到车的轮毂没有转动, 一种视角暂留现象,因为车轮转动速度太快了,人眼是看不清的,会感觉车轮好像是静止的,事实上,车轮肯定是在转动着。本小节的内容到这里就结束了,理解了本小节的内容,对于后面内容的将会有很大的帮助、也可以帮助大家快速理解后面的内容,大家加油!

线程 ID

就像每个进程都有一个进程 ID 一样,每个线程也有其对应的标识,称为线程 ID。进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示,一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:

#include <pthread.h>
pthread_t pthread_self(void);

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

#include <pthread.h>
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 作为动态数据结构的标签,在某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。

创建线程

启动程序时, 创建的进程只是一个单线程的进程,称之为初始线程或主线程,本小节我们讨论如何创建一个新的线程。
主线程可以使用库函数 pthread_create()负责创建一个新的线程, 创建出来的新线程被称为主线程的子线程, 其函数原型如下所示:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

使用该函数需要包含头文件<pthread.h>。
函数参数和返回值含义如下:
thread: pthread_t 类型指针, 当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
attr: pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性,关于线程属性将会在后面小节介绍。如果将参数 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 系统中,多核线程可能会在不同的核心上同时执行)?如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现。 这与前面学习父、子进程时也出现了这个问题, 无法确定父进程、子进程谁先被系统调度。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>static void *new_thread_start(void *arg)
{printf("新线程: 进程 ID<%d> 线程 ID<%lu>\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<%d> 线程 ID<%lu>\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,将结果打印出来,运行结果如下所示:

编译时出现了错误,提示“对‘pthread_create’未定义的引用”,示例代码确实已经包含了<pthread.h>头文件,但为什么会出现这样的报错,仔细看,这个报错是出现在程序代码链接时、而并非是编译过程,所以可知这是链接库的文件,如何解决呢?

gcc -o testApp testApp.c -lpthread

使用-l 选项指定链接库 pthread,原因在于 pthread 不在 gcc 的默认链接库中,所以需要手动指定。再次编译便不会有问题了,如下:

从打印信息可知,正如前面所介绍那样,两个线程的进程 ID 相同,说明新创建的线程与主线程本来就属于同一个进程,但是它们的线程 ID 不同。从打印结果可知, Linux 系统下线程 ID 数值非常大,看起来像是一个指针。

终止线程

我们在新线程的启动函数(线程 start 函数) new_thread_start()通过 return 返回之后,意味着该线程已经终止了,除了在线程 start 函数中执行 return 语句终止线程外, 终止线程的方式还有多种,可以通过如下方式终止线程的运行:
⚫ 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
⚫ 线程调用 pthread_exit()函数;
⚫ 调用 pthread_cancel()取消线程;
如果进程中的任意线程调用 exit()、 _exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!
pthread_exit()函数将终止调用它的线程,其函数原型如下所示:

#include <pthread.h>
void pthread_exit(void *retval);

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

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>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()终止,编译测试看看打印结果:

正如上面介绍到,主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。

回收线程

在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid()) 阻塞等待子进程退出并获取其终止状态,回收子进程资源; 而在线程当中, 也需要如此, 通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码, 回收线程资源; pthread_join()函数原型如下所示:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

使用该函数需要包含头文件<pthread.h>。
函数参数和返回值含义如下:
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()等待指定线程的终止,那么结果将是不确定的。若线程并未分离,则必须使用 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()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>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 所指向的内存中。测试结果如下:

取消线程

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

取消一个线程

通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

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

取消状态以及类型

默认情况下,线程是响应其它线程发送过来的取消请求的, 响应请求然后退出线程。 当然,线程可以选择不被取消或者控制如何被取消,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

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

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>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()函数,那么此时会被阻塞等待新线程终止,接下来运行测试看看,结果会不会是这样:

pthread_setcanceltype()函数

如果线程的取消性状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型,而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果对之前的类型不敢兴趣, Linux下允许将参数 oldtype 设置为 NULL。 同样 pthread_setcanceltype()函数调用成功将返回 0,失败返回非 0 值的错误码。pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。参数 type 必须是以下值之一:
⚫ PTHREAD_CANCEL_DEFERRED: 取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型。
⚫ PTHREAD_CANCEL_ASYNCHRONOUS: 可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少, 不再介绍!
当某个线程调用 fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型,而当某线程调 用 exec 函 数 时 , 会 将 新 程 序 主 线 程 的 取 消 性 状 态 和 类 型 重 置 为 默 认 值 , 也 就 是PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED。

取消点

若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。那什么是取消点呢? 所谓取消点其实就是一系列函数, 当执行到这些函数的时候,才会真正响应取消请求, 这些函数就是取消点; 在没有出现取消点时,取消请求是无法得到处理的, 究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。取消点函数包括哪些呢?下表给大家简单地列出了一些:

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()

除了表所列函数之外,还有大量的函数,系统实现可以将其作为取消点,这里便不再一一列举出来了, 大家也可以通过 man 手册进行查询,命令为"man 7 pthreads",如下所示:

线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)。

线程可取消性的检测

假设线程执行的是一个不含取消点的循环(譬如 for 循环、 while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它。在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。 其函数原型如下所示:

#include <pthread.h>
void pthread_testcancel(void);

分离线程

默认情况下,当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用 pthread_detach()将指定线程进行分离,也就是分离线程, pthread_detach()函数原型如下所示:

#include <pthread.h>
int pthread_detach(pthread_t thread);

使用该函数需要包含头文件<pthread.h>,参数 thread 指定需要分离的线程,函数 pthread_detach()调用成功将返回 0;失败将返回一个错误码。一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如:

pthread_detach(pthread_self());

一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。

注册线程清理处理函数

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

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

使用这些函数需要包含头文件<pthread.h>。
调用 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);

否则会编译报错, 如下所示:

线程属性

如前所述,调用 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 <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

使用这些函数需要包含头文件<pthread.h>,参数 attr 指向一个 pthread_attr_t 对象,即需要进行初始化的线程属性对象。在调用成功时返回 0,失败将返回一个非 0 值的错误码。
调用 pthread_attr_init()函数会将指定的 pthread_attr_t 对象中定义的各种线程属性初始化为它们各自对应的默认值。pthread_attr_t 数据结构中包含的属性比较多,本小节并不会一一点出,可能比较关注属性包括:线程栈的位置和大小、线程调度策略和优先级,以及线程的分离状态属性等。 Linux 为 pthread_attr_t 对象的每种属性提供了设置属性的接口以及获取属性的接口。

线程栈属性

每个线程都有自己的栈空间, pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小,调用函数pthread_attr_getstack()可以获取这些信息,函数 pthread_attr_setstack()对栈起始地址和栈大小进行设置,其函数原型如下所示:

#include <pthread.h>
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.h>,函数 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 <pthread.h>
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。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>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);
}

分离状态属性

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

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

需要包含头文件<pthread.h>,参数 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 所指定的内存中。

线程安全

当我们编写的程序是一个多线程应用程序时,就不得不考虑到线程安全的问题,确保我们编写的程序是一个线程安全(thread-safe)的多线程应用程序,什么是线程安全以及如何保证线程安全?带着这些问题,本小节将讨论线程安全相关的话题。

线程栈

进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。譬如主线程调用 pthread_create()创建了一个新的线程,那么这个新的线程有它自己独立的栈地址空间、而主线程也有它自己独立的栈地址空间。在创建一个新的线程时,可以配置线程栈的大小以及起始地址,当然在大部分情况下,保持默认即可!既然每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量) 都是分配在自己的线程栈中的, 它们不会相互干扰。主线程创建了 5 个新的线程,这 5 个线程使用同一个 start 函数 new_thread, 该函数中定义了局部变量 number 和 tid 以及 arg 参数,意味着这 5个线程的线程栈中都各自为这些变量分配了内存空间,任何一个线程修改了 number 或 tid 都不会影响其它线程。

可重入函数

要解释可重入(Reentrant) 函数为何物, 首先需要区分单线程程序和多线程程序。 本章开头部分已向各位读者进行了详细介绍,单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于多线程程序而言,同一进程却存在多条独立、并发的执行流。进程中执行流的数量除了与线程有关之外,与信号处理也有关联。因为信号是异步的,进程可能会在其运行过程中的任何时间点收到信号,进而跳转、执行信号处理函数,从而在一个单线程进程(包含信号处理)中形成了两条(即主程序和信号处理函数)独立的执行流。接下来再来介绍什么是可重入函数,如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。重入指的是同一个函数被不同执行流调用,前一个执行流还没有执行完该函数、另一个执行流又开始调用该函数了,其实就是同一个函数被多个执行流并发/并行调用,在宏观角度上理解指的就是被多个执行流同时调用。
在多线程环境以及信号处理有关应用程序中,需要注意不可重入函数的问题,如果多条执行流同时调用一个不可重入函数则可能会得不到预期的结果、甚至有可能导致程序崩溃! 不止是在应用程序中,在一个包含了中断处理的裸机应用程序中亦是如此! 所以不可重入函数通常存在着一定的安全隐患。

可重入函数的分类
⚫ 绝对的可重入函数: 所谓绝对,指的是该函数不管如何调用,都断言它是可重入的,都能得到预期的结果。
⚫ 带条件的可重入函数: 指的是在满足某个/某些条件的情况下,可以断言该函数是可重入的,不管怎么调用都能得到预期的结果。

总结下绝对可重入函数的特点:
⚫ 函数内所使用到的变量均为局部变量,换句话说,该函数内的操作的内存地址均为本地栈地址;
⚫ 函数参数和返回值均是值类型;
⚫ 函数内调用的其它函数也均是绝对可重入函数。

很多的 C 库函数有两个版本:可重入版本和不可重入版本,可重入版本函数其名称后面加上了“_r”,用于表明该函数是一个可重入函数;而不可重入版本函数其名称后面没有“_r”,前面章节内容中也已经遇到过很多次了,譬如 asctime()/asctime_r()、 ctime()/ctime_r()、 localtime()/localtime_r()等。通过 man 手册可以查询到它们“ATTRIBUTES”信息,譬如执行"man 3 ctime",在帮助页面上往下翻便可以找到,如下所示:

可以看到上图中有些函数 Value 这栏会显示 MT-Unsafe、而有些函数显示的却是 MT-Safe。 MT 指的是multithreaded(多线程),所以 MT-Unsafe 就是多线程不安全、 MT-Safe 指的是多线程安全,通常习惯上将MT-Safe 和 MT-Unsafe 称为线程安全或线程不安全。Value 值为 MT-Safe 修饰的函数表示该函数是一个线程安全函数,使用 MT-Unsafe 修饰的函数表示它是 一 个 线 程 不 安 全 函 数 , 下 一 小 节 会 给 大 家 介 绍 什 么 是 线 程 安 全 函 数 。 从 上 图 可 以 看 出 ,asctime_r()/ctime_r()/gmtime_r()/localtime_r()这些可重入函数都是线程安全函数,但这些函数都是带条件的可重入函数,可以发现在 MT-Safe 标签后面会携带诸如 env 或 locale 之类的标签,这其实就表示该函数需要在满足 env 或 locale 条件的情况下才是可重入函数;如果是绝对可重入函数, MT-Safe 标签后面不会携带任何标签,譬如数学库函数 sqrt:

诸如 env 或 locale 等标签,可以通过 man 手册进行查询,命令为"man 7 attributes",
⚫ env:这个标签指的是该函数内部会读取进程的某个/某些环境变量,譬如 getenv()函数,前面也给大家介绍过,进程的环境变量其实就是程序的一个全局变量,前面也讲了,对于这类读取(但没更改)了全局变量的可重入函数应该要满足的条件,这里就不再重述了;
⚫ local: local 指的是本地,很容易理解,通常该类函数传入了指针,前面也提到了传入了指针的可重入函数应该要满足什么样的条件才是可重入的,这里也不再重述!

线程安全函数

了解了可重入函数之后,再来看看线程安全函数。一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流) 同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。 线程安全函数包括可重入函数, 可重入函数是线程安全函数的一个真子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数,它们之间的关系如下:

譬如下面这个函数是一个不可重入函数,同样也是一个线程不安全函数

static int glob = 0;static void func(int loops)
{int local;int j;for (j = 0; j < loops; j++) {local = glob;local++;glob = local;}
}

如果对该函数进行修改,使用线程同步技术(譬如互斥锁)对共享变量 glob 的访问进行保护,在读写该变量之前先上锁、读写完成之后再解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重入函数,因为该函数更改了外部全局变量的值。可重入函数只是单纯从语言语法角度分析它的可重入性质,不涉及到一些具体的实现机制,譬如线程同步技术,这是判断可重入函数和线程安全函数的区别,因为你单从概念上去分析的话,其实可以发现可重入函数和线程安全函数好像说的是同一个东西,“一个函数被多个线程同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数”,多个线程指的就是多个执行流(不包括信号处理函数执行流) , 所以从这里看跟可重入函数的概念是很相似的。
判断一个函数是否为线程安全函数的方法是, 该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。 判读一个函数是否为可重入函数的方法是,从语言语法角度分析,该函数被多个执行流同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个可重入函数。
POSIX.1-2001 和 POSIX.1-2008 标准中规定的所有函数都必须是线程安全函数,但以下函数除外:

asctime()basename()catgets()crypt()
ctermid()ctime()dbm_clearerr()dbm_close()
dbm_delete()dbm_error()dbm_fetch()dbm_firstkey()
dbm_nextkey()dbm_open()dbm_store()dirname()
dlerror()drand48()ecvt()encrypt()
endgrent()endpwent()endutxent()fcvt()
ftw()gcvt()getc_unlocked()getchar_unlocked()
getdate()getenv()getgrent()getgrgid()
getgrnam()gethostbyaddr()gethostbyname()gethostent()
getlogin()getnetbyaddr()getnetbyname()getnetent()
getopt()getprotobyname()getprotobynumber()getprotoent()
getpwent()getpwnam()getpwuid()getservbyname()
getservbyport()getservent()getutxent()getutxid()
getutxline()gmtime()hcreate()hdestroy()
hsearch()inet_ntoa()l64a()lgamma()
lgammaf()lgammal()localeconv()localtime()
lrand48()mrand48()nftw()nl_langinfo()
ptsname()putc_unlocked()putchar_unlocked()putenv()
pututxline()rand()readdir()setenv()
setgrent()setkey()setpwent()setutxent()
strerror()strsignal()strtok()system()
tmpnam()ttyname()unsetenv()wcrtomb()
wcsrtombs()wcstombs()wctomb()

以上所列举出的这些函数被认为是线程不安全函数,大家也可以通过 man 手册查询到这些函数, "man7 pthreads",如下所示:

如果想确认某个函数是不是线程安全函数可以上小节给大家提到过, man 手册可以查看库函数的 ATTRIBUTES 信息,如果函数被标记为 MT-Safe,则表示该函数是一个线程安全函数,如果被标记为 MT-Unsafe,则意味着该函数是一个非线程安全函数,对于非线程安全函数,在多线程编程环境下尤其要注意,如果某函数可能会被多个线程同时调用时,该函数不能是非线程安全函数,一定要是线程安全函数,否则将会出现意想不到的结果、甚至使得整个程序崩溃!对于一个中大型的多线程应用程序项目来说,能够保证整个程序的安全性,这是非常重要的,程序员必须要正确对待线程安全以及信号处理等这类在多线程环境下敏感的问题,这通常对程序员提出了更高的要求。

一次性初始化

在多线程编程环境下,有些代码段只需要执行一次,譬如一些初始化相关的代码段, 通常比较容易想到的就是将其放在 main()主函数进行初始化,这样也就是意味着该段代码只在主线程中被调用,只执行过一次。大家想一下这样的问题:当你写了一个 C 函数 func(),该函数可能会被多个线程调用,并且该函数中有一段初始化代码,该段代码只能被执行一次(无论哪个线程执行都可以) 、如果执行多次会出现问题,如下所示:

static void func(void)
{
/* 只能执行一次的代码段 */
init_once();
/***********************/
.....
.....
}

那我们如何去保证这段代码只能被执行一次呢(被进程中的任一线程执行都可以)?本小节向大家介绍 pthread_once()函数,该函数原型如下所示:

#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

在多线程编程环境下,尽管 pthread_once()调用会出现在多个线程中, 但该函数会保证 init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。函数参数和返回值含义如下:once_control: 这是一个 pthread_once_t 类型指针,在调用 pthread_once()函数之前,我们需要定义了一个 pthread_once_t 类型的静态变量,调用 pthread_once()时参数 once_control 指向该变量。通常在定义变量时会使用 PTHREAD_ONCE_INIT 宏对其进行初始化,譬如:

pthread_once_t once_control = PTHREAD_ONCE_INIT;

init_routine: 一个函数指针,参数 init_routine 所指向的函数就是要求只能被执行一次的代码段,pthread_once()函数内部会调用 init_routine(),即使 pthread_once()函数会被多次执行,但它能保证 init_routine()仅被执行一次。
返回值: 调用成功返回 0;失败则返回错误编码以指示错误原因。
如果参数 once_control 指向的 pthread_once_t 类型变量,其初值不是 PTHREAD_ONCE_INIT,pthread_once()的行为将是不正常的; PTHREAD_ONCE_INIT 宏在<pthread.h>头文件中定义。如果在一个线程调用 pthread_once()时,另外一个线程也调用了 pthread_once,则该线程将会被阻塞等待, 直到第一个完成初始化后返回。换言之,当调用 pthread_once 成功返回时,调用总是能够肯定所有的状态已经初始化完成了

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>static pthread_once_t once = PTHREAD_ONCE_INIT;static void initialize_once(void)
{printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}static void func(void)
{pthread_once(&once, initialize_once);//执行一次性初始化函数printf("函数 func 执行完毕.\n");
}static void *thread_start(void *arg)
{printf("线程%d 被创建: 线程 ID<%lu>\n", *((int *)arg), pthread_self());func(); //调用函数 funcpthread_exit(NULL); //线程终止
}static int nums[5] = {0, 1, 2, 3, 4};int main(void)
{pthread_t tid[5];int j;/* 创建 5 个线程 */for (j = 0; j < 5; j++)pthread_create(&tid[j], NULL, thread_start, &nums[j]);/* 等待线程结束 */for (j = 0; j < 5; j++)pthread_join(tid[j], NULL);//回收线程exit(0);
}

程序中调用 pthread_create()创建了 5 个子线程,新线程的入口函数均为 thread_start(), thread_start()函数会调用 func(),并在 func()函数调用 pthread_once(),需要执行的一次性初始化函数为 initialize_once(),换言之, pthread_once()函数会被执行 5 次,每个子线程各自执行一次。编译运行:

从打印信息可知, initialize_once()函数确实只被执行了一次,也就是被编号为 1 的线程所执行,其它线程均未执行该函数。

线程特有数据

线程特有数据也称为线程私有数据, 简单点说,就是为每个调用线程分别维护一份变量的副本(copy) ,每个线程通过特有数据键(key) 访问时,这个特有数据键都会获取到本线程绑定的变量副本。 这样就可以避免变量成为多个线程间的共享数据。C 库中有很多函数都是非线程安全函数,非线程安全函数在多线程环境下,被多个线程同时调用时将会
发生意想不到的结果,得不到预期的结果。譬如很多库函数都会返回一个字符串指针,譬如 asctime()、ctime()、localtime()等,返回出来的字符串可以被调用线程直接使用, 但该字符串缓冲区通常是这些函数内部所维护的静态数组或者是某个全局数组。既然如此, 多次调用这些函数返回的字符串其实指向的是同一个缓冲区,每次调用都会刷新缓冲区中的
数据。 这些函数是非线程安全的, 譬如当 ctime()被多个线程同时调用时,返回的字符串中的数据可能是混乱的,因为某一线程调用它时,缓冲区中的数据可能被另一个调用线程修改了。 针对这些非线程安全函数,可以使用线程特有数据将其变为线程安全函数,线程特有数据通常会在编写一些库函数的时使用到, 后面我们会演示如何使用线程特有数据。线程特有数据的核心思想其实非常简单,就是为每一个调用线程(调用某函数的线程,该函数就是我们要通过线程特有数据将其实现为线程安全的函数) 分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。线程特有数据主要涉及到 3 个函数: pthread_key_create()、pthread_setspecific()以及 pthread_getspecific(),接下来一一向大家进行介绍。

pthread_key_create()函数

在为线程分配私有数据区之前,需要调用 pthread_key_create()函数创建一个特有数据键(key) , 并且只需要在首个调用的线程中创建一次即可,所以通常会使用到上小节所学习的 pthread_once()函数。pthread_key_create()函数原型如下所示:

#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

使用该函数需要包含头文件<pthread.h>。
函数参数和返回值含义如下:
key: 调用该函数会创建一个特有数据键,并通过参数 key 所指向的缓冲区返回给调用者,参数 key 是一个 pthread_key_t 类型的指针,可以把 pthread_key_t 称为 key 类型。调用 pthread_key_create()之前,需要定义一个 pthread_key_t 类型变量,调用 pthread_key_create()时参数 key 指向 pthread_key_t 类型变量。
destructor: 参数 destructor 是一个函数指针, 指向一个自定义的函数,其格式如下:

void destructor(void *value)
{
/* code */
}

调用 pthread_key_create()函数允许调用者指定一个自定义的解构函数(类似于 C++中的析构函数),使用参数 destructor 指向该函数;该函数通常用于释放与特有数据键关联的线程私有数据区占用的内存空间,当使用线程特有数据的线程终止时, destructor()函数会被自动调用。
返回值: 成功返回 0;失败将返回一个错误编号以指示错误原因,返回的错误编号其实就是全局变量errno,可以使用诸如 strerror()函数查看其错误字符串信息。

pthread_setspecific()函数
调用 pthread_key_create()函数创建特有数据键(key)后通常需要为调用线程分配私有数据缓冲区,譬如通过 malloc()(或类似函数)申请堆内存,每个调用线程分配一次,且只会在线程初次调用此函数时分配。为线程分配私有数据缓冲区之后,通常需要调用 pthread_setspecific()函数, pthread_setspecific()函数其实完成了这样的操作:首先保存指向线程私有数据缓冲区的指针,并将其与特有数据键以及当前调用线程关联起来;其函数原型如下所示:

#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);

函数参数和返回值含义如下:
key: pthread_key_t 类型变量,参数 key 应赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数的参数 key 所指向的 pthread_key_t 变量。
value: 参数 value 是一个 void 类型的指针,指向由调用者分配的一块内存,作为线程的私有数据缓冲区, 当线程终止时,会自动调用参数 key 指定的特有数据键对应的解构函数来释放这一块动态申请的内存空间。
返回值: 调用成功返回 0;失败将返回一个错误编码,可以使用诸如 strerror()函数查看其错误字符串信息。

pthread_getspecific()函数
调用 pthread_setspecific()函数将线程私有数据缓冲区与调用线程以及特有数据键关联之后,便可以使用pthread_getspecific()函数来获取调用线程的私有数据区了。其函数原型如下所示:

#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);

参数 key 应赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数的参数 key 指向的 pthread_key_t 变量。
pthread_getspecific()函数应返回当前调用线程关联到特有数据键的私有数据缓冲区,返回值是一个指针,指向该缓冲区。如果当前调用线程并没有设置线程私有数据缓冲区与特有数据键进行关联,则返回值应为NULL,函数中可以利用这一点来判断当前调用线程是否为初次调用该函数,如果是初次调用,则必须为该线程分配私有数据缓冲区。

pthread_key_delete()函数
除了以上介绍的三个函数外,如果需要删除一个特有数据键(key) 可以使用函数 pthread_key_delete(),pthread_key_delete()函数删除先前由 pthread_key_create()创建的键。 其函数原型如下所示:

#include <pthread.h>
int pthread_key_delete(pthread_key_t key);

参数 key 为要删除的键。函数调用成功返回 0,失败将返回一个错误编号。
调用 pthread_key_delete()函数将释放参数 key 指定的特有数据键,可以供下一次调用 pthread_key_create()时使用; 调用 pthread_key_delete()时, 它并不检查当前是否有线程正在使用该键所关联的线程私有数据缓冲区,所以它并不会触发键的解构函数,也就不会释放键关联的线程私有数据区占用的内存资源,并且调用pthread_key_delete()后,当线程终止时也不再执行键的解构函数。所以,通常在调用 pthread_key_delete()之前,必须确保以下条件:
⚫ 所有线程已经释放了私有数据区(显式调用解构函数或线程终止)。
⚫ 参数 key 指定的特有数据键将不再使用。
任何在调用 pthread_key_delete()之后使用键的操作都会导致未定义的行为,譬如调用 pthread_setspecific()或 pthread_getspecific()将会以错误形式返回。

使用示例
接下来编写一个使用线程特有数据的例子,很多书籍上都会使用 strerror()函数作为例子, 通过 man 手册查询到 strerror()函数是一个非线程安全函数,其实它有对应的可重入
版本 strerror_r(), 可重入版本 strerror_r()函数则是一个线程安全函数。这里暂且不管 strerror_r()函数, 我们来聊一聊 strerror()函数, 函数内部的实现方式,这里简单地提一下:
调用 strerror()函数,需要传入一个错误编号,错误编号赋值给参数 errnum,在 Linux 系统中,每一个错误编号都会对应一个字符串,用于描述该错误, strerror()函数会根据传入的 errnum 找到对应的字符串,返回指向该字符串的指针。
事实上,在 Linux 的实现中,标准 C 语言函数库(glibc)提供的 strerror()函数是线程安全的, 但在 man手册中记录它是一个非线程安全函数,笔者猜测可能在某些操作系统的 C 语言函数库实现中,该函数是非线程安全函数的;但在 glibc 库中,它确实是线程安全函数,为此笔者还特意去查看了 glibc 库中 strerror 函数的源码,证实了这一点,这里大家一定要注意。以下是 strerror()函数以非线程安全方式实现的一种写法(具体的写法不止这一种,这里只是以此为例):

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>#define MAX_ERROR_LEN 256
static char buf[MAX_ERROR_LEN];static char *strerror(int errnum)
{if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);else {strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);buf[MAX_ERROR_LEN - 1] = '\0';//终止字符}return buf;
}

再次说明, glibc 库中 strerror()是线程安全函数,本文为了向大家介绍/使用线程特有数据,以非线程安全方式实现了 strerror()函数。首先在源码中需要定义_GNU_SOURCE 宏, _GNU_SOURCE 宏在前面章节已有介绍,源码中需要定义_GNU_SOURCE 宏, 不然编译源码将会提示_sys_nerr 和_sys_errlist 找不到。该函数利用了glibc 定义的一对全局变量: _sys_errlist 是一个指针数组,其中的每一个元素指向一个与 errno 错误编号相匹配的描述性字符串; _sys_nerr 表示_sys_errlist 数组中元素的个数。可以看到该函数返回的字符串指针,其实是一个静态数组,当多个线程同时调用该函数时,那么 buf 缓冲区中的数据将会出现混乱, 因为前一个调用线程拷贝到 buf 中的数据可能会被后一个调用线程重写覆盖等情况。
对此,我们可以进行测试,让多个线程都调用它,看看测试结果,测试代码如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>#define MAX_ERROR_LEN 256
static char buf[MAX_ERROR_LEN];/**********************************
* 为了避免与库函数 strerror 重名
* 这里将其改成 my_strerror
**********************************/
static char *my_strerror(int errnum)
{if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);else {strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);buf[MAX_ERROR_LEN - 1] = '\0';//终止字符}return buf;
}static void *thread_start(void *arg)
{char *str = my_strerror(2); //获取错误编号为 2 的错误描述信息printf("子线程: str (%p) = %s\n", str, str);pthread_exit(NULL);
}int main(int argc, char *argv[])
{pthread_t tid;char *str = NULL;int ret;str = my_strerror(1); //获取错误编号为 1 的错误描述信息/* 创建子线程 */if (ret = pthread_create(&tid, NULL, thread_start, NULL)) {fprintf(stderr, "pthread_create error: %d\n", ret);exit(-1);}/* 等待回收子线程 */if (ret = pthread_join(tid, NULL)) {fprintf(stderr, "pthread_join error: %d\n", ret);exit(-1);}printf("主线程: str (%p) = %s\n", str, str);exit(0);
}

主线程首先调用 my_strerror()获取到了编号为 1 的错误描述信息,接着创建了一个子线程,在子线程中调用 my_strerror()获取编号为 2 的错误描述信息,并将其打印出来,包括字符串的地址值;子线程结束后,主线程也打印了之前获取到的错误描述信息。我们想看到的结果是,主线程和子线程打印的错误描述信息是不一样的,因为错误编号不同,但上面的测试结果证实它们打印的结果是相同的:

从以上测试结果可知,子线程和主线程所获取到的错误描述信息是相同的,字符串指针指向的是同一个缓冲区; 原因就在于, my_strerror()函数是一个非线程安全函数,函数内部修改了全局静态变量、并返回了它的指针,每一次调用访问的都是同一个静态变量,所以后一次调用会覆盖掉前一次调用的结果。接下来我们使用本小节所介绍的线程特有数据技术对示例代码中 strerror()函数进行修改,如下所示:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>#define MAX_ERROR_LEN 256
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerror_key;static void destructor(void *buf)
{free(buf); //释放内存
}static void create_key(void)
{/* 创建一个键(key),并且绑定键的解构函数 */if (pthread_key_create(&strerror_key, destructor))pthread_exit(NULL);
}/******************************
* 对 strerror 函数重写
* 使其变成为一个线程安全函数
******************************/
static char *strerror(int errnum)
{char *buf;/* 创建一个键(只执行一次 create_key) */if (pthread_once(&once, create_key))pthread_exit(NULL);/* 获取 */buf = pthread_getspecific(strerror_key);if (NULL == buf) { //首次调用 my_strerror 函数,则需给调用线程分配线程私有数据buf = malloc(MAX_ERROR_LEN);//分配内存if (NULL == buf)pthread_exit(NULL);/* 保存缓冲区地址,与键、线程关联起来 */if (pthread_setspecific(strerror_key, buf))pthread_exit(NULL);}if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);else {strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);buf[MAX_ERROR_LEN - 1] = '\0';//终止字符}return buf;
}

改进版的 strerror()所做的第一步是调用 pthread_once(),以确保只会执行一次 create_key()函数,而在create_key()函数中便是调用 pthread_key_create()创建了一个键、并绑定了相应的解构函数 destructor(),解构函数用于释放与键关联的所有线程私有数据所占的内存空间。接着,函数 strerror()调用 pthread_getspecific()以获取该调用线程与键相关联的私有数据缓冲区地址,如果返回为 NULL,则表明该线程是首次调用 strerror()函数,因为函数会调用 malloc()为其分配一个新的私有数据缓冲区,并调用 pthread_setspecific()来保存缓冲区地址、并与键以及该调用线程建立关联。如果pthread_getspecific()函数的返回值并不等于 NULL,那么该值将指向已存在的私有数据缓冲区,此缓冲区由之前对 strerror()的调用所分配。 buf 是线程特有数据的缓冲区地址,而非全局的静态变量。
改进版的 strerror 就是一个线程安全函数,编写一个线程安全函数当然要保证该函数中调用的其它函数也必须是线程安全的,那如何确认自己调用的函数是线程安全函数呢?其实非常简单, 前面也给大家介绍过, 譬如通过 man 手册查看函数的 ATTRIBUTES 描述信息,或者查看 man 手册中记录的非线程安全函数列表(执行"man 7 pthreads"命令查看)、进行对比。

线程局部存储

通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量; 而线程局部存储在定义全局或静态变量时, 使用__thread 修饰符修饰变量, 此时, 每个线程都会拥有一份对该变量的拷贝。 线程局部存储中的变量将一直存在, 直至线程终止, 届时会自动释放这一存储。线程局部存储的主要优点在于,比线程特有数据的使用要简单。 要创建线程局部变量,只需简单地在全局或静态变量的声明中包含__thread 修饰符即可! 譬如:

static __thread char buf[512];

但凡带有这种修饰符的变量,每个线程都拥有一份对变量的拷贝,意味着每个线程访问的都是该变量在本线程的副本,从而避免了全局变量成为多个线程的共享数据。
关于线程局部变量的声明和使用,需要注意以下几点:
⚫ 如果变量声明中使用了关键字 static 或 extern,那么关键字__thread 必须紧随其后。
⚫ 与一般的全局或静态变量申明一样,线程局部变量在申明时可设置一个初始值。
⚫ 可以使用 C 语言取值操作符(&)来获取线程局部变量的地址。
Tips:线程局部存储需要内核、 Pthreads 以及 GCC 编译器的支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/846872.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GaussDB数据库如何创建修改数据库和数据表

目录 一、背景 二、创建数据库和数据表 1. 创建数据库 2.创建数据表 三、修改表结构 1. 添加列 2. 修改列 3. 删除列 四、添加约束 1. 添加主键约束 2. 添加外键约束 3.添加唯一性约束 五、示例代码 -- 创建数据库 -- 使用新创建的数据库 -- 创建 department 表…

发送Http请求的两种方式

说明&#xff1a;在项目中&#xff0c;我们有时会需要调用第三方接口&#xff0c;获取调用结果&#xff0c;来实现自己的业务逻辑。调用第三方接口&#xff0c;通常是双方确定好&#xff0c;由对方开放一个接口&#xff0c;需要我们根据他们提供的接口文档&#xff0c;组装Http…

MySQL 使用方法以及教程

一、引言 MySQL是一个流行的开源关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;广泛应用于Web开发、数据分析等领域。它提供了高效、稳定的数据存储和查询功能。同时&#xff0c;Python作为一种强大的编程语言&#xff0c;也提供了多种与MySQL交互的库&#…

Ubuntu 24.04 LTS 安装Docker

1 更新软件包索引&#xff1a; sudo apt-get update 2 安装必要的软件包&#xff0c;以允许apt通过HTTPS使用仓库&#xff1a; sudo apt-get install apt-transport-https ca-certificates curl software-properties-common 3 添加Docker的官方GPG密钥&#xff1a; curl -fs…

算法金 | 你真的完全理解 Logistic 回归算法了吗

大侠幸会&#xff0c;在下全网同名「算法金」 0 基础转 AI 上岸&#xff0c;多个算法赛 Top 「日更万日&#xff0c;让更多人享受智能乐趣」 今日 178/10000 1. 引言 吴恩达&#xff1a;机器学习的六个核心算法&#xff01;&#xff0c; 通透&#xff01;&#xff01;十大回…

Python专为开发和部署数据驱动的应用程序库之taipy使用详解

概要 Taipy 是一个强大的 Python 库,专为开发和部署数据驱动的应用程序而设计。它通过提供一套丰富的工具和组件,使开发者能够快速构建和维护复杂的业务逻辑和数据交互界面。无论是金融分析、供应链管理还是任何需要高度数据交互的应用,taipy 都能提供高效的解决方案。 安装…

Orange AIpro开箱上手

0.介绍 首先感谢官方给到机会&#xff0c;有幸参加这次活动。 OrangePi AIpro(8T)采用昇腾AI技术路线&#xff0c;具体为4核64位处理器AI处理器&#xff0c;集成图形处理器&#xff0c;支持8TOPS AI算力&#xff0c;拥有8GB/16GB LPDDR4X&#xff0c;可以外接32GB/64GB/128GB/2…

小程序抓包详细教程

小程序抓包详细教程 前言&#xff1a;关于小程序抓包一直想写出一个比较详细的教程 实验设备&#xff1a; ​ 微信: https://windows.weixin.qq.com/?langzh_CN ​ Proxifier&#xff1a;https://www.proxifier.com/download/ (需要挂梯子访问下载) ​ burpsuite&#xff…

1.JAVA小项目(零钱通)

一、说明 博客内容&#xff1a;B站韩顺平老师的视频&#xff0c;以及代码的整理。此项目分为两个版本&#xff1a; 面向过程思路实现面向对象思路实现 韩老师视频地址&#xff1a;【【零基础 快速学Java】韩顺平 零基础30天学会Java】 https://www.bilibili.com/video/BV1fh4…

Spring 源码:深度解析AOP源码配置解析

文章目录 一、 解析AOP配置的入口1.1 从XML配置到AOP Namespace的解析流程1.2 分析注解驱动的AOP配置解析流程 二、AOP配置解析的核心流程2.1 ConfigBeanDefinitionParser 类2.2 parse()2.3 parseAdvisor()2.4 parseAspect()2.5 parsePointcut()2.6 createAdvisorBeanDefinitio…

算法每日一题(python,2024.05.29) day.11

题目来源&#xff08;力扣. - 力扣&#xff08;LeetCode&#xff09;&#xff0c;简单&#xff09; 解题思路&#xff1a; 法一&#xff1a;切片函数法 直接用python中的切片函数直接解决 法二&#xff1a;交换法 从俩头开始交换字符串的数字&#xff0c;若为奇数&#xff…

GITLAB常见问题总结

Troubleshooting GitLab Pages administration (FREE SELF) 原文地址 stage: Plan group: Knowledge info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/…

STM8单片机变频器设计

变频调速技术是现代电力传动技术的重要发展方向,而作为变频调速系统的核心—变频器的性能也越来越成为调速性能优劣的决定因素,除了变频器本身制造工艺的“先天”条件外,对变频器采用什么样的控制方式也是非常重要的。随着电力电子技术、微电子技术、计算机网络等高新技术的…

Kompas AI:智能生活的开启者

引言 在现代社会&#xff0c;**人工智能&#xff08;AI&#xff09;**已经深刻地影响了我们的生活和工作。无论是智能家居、自动驾驶&#xff0c;还是医疗诊断&#xff0c;AI的应用无处不在。而在众多AI平台中&#xff0c;Kompas AI 作为一个先进的对话式AI平台&#xff0c;通过…

R语言数据分析-针对芬兰污染指数的分析与考察

1. 研究背景及意义 近年来&#xff0c;随着我国科技和经济高速发展&#xff0c;人们生活质量也随之显著提高。但是&#xff0c; 环境污染问题也日趋严重&#xff0c;给人们的生活质量和社会生产的各个方面都造成了许多不 利的影响。空气污染作为环境污染主要方面&#xff0c;更…

【漏洞复现】海康威视综合安防管理平台 orgManage/v1/orgs/download 任意文件读取漏洞

0x01 产品简介 海康威视综合安防管理平台是一套“集成化”、“智能化”的平台,通过接入视频监控、一卡通、停车场、报警检测等系统的设备。海康威视集成化综合管理软件平台,可以对接入的视频监控点集中管理,实现统一部署、统一配置、统一管理和统一调度。 0x02 漏洞概述 海康…

7-8 矩阵字符

给定一个仅包含小写字母的字符串S,用这些字符恰好排成一个n行m列的矩阵(m≥n)&#xff0c;请找出所有符合要求的矩阵中最接近于正方形的那个矩阵。然后从第一列开始&#xff0c;逐列按照从上到下的顺序输出矩阵中的字符。 例如&#xff1a;S "abcdefgh"。按要求m≥…

动态规划-求买卖股票所能获得的最大收益(hard)

一、问题描述 二、解题思路 1.先看有哪几个可变参数&#xff1a; (1).当前第几天 nowday(范围&#xff1a;0->n-1) (2).剩余交易次数 restTime(范围&#xff1a;k->0) (3).当天可买入还是可卖出 isnowHold(0 表示当前未持有可买入&#xff0c;1 表示当前持有可卖出) 2.…

WIN10环境下xposed环境搭建

禁止拿来干坏事&#xff0c;仅做学习为目的 环境需求 1.夜神模拟器7.1 2.Android stdio 2022.3.1 3. Adb环境配置 具体实现 1.安装xposed 打开可一键安装&#xff0c;重启 2.连接虚拟机 adb connect 127.0.0.1:620013.打开as,进入project 4.在lib下添加准备好的jar包 …

AD软件底层丝印反转

快捷键VB&#xff0c;翻转后底部视图所有显示就正常了&#xff0c;当底层确认之后再按VB就回到正常状态。 否则你就看到一个镜像的丝印。 快捷键VB后 注意&#xff0c;经过VB反转BOTTOM后TOP层的丝印变镜像翻转了。 设计完毕后调整过来即可。