用于处理线程取消和清理处理程序问题的疯狂宏hack

时间:2017-07-29 07:07:28

标签: c multithreading pthreads c-preprocessor cancellation

由于代码片段和详细解释,这是一个非常长的问题。 TL; DR,下面显示的宏是否有问题,这是一个合理的解决方案,如果没有,那么解决下面提出的问题的最合理方法是什么?

我目前正在编写一个处理POSIX线程的C库,并且必须能够干净地处理线程取消。特别是,可以从用户设置为可取消(PTHREAD_CANCEL_DEFFEREDPTHREAD_CANCEL_ASYNCHRONOUS canceltype)的线程调用库函数。

目前与用户交互的库函数都以调用pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate)开头,并且在每个返回点,我确保调用pthread_setcancelstate(oldstate, &dummy)以恢复任何取消设置以前的线程。

这基本上可以防止线程在库代码中被取消,从而确保全局状态保持一致,并在返回之前正确管理资源。

遗憾的是,这种方法有一些缺点:

  1. 必须确保在每个返回点恢复取消状态。如果函数具有多个返回点的非平凡控制流,则这使得管理起来有些困难。忘记这样做可能会导致即使从图书馆返回也不会取消的帖子。

  2. 我们只需要在分配资源或全局状态不一致的点上防止取消。库函数可以依次调用其他可以取消安全的内部库函数,理想情况下可以在这些点上进行取消。

  3. 以下是问题的示例说明:

    #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_setcancelstatepthread_cleanup_[push|pop]调用之下。也可能并不总是可以将所有资源分配放在一起,例如,如果它们依赖于计算结果。此外,需要更改控制流,因为无法在pthread_cleanup_pushpthread_cleanup_pop对之间返回(例如,如果malloc返回NULL则会出现这种情况)。< / p>

    为了解决这两个问题,我提出了另一种可能的方法,涉及使用宏进行脏攻击。我们的想法是模拟其他语言中的关键部分块,在&#34;取消安全&#34;中插入一段代码。范围。

    这就是库代码的样子(用-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;
    }
    

    这在一定程度上解决了这两个问题。 cancelstate设置和清理push / pop调用隐含在宏中,因此程序员只需指定需要取消安全的代码部分以及要推送的清理处理程序。其余的工作在幕后完成,编译器将确保每个CANCELSAFE_PUSHCANCELSAFE_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_PUSHCANCELSAFE_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_INITCANCELSAFE_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;
    }
    

    现在,这组宏看起来很可怕,理解它们如何正常工作有点棘手。另一方面,这是一次性任务,一旦写完,它们就可以离开,项目的其余部分可以从它们的好处中受益。

    我想知道我可能忽略的宏是否有任何问题,以及是否有更好的方法来实现类似的功能。另外,您认为哪种解决方案最合理?是否还有其他想法可以更好地解决这些问题(或许,它们真的不是问题)?

1 个答案:

答案 0 :(得分:0)

除非您使用异步取消(这总是很成问题),否则您不必在mallocfree(以及许多其他POSIX函数)周围禁用取消。同步取消仅发生在取消点,而这些功能不是。

您正在滥用POSIX取消处理工具来实现范围退出挂钩。一般来说,如果你发现自己在C中做这样的事情,你应该认真考虑使用C ++。这将为您提供更加精美的功能,并提供丰富的文档,程序员已经拥有丰富的经验。