1. 线程安全和可重入函数的重要性
安全访问竟态数据,涉及两个概念
线程安全:
不访问全局资源的函数;
或者访问全局资源,但是加 mutex 的函数;
比如常见的 malloc, free, printf 等函数,会访问全局资源,其内部都添加了锁保护,因此是线程安全的;
特点,多线程对竟态的资源访问,加锁即可;
信号安全:
不访问全局资源的函数;
访问全局资源,但是做保存和恢复的函数;
信号函数,属于线程内资源,不能加锁 (可能引起死锁);
信号函数打断线程后,信号函数会执行完,才会重新调度,因此只要对竟态资源添加保存和恢复,不会影响原线程数据;
特点: 不能用 mutex 等锁 ;
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <ctype.h>
#include <sys/types.h>
char *strtoupper(char *string)
{static char buffer[1000];
int index;
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0;
return buffer;
}
void *thread_fun(void *param)
{while (1) {usleep(100);
printf("%s\n", strtoupper((char *)param));
}
}
int main(void)
{
pthread_t tid1, tid2;
int ret;
printf("main pid:%d, tid:%lu\n", getpid(), pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, "hello world");
if (ret == -1) {perror("cannot create new thread");
return 1;
}
ret = pthread_create(&tid2, NULL, thread_fun, "world hello");
if (ret == -1) {perror("cannot create new thread");
return 1;
}
if (pthread_join(tid1, NULL) != 0) {perror("call pthread_join function fail");
return 1;
}
if (pthread_join(tid2, NULL) != 0) {perror("call pthread_join function fail");
return 1;
}
return 0;
}
2. 非学究的概括,可重入函数要满足两个条件:
(1) 函数是线程安全的;
(2) 函数是可中断的 (对于 Linux, 异步的信号),执行了中断处理后,再回来继续执行函数,结果仍然正确;
线程安全和信号安全的案例
https://deadbeef.me/2017/09/reentrant-threadsafe
线程安全,信号不安全案例:
__thread int test = 123;
#include <threads.h>
// `t` is now local to each thread
thread_local int t;// 线程级全局变量,线程安全,但可以被信号访问
// 每个线程都会创建一个实例,线程退出,被释放;void swap(int *x, int *y) {
t = *x;
*x = *y;
// my_func() could be called here
*y = t;
}
void my_func() {
int x = 1, y = 2;
swap(&x, &y);
}
信号安全,线程不安全案例
int t;
// t 可以恢复 (信号函数执行完,才继续执行线程),void swap(int *x, int *y) {
int s;
// save global variable
s = t;
t = *x;
*x = *y;
// my_func() could be called here
*y = t;
// restore global variable
t = s;
}
void my_func() {
int x = 1, y = 2;
swap(&x, &y);
}
#include <signal.h>
#include <stdio.h>
struct two_long {long a, b;} data;
void signal_handler(int signum){printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){static struct two_long zeros = { 0, 0}, ones = {1, 1};
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1)
{data = zeros; data = ones;}
典型的程序设计满足下面条件之一的多数是不可重入函数:
(1) 使用了静态数据结构;
(2) 调用了 malloc 或 free; 申请的内存在共同的堆里;
(3) 调用了标准 I / O 函数; 标准 io 库很多实现都以不可重入的方式使用全局数据结构。
(4) 进行了浮点运算. 许多的处理器 / 编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现。)
3 非学究可重入与线程安全注意事项
如果一个函数中用到了全局或静态变量(const 除外),那么它不是线程安全的,也不是可重入的;
如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题 (比如信号里调用加锁函数,可能出现死锁);
如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入。
4 多线程情况下的信号处理
方法 1:替换掉不可重入函数
Signal_handler()
{Printf->write}
打印,但是不会涉及线程安全问题;
方法 2:将信号的异步问题,转为同步化
在主线程将信号屏蔽掉,则其他同组线程也会屏蔽掉对应信号;
然后专门创建一个子线程,重新设置该信号,并且用 sigwaitinfo 函数,通过同步等待的方式获取信号,然后做信号处理
#include <signal.h>
#include <stdio.h>
struct two_long {long a, b;} data;
void signal_handler(int signum){printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){static struct two_long zeros = { 0, 0}, ones = {1, 1};
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1)
{data = zeros; data = ones;}
}
leon@pc:$ cat signalsync.c
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
// used to show which thread the signal is handled in.
if (signum == SIGUSR1) {printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
//SIGRTMIN should not be considered constants from userland,
//there is compile error when use switch case
} else if (signum == SIGRTMIN) {printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}
void* worker_thread()
{pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}
void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();
pthread_detach(ppid);
sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);
while (1) {rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}
int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;
// Block SIGRTMIN and SIGUSR1 which will be handled in
//dedicated thread sigmgr_thread()
// Newly created threads will inherit the pthread mask from its creator
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
//A new thread inherits a copy of its creator's signal mask.
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
// Create the dedicated thread sigmgr_thread() which will handle
// SIGUSR1 and SIGRTMIN synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);
// Create 5 worker threads, which will inherit the thread mask of
// the creator main thread
for (i = 0; i < 5; i++) {pthread_create(&ppid, NULL, worker_thread, NULL);
}
// send out 50 SIGUSR1 and SIGRTMIN signals
for (i = 0; i < 50; i++) {kill(pid, SIGUSR1);
printf("main thread, send SIGUSR1 No. %d\n", i);
kill(pid, SIGRTMIN);
printf("main thread, send SIGRTMIN No. %d\n", i);
sleep(10);
}
exit (0);
}
4 典型的不安全的 Linux 库函数