简介
线程
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 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
#include <pthread.h>
pthread_t pthread_self(void);该函数返回当前线程的线程ID。
还可以使用pthread_equal()函数检查两个线程ID是否相等:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);如果相等,返回非零值;否则返回0.
创建线程
#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资源(主线程或新创建的线程)。如果程序对不同线程的执行顺序有要求,就必须采用一些同步技术来实现。
简单的创建线程的例子:
#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()将终止调用它的线程:
#include <pthread.h>
void pthread_exit(void *retval);形参retval指定了线程的返回值,也就是线程的退出码。retval指向的内容不应分配在线程栈中,因为线程终止后将无法确认线程栈中的内存还是否有效。
回收线程
可调用pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源:
#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()一旦被调用,对应线程即阻塞。
例子:
#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():
#include <pthread.h>
int pthread_cancel(pthread_t thread);该函数仅提出请求,不会等待目标线程退出,而是会立即返回。
取消状态和类型
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);int pthread_setcanceltype(int type, int *oldtype);pthread_setcancelstate和pthread_setcanceltype可用于设置线程的取消性状态和类型。pthread_setcancelstate会将调用线程的取消性状态设置为state,并将线程之前的取消性状态保存在oldstate中。如果对旧状态不感兴趣,可设置为NULL。state必须是以下值之一:
PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE
示例:
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:
#include <pthread.h>
void pthread_testcancel(void);该函数会产生一个取消点:
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()将指定线程进行分离,也就是分离线程:
#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()返回:
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()用于进程,不能用于线程同步
应用示例:
#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()分别负责向调用线程的清理函数栈中添加和移除清理函数:
#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()实际上是两个宏,必须成对出现,并在同一个函数作用域中使用。
示例:
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_JOINABLE或PTHREAD_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_SCHED或PTHREAD_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_SYSTEM或PTHREAD_SCOPE_PROCESS) -
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
// 获取线程作用域
代码范例
#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);}