Linux 进程与线程(二)线程

简介

线程

线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中。RTOS中的任务就是线程的一种表现形式。

线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。

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

在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被 CPU 执行,线程具有以下一些特点:

  • 不单独存在,包含在进程中
  • 线程是参与系统调度的基本单位
  • 同一进程的多线程可并发执行,宏观上实现同时运行的效果
  • 同一进程的各个线程共享该进程拥有的资源:
    • 所有线程具有相同的地址空间
    • 线程可访问该地址空间的每一个虚地址
    • 可访问进程所拥有的已打开文件、定时器和信号量

线程与进程的优劣

多进程编程的劣势:

  • 内存空间不共享:每个进程有独立的虚拟地址空间,数据共享麻烦,必须通过 IPC(如管道、共享内存、消息队列等)
  • 创建开销大:使用 fork() 创建新进程代价高(复制页表、上下文等)。频繁创建/销毁进程会显著降低性能
  • 通信复杂:不同进程间通信(IPC)机制种类多、实现复杂,尤其是同步控制(例如多个进程同时写共享内存)容易出错
  • 切换成本高:进程上下文切换比线程慢很多,因为需要切换整个地址空间、页表等
  • 资源管理复杂:每个进程资源独立,协调管理(如关闭文件描述符、回收子进程)更复杂

多线程编程的优势:

  • 共享地址空间:线程共享进程的内存,通信和共享数据非常方便,比如共享变量、缓冲区
  • 创建/销毁成本小:创建线程用 pthread_create() 等,比 fork() 快很多,开销更小
  • 上下文切换快:线程切换只需切换寄存器、栈等,不涉及页表等操作,切换代价低
  • 通信简单:直接通过全局变量或堆内存通信,不需要复杂的 IPC 机制
  • 适合高并发任务:在 Web 服务器、实时系统中常见,用来处理高并发连接更高效

多线程编程的劣势:

  • 数据竞争:线程共享数据,必须使用互斥锁、读写锁等手段保护,稍不注意就出 bug
  • 调试困难:多线程问题(死锁、竞态条件)很难重现和排查
  • 一个线程崩溃可能导致整个进程崩溃

并发和并行

  • 串行:一件事、一件事接着做
    • 只用一口锅,先做完第一碗面,再做第二碗,最后做第三碗
  • 并发:交替做不同的事
    • 烧水的时候准备下碗;水烧着时你不闲着;一会煮面,一会加料;只要锅没闲着,切来切去地忙
  • 并行:同时做不同的事
    • 3个厨师 + 3口锅,每人煮一碗面,互不干扰

对于单核处理器,只有一个执行单元,因此同时只能执行一条指令;对于多核处理器,可并行执行多条指令,譬如8核处理器可并行执行8条不同的指令。

线程ID

1
2
3
#include <pthread.h>

pthread_t pthread_self(void);

该函数返回当前线程的线程ID。

还可以使用pthread_equal()函数检查两个线程ID是否相等:

1
2
3
#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

如果相等,返回非零值;否则返回0.

创建线程

1
2
3
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

形参:

  • thread:函数成功返回时,创建成功的线程ID会保存在thread指向的内存中
  • attr:定义了线程的各种属性,如果设置为NULL,表示所有属性设为默认值
  • start_routine:新创建的线程从start_routine()函数开始执行
  • arg:要传递给start_routine()函数的参数,一般情况下需要将arg指向一个全局或堆变量,即需要保证线程的生命周期中arg指向的对象必须存在,否则会出现段错误。如果设置为NULL,表示不需要传入参数。

和进程创建时相同,一旦线程创建成功,新线程就会立即被加入到系统调度队列并开始执行,通常无法确定系统会首先调度哪一个线程来使用CPU资源(主线程或新创建的线程)。如果程序对不同线程的执行顺序有要求,就必须采用一些同步技术来实现。

简单的创建线程的例子:

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
#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_theard_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_theard_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函数执行return并返回指定值,返回值就是线程的退出码
  • 线程调用pthread_exit()函数
  • 调用pthread_cancel()取消线程

pthread_exit()将终止调用它的线程:

1
2
3
#include <pthread.h>

void pthread_exit(void *retval);

形参retval指定了线程的返回值,也就是线程的退出码。retval指向的内容不应分配在线程栈中,因为线程终止后将无法确认线程栈中的内存还是否有效。

回收线程

可调用pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源:

1
2
3
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • retval:如果不为NULL,函数将目标线程的退出状态复制到*retval;如果目标线程被cancel,则将PTHREAD_CANCELED放在*retval中;如果对目标线程的终止状态不感兴趣,可设置为NULL

pthread_join()类似于针对进程的waitpid(),但是:

  • 线程之间关系是平等的。同一进程中的任意线程均可调用pthread_join()来等待另一个线程的终止
  • 不能以非阻塞的方式调用pthread_join()pthread_join()一旦被调用,对应线程即阻塞。

例子:

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
#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_theard_start(void *arg)
{
/* 线程创建2秒后即终止 */
printf("New thread start\n");
sleep(2);
printf("New thread end\n");
pthread_exit((void *)10);
}

int main(void)
{
pthread_t tid;
void *tret;
int ret;

ret = pthread_create(&tid, NULL, new_theard_start, NULL);
if (ret){
fprintf(stderr, "Error:%s\n", strerror(ret));
exit(-1);
}

/* 阻塞等待tid结束 */
ret = pthread_join(tid, &tret);
if (ret){
fprintf(stderr, "pthread join error: %s\n", strerror(ret));
exit(-1);
}
printf("New thread end, code = %ld\n", (long)tret);

exit(0);
}

取消线程

取消线程特指某一个线程向另一个线程发送请求,要求其立即退出的操作。

取消一个线程

通过pthread_cancel()

1
2
3
#include <pthread.h>

int pthread_cancel(pthread_t thread);

该函数仅提出请求,不会等待目标线程退出,而是会立即返回。

取消状态和类型

1
2
3
4
#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

pthread_setcancelstatepthread_setcanceltype可用于设置线程的取消性状态和类型。pthread_setcancelstate会将调用线程的取消性状态设置为state,并将线程之前的取消性状态保存在oldstate中。如果对旧状态不感兴趣,可设置为NULL。state必须是以下值之一:

  • PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的
  • PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE

示例:

1
2
3
4
5
6
7
8
9
10
11
12
static void *new_theard_start(void *arg)
{
/* 设置线程为不可取消 */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);

for (; ;)
{
printf("new thread is running\r\n");
sleep(2);
}
return (void *) 0;
}

如果线程的取消性状态为ENABLE,那么其对取消请求的处理取决于线程的取消性类型,可通过pthread_setcanceltype()来设置。type必须是以下值之一:

  • PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point)为止,这是所有新建线程包括主线程默认的取消性类型。
  • PTHREAD_CANCEL_ASYNCHRONOUS:很少使用。

取消点

当线程可以取消并且canceltype设置为PTHREAD_CANCEL_DEFERRED时,当收到其他线程通过pthread_cancel()发送到来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

取消点的作用是更精确的设置响应取消请求的位置,未到达取消点时,已经到达的取消请求将不会被响应。这是系统的一种保护机制,用于保证取消请求不会影响到关键代码的执行。man 7 pthreads可查询可用作取消点的函数。

可取消性的检测

如果线程执行的是一个无取消点的死循环,比如while(1),那么似乎这时候线程永远都不会响应外部的取消请求;除了线程自己主动退出,其他线程将无法通过向它发送取消请求而终止它。

此时可以使用pthread_testcancel:

1
2
3
#include <pthread.h>

void pthread_testcancel(void);

该函数会产生一个取消点:

1
2
3
4
5
6
7
8
9
static void *new_theard_start(void *arg)
{
printf("new thread is running\r\n");
for (; ;){
/* 人为产生取消点 */
pthread_testcancel();
}
return (void *) 0;
}

分离线程

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

1
2
3
#include <pthread.h>

int pthread_detach(pthread_t thread);

一个线程可以将另一个线程分离,也可以将自己分离:pthread_detach(pthread_self())

分离之后的线程无法通过pthread_join获取终止状态,该过程不可逆,分离之后就无法恢复到之前的状态。处于分离状态的线程终止后能够自动回收线程资源。

线程的分离状态通过pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)设置,参数可选为:

  • PTHREAD_CREATE_DETACHED:分离线程
  • PTHREAD _CREATE_JOINABLE:非分离线程

注意:如果一个线程被设置为分离线程,而该线程运行又非常快,很有可能在pthread_create()返回之前该线程就终止了,终止之后它就可能将线程号和系统资源移交给其他线程,这样pthread_create()可能就会返回错误的线程号。避免这种情况最常见的措施就是同步通知,比如在被创建的线程里调用pthread_cond_timedwait()函数,让线程等待一会儿,留出足够的时间让
pthread_create()返回:

1
2
3
4
int pthread_cond_timedwait(pthread_cond_t *cond, 
pthread_mutex_t *mutex,
const struct timespec *abstime);

  • cond:条件变量指针,是一个事件通知器,用于判断某种条件是否成立
  • mutex:互斥锁指针
  • abstime:绝对时间,等待超时的截止时间,单位为秒+纳秒
    注意:pthread_cond_timedwait() 等待的是 “到指定时间点” 为止,而不是 “等待N秒”!

pthread_cond_timedwait()本质上是一个带锁的同步等待机制,目的是等待某个线程间的“条件”成立。它会阻塞线程,但可以通过事件唤醒,还可以设置超时时间,防止死锁。适合:

  • 等待某个事件的发生,如数据就绪或任务完成
  • 在一段时间内等待,而非无限等待
  • 希望在等待期间自动释放资源

该函数一般不能用sleep()wait()代替:

  • sleep()只是简单的挂起线程,无法被事件唤醒,也无法和条件变量配合使用
  • wait()用于进程,不能用于线程同步

应用示例:

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
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static void *thread_func(void *arg)
{
printf("线程启动了(ID=%lu)\n", pthread_self());

struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 1; // 等待1秒钟

pthread_mutex_lock(&mutex);
// 等待条件变量1秒,让主线程有足够时间返回pthread_create()
pthread_cond_timedwait(&cond, &mutex, &ts);
pthread_mutex_unlock(&mutex);

printf("线程准备退出\n");

pthread_exit(NULL);
}

int main()
{
pthread_t tid;
pthread_attr_t attr;

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置为分离状态

if (pthread_create(&tid, &attr, thread_func, NULL) != 0) {
perror("pthread_create error");
exit(EXIT_FAILURE);
}

// 保证pthread_create返回前线程还在运行
printf("主线程:pthread_create成功,线程ID=%lu\n", tid);

pthread_attr_destroy(&attr);

sleep(2); // 等待子线程退出

printf("主线程退出\n");
return 0;
}

线程清理函数(Thread cleanup handler)

线程可通过函数 pthread_cleanup_push()pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数:

1
2
3
4
#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

pthread_cleanup_push()用于注册一个清理函数routine(arg)pthread_cleanup_pop()负责移除栈顶的cleanup handler,如果excute != 0,则调用它。

线程执行以下动作时,清理函数栈中的清理函数才会被执行:

  • 调用pthread_exit()退出时
  • 响应取消请求时(pthread_cancel)
  • 用非0参数调用pthread_cleanup_pop()

pthread_cleanup_push()pthread_cleanup_pop()实际上是两个宏,必须成对出现,并在同一个函数作用域中使用。

示例:

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

线程属性

线程属性是pthread_attr_t类型的,其在线程创建前被创建、初始化和修改,并在线程创建时通过形参传入线程。

初始化与销毁属性对象

  • int pthread_attr_init(pthread_attr_t *attr);
    // 初始化线程属性对象

  • int pthread_attr_destroy(pthread_attr_t *attr);
    // 销毁属性对象,释放资源


设置线程是否可分离(joinable/detached)

  • int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    // 设置线程分离属性(PTHREAD_CREATE_JOINABLEPTHREAD_CREATE_DETACHED

  • int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
    // 获取当前分离属性状态


设置线程栈大小与起始地址

  • 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_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
    // 设置线程栈地址和大小

  • int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
    // 获取线程栈地址和大小


设置线程调度策略和参数

  • int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
    // 设置调度策略(如 SCHED_FIFO, SCHED_RR, SCHED_OTHER

  • int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
    // 获取调度策略

  • int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
    // 设置调度参数(如优先级)

  • int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
    // 获取调度参数


设置调度继承属性

  • int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);
    // 设置调度属性是否继承(PTHREAD_INHERIT_SCHEDPTHREAD_EXPLICIT_SCHED

  • int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);
    // 获取调度继承属性


设置线程作用域(系统级 / 进程级)

  • int pthread_attr_setscope(pthread_attr_t *attr, int scope);
    // 设置作用域(PTHREAD_SCOPE_SYSTEMPTHREAD_SCOPE_PROCESS

  • int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
    // 获取线程作用域

代码范例

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
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *thread_1_start(void *arg)
{
size_t stacksize;

/* 获取线程栈大小 */
pthread_attr_getstacksize((pthread_attr_t *)arg, &stacksize);

printf("Thread 1 start, stack size: %zu\n", stacksize);

sleep(2);

/* 退出线程 */
pthread_exit(NULL);
}

int main()
{
int ret;
pthread_t thread_1;
pthread_attr_t thread_1_attr;

/* 初始化线程属性 */
pthread_attr_init(&thread_1_attr);

/* 设置线程为分离态 */
pthread_attr_setdetachstate(&thread_1_attr, PTHREAD_CREATE_DETACHED);

/* 设置线程栈大小 */
ret = pthread_attr_setstacksize(&thread_1_attr, 88888);
if (ret != 0){
fprintf(stderr, "pthread_attr_setstacksize failed: %s\n", strerror(ret));
exit(-1);
}

/* 创建线程 */
ret = pthread_create(&thread_1, &thread_1_attr, thread_1_start, &thread_1_attr);

/* 销毁线程属性 */
pthread_attr_destroy(&thread_1_attr);

sleep(3);

/* 退出线程 */
pthread_exit(NULL);
}

Linux 进程与线程(二)线程
http://akichen891.github.io/2025/04/21/Linux线程2/
作者
Aki
发布于
2025年4月21日
更新于
2025年4月21日
许可协议