Linux线程(一)
一些注意事项
- Linux中每个线程都有单独的进程ID,inux中每个线程都有单独的进程ID。在Linux中,线程其实是通过轻量级进程(LWP)实现的,因此Linux中每个线程都是一个进程,都拥有一个PID。换句话说,操作系统原理中的线程,对应的其实是Linux中的进程
- Linux的线程中,假如主线程(也就是
main函数)执行结束退出(比如exit()或者return 0),会导致整个进程所有线程被强制停止 - 即使是正在执行尚且没有结束的线程也会被停止
线程概念
- 线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进 程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现 并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将 两个不同的任务分别放置在两个线程中。
关于线程的更深层次的理解
- 参考
- 对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但是页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。 - 实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone
- 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”
- Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用
- 进程:独立地址空间,拥有PCB
- 线程:也有PCB,但没有独立的地址空间
线程的创建
- 当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通 常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为 入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的 任务。
- 所以由此可知,任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,譬如前面章节内 容中所编写的所有应用程序都是单线程程序,它们只有主线程;既然有单线程进程,那自然就存在多线程进 程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用 pthread_create 创建一个新的线程),那么创建的新线程就是主线程的子线程。
- 其它新的线程(也就是子线程)是由主线程创建的;
- 主线程通常会在最后结束运行,执行各种清理工作,譬如回收各个子线程。
线程vs进程
- 进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上 依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中 小型应用程序来说不划算。
- 进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间 中,因此相互通信较为麻烦,在上一章节给大家有所介绍。
- 同一进程的多个线程间切换开销比较小。
- 同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间 中,通信容易。
- 线程创建的速度远大于进程创建的速度。
- 多线程在多核处理器上更有优势!
并发和并行
并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单 元,所以可以实现并行运行,譬如并行运行 task1、task2、task3。

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

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并 行,仅仅只是串行。
你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替 /交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器 的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就 是同时运行着所有线程。
进程ID
每个线程也有其对应的标识,称为线程 ID。进程 ID 在整个系统 中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。
一个线程可通过库函数 pthread_self()来获取自己的线程 ID
|
- 该函数调用总是成功,返回当前线程的线程 ID
- 可以使用 pthread_equal()函数来检查两个线程 ID 是否相等
|
在 Linux 系统中,使 用无符号长整型(unsigned long int)来表示 pthread_t 数据类型
创建线程
- 主线程可以使用库函数
pthread_create()负责创建一个新的线程
|
注意,这个线程本质上是通过库函数调用系统的
clone实现的,实际上就是创建了一个新的进程(类似vfork),但是这个进程的所有数据资源都直接指向创建它的进程的数据资源,导致二者实际使用起来是共享变量和数据等等- 因此所谓的线程也是进程,只不过是与创建它的进程共享资源的进程
thread:pthread_t 类型指针,当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread 所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
attr:pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区,pthread_attr_t 数据类型定义了线程的 各种属性,关于线程属性将会在 11.8 小节介绍。如果将参数 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 资源
在编译含有
pthread的库函数的文件的时候,需要通过gcc的-l选项指定链接库,比如
gcc -o 文件名 文件名.c -lpthread |
应用举例:
|
输出

- 主线程休眠了 1 秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创 建的线程还没有机会运行,整个进程就结束了。
线程终止
- 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
- 线程调用
pthread_exit()函数; - 调用
pthread_cancel()取消线程(将在 11.6 小节介绍);
如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!
pthread_exit()函数将终止调用它的线程
|
参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线 程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返 回值也是可以通过 pthread_join()来获取的。
调用 pthread_exit()相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所 调用的任意函数中调用 pthread_exit()来终止线程。如果主线程调用了
pthread_exit(),那么主线程也会终止, 但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止。
|
- 注意,将上一个程序中的所有
return都改成pthread_exit(NULL)之后,程序会话并没有在主线程停止之后停止,而是等待子线程停止之后才停止
输出为

回收线程
- 调用
pthread_join()函数来阻塞等待线程的终止, 并获取线程的退出码,回收线程资源(类似于多进程中的wait()函数)
|
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.6.1 小节介绍),则必须使用 pthread_join()来等待线程终止,回收 线程资源;如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应 用程序无法创建新的线程。
如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸 线程同样也会被回收。
进程还具有以下特点
线程之间关系是对等的。进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。 譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待 线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;这与进程间层次关系不同, 父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间 不存在这样的关系。
不能以非阻塞的方式调用 pthread_join()。对于进程,调用 waitpid()既可以实现阻塞方式等待、也可 以实现非阻塞方式等待。
取消线程
- 有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为 取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算, 一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。
- 调用 pthread_cancel()库函数向一个指定的线程发送取消请求
|
- 发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程 也会立刻退出
- 线程可以设置自己不被取消或者控制如何被取消
使用例:
|

- 由打印结果可知,当主线程发送取消请求之后,新线程便退出了,而且退出码为
-1,也就是PTHREAD_CANCELED。
线程控制自己被取消的时候的行为
- 默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选 择不被取消或者控制如何被取消,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性 状态和类型。
|
参数
PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以 新建线程以及主线程默认都是可以取消的。
PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求 挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。
pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。
参数 type 必须是以下值之一:
- PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直 到线程到达某个取消点(cancellation point,将在 11.6.3 小节介绍)为止,这是所有新建线程包括 主线程默认的取消性类型。
- PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定) 取消线程,这种取消性类型应用场景很少,不再介绍!
取消点:
- 取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请 求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但 没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会 导致出现意想不到的异常发生。
检测线程的可取消性
pthread_testcancel(void);,头文件同上