目录
- 线程的取消
- 线程的清理函数
- 线程的局部存储
- 向线程发送信号
线程的取消
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 线程]#
线程的局部存储适用于许多不同的场景,特别是在需要处理多线程并发的程序中。以下是一些适合使用线程局部存储的场景:
- 线程安全的日志记录:每个线程可以有自己的日志记录器,而不必担心线程间的竞态条件。这样可以确保日志记录的线程安全性,而不需要额外的同步机制。
- 线程特定的配置参数:不同的线程可能需要不同的配置参数,例如线程池中的工作线程可能需要不同的超时设置或日志级别。使用线程局部存储可以轻松地为每个线程保存其特定的配置参数。
- 线程安全的资源管理:每个线程可能需要管理自己的资源,例如内存池、文件描述符等。通过线程局部存储,每个线程可以独立地管理自己的资源,避免了资源共享带来的竞态条件和同步问题。
- 线程特定的上下文信息:有些情况下,每个线程可能需要保存自己的上下文信息,例如线程池中的工作线程可能需要保存任务相关的上下文信息。线程局部存储可以用来保存这些线程特定的上下文信息,避免了上下文信息在线程间的共享和同步问题。
- 线程安全的数据缓存:每个线程可以有自己的数据缓存,例如线程池中的工作线程可以缓存一些计算结果或中间数据,以提高性能。使用线程局部存储可以确保数据缓存的线程安全性,而不需要额外的同步机制。
总之,线程的局部存储适用于需要在多线程环境下独立管理数据的场景,能够提高程序的可维护性、可扩展性和线程安全性。
向线程发送信号
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.