用于处理线程取消和清理处理程序问题的疯狂宏 hack
Posted
技术标签:
【中文标题】用于处理线程取消和清理处理程序问题的疯狂宏 hack【英文标题】:Crazy macro hack for handling issues with thread cancellations and cleanup handlers 【发布时间】:2018-01-05 06:54:22 【问题描述】:由于代码 sn-ps 和详细解释,这是一个非常长的问题。 TL;DR,下面显示的宏是否存在问题,这是一个合理的解决方案,如果没有,那么解决下面提出的问题的最合理方法是什么?
我目前正在编写一个处理 POSIX 线程的 C 库,并且必须能够干净地处理线程取消。特别是,可以从用户设置为可取消(PTHREAD_CANCEL_DEFFERED
或PTHREAD_CANCEL_ASYNCHRONOUS
canceltype)的线程调用库函数。
目前与用户交互的库函数都以调用pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate)
开始,并且在每个返回点,我确保调用pthread_setcancelstate(oldstate, &dummy)
以恢复线程之前的任何取消设置。
这基本上可以防止线程在库代码中被取消,从而确保全局状态保持一致并在返回之前正确管理资源。
不幸的是,这种方法有一些缺点:
必须确保在每个返回点恢复取消状态。如果函数具有具有多个返回点的非平凡控制流,这使得管理起来有些困难。忘记这样做可能会导致即使从库返回后线程也不会被取消。
我们只需要防止在资源分配或全局状态不一致的点取消。一个库函数可能会依次调用其他可取消安全的内部库函数,理想情况下,取消可能发生在这些点上。
以下是问题的示例说明:
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
static void do_some_long_computation(char *buffer, size_t len)
(void)buffer; (void)len;
/* This is really, really long! */
int mylib_function(size_t len)
char *buffer;
int oldstate, oldstate2;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
buffer = malloc(len);
if (buffer == NULL)
pthread_setcancelstate(oldstate, &oldstate2);
return -1;
do_some_long_computation(buffer, len);
fd = open("results.txt", O_WRONLY);
if (fd < 0)
free(buffer);
pthread_setcancelstate(oldstate, &oldstate2);
return -1;
write(fd, buffer, len); /* Normally also do error-check */
close(fd);
free(buffer);
pthread_setcancelstate(oldstate, &oldstate2);
return 0;
这里还不错,因为只有 3 个返回点。甚至可以以强制所有路径到达单个返回点的方式重构控制流,可能使用goto cleanup
模式。但是第二个问题仍然没有解决。想象一下,许多库函数都必须这样做。
第二个问题可以通过调用pthread_setcancelstate
包装每个资源分配来解决,这只会在资源分配期间禁用取消。在禁用取消的同时,我们还推送了一个清理处理程序(使用pthread_cleanup_push
)。还可以将所有资源分配一起移动(在进行长时间计算之前打开文件)。
虽然解决了第二个问题,但仍然有些难以维护,因为每个资源分配都需要包装在这些 pthread_setcancelstate
和 pthread_cleanup_[push|pop]
调用下。此外,可能并不总是可以将所有资源分配放在一起,例如,如果它们取决于计算结果。此外,需要更改控制流,因为不能在 pthread_cleanup_push
和 pthread_cleanup_pop
对之间返回(例如,如果 malloc
返回 NULL
就会出现这种情况)。
为了解决这两个问题,我想出了另一种可能的方法,该方法涉及使用宏进行肮脏的黑客攻击。这个想法是模拟其他语言中的临界区块之类的东西,在“取消安全”范围内插入代码块。
这就是库代码的样子(使用-c -Wall -Wextra -pedantic
编译):
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include "cancelsafe.h"
static void do_some_long_computation(char *buffer, size_t len)
(void)buffer; (void)len;
/* This is really, really long! */
static void free_wrapper(void *arg)
free(*(void **)arg);
static void close_wrapper(void *arg)
close(*(int *)arg);
int mylib_function(size_t len)
char *buffer;
int fd;
int rc;
rc = 0;
CANCELSAFE_INIT();
CANCELSAFE_PUSH(free_wrapper, buffer)
buffer = malloc(len);
if (buffer == NULL)
rc = -1;
CANCELSAFE_BREAK(buffer);
do_some_long_computation(buffer, len);
CANCELSAFE_PUSH(close_wrapper, fd)
fd = open("results.txt", O_WRONLY);
if (fd < 0)
rc = -1;
CANCELSAFE_BREAK(fd);
write(fd, buffer, len);
CANCELSAFE_POP(fd, 1); /* close fd */
CANCELSAFE_POP(buffer, 1); /* free buffer */
CANCELSAFE_END();
return rc;
这在一定程度上解决了这两个问题。取消状态设置和清理推送/弹出调用隐含在宏中,因此程序员只需指定需要取消安全的代码部分以及要推送的清理处理程序。其余的在幕后完成,编译器将确保每个 CANCELSAFE_PUSH
与 CANCELSAFE_POP
配对。
宏的实现如下:
#define CANCELSAFE_INIT() \
do \
int CANCELSAFE_global_stop = 0
#define CANCELSAFE_PUSH(cleanup, ident) \
do \
int CANCELSAFE_oldstate_##ident, CANCELSAFE_oldstate2_##ident;\
int CANCELSAFE_stop_##ident;\
\
if (CANCELSAFE_global_stop)\
break;\
\
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_##ident);\
pthread_cleanup_push(cleanup, &ident);\
for (CANCELSAFE_stop_##ident = 0; CANCELSAFE_stop_##ident == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_##ident = 1, pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident))
#define CANCELSAFE_BREAK(ident) \
do \
CANCELSAFE_global_stop = 1;\
pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident);\
goto CANCELSAFE_POP_LABEL_##ident;\
while (0)
#define CANCELSAFE_POP(ident, execute) \
CANCELSAFE_POP_LABEL_##ident:\
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_##ident);\
pthread_cleanup_pop(execute);\
pthread_setcancelstate(CANCELSAFE_oldstate_##ident, &CANCELSAFE_oldstate2_##ident);\
while (0)
#define CANCELSAFE_END() \
while (0)
这结合了我之前遇到的几个宏技巧。
do while (0)
模式用于具有多行函数式宏(需要分号)。
CANCELSAFE_PUSH
和 CANCELSAFE_POP
宏通过使用与 pthread_cleanup_push
和 pthread_cleanup_pop
相同的技巧强制成对出现,分别使用不匹配的 和
大括号(这里是不匹配的
do
和 while (0)
代替)。
for
循环的使用在某种程度上受到了question 的启发。这个想法是我们想在宏体之后调用pthread_setcancelstate
函数 来恢复CANCELSAFE_PUSH 块之后的取消。我在第二次循环迭代中使用了一个设置为 1 的停止标志。
ident 是要释放的变量的名称(这需要是一个有效的标识符)。 cleanup_wrappers 将被赋予其地址,根据此answer,该地址在清理处理程序范围内始终有效。这样做是因为变量的值在清理推送时尚未初始化(如果变量不是指针类型也不起作用)。
ident 还用于避免临时变量和标签中的名称冲突,方法是将其作为后缀附加到 ##
连接宏中,并赋予它们唯一的名称。
CANCELSAFE_BREAK
宏用于跳出 cancelsafe 块并直接进入对应的CANCELSAFE_POP_LABEL
。这受到goto cleanup
模式的启发,如here 所述。它还设置全局停止标志。
全局停止用于避免在同一范围级别中可能存在两个 PUSH/POP 对的情况。这似乎不太可能发生,但如果发生这种情况,那么当全局停止标志设置为 1 时,基本上会跳过宏的内容。CANCELSAFE_INIT
和 CANCELSAFE_END
宏并不重要,它们只是避免需要我们自己声明全局停止标志。如果程序员总是连续完成所有的推送,然后所有的弹出,则可以跳过这些。
扩展宏后,我们得到mylib_function的如下代码:
int mylib_function(size_t len)
char *buffer;
int fd;
int rc;
rc = 0;
do
int CANCELSAFE_global_stop = 0;
do
int CANCELSAFE_oldstate_buffer, CANCELSAFE_oldstate2_buffer;
int CANCELSAFE_stop_buffer;
if (CANCELSAFE_global_stop)
break;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_buffer);
pthread_cleanup_push(free_wrapper, &buffer);
for (CANCELSAFE_stop_buffer = 0; CANCELSAFE_stop_buffer == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_buffer = 1, pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer))
buffer = malloc(len);
if (buffer == NULL)
rc = -1;
do
CANCELSAFE_global_stop = 1;
pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer);
goto CANCELSAFE_POP_LABEL_buffer;
while (0);
do_some_long_computation(buffer, len);
do
int CANCELSAFE_oldstate_fd, CANCELSAFE_oldstate2_fd;
int CANCELSAFE_stop_fd;
if (CANCELSAFE_global_stop)
break;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_fd);
pthread_cleanup_push(close_wrapper, &fd);
for (CANCELSAFE_stop_fd = 0; CANCELSAFE_stop_fd == 0 && CANCELSAFE_global_stop == 0; CANCELSAFE_stop_fd = 1, pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSTATE_oldstate2_fd))
fd = open("results.txt", O_WRONLY);
if (fd < 0)
rc = -1;
do
CANCELSAFE_global_stop = 1;
pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSAFE_oldstate2_fd);
goto CANCELSAFE_POP_LABEL_fd;
while (0);
write(fd, buffer, len);
CANCELSAFE_POP_LABEL_fd:
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_fd);
pthread_cleanup_pop(1);
pthread_setcancelstate(CANCELSAFE_oldstate_fd, &CANCELSAFE_oldstate2_fd);
while (0);
CANCELSAFE_POP_LABEL_buffer:
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &CANCELSAFE_oldstate_buffer);
pthread_cleanup_pop(1);
pthread_setcancelstate(CANCELSAFE_oldstate_buffer, &CANCELSAFE_oldstate2_buffer);
while (0);
while (0);
return rc;
现在,这组宏看起来很可怕,要理解它们的确切工作原理有些棘手。另一方面,这是一项一次性任务,一旦编写完成,它们就可以离开,项目的其余部分可以从它们的好处中受益。
我想知道我可能忽略的宏是否存在任何问题,以及是否有更好的方法来实现类似的功能。另外,您认为提出的哪种解决方案最合理?是否有其他想法可以更好地解决这些问题(或者,它们真的不是问题)?
【问题讨论】:
似乎更适合代码审查,对吧?或者您是否面临实际上“不起作用”的问题? 我不确定哪个 stackexchange 站点最适合这个问题。我很乐意将它迁移到合适的地方。 我个人不喜欢这样的宏,原因有很多。在这种情况下。使用内联函数要安全得多。多写一点——调试少很多:)。 你没有说你需要定位哪些操作系统,以及哪些工具链。有些提供额外的设施来处理线程取消,也许提供了一种更清洁的方法。 由于我试图使代码尽可能可移植,因此我的目标是任何可以支持 pthreads 和 pthread_cancel 的系统(特别是 Linux 和 OS X)和标准 C 编译器(我正在使用 gcc ,但强烈希望避免使用 gcc 特定的魔法)。 【参考方案1】:除非您使用异步取消(这总是很成问题),否则您不必禁用 malloc
和 free
(以及许多其他 POSIX 函数)周围的取消。同步取消只发生在取消点,而这些函数不是。
您正在滥用 POSIX 取消处理工具来实现范围退出挂钩。一般来说,如果你发现自己在 C 中做这样的事情,你应该认真考虑改用 C++。这将为您提供更加完善的功能版本,并提供充足的文档,并且程序员已经拥有使用它的经验。
【讨论】:
以上是关于用于处理线程取消和清理处理程序问题的疯狂宏 hack的主要内容,如果未能解决你的问题,请参考以下文章