问题
线程执行的过程中可以强制退出吗?
主动退出?被动退出?
问题抽象示例
需要解决的问题
g_run 全局变量需要保护吗?
如何编码使得线程中每行代码的执行可被 g_run 控制?
线程代码在被 g_run 控制并 "强制退出" 时:
- 如何指定线程的返回值?
- 如何回收初始化时申请的资源?
关键:如何实现 g_run 的原子化操作
以下是原子操作吗?如果不是,有什么影响?
- g_run = 1
- if(g_run) {}
- while(g_run) {}
- g_run++, g_run--, g_run += n, ...
下面的程序会输出什么?为什么?
该程序创建100个线程,每个线程会对全局变量 g_var 进行 10000次加一操作,但由于 g_var 是全局变量,是临界资源,并且 g_var++ 操作不是原子操作,g_var++ 在汇编层面分为三个步骤,首先是将 g_var的值读到某个寄存器上,然后是在寄存器上对这个值加一,最后将加一后的值写入到 g_var,所以这个程序的运行结果的打印,g_var 的值会小于等于 1000000
现代 C 语言的原子化特性
_Atomic 是一个关键字,被修饰的变量具有原子化特性
_Atomic 是与 const 和 volatile 同类型的关键字 (可搭配 typedef 使用)
思考
_Atomic 是现代 C 语言中的关键字,如果不使用它,int 类型变量的操作还是原子的吗?
变量原子性测试
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 <signal.h>
#include <memory.h>
#include <semaphore.h>static _Atomic volatile int g_var = 0;void* thread_func(void* arg)
{ int i = 0;for(i=0; i<10000; i++){g_var++;}return NULL;
}int main()
{pthread_t t[100] = {0};int i = 0;void* ret = NULL;for(i=0; i<100; i++){pthread_create(&t[i], NULL, thread_func, NULL);}for(i=0; i<100; i++){pthread_join(t[i], &ret);}printf("g_var = %d\n", g_var);return 0;
}
第 11 行,我们将 g_var 通过 _Atomic 关键字将它修饰为原子变量,对 g_var 的操作不可被打断
程序运行结果如下图所示:
由于g_var 通过 _Atomic 关键字将它修饰为原子变量,所以 g_var++ 变成了原子操作
从微观进行分析
将 C 代码转换为汇编代码可以看出,g_var = 1 和 if(g_var) 是原子操作,而 g_var++ 不是原子操作
结论
对于 int 型全局变量,赋值操作是原子操作 (g_var = 1)
基于值的条件判断是原子操作 (if(g_var))
递增 / 递减 操作不是原子操作 (++, --, +=, -=)
线程退出解决方案
({}) 是 gcc 中的语法,这种语法允许在一个表达式的上下文中包含多条语句,这样可以使得宏定义 R 具有返回值,返回值为 code
R 这个宏定义会判断 g_run,如果 g_run 为 0,则调用 pthread_exit() 来结束线程的执行,线程结束的返回值默认为 NULL,也可填在 R 中的第二个参数指定;非 0 则执行 code
注意事项
不是每一行代码都需要使用 R(...) 宏进行线程退出控制
建议编码时,精心设计线程退出的 关键点 及 返回值
实践中,通常以代码块为粒度进行线程退出控制
临界区代码一定不要 R(...) 宏进行线程退出控制 (可能造成死锁)
线程退出解决方案
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 volatile int g_run = 0;
static _Atomic volatile int g_var = 0;#define R(code, ...) ({ \if( !g_run ) \pthread_exit((NULL, ##__VA_ARGS__)); \\code; \
})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);if( pc ){pthread_cleanup_push(cleanup_handler, pc);R(strcpy(pc, "Hello World!"), (void*)111);R(printf("%s\n", pc));for(i=0; i<10000; i++){R(g_var++);}pthread_cleanup_pop(1);}return NULL;
}int main()
{pthread_t t[100] = {0};int i = 0;void* ret = NULL;for(i=0; i<100; i++){pthread_create(&t[i], NULL, thread_func, NULL);}for(i=0; i<100; i++){pthread_join(t[i], &ret);if( ret ){printf("ret = %lld\n", (long long)ret);}}printf("g_var = %d\n", g_var);return 0;
}
第 34 行,我们通过 pthread_cleanup_push(...) 函数来注册线程退出时的资源清理函数,当 thread_func 线程退出时,会调用 cleanup_handler(...) 函数来清理资源,这里是用来释放 pc 所指向的堆空间
在线程可能会中途退出的代码片段中使用宏定义 R(...),但不宜每个地方都使用,这样会影响程序的执行效率
程序运行结果如下图所示:
由于 g_run 的值为 0,所以每个子线程执行到第 36 行,就会结束,并调用到资源清理函数 cleanup_handler,拿到的 pthread_exit(...) 的返回值也是 111
思考
是否存在其它中途退出线程的方法?