由於代碼片段和詳細解釋,這是一個非常長的問題。 TL; DR,下面顯示的宏是否存在問題,這是一個合理的解決方案,如果不是,那麼解決下面提出的問題的最合理的方法是什麼?用於處理線程取消和清除處理程序問題的瘋狂宏攻擊
我目前正在編寫一個處理POSIX線程的C庫,並且必須能夠乾淨地處理線程取消。特別是,可以由用戶設置爲可取消的線程(PTHREAD_CANCEL_DEFFERED
或PTHREAD_CANCEL_ASYNCHRONOUS
取消類型)調用庫函數。
當前與用戶接口的庫函數都以pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate)
的調用開始,並且在每個返回點,我確保對pthread_setcancelstate(oldstate, &dummy)
進行調用以恢復線程以前的任何取消設置。
這基本上可以防止線程在庫代碼中被取消,從而確保全局狀態保持一致,並在返回之前正確管理資源。
這種方法不幸的是有一些缺點:
一個必須確保在每一個折返點恢復cancelstate。如果函數具有多個返回點的非平凡控制流,這使得管理有點難。忘記這樣做可能會導致線程在從庫中返回後不會被取消。
我們確實需要防止在資源被分配或全局狀態不一致的地方取消。庫函數可能會調用其他可以取消安全的內部庫函數,理想情況下可能會在這些點上取消。
這裏的問題樣本圖解:
#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_wrapper將被賦予其地址,根據此answer,這將始終在清理處理程序範圍內有效。這是因爲變量的值尚未在清除推送點初始化(並且如果變量不是指針類型也不起作用)。
該ident還用於避免臨時變量和標籤中的名稱衝突,方法是將它作爲後綴與##
級聯宏附加在一起,並賦予它們唯一的名稱。
CANCELSAFE_BREAK
宏用於跳出取消安全塊,並跳入相應的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;
}
現在,這組宏是可怕的看,這是有點棘手,瞭解他們究竟是如何工作的。另一方面,這是一次性任務,一旦寫完,就可以留下,而項目的其他部分可以從他們的好處中受益。
我想知道是否有任何我可能忽略的宏的問題,以及是否有更好的方法來實現類似的功能。此外,您認爲哪種解決方案最合理?是否還有其他想法可以更好地解決這些問題(或者,它們是否真的不是問題)?
似乎更好的套件代碼審查,對吧?或者你正在面對一些實際上「*不起作用」的東西? – alk
我不確定哪個stackexchange網站最適合這個問題。我很樂意將它遷移到適當的地方。 –
我個人不喜歡這樣的宏,原因很多。在這種情況下。使用內聯函數更安全。多一點寫作 - 少了很多調試:)。 –