问题
是否存在其它中途线程退出的方法?
通过调用Linux系统函数 pthread_cancel(...) 可中途退出线程
Linux 提供了线程取消函数
取消状态
- 接受取消状态: PTHREAD_CANCEL_ENABLE
- 拒绝取消状态: PTHREAD_CANCEL_DISABLE
取消请求
- 延迟取消: PTHREAD_CANCEL_DEFERRED => 线程继续执行,在下一次取消掉退出执行
- 异步取消: PTHREAD_CANCEL_ASYNCHRONOUS => 可能在任何位置退出执行
什么是线程取消点?
取消点即特殊函数的调用点
- 线程允许取消并且取消类型是延迟取消
- 当接收到取消请求后,执行到特殊函数调用点时,线程退出
常用取消点函数
- void pthread_testcancel(void)
- 在需要退出的 "关键点" 调用此函数,线程返回值为 PTHREAD_CANCELED
线程被动退出示例
线程被动退出实验
test1.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <semaphore.h>
void cleanup_handler(void *arg)
{
printf("%s: %p\n", __FUNCTION__, arg);
free(arg);
}
void* thread_func(void* arg)
{
int i = 0;
char* pc = malloc(16); // initialize
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
if( pc )
{
pthread_cleanup_push(cleanup_handler, pc);
printf("pc = %p\n", pc);
strcpy(pc, "Hello World!");
while( 1 )
{
printf("pc = %s\n", pc);
printf("begin...\n");
pthread_testcancel();
printf("end...\n");
// sleep(1);
}
pthread_cleanup_pop(1);
}
return NULL;
}
int main()
{
pthread_t t = 0;
void* ret = NULL;
pthread_create(&t, NULL, thread_func, NULL);
sleep(3);
pthread_cancel(t);
pthread_join(t, &ret);
printf("ret = %lld\n", (long long)ret);
printf("PTHREAD_CANCELED = %lld\n", (long long)PTHREAD_CANCELED);
return 0;
}
第55行,在主线程中创建子线程 thread_func,来测试线程中途退出
第 22 行和 23行,在子线程中设置可以接收取消状态,取消类型为延迟取消
第 27 行和 44 行,通过 pthread_cleanup_push(...) 和 pthread_cleanup_pop(1),来设置线程清理函数为 cleanup_handler(...); 即使线程中途退出,线程清理函数也会被自动调用
第 38 行,通过 pthread_testcancel() 函数来设置线程的取消点,当代码执行到这个函数,并且收到了线程取消请求,线程就会中途退出了
第 59 行,在主线程 sleep 3s后,调用 pthread_cancel(...) 函数来通知子线程退出
第 63 行,打印子线程中途退出时的返回值
程序运行结果如下图所示:
子线程在 3s 后退出,通过 pthread_cancel(...) 来退出线程,该线程退出的返回值等同于 PTHREAD_CANCELED,值为 -1
实验总结
必须在线程中调用取消状态和取消类型的设置函数
除了 pthread_testcancel() 函数, sleep() 函数也是取消点
线程接收取消请求后,会执行线程清理函数 (释放资源)
进入临界区之前,将取消状态设置为 PTHREAD_CANCEL_DISABLE
退出临界区之后,可将取消状态重新设置为 PTHREAD_CANCEL_ENABLE
对于取消类型,永远不要使用 PTHREAD_CANCEL_ASYNCHRONOUS
线程与信号
信号是进程层面的概念,进程内的所有线程均可处理信号
进程收到信号后,任意挑选线程对信号进行处理 (未屏蔽目标信号)
可针对进程中特定的线程发送信号,此时只有目标线程收到信号
线程可独立设置各自的信号掩码 (独立配置目标信号集合)
线程信号发送示例
注意事项
"父线程" 中的信号屏蔽会传递到 "子线程" 中
发送给进程的信号首选主线程处理 (使用已注册的信号处理函数)
若主线程屏蔽目标信号,则选择其他未屏蔽目标信号的线程完成信号处理
线程中注册的信号处理函数,对于进程全局有效
A 线程注册处理函数 handler_x(), B 线程注册处理函数 handler_y()
A 线程和 B 线程 收到 x 和 y 信号均会调用对应的处理函数
重要提醒
在多线程程序中,使用信号的第一原则就是不要使用信号!
线程与信号实验
test2.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <memory.h>
#include <semaphore.h>
static void mask_all_signal()
{
sigset_t set = {0};
sigfillset(&set);
pthread_sigmask(SIG_SETMASK, &set, NULL);
}
void signal_handler(int sig, siginfo_t* info, void* ucontext)
{
printf("handler : thread = %ld\n", pthread_self());
printf("handler : sig = %d\n", sig);
printf("handler : info->si_signo = %d\n", info->si_signo);
printf("handler : info->si_code = %d\n", info->si_code);
printf("handler : info->si_pid = %d\n", info->si_pid);
printf("handler : info->si_value = %d\n", info->si_value.sival_int);
}
static void* thread_entry(const char* name, int sig, void* arg)
{
struct sigaction act = {0};
act.sa_sigaction = signal_handler;
act.sa_flags = SA_SIGINFO;
sigaddset(&act.sa_mask, sig);
sigaction(sig, &act, NULL);
while( 1 )
{
printf("%s ==> %ld : run...\n", name, pthread_self());
sleep(1);
}
return NULL;
}
void* thread_func(void* arg)
{
return thread_entry(__FUNCTION__, 40, arg);
}
void* thread_exit(void* arg)
{
return thread_entry(__FUNCTION__, SIGINT, arg);
}
int main()
{
pthread_t tf = 0;
pthread_t te = 0;
void* ret = NULL;
union sigval sv = {1234567};
printf("thread %ld : run...\n", pthread_self());
pthread_create(&tf, NULL, thread_func, NULL);
// mask_all_signal();
pthread_create(&te, NULL, thread_exit, NULL);
sleep(3);
pthread_sigqueue(tf, 40, sv);
sleep(3);
pthread_kill(te, SIGINT);
pthread_join(tf, &ret);
pthread_join(te, &ret);
return 0;
}
第 69 和 73 行,主线程创建了2个子线程
在子线程 thread_func 中,捕获信号值为40的信号,当收到值为40的信号后,调用信号捕捉函数 signal_handler()
在子线程 thread_exit 中,捕获SIGNAL信号,当收到SIGNAL的信号后,调用信号捕捉函数 signal_handler()
在主线程中先 sleep 3s,然后通过 pthread_sigqueue(...) 函数向 thread_func 发送值为 40 的信号,并携带了一个参数,这个参数的值为 1234567;随后又 sleep 3s,通过 pthread_kill(...) 向 thread_exit 发送 SIGNAL 信号
pthread_sigqueue(...) 和 pthread_kill(...)都是向线程发送信号,但 pthread_sigqueue(...)可以多携带一个参数
程序运行结果如下图所示:
两个子线程均收到了信号,并调用到了信号处理函数
我们在 shell 中键入 Ctrl C,向进程发送 SIGNAL 信号,结果如下图所示:
该信号被处理了,是主线程处理的,并且处理方式是在子线程 thread_exit 中设置的信号处理方式,说明发送给进程的信号首选主线程处理,线程中注册的信号处理函数,对于进程全局有效
将 71 行,mask_all_signal() 的注释打开,程序运行结果如下图所示:
通过打印可以看出,只有值为40的信号被捕获了,thread_exit 线程是在主线程调用 mask_all_signal(),屏蔽所有的信号后创建出来的,thread_exit 线程会继承主线程的信号屏蔽集,屏蔽所有信号
思考
主线程创建子线程,子线程执行过程中调用 fork(),会发生什么 ???
下面的程序输出什么?为什么?
多线程 fork() 实验
test3.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <memory.h>
#include <semaphore.h>
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void* arg)
{
if( arg )
fork();
while(1)
{
printf("thread_func : %d => %ld\n", getpid(),
pthread_self());
sleep(1);
}
}
int main()
{
pthread_t t = 0;
pthread_create(&t, NULL, thread_func, NULL);
pthread_create(&t, NULL, thread_func, (void*)1);
while(1)
{
printf("main : %d => %ld\n", getpid(),
pthread_self());
sleep(1);
}
return 0;
}
程序运行结果如下图所示:
主线程创建了 2 个子线程,其中一个子线程调用了 fork(),可以看出在子线程中 fork(),创建出来的进程是在子线程的代码片段去执行的
问题出在哪里?
fork() 是针对进程复制的系统调用 (历史比较悠久)
线程在 Linux 内核中是轻量级进程 (fork() 只会复制当前进程)
所以,多线程中 fork() 之后:
整个进程的资源都被复制,如:全局变量,代码段,堆...
当前线程 (轻量级进程) 被复制,如:寄存器,执行流
其他线程不会被复制 (fork() 只针对当前进程)
应用场景 => 多进程服务端
多进程服务端的好处是即使一个进程崩溃了,也不会影响整个服务端的正常运行;并且在子线程中 fork() 去创建进程,上下文更简短,代码执行逻辑更加清晰
注意事项
多线程中的 fork() 可能导致死锁!
test4.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <memory.h>
#include <semaphore.h>
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_2(void* arg)
{
int i = 0;
sleep(1);
if( fork() )
{
return NULL;
}
while( i < 100 )
{
pthread_mutex_lock(&g_mutex);
printf("fork : %d => %ld\n", getpid(),
pthread_self());
pthread_mutex_unlock(&g_mutex);
sleep(1);
i++;
}
return NULL;
}
void* thread_1(void* arg)
{
int i = 0;
while( i < 100 )
{
pthread_mutex_lock(&g_mutex);
sleep(3);
pthread_mutex_unlock(&g_mutex);
i++;
}
return NULL;
}
int main()
{
pthread_t t = 0;
pthread_create(&t, NULL, thread_1, NULL);
pthread_create(&t, NULL, thread_2, NULL);
printf("main : %d => %ld\n", getpid(),
pthread_self());
while(1)
{
sleep(1);
}
return 0;
}
主线程中创建两个子线程,thread_1 和 thread_2,thread_1 会先获取到锁,然后 thread_2 执行 fork(),也去获取锁,由于 fork() 后整个进程资源都会被复制,fork() 前 g_mutx 已被上锁,所以 fork() 后去获取锁,会导致死锁
程序运行结果如下图所示:
fork() 后导致了死锁,所以在多线程中去执行 fork() 去获取锁的场景下,需要在 fork() 前先将需要获取的锁都进行解锁