可重入函数
如果一个函数能被多个线程同时调用且不发生竞态条件,则我们称它为线程安全的(thread safe),或者说它是可重入函数。Linux库函数只有一小部分是不可重入的。这些库函数之所以不可重入,主要是因为其内部使用了静态变量。不过Linux对很多不可重入的库函数提供了对应的可重入版本,这些可重入版本的函数名是在原函数名尾部加上_r。比如函数localtime对应的可重入函数是localtime_r。在多线程程序中,一定要使用其可重入版本,否则可能导致预想不到的结果。
线程和进程
思考这样一个问题:如果一个多线程程序的某个线程fork函数,那么新创建的子进程是否将自动创建和父进程相同数量的线程呢?答案是“否”,正如我们所期望的样子。子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制。并且子进程将自动继承父进程互斥锁(条件变量与之类似)的状态。也就是说,父进程中已经被加锁的互斥锁在在子进程中也是被锁住的。这就引起了一个问题:子进程可能不清楚父进程继承而来的互斥锁的具体状态(是加锁状态还是解锁状态)。这个互斥锁可能被锁住了,但并不是由调用fork的那个线程锁住的,而是由其他线程锁住的。如果是这种情况,则子进程若再次对该互斥锁执行加锁就会导致死锁,示例代码如下:
#include #include #include #include #include pthread_mutex_t mutex;/*子线程运行的函数。它首先获得互斥锁mutex,然后暂定5s,再释放该互斥锁*/void * another(void *arg){ printf("in child thread, lock the mutex"); pthread_mutex_lock(&mutex); sleep(5); pthread_mutex_unlock(&mutex);}int main(){ pthread_mutex_init(&mutex, NULL); pthread_t id; pthread_create(&id, NULL, another, NULL); /*在父进程中主线程暂定1s,以确保在执行fork之前, 子线程已经开始运行并获得了互斥锁变量mutex*/ sleep(1); int pid = fork(); if(pid < 0){ pthread_join(id, NULL); pthread_mutex_destroy(&mutex); return 1; } else if(pid == 0){ printf("I am in the child, want to get the lock"); /*子进程从父进程继承了互斥锁mutex的状态, 该互斥锁处于锁住的状态,这是由于父进程的子线程执行 pthread_mutex_lock引起的*/ pthread_mutex_lock(&mutex); printf("I can not run to here. oop..."); pthread_mutex_unlock(&mutex); exit(0); } else{ wait(NULL); } pthread_join(id, NULL); pthread_mutex_destroy(&mutex); return 0;}
不过,pthread提供了一个专门的函数pthread_atfork,以确保fork调用之后父进程和子进程都拥有一个清楚的锁状态。该函数的定义如下:
#include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void(*child)(void));
这个函数将建立3个fork句柄来帮助我们清理互斥锁的状态,prepare句柄将在fork调用创建出子进程之前被执行。它可以用来锁住所有父进程中的互斥锁。parent句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare句柄中被锁住的互斥锁。child句柄是fork返回之前,在子进程中被执行。和parent句柄一样,child句柄也要用于释放所有在prepare句柄中被锁住的互斥锁。
这个函数成功返回0,设备则设置错误吗。
因此如果要让上述代码正常工作,就应该如下修改:
线程和信号
每个线程都可以独立地设置信号掩码。设置进程掩码的函数是sigprocmask,但在多线程环境下我们应该使用如下所示的pthread版本的sigprocmask函数来设置信号掩码:
#include #include int pthread_sigmask(int how, const sigset_t* newmask, const sigset_t* oldmask);
该函数的参数含义与sigprocmask的参数完全相同,参见信号 - Linux Signal - 信号函数&信号集。
pthread_sigmask成功时返回0,失败则返回错误吗。
由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。这可以通过如下两个步骤来实现:
- 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会相应被屏幕的信号了。
- 在某个线程中调用如下函数来等待信号并处理之:
#include int sigwait(const sigset_t* set, int *sig);
set参数指定需要等待的信号的集合。我们可以简单得将其制定为第一步中创建的型号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig指向的整数用于存储该函数返回的信号值。sigwait成功返回0,失败则返回错误码。一旦sigwait正确返回,我们就可以对接收到的信号做处理了。很显然,如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到信号时,二者中只有一个起作用。
下面的代码展示了如何通过上述两个步骤实现在一个线程中统一处理所有信号:
#include #include #include #include #include #include #define handle_error_en(en, msg) do {errno = en; perror(msg);exit(EXIT_FAILURE);}while(0)static void *sig_thread(void *arg){ sigset_t *set = (sigset_t*)arg; int s, sig; for(;;){ /*第二个步骤,调用sigwait等待信号*/ s = sigwait(set, &sig); if(s != 0){ handle_error_en(s, "sigwait"); } printf("Signal handing thread got signal %d", sig); }}int main(int argc, char const *argv[]){ pthread_t thread; sigset_t set; int s; /*第一个步骤,在主线程中设置信号掩码*/ sigemptyset(&set); sigaddset(&set, SIGQUIT); sigaddset(&set, SIGUSR1); s = pthread_sigmask(SIG_BLOCK, &set, NULL); if(s!=0){ handle_error_en(s, "pthread_sigmask"); } s = pthread_create(&thread, NULL, *sig_thread, (void*)&set); if(s!=0){ handle_error_en(s, "pthread_create"); } pause();}
最后,pthread还提供了下面的方法,使得我们可以明确地将一个信号发送给指定的线程:
#include int pthread_kill(pthread_t thread, int sig);
其中,thread参数指定目标线程,sig参数指定待发送的信号。如果sig为0,则pthread_kill不发送信号,但它仍然会执行错误检查。我们可以利用这种方式来检查目标线程是否存在。pthread_kill成功时返回0,失败则返回错误码。