第十一章:线程
前言:
与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元为线程,而不是进程
1:线程的概念
什么是线程?
线程是参与系统调用的最小单位。它被包含在进程中,是进程中的实际运行单位。
一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务
线程如何创建起来?
当一个程序启动时,就存在一个进程被OS创建,与此同时一个线程也立刻运行,该线程叫做程序的主线程,因为它是程序一开始就运行的线程
应用程序以main作为入口开始运行,所以main是主线程的入口函数,main函数所执行的任务就是主线程需要执行的任务
由此可见,任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程。而所谓多线程是指除了主线程之外,还包含其他线程,其他线程一般为主线程创建,那么创建的新线程就是主线程的子线程
主线程的两个重点:
1:子线程由主线程创建,主线程是子线程的父辈
2:主线程通常需要最后结束运行,执行各种清理工作,譬如回收各个子线程
线程是程序基本的运行单位,而进程不是,真正运行的是进程中的线程,抢占CPU资源的也是线程。可以认为进程仅仅是一个容器,它包含了线程所允许所需要的数据结果、环境变量等信息
同一个进程中的多个线程共享该进程的全部系统资源,如:虚拟空间、文件描述符、全局变量。但各个线程有各自的调用栈(线程栈),自己的寄存器环境、自己的线程本地存储
1:线程不单独存在、而是包含在进程中
2:多线程宏观实现同时运行的效果
3:共享进程资源,首先表现在:所有线程具有相同的地址空间,意味着,线程可以访问地址空间的每一个虚拟地址
多进程和多线程的优劣势分析
多进程劣势
1:进程间的切换开销大。进程间的切换开销大于同进程中线程的切换
2:进程间的通信较为麻烦。每个进程都有自己的地址空间,相互独立,处于不同的地址空间,不像线程地址空间一样
多线程优点
1:开销少
2:通信容易
3:线程的创建速度大于进程的创建速度
4:在多核处理器中多线程更具有优势
总结
1:多线程较为广泛,相比较多进程
2:多线程编程难度大,对程序员的功底深,因为需要在多线程环境下考虑多个问题,例如:线程安全、信号处理
3:多进程通常在大型应用程序项目中,中小型项目使用多线程更多
并发与并行
并发是一个一个运行,并行是真实的一次运行多个。
2:线程ID
进程ID在整个系统中是唯一的,而线程ID不同,线程ID只有在它所属的进程上下文中才有意义
进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示,
获取线程ID函数pthread_self
一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
使用该函数需要包含头文件<pthread.h>。
该函数调用总是成功,返回当前线程的线程 ID线程 ID 在应用程序中非常有用,原因如下:
1:很多线程相关函数,譬如后面将要学习的 pthread_cancel()、 pthread_detach()、 pthread_join()等,它们都是利用线程 ID 来标识要操作的目标线程;
2:在一些应用程序中,以特定线程的线程 ID 作为动态数据结构的标签,这某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。
3:线程创建 pthread_create
主线程可以使用库函数 pthread_create()负责创建一个新的线程, 创建出来的新线程被称为主线程的子线程,
其函数原型如下所示:
#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 指向的内容是不确定的
线程创建成功, 新线程就会加入到系统调度队列中,获取到 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("thread:%d,%lu\n",getpid(),pthread_self()); return (void*)0; //返回值亮点 } int main() { pthread_t thread; //这个变量的生存周期需要值得注意一下 int ret; ret = pthread_create( (pthread_t *)&thread, NULL, new_thread_start, NULL); if(ret != 0 ) //判断函数是否使用成功,形成习惯 { printf("pthread_create fail\n"); exit(1); } printf("zhu:%d,%lu\n",getpid(),pthread_self()); sleep(1); //等待子线程执行完 return 0; }
代码总结
1:明确pthread_t的数据类型,因为在linux是usnigned long int ,而在其他的平台,就不是了,可能
2:主线程休眠一下sleep(1),表示休眠一秒
3:编译出现的问题,gcc的时候需要加 -lpthread
4:注意使用变量的生存时间
除了在线程 start 函数中执行 return 语句终止线程外, 终止线程的方式还有
多种,可以通过如下方式终止线程的运行:
1:线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
2:线程调用 pthread_exit()函数;
3:调用 pthread_cancel()取消线程;
如果进程中的任意线程调用 exit()、 _exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意,因为exit结束的是主线程,而上面的结束的是当前使用线程
pthread_exit()函数将终止调用它的线程,其函数原型如下所示:
#include <pthread.h>
void pthread_exit(void *retval);
参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的。
参数 retval 所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效;
出于同样的理由,也不应在线程栈中分配线程 start 函数的返回值。因为我们定义的线程函数的类型是void*,因此返回值是地址,而我们知道在C语言中如果需要返回地址的话,那么这个地址不应该在栈中。
调用 pthread_exit()相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所调用的任意函数中调用 pthread_exit()来终止线程。 如果主线程调用了 pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止
4: 回收线程pthread_join
在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid()) 阻塞等待子进程退出并获取其终止状态,回收子进程资源; 而在线程当中, 也需要如此, 通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码, 回收线程资源;
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),则必须使用 pthread_join()来等待线程终止,回收
线程资源;如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收
pthread_join()与waitpid()区别
1:线程之间关系是对等的。进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。
譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;这与进程间层次关系不同,
父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间不存在这样的关系。
2:不能以非阻塞的方式调用 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> void *threadret ; static void *new_thread_start(void *arg) { printf("thread:%d,%lu\n",getpid(),pthread_self()); pthread_exit((void*)2); // return (void *)0; } int main() { pthread_t thread; int ret; ret = pthread_create( (pthread_t *)&thread, NULL, new_thread_start, NULL); if(ret != 0 ) { printf("pthread_create fail\n"); exit(1); } printf("zhu:%d,%ld\n",getpid(),pthread_self()); pthread_join(thread,(void **)&threadret); printf("ret=%d\n",threadret); //这里形式自己写错误了,写成了*threadret,是没有*的 //exit(); return 0; } //错误码打印 ret = pthread_join(tid, &tret); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); }
打印的结果为2
5:取消线程
在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。
有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出
取消一个线程函数pthread_cancel
int pthread_cancel(pthread_t thread);
参数 thread 指定需要取消的目标线程;成功返回 0,失败将返回错误码。
发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1) 的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消 ,所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。就类似于信号一样,信号处理的方式我们也可以自己选择,信号也只是请求,如果你不管它,那么默认执行因此当我们的主线程执行取消请求给子线程时(此时子线程未完成),那么子线程就会立刻退出,使用pthread_join
当主线程发送取消请求之后,新线程便退出了,而且退出码为-1
取消状态以及类型
默认情况下,线程是响应其它线程发送过来的取消请求的, 响应请求然后退出线程。 当然,线程可以选择不被取消或者控制如何被取消,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型
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。
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取消点
若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
那什么是取消点呢? 所谓取消点其实就是一系列函数, 当执行到这些函数的时候,才会真正响应取消请求, 这些函数就是取消点; 在没有出现取消点时,取消请求是无法得到处理的, 究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。
取消点函数包括哪些呢?下表给大家简单地列出了一些线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)
//该函数就不存在取消点,因此我们需要自己产生一个取消点 static void *new_thread_start(void *arg) { printf("新线程--start run\n"); for ( ; ; ) { } return (void *)0; }
线程可取消性的检测
假设线程执行的是一个不含取消点的循环(譬如 for 循环、 while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它,就如上给大家列举的例子。
在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已
有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。 其函数原型如下所示:
#include <pthread.h>
void pthread_testcancel(void);直接调用即可