Linux-线程知识点(2)

线程的取消

Linux提供了如下函数来控制线程的取消:

int pthread_cancel(pthread_t thread);

一个线程可以通过该函数向另一个线程发送取消请求。这不是一个阻塞接口,发出请求后,函数就立刻返回了,而不是等待目标线程退出之后才返回。

如果成功,该函数返回0,失败返回错误码。

线程收到取消请求后,会采取什么行动呢?这取决于该线程的设定。NPTL提供了函数来设置线程是否允许取消,以及在允许取消的情况下,如何取消。

pthread_setcancelstate函数用来设置线程是否允许取消,函数定义如下:

int pthread_setcancelstate(int state, int *oldstate);

state参数有两种可能的值:

  • PTHREAD_CANCEL_ENABLE:线程的默认取消状态,如果是这种取消状态,会发生的事情取决于线程的取消类型;
  • PTHREAD_CANCEL_DISABLE:线程不理会取消请求,取消请求会被暂时挂起,不予理会。

pthread_setcanceltype函数用来设置线程的取消类型,其定义如下:

int pthread_setcanceltype(int type, int *oldtype);

取消类型有两种值:

  • PTHREAD_CANCEL_DEFERRED:延迟取消

  • PTHREAD_CANCEL_ASYNCHRONOUS:异步取消

PTHREAD_CANCEL_ASYNCHRONOUS为异步取消,即线程可能在任何时间点(可能是立即取消,但也不一定)取消线程。这种取消方式的最大问题在于,你不知道取消时线程执行到了哪一步。所以,这种取消方式太粗暴,很容易造成后续的混乱。因此不建议使用该取消方式。

PTHREAD_CANCEL_DEFERRED是延迟取消,线程会一直执行,直到遇到一个取消点,这种方式也是新建线程的默认取消类型。

取消点就是对于某些函数,如果线程允许取消且取消类型是延迟取消,并且线程也收到了取消请求,那么当执行到这些函数的时候,线程就可以退出了。

// 示例

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

void* thread_function(void* arg)
{
    // 设置线程的取消状态为可取消
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

    // 设置线程的取消类型为延迟取消
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    printf("Thread is running...\n");

    while (1)
    {
        // 在循环中检查取消请求
        // 把这段代码注掉也会取消,但是这种取消是线程随机的,在任意代码中取消,可能会造成内存泄露、资源无法释放等
        pthread_testcancel();

        // 线程执行的任务
        printf("Working...\n");
        sleep(1);
    }

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

int main() {
    pthread_t tid;

    // 创建线程
    if (pthread_create(&tid, NULL, thread_function, NULL) != 0)
    {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 主线程等待一段时间后取消子线程
    sleep(3);
    printf("Canceling thread...\n");

    // 取消线程
    if (pthread_cancel(tid) != 0)
    {
        perror("pthread_cancel");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束
    if (pthread_join(tid, NULL) != 0)
    {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    printf("Thread has been canceled\n");

    return 0;
}

[root@Zhn 线程]# g++ pthread_cancel.cpp -o pthread_cancel -lpthread
[root@Zhn 线程]# ./pthread_cancel
Thread is running...
Working...
Working...
Working...
Canceling thread...
Thread has been canceled
[root@Zhn 线程]# 

对编程人员而言,应该遵循以下原则:

  • 第一,轻易不要调用pthread_cancel函数,在外部杀死线程是很糟糕的做法,毕竟如果想通知目标线程退出,还可以采取其他方法。
  • 第二,如果不得不允许线程取消,那么在某些非常关键不容有失的代码区域,暂时将线程设置成不可取消状态,退出关键区域之后,再恢复成可以取消的状态。
  • 第三,在非关键的区域,也要将线程设置成延迟取消,永远不要设置成异步取消。

线程的清理函数

假设遇到取消请求,线程执行到了取消点,却没有来得及做清理动作(如动态申请的内存没有释放,申请的互斥量没有解锁等),可能会导致错误的产生,比如死锁,甚至是进程崩溃。

// 示例

void* cancel_unsafe(void*) 
{
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);              // 此处不是撤消点
    struct timespec ts = {3, 0};
    nanosleep(&ts, 0);                        // 是撤消点
    pthread_mutex_unlock(&mutex);          // 此处不是撤消点
    return 0;
}

int main(void) 
{
    pthread_t t;
    pthread_create(&t, 0, cancel_unsafe, 0);
    pthread_cancel(t);
    pthread_join(t, 0);
    cancel_unsafe(0); // 发生死锁!
    return 0;
}

在上面的例子中,nanosleep是取消点,如果线程执行到此处时被其他线程取消,就会出现以下情况:互斥量还没有解锁,但持有锁的线程已不复存在。这种情况下其他线程再也无法申请到互斥量,很有可能在某处就会陷入死锁的境地。

为了避免这种情况,线程可以设置一个或多个清理函数,线程取消或退出时,会自动执行这些清理函数,以确保资源处于一致的状态。其相关接口定义如下:

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

第一个注意的点:

这两个函数必须同时出现,并且属于同一个语法块。

// 错误示范1
// 这两个函数在两个不同的函数内

void foo()
{
    .....
    pthread_cleanup_pop(0)
    .....
}
void *thread_work(void *arg)
{
    ......
    pthread_cleanup_push(clean,clean_arg);
    ......
    foo()
    ......
}

// 错误示范2
// 在日常编码中很容易犯上面这种错误。因为pthread_cleanup_push和phtread_cleanup_pop的实现中包含了{和},
// 所以将pop放入if{}的代码块中,会导致括号匹配错乱,最终会引发编译错误。
pthread_cleanup_push(clean_func,clean_arg);
......
if(cond)
{
    pthread_cleanup_pop(0);
}

第二个注意的点:

pthread_cleanup_push(clean_func_1,clean_arg_1)
pthread_cleanup_push(clean_func_2,clean_arg_2)
...
pthread_cleanup_pop(execute_2);
pthread_cleanup_pop(execute_1);

从push和pop的名字可以看出,这是栈的风格,后入先出,就是后注册的清理函数会先执行。其中pthread_cleanup_pop的用处是,删除注册的清理函数。如果参数是非0值,那么执行一次清理函数,再删除清理函数。否则的话,就直接删除清理函数。

第三个注意的点,何时会触发注册的清理函数:

  • 当线程的主函数是调用pthread_exit返回的,清理函数总是会被执行。
  • 当线程是被其他线程调用pthread_cancel取消的,清理函数总是会被执行。
  • 当线程的主函数是通过return返回的,并且pthread_cleanup_pop的唯一参数execute是0时,清理函数不会被执行。
  • 当线程的主函数是通过return返回的,并且pthread_cleanup_pop的唯一参数execute是非零值时,清理函数会执行一次。
// 示例

#include <iostream>
#include<pthread.h>
#include<string.h>

void clean(void* arg)
{
    std::cout << "Clean up: " << *reinterpret_cast<int*>(arg) << std::endl;
}

void* thread(void* param)
{
    int input = *reinterpret_cast<int*>(param);
    printf("thread start\n");

    int* arg1 = new int(1);
    pthread_cleanup_push(clean, reinterpret_cast<void*>(arg1));

    int* arg2 = new int(2);
    pthread_cleanup_push(clean, reinterpret_cast<void*>(arg2));

    if (input != 0)
        /*pthread_exit退出,清理函数总会被执行*/
        pthread_exit(reinterpret_cast<void*>(arg1));

    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);

    /*return 返回,如果上面pop函数的参数是0,则不会执行清理函数*/
    int* revtal = new int(0);
    return (reinterpret_cast<void*>(revtal));
}

int main()
{
    pthread_t tid;

    int* arg1 = new int;
    *arg1 = 0;

    int ret = pthread_create(&tid, NULL, thread, reinterpret_cast<void*>(arg1));
    if (ret != 0)
        return -1;

    void* res;
    pthread_join(tid, &res);
    std::cout << "first thread exit,return code is " << *reinterpret_cast<int*>(res) << std::endl;

    *arg1 = 1;
    ret = pthread_create(&tid, NULL, thread, reinterpret_cast<void*>(arg1));
    if (ret != 0)
        return -1;

    res = nullptr;
    pthread_join(tid, &res);
    std::cout << "second thread exit,return code is " << *reinterpret_cast<int*>(res) << std::endl;

    return 0;
}

[root@Zhn 线程]# g++ pthread_cleanup.cpp -o pthread_cleanup -lpthread
[root@Zhn 线程]# ./pthread_cleanup 
thread start
first thread exit,return code is 0
thread start
Clean up: 2
Clean up: 1
second thread exit,return code is 1
[root@Zhn 线程]# 

线程的局部存储

// 示例
// 线程的局部存储

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

pthread_key_t key;

// 线程退出时调用
void destructor(void* value) {
    printf("Freeing memory for thread %ld\n", (long)pthread_self());
    free(value);
}

void* thread_func(void* arg) {
    long thread_id = (long)arg;

    char* data = (char*)malloc(100);
    sprintf(data, "Data for thread %ld", thread_id);

    pthread_setspecific(key, data);

    // 获取线程局部存储中的数据
    char* retrieved_data = (char*)pthread_getspecific(key);
    printf("Thread %ld retrieved data: %s\n", thread_id, retrieved_data);

    return NULL;
}

int main() {
    pthread_t threads[3];

    // 创建线程局部存储键
    pthread_key_create(&key, destructor);

    // 创建线程
    for (long i = 0; i < 3; ++i) {
        pthread_create(&threads[i], NULL, thread_func, (void*)i);
    }

    // 等待线程结束
    for (int i = 0; i < 3; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 销毁线程局部存储键
    pthread_key_delete(key);

    return 0;
}

[root@Zhn 线程]# g++ pthread_key_create.cpp -o pthread_key_create -lpthread
[root@Zhn 线程]# ./pthread_key_create 
Thread 0 retrieved data: Data for thread 0
Freeing memory for thread 139735337002752
Thread 1 retrieved data: Data for thread 1
Freeing memory for thread 139735328610048
Thread 2 retrieved data: Data for thread 2
Freeing memory for thread 139735320217344
[root@Zhn 线程]# 

线程的局部存储适用于许多不同的场景,特别是在需要处理多线程并发的程序中。以下是一些适合使用线程局部存储的场景:

  1. 线程安全的日志记录:每个线程可以有自己的日志记录器,而不必担心线程间的竞态条件。这样可以确保日志记录的线程安全性,而不需要额外的同步机制。
  2. 线程特定的配置参数:不同的线程可能需要不同的配置参数,例如线程池中的工作线程可能需要不同的超时设置或日志级别。使用线程局部存储可以轻松地为每个线程保存其特定的配置参数。
  3. 线程安全的资源管理:每个线程可能需要管理自己的资源,例如内存池、文件描述符等。通过线程局部存储,每个线程可以独立地管理自己的资源,避免了资源共享带来的竞态条件和同步问题。
  4. 线程特定的上下文信息:有些情况下,每个线程可能需要保存自己的上下文信息,例如线程池中的工作线程可能需要保存任务相关的上下文信息。线程局部存储可以用来保存这些线程特定的上下文信息,避免了上下文信息在线程间的共享和同步问题。
  5. 线程安全的数据缓存:每个线程可以有自己的数据缓存,例如线程池中的工作线程可以缓存一些计算结果或中间数据,以提高性能。使用线程局部存储可以确保数据缓存的线程安全性,而不需要额外的同步机制。

总之,线程的局部存储适用于需要在多线程环境下独立管理数据的场景,能够提高程序的可维护性、可扩展性和线程安全性。

向线程发送信号

pthread_kill函数接口如下:

int pthread_kill(pthread_t thread, int sig);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>

// 信号处理函数
void signal_handler(int signum)
{
    if (signum == SIGUSR1) {
        printf("Received SIGUSR1 signal.\n");
    }
    else if (signum == SIGUSR2) {
        printf("Received SIGUSR2 signal.\n");
    }
}

// 线程函数
void* thread_function(void* arg)
{
    // 注册信号处理函数
    signal(SIGUSR1, signal_handler);
    signal(SIGUSR2, signal_handler);

    printf("Thread is waiting for signals...\n");

    // 无限循环等待信号
    while (1) {
        // 睡眠1秒钟,以便演示
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t tid;

    // 创建线程
    if (pthread_create(&tid, NULL, thread_function, NULL) != 0) {
        fprintf(stderr, "Error creating thread.\n");
        return 1;
    }

    printf("Thread created successfully.\n");

    // 等待一段时间,以便线程有机会开始运行
    sleep(2);

    // 向线程发送信号
    pthread_kill(tid, SIGUSR1);
    sleep(2);
    pthread_kill(tid, SIGUSR2);

    // 等待线程结束
    pthread_join(tid, NULL);

    printf("Thread finished.\n");

    return 0;
}

[root@Zhn 线程]# g++ pthread_signal.cpp -o pthread_signal -lpthread
[root@Zhn 线程]# ./pthread_signal 
Thread created successfully.
Thread is waiting for signals...
Received SIGUSR1 signal.
Received SIGUSR2 signal.
  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值