线程的取消
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.