UNIX高级环境编程 第12章 [12.5-12.9]

第12章 线程控制

12.5 重入

可重入函数数组

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

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

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

线程安全:函数

  • 若是一个函数在相同的时间点能够被多个线程安全地调用,就称该函数是线程安全的。spa

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

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

clipboard.png

-------------------------------[Figure 1]--------------------------------指针

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

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函数

相关文章
相关标签/搜索