与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元是线程、而并非进程。
一、线程概述
什么是线程?
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
线程具有以下一些特点:
⚫ 线程不单独存在、而是包含在进程中;
⚫ 线程是参与系统调度的基本单位;
⚫ 可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;
⚫ 共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;
此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。
线程与进程?
进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程)并发处理多任务的需求,那我们究竟选择哪种处理方式呢?首先我们就需要来分析下多进程和多线程两种编程模型的优势和劣势。
多进程编程的劣势:
⚫ 进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。
⚫ 进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦。
解决方案便是使用多线程编程,多线程能够弥补上面的问题:
⚫ 同一进程的多个线程间切换开销比较小。
⚫ 同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易。
⚫ 线程创建的速度远大于进程创建的速度。
⚫ 多线程在多核处理器上更有优势!
1.1、并发和并行
对于串行比较容易理解,它指的是一种顺序执行,譬如先完成 task1,接着做 task2、直到完成 task2,然后做 task3、直到完成 task3……依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。
并行与串行则截然不同,并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,譬如并行运行 task1、task2、task3。
并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着,譬如:
在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样
轮训(交叉/交替执行),这就是并发运行。如下图所示:
二、 线程 ID
就像每个进程都有一个进程 ID 一样,每个线程也有其对应的标识,称为线程 ID。进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。
一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
该函数调用总是成功,返回当前线程的线程 ID。
可以使用 pthread_equal()函数来检查两个线程 ID 是否相等,其函数原型如下所示:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
如果两个线程 ID t1 和 t2 相等,则 pthread_equal()返回一个非零值;否则返回 0。
线程 ID 在应用程序中非常有用,原因如下:
⚫ 很多线程相关函数,譬如后面将要学习的 pthread_cancel()、pthread_detach()、pthread_join()等,它们都是利用线程 ID 来标识要操作的目标线程;
⚫ 在一些应用程序中,以特定线程的线程 ID 作为动态数据结构的标签,这某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。
三、创建线程
#include <pthread.h>
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 数据类型定义了线程的各种属性,如果将参数 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 指向的内容是不确定的。
使用示例
#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);
}
主线程休眠了 1 秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创建的线程还没有机会运行,整个进程就结束了。
编译时出现了错误,提示“对‘pthread_create’未定义的引用”,示例代码确实已经包含了<pthread.h>头文件,但为什么会出现这样的报错,仔细看,这个报错是出现在程序代码链接时、而并非是编译过程,所以可知这是链接库的文件,如何解决呢?
gcc -o testApp testApp.c -lpthread
使用-l 选项指定链接库 pthread,原因在于 pthread 不在 gcc 的默认链接库中,所以需要手动指定。再次编译便不会有问题了,如下:
四、 终止线程
可以通过如下方式终止线程的运行:
⚫ 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
⚫ 线程调用 pthread_exit()函数;
⚫ 调用 pthread_cancel()取消线程
如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!
#include <pthread.h>
void pthread_exit(void *retval);
调用 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);
}
五、回收线程
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread:pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程;
retval:如果参数 retval 不为 NULL,则 pthread_join()
将目标线程的退出状态复制到*retval
所指向的内存区域;如果目标线程被 pthread_cancel()
取消,则将 PTHREAD_CANCELED
放在*retval
中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL。
返回值:成功返回 0;失败将返回错误码。
如果线程终止后,其它线程没有调用 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);
}
六、取消线程
有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。
1、取消一个线程
通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求
#include <pthread.h>
int pthread_cancel(pthread_t thread);
发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消,所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。
使用示例
#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("新线程--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);
}
2、取消状态以及类型
通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型。
#include <pthread.h>
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 值的错误码。
参数 state 必须是以下值之一:
⚫ PTHREAD_CANCEL_ENABLE:线程可以取消。
⚫ PTHREAD_CANCEL_DISABLE:线程不可被取消。
使用示例
#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);
}
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:可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少,不再介绍!
3、取消点
若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
那什么是取消点呢?所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。
static void *new_thread_start(void *arg)
{printf("新线程--running\n");for ( ; ; ) {}return (void *)0;}
那么线程将永远无法被取消,因为这里不存在取消点。大家可以将代码进行修改测试,看结果是不是如此!
4、线程可取消性的检测
该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。其函数原型如下所示:
#include <pthread.h>
void pthread_testcancel(void);
功能测试
#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 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);
}
#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 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_join()获取其返回状态、回收线程资源,有时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用 pthread_detach()将指定线程进行分离,也就是分离线程。
#include <pthread.h>
int pthread_detach(pthread_t thread);
函数 pthread_detach()调用成功将返回 0;
一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如:
pthread_detach(pthread_self());
一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。
#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)
{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()必然会失败。
八、注册线程清理处理函数
线程通过函数 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_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);
否则会编译报错,如下所示:
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>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()配对使用,否则编译不通过。
从打印结果可知,先添加到线程清理函数栈中的函数会后被执行,添加顺序与执行顺序相反。将新线程中调用的 pthread_exit()替换为 return,在进行测试,发现并不会执行清理函数。
有时在线程功能设计中,线程清理函数并不一定需要在线程退出时才执行,譬如当完成某一个步骤之后,就需要执行线程清理函数,此时我们可以调用 pthread_cleanup_pop()并传入非 0 参数,来手动执行线程清理函数,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>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_cleanup_pop(1)
执行了最后一次注册的清理函数,调用 pthread_exit()退出线程时执行了 2 次清理函数,因为前面调用 pthread_cleanup_pop()已经将顶层的清理函数移除栈中了,自然在退出时就不会再执行了。
九、线程属性
调用 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);
参数 attr 指向一个 pthread_attr_t 对象,即需要进行初始化的线程属性对象。在调用成功时返回 0,失败将返回一个非 0 值的错误码。
调用 pthread_attr_init()
函数会将指定的 pthread_attr_t
对象中定义的各种线程属性初始化为它们各自对应的默认值。
1、线程栈属性
每个线程都有自己的栈空间,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);
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);
}
2、分离状态属性
如果我们在创建线程时就确定要将该线程分离,可以修改 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);
参数 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 所指定的内存中。
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>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);
}
十、线程安全
1、线程栈
主线程创建了 5 个新的线程,这 5 个线程使用同一个 start 函数 new_thread,该函数中定义了局部变量 number 和 tid 以及 arg 参数,意味着这 5个线程的线程栈中都各自为这些变量分配了内存空间,任何一个线程修改了 number 或 tid 都不会影响其它线程。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>static void *new_thread(void *arg)
{int number = *((int *)arg);unsigned long int tid = pthread_self();printf("当前为<%d>号线程, 线程 ID<%lu>\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 < 5; j++)pthread_create(&tid[j], NULL, new_thread, &nums[j]);/* 等待线程结束 */for (j = 0; j < 5; j++)pthread_join(tid[j], NULL);//回收线程exit(0);
}
2、可重入函数
单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于多线程程序而言,同一进程却存在多条独立、并发的执行流。
接下来再来介绍什么是可重入函数,如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>static void func(void)
{/*...... */
}static void sig_handler(int sig)
{func();
}int main(int argc, char *argv[])
{sig_t ret = NULL;ret = signal(SIGINT, (sig_t)sig_handler);if (SIG_ERR == ret) {perror("signal error");exit(-1);}/* 死循环 */for ( ; ; )func();exit(0);
}
当 main()函数正在执行 func()函数代码,此时进程收到了 SIGINT 信号,便会打断当前正常执行流程、跳转到 sig_handler()函数执行,进而调用 func、执行 func()函数代码;这里就出现了主程序与信号处理函数并发调用 func()的情况,示意图如下所示:
举例说明了函数被多个执行流同时调用的两种情况:
⚫ 在一个含有信号处理的程序当中,主程序正执行函数 func(),此时进程接收到信号,主程序被打断,跳转到信号处理函数中执行,信号处理函数中也调用了 func()。
⚫ 在多线程环境下,多个线程并发调用同一个函数。
可重入函数的分类
笔者认为可重入函数可以分为两类:
⚫ 绝对的可重入函数:所谓绝对,指的是该函数不管如何调用,都刚断言它是可重入的,都能得到预期的结果。(一般无全局变量)
⚫ 带条件的可重入函数:指的是在满足某个/某些条件的情况下,可以断言该函数是可重入的,不管怎么调用都能得到预期的结果。(只读的全局变量)
3、线程安全函数
一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。线程安全函数包括可重入函数,可重入函数是线程安全函数的一个真子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数,它们之间的关系如下:
譬如下面这个函数是一个不可重入函数,同样也是一个线程不安全函数
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 的访问进行保护,在读写该变量之前先上锁、读写完成之后在解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重入函数,因为该函数更改了外部全局变量的值。
判断一个函数是否为线程安全函数的方法是,该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。判读一个函数是否为可重入函数的方法是,从语言语法角度分析,该函数被多个执行流同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个可重入函数。
man 手册可以查看库函数的 ATTRIBUTES 信息,如果函数被标记为 MT-Safe,则表示该函数是一个线程安全函数,如果被标记为 MT-Unsafe,则意味着该函数是一个非线程安全函数,对于非线程安全函数,在多线程编程环境下尤其要注意,如果某函数可能会被多个线程同时调用时,该函数不能是非线程安全函数,一定要是线程安全函数,否则将会出现意想不到的结果、甚至使得整个程序崩溃!
4、一次性初始化
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
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;失败则返回错误编码以指示错误原因。
接下来我们测试下,当 pthread_once()被多次调用时,init_routine()函数是不是只会被执行一次,示例代码如下所示:
#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 的线程所执行,其它线程均未执行该函数。
5、线程特有数据
pthread_key_create()函数
线程特有数据的核心思想其实非常简单,就是为每一个调用线程(调用某函数的线程,该函数就是我们要通过线程特有数据将其实现为线程安全的函数)分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。
线程特有数据主要涉及到 3 个函数:pthread_key_create()、pthread_setspecific()以及 pthread_getspecific()
在为线程分配私有数据区之前,需要调用 pthread_key_create()函数创建一个特有数据键(key),并且只需要在首个调用的线程中创建一次即可。
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
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);
pthread_getspecific()函数应返回当前调用线程关联到特有数据键的私有数据缓冲区,返回值是一个指针,指向该缓冲区。
pthread_key_delete()函数
如果需要删除一个特有数据键(key)可以使用函数 pthread_key_delete(),pthread_key_delete()函数删除先前由 pthread_key_create()创建的键。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
调用pthread_key_delete()后,当线程终止时也不再执行键的解构函数。
通常在调用 pthread_key_delete()之前,必须确保以下条件:
⚫ 所有线程已经释放了私有数据区(显式调用解构函数或线程终止)。
⚫ 参数 key 指定的特有数据键将不再使用。