线程的基本控制

线程终止

exit是危险的
如果进程中的任意一个线程调用了exit,那么整个进程终止。

不终止进程的退出方式
普通单个线程的退出方法,以下方法退出不会导致进程终止:
(1)从启动例程中返回,返回值是线程的退出码(return)。
(2)线程可以被同一进程的其他线程取消。
(3)线程调用pthread_exit(void *rval)函数,rval是退出码。

例子

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

void *thread_func(void *arg){
    char *num=(char *)arg;
    if(strcmp("1",num)==0){
        printf("new thread return\n");
        return (void *)1;
    }else if(strcmp("2",num)==0){
        printf("new thread pthread_exit\n");
        pthread_exit((void*)2);
    }else if(strcmp("3",num)==0){
        printf("new thread exit\n");
        exit(3);
    }
    return (void *)0;
}



int main(int argc,char *argv[]){
    pthread_t ntid;
    int err;
    err=pthread_create(&ntid,NULL,thread_func,argv[1]);
    if(err!=0){
        printf("new thread create failed!\n");
        return 0;
    }

    pthread_join(ntid,NULL ); 

    printf("main thread\n");

    return 0;

}

运行结果

machine:~/Desktop/C/thread$ ./pthread_exit  1
new thread return
main thread

machine:~/Desktop/C/thread$ ./pthread_exit  2
new thread pthread_exit
main thread

machine:~/Desktop/C/thread$ ./pthread_exit  3
new thread exit

return和pthread_exit()的区别

应用场景推荐方法选择依据
线程入口函数正常返回return语法简洁,符合常规函数返回逻辑
子函数中终止线程pthread_exit()return仅返回上层函数,无法直接终止线程
主线程退出但不影响子线程pthread_exit()使用return会导致进程退出,从而强制终止所有子线程

线程连接

pthread_join()
1‌.函数原型‌:
int pthread_join(pthread_t tid, void **rval);

2‌.功能描述‌:
pthread_join函数用于等待指定线程结束,并以阻塞的方式执行。调用该函数的线程会一直阻塞,直到指定的线程tid调用pthread_exit、从启动例程返回或者被取消

3.参数说明‌:
tid:指定线程的ID,即要等待的线程的标识符。
rval:指向指针的指针,用于获取被等待线程的返回值。如果线程被取消,rval被置为PTHREAD_CANCELED。

4.返回值‌:
成功时返回0,失败时返回错误码。

‌5.线程状态‌:
调用pthread_join会使指定的线程处于可连接(joinable)状态等待回收。如果指定线程已经处于分离(detached)状态,那么调用pthread_join会失败。

注意每个线程‌只能被 join 一次‌。

6.显式回收:
对于处于 ‌joinable(可结合)状态‌的线程(默认创建的线程),必须‌主动调用 pthread_join() 函数‌来阻塞等待其终止并释放其占用的系统资源(如栈空间、线程描述符等)。
显式回收 = ‌对 joinable 线程调用 pthread_join()‌。 忽略此操作将导致资源持续占用,最终可能耗尽系统资源。

pthread_detach()
pthread_detach(pthread_t thread);

1.用于分离一个线程,使其处于分离状态。线程可以自己分离自己。
2.detach不会阻塞调用者,会立即返回。
3.不关心执行结果的后台任务,后台任务线程不需要与其他线程同步。
4.成功返回0,失败返回错误码。
5.线程终止时自动释放资源,无需其他线程回收。

注意:
调用限制:detach后的线程不能再join。

实践
1.阻塞方式,需要获取线程返回值或确认完成时用pthread_join(),主线程等待子线程完成工作。
2.不关心执行结果的后台任务用pthread_detach()。

线程取消

取消函数

int pthread_cancel (pthread_t tid)

功能:取消tid指定的线程。
返回值:成功返回0。

‌重要说明‌
1.取消只是发送一个请求,并不意味着等待线程终止。
2.发送成功也不意味着tid一定会终止。
一个线程到底会不会终止,具体得看它的取消状态。

取消状态
取消状态:就是线程对取消信号的处理方式,忽略或者响应。
‌1. 默认行为‌:
线程在创建时,默认会响应取消信号。

‌2. 设置取消状态‌:

pthread_setcancelstate(int state,int *oldstate)

使用pthread_setcancelstate(int state,int *oldstate)函数可以设置本线程对取消信号的反应。该函数有两个参数:
state:指定新的取消状态,可以是PTHREAD_CANCEL_ENABLE(缺省,收到信号后线程设为CANCELED状态)或PTHREAD_CANCEL_DISABLE(忽略取消信号继续运行)。
oldstate:如果不为NULL,则存入原来的取消状态以便恢复。

‌3. 取消状态的重要性‌:
在多线程编程中,正确设置线程的取消状态可以防止线程在不应该被终止的时候被取消,从而避免数据不一致或资源泄露等问题。

通过合理设置取消点(使用pthread_testcancel()函数),可以在安全的位置终止线程,进一步确保程序的稳定性和可靠性。

一个线程如果响应取消信号要进行终止了,那么什么时候会发生终止呢?是延迟终止还是立马终止?具体得要看取消类型。

取消类型
线程取消类型是指线程对取消信号的响应方式,主要有两种:
1.延时取消(PTHREAD_CANCEL_DEFERRED)‌(默认):
线程在收到取消信号后,会继续运行至下一个取消点再退出。这是线程创建时的默认取消类型。

‌2.立即取消(PTHREAD_CANCEL_ASYNCHRONOUS)‌:
线程在收到取消信号后,会立即执行取消动作并退出

可以通过pthread_setcanceltype函数来设置线程的取消类型,其函数原型为:

int pthread_setcanceltype(int type, int *oldtype);

1.type参数指定新的取消类型,取值为PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。
2.oldtype参数如果不为NULL,则函数会将原来的取消类型值存入该指针所指向的位置。
3.仅当取消状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出(延迟取消)和立即执行取消动作(退出)(立即取消)。

注意:
设置取消类型时,需确保线程的取消状态为Enable(可通过pthread_setcancelstate函数设置),否则取消类型设置将无效。

取消点
‌取消一个线程时,通常需要该线程的配合‌。线程在运行过程中会主动检查是否有取消请求,‌这些检查点被称为取消点‌。

包含取消点的常见函数和系统调用有:
‌pthread_join()‌:等待线程结束。
‌pthread_testcancel()‌:测试线程是否被取消。
‌pthread_cond_wait()‌ 和 ‌pthread_cond_timedwait()‌:条件变量等待。
‌sem_wait()‌:信号量等待。
‌sigwait()‌:等待信号。
‌write‌ 和 ‌read‌:文件读写操作。
printf();
‌大多数会阻塞的系统调用‌。

例子

#include <stdio.h>
#include <pthread.h>

void *thread_func(void *arg){
    int stateval=pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
    if(stateval!=0){
        printf("set cancel state failed\n");
    }
    printf("new thread:set cancel disable\n");
    sleep(4);

    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
    //pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL); //立马取消,无需等待特定的取消点。不设置默认就是延迟取消
    printf("set cancel enable\n"); //消息点1
    printf("I am new thread\n");  //消息点2

    return (void*)10;
}

int main(int argc,char *argv[]){
    pthread_t ntid;
    void *rval;
    int err=pthread_create(&ntid,NULL,thread_func,NULL);
    if(err!=0){
        printf("thread create failed\n");
        return -1;
    }
    sleep(2);

    printf("main thread\n");
    int cval=pthread_cancel(ntid);
    if(cval!=0){
        printf("thread cancel failed\n");
    }

    pthread_join(ntid,&rval);
    printf("main thread\n");
    printf("cval is %d\n",cval);
    printf("rval is %d\n",(int)rval);  //返回-1:表示线程被取消
    return 0;

}

1.默认的延时取消结果:

machine:~/Desktop/C/thread$ ./thread_cancel 
new thread:set cancel disable
main thread
set cancel enable
main thread
cval is 0
rval is -1

2.设置立即取消的结果:

machine:~/Desktop/C/thread$ ./thread_cancel 
new thread:set cancel disable
main thread
main thread
cval is 0
rval is -1

向线程发送信号

一、发送信号

pthread_kill
用于向特定线程发送信号以实现线程间通信。
大部分的signal的默认动作是终止进程的运行,所以需要使用sigaaction()去抓信号并加上处理函数

函数原型‌
int pthread_kill(pthread_t thread, int sig);

‌参数说明‌
‌thread‌:指定目标线程的ID,该ID必须是通过pthread_create创建的合法线程ID。
‌sig‌:要发送的信号值,取值范围为0至63的整数值。特殊值0用于线程存活检测,不发送实际信号。

‌功能特点‌
1.当sig参数不为0时,向指定线程发送信号。若线程未处理该信号,则按信号的默认行为影响整个进程。也就是说,如果你给线程发送了SIGQUIT,但线程没有实现signal处理函数的话,则整个进程退出。
2.当sig参数为0时,仅执行线程有效性检测,不发送实际信号,用于判断线程是否存活。

‌注意事项‌
1.使用pthread_kill发送信号时,需确保线程内已正确实现信号处理函数,否则可能影响整个进程的运行。
2.信号处理函数是进程级全局生效的,不同线程不能定义独立的信号处理逻辑。
3.发送SIGKILL等可能导致进程级终止的信号时,需谨慎处理,以避免资源泄漏或进程异常终止。

‌应用场景‌
1.线程间通信:通过发送信号实现线程间的同步或事件通知。
2.线程状态检测:通过发送0信号判断线程是否存活。
3.异常处理:定向发送异常信号到监控线程,进行异常捕获和处理。

二、信号处理

进程信号处理‌
使用sigaction函数为信号signum设置处理函数。
函数原型:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

act.sa_mask:信号屏蔽字,指定在处理该信号时要屏蔽的信号集合。
act.sa_handler:信号处理程序,指定处理该信号的函数。

‌信号集操作‌:
sigemptyset(sigset_t *set);:清空信号集set。
sigfillset(sigset_t *set);:将所有信号加入信号集set。
sigaddset(sigset_t *set, int signum);:将信号添加到 act.sa_mask 信号集中,表示当信号处理函数执行时,信号应该被临时阻塞。
sigdelset(sigset_t *set, int signum);:从指定的信号集set中移除signum对应的信号。

‌多线程信号屏蔽处理‌

使用pthread_sigmask函数进行多线程中的信号屏蔽处理。
将 act.sa_mask 信号集中的信号添加到线程的信号掩码中,导致该线程永久阻塞添加的那个信号。
函数原型:

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

how参数:
SIG_BLOCK:向当前的信号掩码中添加set(要阻塞的信号组)。
SIG_UNBLOCK:向当前的信号掩码中删除set(要取消阻塞的信号组)。
SIG_SETMASK:将当前的信号掩码替换为set(新的信号掩码)。

1.在多线程中,新线程的当前信号掩码会继承创建它的那个线程的信号掩码。
2.一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号SIGKILL和SIGSTOP也无法阻塞

例子

#include <stdio.h>      // 标准输入输出函数
#include <pthread.h>    // POSIX 线程库
#include <string.h>     // 字符串操作函数
#include <signal.h>     // 信号处理函数
#include <unistd.h>     // POSIX 操作系统 API(如 write())

// SIGQUIT 信号的处理函数(线程1)
void sig_handler1(int arg)
{
    // printf("thread1 get signal\n");
    // 使用异步安全的 write() 而不是 printf()
    // STDOUT_FILENO 是标准输出的文件描述符
    write(STDOUT_FILENO, "thread1 get signal\n", strlen("thread1 get signal\n"));
}

// SIGQUIT 信号的处理函数(线程2)
void sig_handler2(int arg)
{
    // printf("thread2 get signal\n");
    write(STDOUT_FILENO, "thread2 get signal\n", strlen("thread2 get signal\n"));
}

// 线程1的执行函数
void *thread_fun1(void *arg)
{
    // printf("new thread 1\n");
    // 线程启动消息(使用 write() 确保原子性)
    write(STDOUT_FILENO, "new thread 1\n", strlen("new thread 1\n"));
    
    // 设置信号处理结构体
    struct sigaction act;
    memset(&act, 0, sizeof(act));  // 初始化为0
    
    // 创建信号集并添加 SIGQUIT
    sigaddset(&act.sa_mask, SIGQUIT);
    
    // 设置信号处理函数
    act.sa_handler = sig_handler1;
    
    // 注册 SIGQUIT 信号处理
    sigaction(SIGQUIT, &act, NULL);
    
    // 永久阻塞 SIGQUIT 信号(关键操作)
    // 该线程将不会接收或处理 SIGQUIT
    pthread_sigmask(SIG_BLOCK, &act.sa_mask, NULL);
    
    // 线程休眠2秒,模拟工作
    sleep(2);
    return NULL;
}

// 线程2的执行函数
void *thread_fun2(void *arg)
{
	// printf("new thread 2\n");
    write(STDOUT_FILENO, "new thread 2\n", strlen("new thread 2\n"));
    
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    
    // 创建信号集并添加 SIGQUIT
    sigaddset(&act.sa_mask, SIGQUIT);
    
    // 设置信号处理函数
    act.sa_handler = sig_handler2;
    
    // 注册 SIGQUIT 信号处理
    sigaction(SIGQUIT, &act, NULL);
    
    // 注释掉了阻塞操作,所以线程2会处理 SIGQUIT
    // pthread_sigmask(SIG_BLOCK, &act.sa_mask, NULL);
    
    sleep(2);
    return NULL;
}

int main()
{
    int err;
    int s;
    pthread_t tid1, tid2;  

    // 创建线程1
    err = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (err != 0) {
        printf("create new thread 1 failed\n");
        return -1;
    }

    // 创建线程2
    err = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (err != 0) {
        printf("create new thread 2 failed\n");
        return -1;
    }

    // 主线程休眠1秒,确保子线程完成初始化
    // 给线程足够时间设置信号处理和阻塞
    sleep(1);

    // 向线程1发送 SIGQUIT 信号
    s = pthread_kill(tid1, SIGQUIT);
    if (s != 0) {
        printf("send signal to thread1 failed\n");
    }

    // 向线程2发送 SIGQUIT 信号
    s = pthread_kill(tid2, SIGQUIT);
    if (s != 0) {
        printf("send signal to thread2 failed\n");
    }

    // 等待线程结束(避免僵尸线程)
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

结果:

machine:~/Desktop/C/thread$ ./thread_kill 
new thread 1
new thread 2
thread2 get signal

machine:~/Desktop/C/thread$ ./thread_kill 
new thread 2
new thread 1
thread2 get signal

使用printf打印时有异常输出(输出 thread1 get signal ):

machine:~/Desktop/C/thread$ ./thread_kill 
new thread 1
new thread 2
thread1 get signal

异常输出的可能原因:
1.竞争条件:线程1的阻塞操作(pthread_sigmask)还未执行完成时,信号就到达了。
2.缓冲区问题:printf() 在信号处理函数中使用不安全,线程2的输出可能被缓冲未显示。

使用 write而不用printf
1.确保信号处理输出立即可见。
2.避免标准I/O的缓冲问题。

线程1和线程2的差异
线程 1:
注册了信号处理函数sig_handler1
阻塞了 SIGQUIT 信号
当收到 SIGQUIT 信号时,由于信号被阻塞,不会立即处理
直到线程 1 解除阻塞或结束时,才可能处理信号

线程 2:
注册了信号处理函数sig_handler2
没有阻塞 SIGQUIT 信号
当收到 SIGQUIT 信号时,会立即调用sig_handler2处理信号

sigaddset()的作用是:当sig_handler1执行时,会临时阻塞SIGQUIT信号(避免递归调用)。
pthread_sigmask()的作用是:在线程 1 中永久阻塞SIGQUIT信号,导致:
当主线程调用pthread_kill(tid1, SIGQUIT)时,信号会被阻塞而无法立即处理
直到线程 1 解除阻塞(或退出),信号才会被处理。

注意:sa_mask的阻塞作用仅在信号处理函数执行期间有效,并非永久性阻塞。

特性sigaddset()pthread_sigmask()
功能描述构建信号集合设置线程信号掩码
作用范围仅作用于信号集对象影响当前线程的信号处理
持久性临时性配置持久有效直至再次更改
典型应用场景信号处理的临时配置线程级的持续信号屏蔽

清除操作

注册、销毁清除函数
线程可以安排退出时的清理操作,这与进程使用atexit函数安排退出时的操作类似。这种函数被称为线程清理处理程序。线程可以建立多个清理处理程序,这些处理程序被记录在中,因此它们执行的顺序与注册的顺序相反。

pthread_cleanup_push(void (rtn)(void), void *args)‌ : 用于注册清理处理程序。
其中rtn是处理程序的入口地址;
args是传递给处理函数的参数。

‌pthread_cleanup_pop(int execute)‌:用于清除处理程序。
如果execute非零,则调用栈顶的清理函数;如果为零,则不调用但清除栈顶的清理函数。

pthread_cleanup_push(void (rtn)(void), void *args)‌和‌pthread_cleanup_pop(int execute)‌这两个函数必须成对出现,否则编译无法通过。

清理函数会在以下情况下被调用:
1‌.调用pthread_exit‌。
2‌.响应取消请求‌。
3‌.用非零参数调用pthread_cleanup_pop‌。


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 清理函数1
void cleanup1(void *arg) {
    printf("执行清理函数1: %s\n", (char*)arg);
}

// 清理函数2
void cleanup2(void *arg) {
    printf("执行清理函数2: %s\n", (char*)arg);
}

// 线程函数
void* thread_func(void *arg) {
    printf("线程开始执行\n");
    
    // 注册清理函数(栈式结构)
    pthread_cleanup_push(cleanup1, "清理参数1");
    pthread_cleanup_push(cleanup2, "清理参数2");
    
    // 模拟线程工作
    for(int i=0; i<3; i++) {
        printf("线程工作中...%d\n", i);
        sleep(1);
    }
    
    // 手动弹出并执行清理函数(pthread_exit也会触发)
    printf("准备弹出清理函数\n");
    pthread_cleanup_pop(1);  // 执行cleanup2
    pthread_cleanup_pop(1);  // 执行cleanup1
    
    return NULL;
}

int main() {
    pthread_t tid;
    
    // 创建线程
    if(pthread_create(&tid, NULL, thread_func, NULL) != 0) {
        perror("线程创建失败");
        return 1;
    }
    
    // 等待线程结束
    pthread_join(tid, NULL);
    printf("主线程结束\n");
    return 0;
}

线程清理处理程序的两种触发方式:
1‌.手动弹出执行‌
当显式调用pthread_cleanup_pop(1)时(参数为非零),会立即执行栈顶的清理函数,这是开发者主动触发的清理行为。

2‌.pthread_exit自动触发‌
如果线程通过pthread_exit退出(而非直接return),即使没有手动调用pthread_cleanup_pop,系统也会自动执行所有已注册的清理函数,这是线程异常终止时的安全机制。

关键区别:
手动弹出:开发者精确控制清理时机。
pthread_exit触发:系统保障异常退出时的资源释放。
两者都遵循"后进先出"的栈顺序执行清理函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值