进程

线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。

任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程.

那自然就存在多线程进程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create 创建一个新的线程),那么创建的新线程就是主线程的子线程。

主线程的重要性体现在两方面:
⚫ 其它新的线程(也就是子线程)是由主线程创建的;
⚫ 主线程通常会在最后结束运行,执行各种清理工作,譬如回收各个子线程。

线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。

多进程编程的劣势:
⚫ 进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。
⚫ 进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦,在上一章节给大家有所介绍。
解决方案便是使用多线程编程,多线程能够弥补上面的问题:
⚫ 同一进程的多个线程间切换开销比较小。
⚫ 同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易。
⚫ 线程创建的速度远大于进程创建的速度。
⚫ 多线程在多核处理器上更有优势!

并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。

内核实现了调度算法,用于控制系统中所有线程的调度,简单点来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。

就像每个进程都有一个进程 ID 一样,每个线程也有其对应的标识,称为线程 ID。进程 ID 在整个系统中是唯一的。一个线程可通过库函数 pthread_self()来获取自己的线程 ID

1
2
3
4
#include <pthread.h>
pthread_t pthread_self(void);
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

如果两个线程 ID t1 和 t2 相等,则 pthread_equal()返回一个非零值;否则返回 0。

创建线程

主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程。

1
2
#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()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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);
}

终止进程

在新线程的启动函数(线程 start 函数)new_thread_start()通过 return 返回之
后,意味着该线程已经终止了,除了在线程 start 函数中执行 return 语句终止线程外,终止线程的方式还有
多种,可以通过如下方式终止线程的运行:
⚫ 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
⚫ 线程调用 pthread_exit()函数;
⚫ 调用 pthread_cancel()取消线程

如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!

pthread_exit()函数将终止调用它的线程。

1
2
#include <pthread.h>
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(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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);
}

取消线程

进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。

在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。

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

1
2
3
4
#include<pthread.h>
int pthread_cancel(pthread_t thread);
#include<pthread.h>
int pthread_join(pthread_t thread, void ** retval);

thread 参数用于指定接收哪个线程的返回值;retval 参数表示接收到的返回值,如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。

pthread_join() 函数会一直阻塞调用它的线程,直至目标线程执行结束(接收到目标线程的返回值),阻塞状态才会解除。如果 pthread_join() 函数成功等到了目标线程执行结束(成功获取到目标线程的返回值),返回值为数字 0;反之如果执行失败,函数会根据失败原因返回相应的非零值,每个非零值都对应着不同的宏 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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);
}

取消状态以及类型

当然,线程可以选择不被取消或者控制如何被取消,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型。

1
2
3
#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 值的错误码。
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_join()获取其返回状态、回收线程资源,有时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用 pthread_detach()将指定线程进行分离,也就是分离线程。

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

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

1
pthread_detach(pthread_self());

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

注册线程清理处理函数

使用 atexit()函数注册进程终止处理函数,当进程调用 exit()退出时就会执行进程终止处理函数;其实,当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数,我们把这个称为线程清理函数(thread cleanup handler)。与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构,也就是说它们的执行顺序与注册(添加)顺序相反,当执行完所
有清理函数后,线程终止。

1
2
3
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);//添加清理函数
void pthread_cleanup_pop(int execute);//移除清理函数

当线程执行以下动作时,清理函数栈中的清理函数才会被执行:
⚫ 线程调用 pthread_exit()退出时;
⚫ 线程响应取消请求时;
⚫ 用非 0 参数调用 pthread_cleanup_pop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#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_attr_t 数据结构中定义了栈的起始地址以及栈大小,调用函数pthread_attr_getstack()可以获取这些信息,函数 pthread_attr_setstack()对栈起始地址和栈大小进行设置,其函
数原型如下所示:

1
2
3
#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_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 值的错误码。

如果想单独获取或设置栈大小、栈起始地址,可以使用下面这些函数:

1
2
3
4
5
#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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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);
}