第十四章 多线程编程
第十四章 多线程编程
14.1 Linux线程概述
线程的实现方式可分为三种模式:
- 完全在用户空间实现.
- 完全由内核实现.
- 双层调度(two level scheduler).
关于每一点的解释:
-
无须内核的支持,内核甚至根本不知道这些线程的存在
优点有
1)创建和调度线程都无须内核的干预,因此速度相当快.
2)不占用额外的内核资源,所以创建了很多线程不会有很明显的影响;
缺点是
1)由于内核是按照其最小调度单位来分配的CPU,所以一个进程的多个进程无法运行在不同CPU上.
2)线程的优先级只对同一个进程中的线程有效,比较不同进程中的优先级没有意义. -
优缺点和"完全在用户空间"交换,现代Linux内核已经大大增强了对线程的支持,完全有内核调度的这种实现方式满足M:N=1:1,即一个用户空间线程被映射为一个内核线程.
-
1和2的混合体,内核调度M个内个线程,线程库调度N个用户线程.线程切换速度快,同时可以充分利用多个处理器的优势.
自Linux内核2.6以来,提供了真正的内核线程,之前有过"管理线程"的概念,但这增加了额外的系统开销.也有过"用进程模拟内核线程"的概念,但会有许多与线程所要求概念不同的语义问题.
现在主要使用的是NPTL(Native POSIX Thread Library),优势有:
-
内核线程不再是一个进程.
-
摒弃了管理线程,终止,回收线程堆栈都可由内核完成.
-
可运行在不同CPU上.
-
线程同步由内核完成.
Linux上有名的线程库是LinuxThreads和NPTL,它们都是采用1:1的方式实现的.
14.2 创建线程和结束线程
线程的相关操作如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);
int pthread_cancel(pthread_t thread);
// 目标线程可以决定是否允许取消以及如何取消.
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
pthread_attr_t 结构体定义了一套完整的线程属性,而且线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性.
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, cpu_set_t * cpuset);
int pthread_attr_getaffinity_np(const pthread_attr_t *attr, size_t cpusetsize, cpu_set_t * cpuset);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setname_np(const pthread_attr_t *attr, const char * name, void * arg);
int pthread_attr_getname_np(const pthread_attr_t *attr, char * name, int len);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
需要的时候可以查手册.
下面介绍三种专门用于线程同步的机制1)POSIX信号量2)互斥锁3)条件变量.
14.4 POSIX信号量
pthread_join也可以看作一种简单的线程同步方式,不过很显然,它无法高效的实现复杂的同步需求,比如控制对共享资源独占式访问,或者是在某个条件满足后唤醒一个线程.POSIX信号量函数都以sem_开头,并不像大多数函数那样以pthread_开头.
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t * sem);
int sem_timedwait(sem_t * sem, const struct timespec *abstime);
int sem_trywait(sem_t * sem);
int sem_post(sem_t * sem);
int sem_post_multiple(sem_t * sem, int number);
int sem_getvalue(sem_t * sem, int * sval);
int sem_destroy(sem_t * sem);
14.5 互斥锁
当进入关键代码段时,需要获得互斥锁并将其加锁(二进制信号量的P操作),当离开关键代码段时,需要对互斥锁解锁(二进制信号量的V操作),以唤醒其他等待该互斥锁的线程.
互斥锁的相关操作:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_consistent(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
同样提供了pthread_mutexattr_t结构体定义了一套完整的互斥锁属性,故线程库提供了一系列操作以方便获取或设置互斥锁属性.
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_setkind_np(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_getkind_np(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
int pthread_mutexattr_getrobust(pthread_mutexattr_t *attr, int *robust);
14.6 死锁
使用互斥锁的一个噩耗是死锁(这个我还遇到过一次,排错真的是挺麻烦的,需要把整体逻辑理清).
提供一个死锁的案例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
void *another(void *arg)
{
pthread_mutex_lock(&mutex_b);
printf("in child thread, got mutex b, waiting for mutex a\n");
sleep(5);
++b;
pthread_mutex_lock(&mutex_a);<- 卡在这,因为主线程没有释放mutex_a,它抢先获得了锁mutex_a
b += a ++;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
pthread_t id;
pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create(&id, NULL, another, NULL);
pthread_mutex_lock(&mutex_a);
printf("in parent thread, got mutex a, waiting for mutex b\n");
sleep(5);
++a;
pthread_mutex_lock(&mutex_b); <- 卡在这,因为子线程没有释放mutex_b
a+= b++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_a);
return 0;
}
14.7 条件变量
互斥锁用于同步线程对于共享数据的访问,那么条件变量用于在线程之间同步共享数据的值.
条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程.
相关函数有:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
这里学习了一下pthread_cond_wait函数为什么要传入mutex.因为mutex用于保护条件变量的互斥锁,以确保pthread_cond_wait操作的原子性.在调用pthread_cond_wait之前,请先确保互斥锁mutex已被加锁,pthread_cond_wait函数执行的时候,首先把调用线程放入条件变量的等待队列中,然后将互斥锁解锁.可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量队列之间的这段时间,pthread_cond_wait函数不会错过目标条件变量的任何变化(pthread_cond_signal和pthread_cond_broadcast等函数不会修改条件变量).
14.8 多线程环境
-
若一个函数能被多个线程同时调用且不会发生竞态条件,则我们称它是线程安全的(thread safe),或者说是可重入函数.
-
在多线程中使用库函数,一定要使用可重入版本.
-
在Linux中,很多不可重入的库函数都提供了可重入版本(在名字后加了_r),不可重入的原因是在其内部使用了静态变量.
线程中的进程:
在一个多进程的某个线程调用了fork函数,那么新创建的子进程不会自动创建个父进程相同的线程,也不清楚从父进程那继承而来的互斥锁的具体状态.
pthread_mutex_t mutex;
//子线程运行的函数。它首先获得互斥锁mutex,然后暂停5s, 在释放该互斥锁
void *another(void *arg)
{
printf("in child thread, lock the mutex\n");
pthread_mutex_lock(&mutex);
sleep(5);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(int argc, const char *argv[])
{
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\n");
//子进程从父进程继承了互斥mutex的状态,该互斥处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,
//因此,下面这句加锁操作会一直阻塞,尽管从逻辑上来说它是不应该阻塞的
pthread_mutex_lock(&mutex);
printf("I can not run to here, oop .....\n");
pthread_mutex_unlock(&mutex);
exit(0);
}
else
{
wait(NULL);
}
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
不过pthread提供了一个专门的函数pthread_afork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态.
int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));
-
prepare:将在fork调用创建出子进程之前被执行.可以锁住所有父进程中的互斥锁.
-
parent:在fork调用创建出子进程之后,在fork返回之前,在父进程中被执行,可以用来释放所有在prepare锁住的互斥锁.
-
child:在fork返回之前,在子进程中被执行,可以用来释放所有在prepare中被锁住的互斥锁.
14.9 线程和信号
每个线程都可以独立的设置信号和掩码,但在多线程下应该使用pthread版本的sigprocmask
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
在某个线程中调用如下函数来等待信号并处理之:
int sigwait(const sigset_t *set, int *sig);
pthrad还提供了一个方法明确的将一个信号发送给指定的线程,也可以利用这种方式检测目标线程是否存在:
int pthread_kill(pthread_t thread, int sig);
关于第十四章的总结
-
复习了线程创建与同步的相关函数,可以使用信号量,互斥锁和条件变量进行同步线程.
-
复习了操作系统中的死锁.
-
子进程对于父进程的子线程的态度,不会继承,但会继承父进程的锁状态.
From
Aaron-z/linux-server-high-performance
2017/2/12 17:46:53