应用程序-2.创建子线程(C语言实现)
1.理论部分1.1线程的定义1.2线程的关键特点1.3同一进程下线程的资源1.3.1共享资源1.3.2私有资源
1.4子线程和子进程相比较1.4.1两者应用场景对比1.4.1.1子进程的应用场景1.4.1.2子线程的应用场景
2.子线程的实现2.1简单实现2.2设置线程名字2.2.1方法一:使用prctl进行设置2.2.2方法二:使用pthread_setname_np进行设置2.2.3两种方法的对比
2.3线程锁2.3.1. 互斥锁(Mutex)。有可能阻塞2.3.2. 读写锁(Read-Write Lock)2.3.3. 自旋锁(Spin Lock)2.3.4三种锁对比
2.4线程的被动取消2.5相关函数说明2.5.1线程创建函数:pthread_create2.5.2.线程属性的设置2.5.2.1 pthread_attr_t 结构体详解2.5.2.2 初始化线程属性: pthread_attr_init2.5.2.3 是否与进程中其他线程脱离同步2.5.2.4 设置新线程的调度策略2.5.2.5 设置线程的继承方式2.5.2.6 线程的作用域2.5.2.7 设置线程栈的大小
2.5.3 线程的回收2.5.4 线程的主动结束2.5.5 线程的被动取消2.5.5.1设置取消的方式:pthread_setcanceltype2.5.5.2 设置取消的状态:pthread_setcancelstate2.5.5.3 发送线程取消请求:pthread_cancel2.5.5.4 主动设置取消点:pthread_testcancel2.5.5.5 线程的清理:pthread_cleanup_push和pthread_cleanup_pop
2.5.6 线程id2.5.6.1 比较两个线程id
3 命令行中查看线程4.编译
注:本文章只考虑Linux系统下的情况
1.理论部分
1.1线程的定义
为什么会出现线程这个概念?
进程之间的转换需要重新设置进程的上下文,开销较大。在多进程执行的场景,特别是进程之间切换十分频繁的时候,系统资源浪费严重。
那如何优化呢?我们发现在进行CPU 调度实体切换的场景下,存在不需要切换进程上下文的情况。
通过引进线程的概念,实现了只切换CPU 调度实体,而不改变进程上下文
线程的定义
线程(Thread)是操作系统能够执行的最小单位,是程序执行的基本调度单位。一个线程是一个进程中的一条执行路径,多个线程可以在同一个进程中并发执行,共享同一进程的资源(如内存空间、文件描述符等)。因此,线程有时被称为轻量级进程(Lightweight Process, LWP)。
1.2线程的关键特点
共享内存:同一进程内的多个线程共享进程的内存空间,可以直接访问同一块数据,这使得数据共享和通信变得简单。独立调度:每个线程可以独立调度,操作系统能够在多个线程之间切换,使得它们看起来是同时执行的。轻量级:创建和切换线程通常比创建和切换进程的开销小,因为线程共享同一进程的资源,不需要复制整个进程的上下文。
1.3同一进程下线程的资源
1.3.1共享资源
可执行指令静态全局数据进程中打开的文件描述符当前的工作目录用户ID用户组ID
1.3.2私有资源
线程ID(TID)PC和相关寄存器堆栈错误号errno优先级执行状态和属性
1.4子线程和子进程相比较
特征子进程子线程内存空间每个子进程拥有独立的地址空间,内存相互隔离。共享同一进程的地址空间和资源。创建开销创建和销毁的开销较大,包括复制进程的地址空间。创建和销毁的开销较小,通常只需要分配栈空间。切换开销上下文切换的开销较大,需要保存和加载进程状态。上下文切换的开销较小,主要是保存和加载线程状态。数据共享进程间数据共享相对复杂,通常需要使用 IPC(进程间通信)机制,如消息队列、管道、共享内存等。线程间数据共享简单,可以直接访问共享变量。调试和管理进程间的调试和管理相对复杂。不容易控制和检测。线程间的调试和管理相对简单,但多线程会增加竞争条件。系统资源每个子进程占用更多的系统资源,如内存和文件描述符。子线程占用较少的系统资源,更加高效。
1.4.1两者应用场景对比
1.4.1.1子进程的应用场景
- 任务隔离:当任务之间需要强隔离时,使用子进程更合适。例如,在执行不受信任的代码或处理敏感数据时,子进程可以有效地防止一个任务影响另一个任务。
- 独立性:适用于需要独立执行且互不影响的任务,如服务进程、后台任务等。
- 重启能力:在需要保证服务的稳定性时,使用子进程可以在崩溃时方便地重启子进程,而不影响主进程的运行。
1.4.1.2子线程的应用场景
1.高并发网络服务:当需要处理多个客户端请求时,使用子线程可以高效地管理并发连接,如 Web 服务器、聊天应用等。
2.数据共享:在需要频繁共享数据的场景中,使用子线程可以简化数据通信,如多线程的数据处理。
3.短时间任务:对于快速完成的小任务,子线程可以有效利用 CPU 资源,减少创建和销毁开销。
4.需要快速响应的用户界面应用:在 GUI 应用中,使用子线程可以实现后台任务处理,而不阻塞用户交互,提高用户体验。
2.子线程的实现
2.1简单实现
#include
#include
#include
#include
#define NUM_THREADS 5 // 定义线程数量
// 线程执行的函数
void *thread_task(void *arg) {
long thread_id = (long)arg; // 获取线程 ID
printf("子线程 [%ld] 正在执行\n", thread_id);
sleep(1); // 模拟子线程执行任务
printf("子线程 [%ld] 执行完毕\n", thread_id);
pthread_exit(NULL); // 退出线程
}
int main() {
pthread_t threads[NUM_THREADS]; // 创建线程数组
// 创建多个子线程
for (long i = 0; i < NUM_THREADS; i++) {
int result = pthread_create(&threads[i], NULL, thread_task, (void *)i);
if (result) {
fprintf(stderr, "错误:无法创建线程 [%ld],错误代码: %d\n", i, result);
exit(EXIT_FAILURE);
}
}
// 等待所有子线程结束
for (int i = 0; i < NUM_THREADS; i++) {
void *status;
pthread_join(threads[i], &status); // 阻塞等待线程结束
printf("子线程 [%d] 已结束,返回值: %s\n", i, (char *)status);
}
printf("主线程所有子线程已完成\n");
return 0;
}
2.2设置线程名字
2.2.1方法一:使用prctl进行设置
函数原型:
#include
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
/*
参数:
option:指定要执行的操作。常见的选项包括:
PR_SET_NAME:设置进程名称(线程名称)。
PR_GET_NAME:获取进程名称(线程名称)
PR_SET_PDEATHSIG:设置父进程终止时发送的信号。
PR_GET_PDEATHSIG:获取父进程终止时发送的信号。
PR_SET_DUMPABLE:设置进程是否可生成核心转储文件。
PR_GET_DUMPABLE:获取进程是否可生成核心转储文件。
(核心转储文件是指在程序崩溃或异常终止时,系统将程序的内存状态保存到一个文件中,这个文件称为核心转储文件(Core Dump)。核心转储文件包含了程序崩溃时的内存、寄存器状态、堆栈信息等,可以用于后续的调试和分析,帮助开发者定位问题。)
arg2, arg3, arg4, arg5:根据 option 的不同,这些参数的含义也不同。通常用于传递额外的参数或返回值。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 以指示错误原因。
*/
以下是一个多线程程序示例,展示如何设置和获取线程名称:
#include
#include
#include
#include
// 线程函数
void *thread_func(void *arg) {
char name[16];
// 设置线程名称
prctl(PR_SET_NAME, "WorkerThread");
// 获取线程名称
prctl(PR_GET_NAME, name);
printf("Thread Name: %s\n", name);
sleep(2); // 模拟工作
return NULL;
}
int main() {
pthread_t thread_id;
// 创建线程
if (pthread_create(&thread_id, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 获取主线程名称
char main_name[16];
prctl(PR_GET_NAME, main_name);
printf("Main Thread Name: %s\n", main_name);
// 等待线程退出
if (pthread_join(thread_id, NULL) != 0) {
perror("pthread_join");
return 1;
}
printf("Main: Thread exited successfully\n");
return 0;
}
2.2.2方法二:使用pthread_setname_np进行设置
pthread_setname_np 是 Linux 系统中用于设置线程名称的函数。它是 POSIX 线程库的扩展函数,专门用于设置线程的名称,方便调试和日志记录。
#define _GNU_SOURCE
#include
int pthread_setname_np(pthread_t thread, const char *name);
/*
参数:
thread:要设置名称的线程标识符(pthread_t 类型)。如果是当前线程,可以传递 pthread_self()。
name:要设置的线程名称。名称的最大长度为 16 字节(包括终止符 \0)。如果名称过长,会被截断。
返回值:
成功:返回 0
失败:返回非零值,并设置 errno 以指示错误原因
*/
以下是一个示例,展示如何使用 pthread_setname_np 设置线程名称:
#define _GNU_SOURCE
#include
#include
#include
#include
// 线程函数
void *thread_func(void *arg) {
// 设置线程名称
if (pthread_setname_np(pthread_self(), "WorkerThread") != 0) {
perror("pthread_setname_np");
}
// 模拟工作
sleep(2);
return NULL;
}
int main() {
pthread_t thread_id;
// 创建线程
if (pthread_create(&thread_id, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 设置主线程名称
if (pthread_setname_np(pthread_self(), "MainThread") != 0) {
perror("pthread_setname_np");
}
// 等待线程退出
if (pthread_join(thread_id, NULL) != 0) {
perror("pthread_join");
return 1;
}
printf("Main: Thread exited successfully\n");
return 0;
}
2.2.3两种方法的对比
特性pthread_setname_npprctl(PR_SET_NAME)作用对象可以设置任意线程的名称(通过 pthread_t 指定)。只能设置调用线程的名称(当前线程)。名称长度限制最大 16 字节(包括终止符 \0)。最大 16 字节(包括终止符 \0)。兼容性GNU 扩展,仅在 Linux 系统中可用。POSIX 标准,跨平台支持较好。获取名称的方法使用 pthread_getname_np 获取线程名称。使用 prctl(PR_GET_NAME) 获取线程名称。
2.3线程锁
线程锁(Thread Lock)是多线程编程中用于控制对共享资源访问的同步机制,确保多个线程在访问共享资源时不会发生冲突或数据竞争。常见的线程锁包括互斥锁(Mutex)、读写锁(Read-Write Lock)、**自旋锁(Spin Lock)**等。以下是线程锁的详细说明和使用方法:
2.3.1. 互斥锁(Mutex)。有可能阻塞
互斥锁是最常用的线程锁,用于确保同一时间只有一个线程可以访问共享资源。 (1) 使用方法
/*
注意事项:
加锁和解锁必须成对出现,否则可能导致死锁。
互斥锁的初始化方式可以是静态初始化(PTHREAD_MUTEX_INITIALIZER)或动态初始化(pthread_mutex_init)。
*/
#include
// 定义互斥锁
pthread_mutex_t mutex;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 加锁
pthread_mutex_lock(&mutex);
// 访问共享资源
// ...
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
示例:
#include
#include
int shared_data = 0;
pthread_mutex_t mutex;
void *thread_func(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
shared_data++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建线程
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// 等待线程退出
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("Shared Data: %d\n", shared_data);
return 0;
}
2.3.2. 读写锁(Read-Write Lock)
读写锁允许多个线程同时读取共享资源,但写操作必须是独占的。 (1) 使用方法
/*
注意事项:
读写锁适合读多写少的场景。
写锁会阻塞所有读锁和写锁。
*/
#include
// 定义读写锁
pthread_rwlock_t rwlock;
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 读锁
pthread_rwlock_rdlock(&rwlock);
// 写锁
pthread_rwlock_wrlock(&rwlock);
// 解锁
pthread_rwlock_unlock(&rwlock);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
示例:
#include
#include
int shared_data = 0;
pthread_rwlock_t rwlock;
void *reader_func(void *arg) {
pthread_rwlock_rdlock(&rwlock);
printf("Reader: %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void *writer_func(void *arg) {
pthread_rwlock_wrlock(&rwlock);
shared_data++;
printf("Writer: %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
pthread_t reader1, reader2, writer1;
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建线程
pthread_create(&reader1, NULL, reader_func, NULL);
pthread_create(&reader2, NULL, reader_func, NULL);
pthread_create(&writer1, NULL, writer_func, NULL);
// 等待线程退出
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(writer1, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
2.3.3. 自旋锁(Spin Lock)
自旋锁在获取锁时不会阻塞线程,而是通过忙等待(Busy Waiting)的方式不断尝试获取锁。 使用方法:
#include
// 定义自旋锁
pthread_spinlock_t spinlock;
// 初始化自旋锁
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
// 加锁
pthread_spin_lock(&spinlock);
// 解锁
pthread_spin_unlock(&spinlock);
// 销毁自旋锁
pthread_spin_destroy(&spinlock);
示例:
#include
#include
int shared_data = 0;
pthread_spinlock_t spinlock;
void *thread_func(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_spin_lock(&spinlock);
shared_data++;
pthread_spin_unlock(&spinlock);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化自旋锁
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
// 创建线程
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// 等待线程退出
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁自旋锁
pthread_spin_destroy(&spinlock);
printf("Shared Data: %d\n", shared_data);
return 0;
}
2.3.4三种锁对比
锁类型适用场景特点互斥锁通用场景简单易用,适合大多数情况。 (中断上下文除外)读写锁读多写少的场景允许多个读锁,写锁独占。自旋锁锁持有时间短的场景通过忙等待获取锁,适合短时间锁定。
2.4线程的被动取消
为了确保资源的正确使用、提升了多线程程序的健壮性和可用性,它为开发者提供了灵活的控制——被动取消
在 POSIX 线程库中,线程的被动取消(也称为普通取消或取消处理)是指允许其他线程请求取消某个线程的执行。与主动取消相比,被动取消是一种更加灵活和安全的取消方式,通常会在特定的点上检查取消请求,从而允许线程清理资源并优雅地终止。
线程的取消状态可以设置为可取消或不可取消。通过调用 pthread_setcancelstate(),线程可以控制其取消状态。可选的状态有:
PTHREAD_CANCEL_ENABLE:允许线程被取消。(默认)PTHREAD_CANCEL_DISABLE:不允许线程被取消。
线程通过调用pthread_setcanceltype,可以设置为两种取消类型:
PTHREAD_CANCEL_DEFERRED(默认):线程在执行的特定点上检查取消请求。PTHREAD_CANCEL_ASYNCHRONOUS:目标线程会立即取消。
取消点:通过 pthread_testcancel() 或在某些系统调用(如 sleep()、pthread_cond_wait())中,线程可以检查取消请求。
线程通过调用pthread_testcancel可以主动检测取消请求。 其他线程或主进程可以通过pthread_cancel杀死特定的线程
在检测到取消点之前,通过pthread_cleanup_push函数声明对应的清除函数。 注:pthread_cleanup_push和pthread_cleanup_pop必须成对出现。 使用 pthread_cleanup_pop(1) 来执行清理项(如果传入的参数为 1),在函数返回或线程取消时自动调用。如果 传入的参数为 0,那么只会弹出该处理程序而不执行它。
#include
#include
#include
#include
// 清理函数
void cleanup_handler(void *arg) {
printf("Cleaning up: %s\n", (char *)arg);
}
void *thread_function(void *arg) {
int count = 0;
// 注册清理函数
pthread_cleanup_push(cleanup_handler, "Thread cleanup");
while (count < 10) {
printf("线程正在执行: %d\n", count);
sleep(1); // 模拟工作
// 线程会在这里检查取消请求
pthread_testcancel(); // 检查有没有取消请求
count++;
}
// 弹出清理函数(不会执行,因为线程会提前退出)
pthread_cleanup_pop(0);
pthread_exit(NULL); // 正常退出
}
int main() {
pthread_t thread;
// 创建新线程
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
fprintf(stderr, "无法创建线程\n");
return 1;
}
sleep(3); // 主线程等待
// 请求取消目标线程
printf("请求取消线程...\n");
pthread_cancel(thread);
// 等待线程结束
pthread_join(thread, NULL);
printf("主线程完成所有工作\n");
return 0;
}
2.5相关函数说明
2.5.1线程创建函数:pthread_create
#include
int pthread_create(
pthread_t *thread, // 输出参数:线程标识符
const pthread_attr_t *attr, // 输入参数:线程属性
void *(*start_routine)(void*), // 输入参数:线程执行的起始函数
void *arg // 输入参数:传递给起始函数的参数
);
/*
参数属性说明:
pthread_t *thread:输出参数,用于返回新创建线程的标识符,成功后该变量中将存储新线程的 ID
const pthread_attr_t *attr:输入参数,用于设置线程的属性。如果传递 NULL,则使用默认属性。可以通过 pthread_attr_init、pthread_attr_setdetachstate 等函数自定义线程属性。
void *(*start_routine)(void*):输入参数,指向新线程执行的函数。这是一个返回类型为 void* 的函数指针,该函数接收一个 void* 类型的参数。
void *arg:输入参数,传递给 start_routine 的参数。可以是任意类型的数据,通常在调用时将其转换为 void*。
返回值:
成功:返回 0,表示线程创建成功。
失败:返回错误代码,表示线程创建失败。常见的错误代码包括:
EAGAIN:系统资源不足,无法创建更多线程。
EINVAL:线程属性无效。
EPERM:当前进程没有足够的权限来设置线程的属性。
*/
2.5.2.线程属性的设置
2.5.2.1 pthread_attr_t 结构体详解
pthread_attr_t 结构体用于描述线程的属性。
typedef struct
{
int detachstate;//线程的分离状态
int schedpolicy;//线程调度策略
struct sched_param schedparam;//线程的调度参数
int inheritsched;//线程的继承性
int scope;//线程的作用域
size_t stacksize;//线程栈大小
}pthread_attr_t
/*
*/
2.5.2.2 初始化线程属性: pthread_attr_init
设置线程属性之前,需要首先进行线程属性的初始化
pthread_attr_t attr;
// 初始化线程属性
if (pthread_attr_init(&attr) != 0) {
fprintf(stderr, "无法初始化线程属性\n");
return 1;
}
2.5.2.3 是否与进程中其他线程脱离同步
detachstate:表示新线程是否与进程中其他线程脱离同步。
如果设置为PTHREAD_CREATE_DETACHED, 则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。
缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACHED状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
pthread_attr_t attr;
// 初始化线程属性
if (pthread_attr_init(&attr) != 0) {
fprintf(stderr, "无法初始化线程属性\n");
return 1;
}
// 将线程设置为分离状态
if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) {
fprintf(stderr, "无法设置线程为分离状态\n");
return 1;
}
//创建新线程。。。,执行结束
// 销毁属性对象
pthread_attr_destroy(&attr);
2.5.2.4 设置新线程的调度策略
schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、抢占式,先入先出)三种。 缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。
在 SCHED_FIFO 和 SCHED_RR 策略下,较高的优先级线程可以抢占其他较低优先级的线程。 对于 SCHED_OTHER 策略,优先级的值通常会被忽略,因为该策略使用时间片轮转来调度线程。
//注:只展现了相关的数据项
struct sched_param {
int sched_priority; // 调度优先级
};
/*
sched_priority:表示线程的调度优先级。优先级范围由系统定义,通常在 1 到 99 之间,数值越高表示优先级越高:
*/
设置方法: 注:实时优先级的设置通常需要适当的权限(例如,以超级用户身份运行程序),否则可能会因权限不足而无法设置。
pthread_attr_t attr;
struct sched_param sched_param;
int result;
// 初始化线程属性
result = pthread_attr_init(&attr);
if (result != 0) {
fprintf(stderr, "无法初始化线程属性: %s\n", strerror(result));
return 1;
}
// 设置调度策略为 SCHED_FIFO
result = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
if (result != 0) {
fprintf(stderr, "无法设置调度策略: %s\n", strerror(result));
pthread_attr_destroy(&attr); // 释放属性对象
return 1;
}
// 设置调度优先级
sched_param.sched_priority = 50; // 设置优先级为 50
result = pthread_attr_setschedparam(&attr, &sched_param);
if (result != 0) {
fprintf(stderr, "无法设置调度参数: %s\n", strerror(result));
pthread_attr_destroy(&attr); // 释放属性对象
return 1;
}
//创建新线程。。。,执行结束
// 销毁属性对象
pthread_attr_destroy(&attr);
2.5.2.5 设置线程的继承方式
线程的继承方式指的是新创建的线程在一些属性(如调度政策、线程优先级、栈大小等)上是否继承父线程的设置。可以通过线程属性结构体 pthread_attr_t 来控制这些属性,并在创建新线程时指定这些属性。
inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED(不继承)和PTHREAD_INHERIT_SCHED(继承)。 前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。 只有不继承父线程的调度策略才可以设置线程的调度策略 pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
pthread_attr_t attr;
// 初始化线程属性
if (pthread_attr_init(&attr) != 0) {
fprintf(stderr, "无法初始化线程属性\n");
return 1;
}
// 设置继承方式为 PTHREAD_INHERIT_SCHED
if (pthread_attr_setinheritsched(&attr, PTHREAD_INHERIT_SCHED) != 0) {
fprintf(stderr, "无法设置线程继承调度: %s\n", strerror(errno));
pthread_attr_destroy(&attr);
return 1;
}
//创建新线程。。。,执行结束
// 销毁属性对象
pthread_attr_destroy(&attr);
2.5.2.6 线程的作用域
scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。 pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
2.5.2.7 设置线程栈的大小
#define STACK_SIZE 1024 * 64 // 64 KB
pthread_attr_t attr;
// 初始化线程属性
if (pthread_attr_init(&attr) != 0) {
fprintf(stderr, "无法初始化线程属性: %s\n", strerror(errno));
return 1;
}
// 设置栈大小
if (pthread_attr_setstacksize(&attr, STACK_SIZE) != 0) {
fprintf(stderr, "无法设置栈大小: %s\n", strerror(errno));
pthread_attr_destroy(&attr);
return 1;
}
//创建新线程。。。,执行结束
// 销毁属性对象
pthread_attr_destroy(&attr);
2.5.3 线程的回收
在不设置线程属性detachstate的情况下,即默认情况下,需要由主进程对结束的线程进行回收。 主进程不对结束的线程进行回收可能会造成:
资源泄露:每个线程在创建时都会分配一定的资源(如内存、系统句柄等)。如果线程结束后,主进程没有进行适当的回收,这些资源可能会继续占用,最终可能导致系统资源耗尽。不确定性:未回收线程的状态可能会变得不确定,导致应用程序中的其他部分无法预测其行为,可能引发不可预期的错误或崩溃。进程变得混乱:如果有多个未回收的线程存在,会使得进程的管理变得复杂,可能混淆线程的状态,增大调试和管理的难度。降低程序稳定性:长时间运行的程序如果不正确回收线程,将会减低程序的稳定性,可能导致频繁的崩溃或挂起。
// 等待子线程结束
pthread_join(thread, NULL);
int pthread_join(pthread_t thread, void **retval);
/*
参数:
thread:要等待的线程的标识符。这个线程必须是已经创建的,并且是可连接状态(即没有处于分离状态)。
retval:一个指向指针的指针,用于存储线程的返回值(如果需要)。如果不需要获取返回值,可以将其设置为 NULL。
返回值:
成功时返回 0
失败时返回一个错误码,常见的返回值包括:
ESRCH:指定的线程无效或不存在。
EINVAL:指定的线程不是一个可连接的线程。
EDEADLK:发生死锁,导致无法完成等待。
*/
pthread_join返回参数的使用案例:
#include
#include
#include
#include
void *thread_function(void *arg) {
long thread_id = (long)arg;
printf("子线程 [%ld] 正在执行\n", thread_id);
sleep(2); // 模拟任务执行
printf("子线程 [%ld] 执行完毕\n", thread_id);
return (void *)thread_id; // 返回线程 ID 作为返回值
}
int main() {
pthread_t thread;
long thread_id = 1;
void *retval;
// 创建新线程
if (pthread_create(&thread, NULL, thread_function, (void *)thread_id) != 0) {
fprintf(stderr, "无法创建线程\n");
return 1;
}
// 等待子线程结束
if (pthread_join(thread, &retval) != 0) {
fprintf(stderr, "无法等待子线程结束\n");
return 1;
}
printf("主线程: 子线程返回值: %ld\n", (long)retval);
printf("主线程完成所有工作\n");
return 0;
}
注:线程回收不会回收线程申请的动态内存
2.5.4 线程的主动结束
当线程使用 return 返回时,不会调用 pthread_cleanup_push 注册的清理函数。只有在线程被取消(通过 pthread_cancel)或显式调用 pthread_exit 时,才会执行清理函数。
void pthread_exit(void *retval);
2.5.5 线程的被动取消
2.5.5.1设置取消的方式:pthread_setcanceltype
#include
int pthread_setcanceltype(int type, int *oldtype);
/*
参数:
type:指定线程的新取消类型。可以是以下两个值之一:
PTHREAD_CANCEL_DEFERRED(默认值):被动取消。线程只会在到达取消点(Cancellation Point)时检查取消请求并退出。
PTHREAD_CANCEL_ASYNCHRONOUS:异步取消。线程可以在任何时候被立即取消,而不需要到达取消点。
oldtype:用于保存线程之前的取消类型。如果不需要保存之前的类型,可以传递 NULL
返回值:
成功:返回 0。
失败:返回一个非零的错误码。
*/
2.5.5.2 设置取消的状态:pthread_setcancelstate
#include
int pthread_setcancelstate(int state, int *oldstate);
/*
参数:
state:指定线程的新取消状态。可以是以下两个值之一:
PTHREAD_CANCEL_ENABLE(默认值):启用取消功能。线程可以响应取消请求。
PTHREAD_CANCEL_DISABLE:禁用取消功能。线程会忽略取消请求。
oldstate:用于保存线程之前的取消状态。如果不需要保存之前的取消状态,可以传递 NULL。
返回值:
成功:返回 0。
失败:返回一个非零的错误码。
*/
2.5.5.3 发送线程取消请求:pthread_cancel
pthread_cancel 通常用于以下场景:
终止长时间运行或陷入死循环的线程。在任务完成或发生错误时,终止不再需要的线程。实现线程的超时机制。
#include
int pthread_cancel(pthread_t thread);
/*
参数:
thread:目标线程的标识符(pthread_t 类型),即需要取消的线程。
返回值:
成功:返回 0。
失败:返回一个非零的错误码。
*/
2.5.5.4 主动设置取消点:pthread_testcancel
#include
void pthread_testcancel(void);//无参数,无返回值
2.5.5.5 线程的清理:pthread_cleanup_push和pthread_cleanup_pop
清理函数在线程被取消或显式调用 pthread_exit 时执行,用于释放资源或恢复状态。 pthread_cleanup_push 将清理函数和参数压入当前线程的清理函数栈中。清理函数在以下情况下执行:
线程被取消(通过 pthread_cancel)。线程显式调用 pthread_exit 退出。显式调用pthread_cleanup_pop 并传递非零值。
清理函数的执行顺序与注册顺序相反,即最后注册的清理函数最先执行。
pthread_cleanup_push 通常用于以下场景:
资源释放:在线程退出时释放动态分配的内存、关闭文件句柄、释放锁等。状态恢复:在线程退出时恢复共享资源的状态,例如解锁互斥锁。错误处理:在线程发生错误时执行清理操作。
#include
void pthread_cleanup_push(void (*routine)(void *), void *arg);
/*
参数:
routine:清理函数的指针。清理函数的原型为 void routine(void *arg),其中 arg 是传递给清理函数的参数。
arg: 传递给清理函数的参数。
返回值:
无返回值。
*/
void pthread_cleanup_pop(int execute);
/*
execute:决定是否执行弹出的清理函数:
非零值(1):执行清理函数。
零值(0):不执行清理函数。
返回值:无
*/
2.5.6 线程id
在线程创建时,可以通过pthread_create函数中的thread参数指针获取该线程tid的值。 除此之外,也可以在线程中通过调用pthread_self函数,查看自己的tid
#include
pthread_t pthread_self(void);
//返回值:返回线程的id
应用举例:
#include
#include
#include
// 线程函数
void *thread_func(void *arg) {
// 获取当前线程的标识符
pthread_t tid = pthread_self();
printf("Thread ID: %lu\n", (unsigned long)tid);
// 模拟工作
sleep(2);
return NULL;
}
int main() {
pthread_t thread_id;
// 创建线程
if (pthread_create(&thread_id, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 获取主线程的标识符
pthread_t main_tid = pthread_self();
printf("Main Thread ID: %lu\n", (unsigned long)main_tid);
// 等待线程退出
if (pthread_join(thread_id, NULL) != 0) {
perror("pthread_join");
return 1;
}
printf("Main: Thread exited successfully\n");
return 0;
}
2.5.6.1 比较两个线程id
由于 pthread_t 是一个不透明的数据类型,不能直接使用 == 运算符进行比较,因此需要使用 pthread_equal 来判断两个线程标识符是否指向同一个线程。 pthread_equal 是 POSIX 线程库中的一个函数,用于比较两个线程的线程标识符(Thread ID)是否相等。
#include
int pthread_equal(pthread_t t1, pthread_t t2);
/*
参数:
t1、t2:需要对比的两个线程标识符
返回值:
非零值:如果 t1 和 t2 指向同一个线程。
零值:如果 t1 和 t2 指向不同的线程。
*/
注:线程标识符的唯一性仅在同一个进程内保证。不同进程的线程可能具有相同的线程标识符。
3 命令行中查看线程
ps -efT
/*
ps -e 或 ps -A:显示系统中所有进程。
ps -ef:以完整格式显示所有进程。
ps -T:显示进程的所有线程。
*/
4.编译
在编译包含线程的程序时,必须使用 -pthread 选项。-pthread 不仅会链接 pthread 库,还会确保编译器启用线程相关的宏定义(如 _REENTRANT)。 编译命令如下:
gcc -o my_program my_program.c -pthread
/*
-o my_program:指定输出文件名为 my_program。
my_program.c:源代码文件。
-pthread:启用 POSIX 线程支持,并链接 pthread 库。
*/
注:-pthread 与 -lpthread 的区别
-pthread:不仅会链接 pthread 库,还会启用线程相关的宏定义和编译器选项。-lpthread:仅链接 pthread 库,不会启用额外的宏定义。 推荐使用 -pthread,因为它更全面。