1

第12章 线程控制

12.5 重入

可重入函数

  • 中断一个可重入函数的执行,转而执行另外一个函数(一般为信号处理程序,注意此时依然为同一个线程),
    返回可重入函数执行不会出现错误。

  • 可重入与异步信号安全等价(APUE 3 edition, 10.6 )

  • 可重入函数除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是可重入的,
    可以允许有该函数的多个副本同时运行

线程安全:

  • 如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。

  • 1) 若一个函数如同可重入函数一样,也没有使用静态数据(全局变量或static局部变量),那么该函数也是线程安全的。
    2) 或使用了静态数据,但采用了同步机制,保证线程的安全调用,那么也是线程安全的。

可重入与线程安全的关系:

clipboard.png

-------------------------------[Figure 1]--------------------------------

举例说明:func是线程安全的,但不是可重入的:
1)线程安全是显然的,第b步在任意时刻只有一个线程访问。
2)假设进程执行完第a步,接收到一个信号,转而执行信号处理程序,恰巧该信号处理程序中也调用了函数func,那么就陷入了死锁。
3) 当然可以利用递归锁,那么func也是可重入的,但是递归锁的使用可能会带来其他严重问题

clipboard.png

-------------------------------[Figure 2]--------------------------------

支持线程安全函数的操作系统实现会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS,对POSIX.1中的一些非线程安全函数,
它会提供可替代的线程安全版本。下面列出这些函数的线程安全版本。
clipboard.png

POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关联的锁。
这个锁是递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。

int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp); 

12.6 线程特定数据(TSD)

使用线程特定数据理由:

  • 需要基于线程来维护一些数据(比如,线程id)

  • 希望能够提供一种机制可以在多线程的环境下使用基于进程的接口。(比如,errno)

int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
void* pthread_getspecific(pthread_key_t key); 
int pthread_setspecific(pthread_key_t key, const void *value); 
int pthread_key_delete(pthread_key_t *key);

注意:取消键与当前线程TSD的关联关系(键与其他线程TSD的关联关系不变),但并不会激活与键关联的析构函数,
   需要应用程序自己利用free函数

使用TSD过程:
1. 启动一个进程并创建了若干线程(A,B),其中一个线程(比如线程A),要申请线程私有数据,系统调用
pthread_key_creat在下图所示的key结构数组中找到第一个未用的元素,并把它的键(0)返回给调用者
2. 线程a通过pthrea_getspecific调用获得线程a的pkey[0]值,返回的是一个空指针NULL,
3. 用malloc在堆里分配一段空间,使用pthread_setspecific调用将pkey[0]指向刚才分配的内存区域。
4. 若线程b若使用相同的键,线程B只需要重复第三步。最终结果如下Figure 3所示。
clipboard.png

------------------------------------------[Figure 3]----------------------------------------

需要确保分配的键并不是由于在初始化阶段的竞争而发生变化:

int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
struct tsd { pthread_t tid; };

pthread_key_t key;
pthread_once_t create_done = PTHREAD_ONCE_INIT;

void destructor(void* arg){
    struct tsd* tsdptr = (struct tsd*)arg;
    free(tsdptr);
}

void key_create(void){
    pthread_key_create(&key, destructor);
}

void key_init(){
    struct tsd *tsdptr = (struct tsd*)malloc(sizeof(struct tsd));
    tsdptr->tid = pthread_self();
    pthread_setspecific(key, tsdptr);
}

void* p_fun(void* arg){
    pthread_once(&create_done, key_create);
    key_init();
    return (void*)0;
}

12.8 线程和信号

1. 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的.
2. 进程中的信号是传递到单个线程的,线程中必须使用pthread_sigmask设置信号屏蔽字.
3. 如果一个信号与硬件故障相关,该信号一般被发送到引起该事件的线程中,否则信号被发送到任意一个线程

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_kill(pthread_t thread, int sig);

[pthread_xxx的函数一般返回错误码,而不是设置errno]

注意,用sigprocmask修改的是进程(主线程)的信号屏障字,而不是本线程的信号屏蔽字

int sigwait(const sigset_t * restrict set, int * restrict signop);
signop为当前接受到的信号[APUE 3 edition 中文版的解释错误]

1. set为等待的信号或信号集
2. 若信号集中的某些信号在sigwait调用时处于挂起状态,那么sigwait将无阻塞地返回,且移除这些挂起信号,若为排队信号,一次移除一个
3. 线程在调用sigwait之前,必须阻塞等待的信号(避免出现窗口期)。sigwait函数会原子地取消信号集的阻塞状态,直到信号到来
4. 任意信号都可以唤醒调用sigwait的线程(和进程的sigsuspend很不同)
5. 使用sigwait的好处在于它可以简化信号处理,利用一个或多个线程专门处理信号,从而将异步产生的信号以同步方式处理
---5.1 假设专用线程A处理SIGINT,SIGQUIT信号, 在创建线程A之前,主线程首先屏蔽这两个信号
---5.2 线程A创建之后,继承主线程的信号屏蔽字,线程A调用sigwait函数
---5.3 sigwait取消对SIGINT与SIGQUIT的屏蔽,接着线程A阻塞,等待任意信号的到来
---5.4 当一个信号到来时,唤醒线程A,在线程A的正常的上下文中调用信号的处理程序
6. 若多个线程在sigwait的调用中因为同一个信号而阻塞,那么在信号传递的时候,就只有一个线程从中返回
7. 若一个信号被捕获(使用sigaction建立了一个信号处理程序),而一个线程正在sigwait调用中等待同一个信号,那么不同操作系统的处理方式不同,所以尽量避免

void* p_fun(void* arg){
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs, (int)arg);
    int sig;
    for(;;){
        if (sigwait(&sigs, &sig) != 0){
            fprintf(stderr, "sigwait err");
            return (void*)-1;
        }
        if (sig == SIGINT){
            printf("tid : %ld receive SIGINT.\n", pthread_self());
        }
        if (sig == SIGTSTP){
            printf("tid : %ld receive SIGTSTP.\n", pthread_self());
        }
    }
    return (void*)0;
}
int main(){
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs, SIGINT);
    sigaddset(&sigs, SIGTSTP);
    sigprocmask(SIG_SETMASK, &sigs, NULL);
    pthread_t tid[2];
    if (pthread_create(&tid[0], NULL, p_fun, (void*)SIGINT) != 0){
        fprintf(stderr, "pthread_create err");
        exit(-1);
    }
    if (pthread_create(&tid[1], NULL, p_fun, (void*)SIGTSTP) != 0){
        fprintf(stderr, "pthread_create err");
        exit(-1);
    }
    for(;;){
        /*main proccess*/
    }
    exit(0);
}
int pthread_kill(pthread_t tid, int signo);

可以传一个0值来检查线程tid是否存在,类似kill检查进程的存在性
闹钟定时器是进程资源,所有线程共享相同的闹钟

12.9 线程和fork

在子进程内部,只存在一个线程,如下图所示,子进程中只有线程T3,因为在父进程中由线程T3创建了子进程。
所以互斥量M1,M2在子进程中就无法解锁。
``

clipboard.png

不一致解决方法:

  • 1. 如果子进程从fork返回以后马上调用其中一个exec函数,可以避免这种不一致,在fork返回和子进程调用其中一个exec函数之间,
    子进程只能调用异步信号安全的函数

  • 2. 调用pthread_atfork函数建立fork处理程序

int pthread_atfork()(void (*prepare)(void),void (*parent)(void),void (*child)(void));

------2.1 prepare由父进程在fork创建子进程前调用,prepare的任务是获取父进程定义的所有锁
------2.2 parent是在fork创建了子进程以后,在fork返回之前在父进程环境中调用的,parent的任务是对prepare获得的锁进行解锁
------2.3 child在fork返回之前在子进程环境中调用,与parent一样,child必须释放prepare处理程序获得的所有锁
------2.4 可以多次调用pthread_atfork从而设置多套fork处理程序。parent和child是以他们注册时的顺序进行调用,
----------prepare的调用顺序与他们注册时的顺序相反,比如若模块A调用模块B,每个模块都有自己的一套锁。
----------那么模块B必须在模块A之前设置它的pthread_atfork函数


shiyang6017
158 声望59 粉丝

引用和评论

0 条评论