资源保护
进程的资源保护
对于进程来说,由于每个进程空间是完全独立的,相互间不可能篡改对方进程空间的数据,所以进程空间内部的数据(资源)保护的非常到位,不需要加什么额外的保护机制。只有当它们共享操作第三方资源时才会涉及到资源保护问题,比如共享操作第三方文件(或者共享内存)的数据时,才会使用到进程信号量这样的资源保护机制。我们在讲进程IPC的时候就说过,虽然进程信号量被划到“IPC”中,但是进程信号量的作用实际上是借助通信来实现资源(数据)保护。对于进程来说,因为进程空间的独立性,因此进程资源的保护很到位,反倒是进程间共享数据很困难,因此OS提供了管道、消息队列等进程间通信机制。
线程的资源保护
对于线程来说,由于进程内部的所有线程共享进程空间,因此线程间使用全局变量即可实现数据共享,数据通信的实现非常容易,不过数据共享越是容易,数据相互篡改的危险性就越高,因此对于线程来说,需要重点考虑如何保护资源(数据),防止相互篡改。
总结
进程:进程空间天然是独立的,因此进程间资源的保护是天然的(现成的),需要重点关心的进程间的通信
线程:多线程天然的共享进程空间,因此线程数据共享是天然的(现成的),需要重点关心的是资源的保护
线程的资源保护机制
C线程的资源保护机制有:互斥锁、信号量、条件变量
互斥锁
互斥锁的作用就是用来实现互斥的。原理同进程信号量那里的互斥。
互斥锁使用的步骤
①定义一个互斥锁(变量)
②初始化互斥锁:预设互斥锁的初始值
③加锁解锁
④进程退出时销毁互斥锁
初始化互斥锁的函数
原型
#includeint pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
注:restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码
功能
初始化定义的互斥锁。所谓初始化,就是设置互斥锁所需要的值。
参数
mutex:互斥锁,需要我们自己定义。
比如:pthread_mutex_t mutex;
pthread_mutex_t是一个结构体类型,所以mutex实际上是一个结构体变量。
attr:互斥锁的属性
设置NULL表示使用默认属性,除非我们想要实现一些互斥锁的特殊功能,否则默认属性就够用了。
返回值
总是返回0,所以这个函数不需要进行出错处理。
加锁解锁函数
原型
#includeint pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能
pthread_mutex_lock:阻塞加锁
如果锁没有解开时,当前线程尝试加锁时会阻塞,直到加锁成功为止。
兄弟函数:pthread_mutex_trylock(pthread_mutex_t *mutex)
非阻塞加锁,加锁成功是最好,如果不成功就错误返回,不会阻塞。
pthread_mutex_unlock:解锁,解锁不会阻塞
参数
mutex:需要加锁和解锁的互斥锁
返回值
成功返回0,失败返回错误号。
销毁互斥锁函数
原型
#includeint pthread_mutex_destroy(pthread_mutex_t *mutex);
功能
销毁互斥锁。所谓销毁,说白了就是删除互斥锁相关的数据,释放互斥锁数据所占用的各种内存资源。
参数
mutex:需要被销毁的互斥锁
返回值
成功返回0,失败返回非零错误号
再说说互斥锁
初始化互斥锁有两种方法
第1种:使用pthread_mutex_init实现
第2种:定义互斥锁时直接初始化实现
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 与 pthread_mutex_init(&mutex, NULL);的功能是一样的,都是将互斥锁设置为快锁。
怎么理解pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ?
这句话的本来面目是:struct mutex*** mutex = {**,**,**,...};
这个是典型的结构体变量的初始化,pthread_mutex_t其实就是对struct mutex*** typedef后的类型,PTHREAD_MUTEX_INITIALIZER的宏值为{**,**,**,...}。
以下写法对不对
pthread_mutex_t mutex;mutex = PTHREAD_MUTEX_INITIALIZER;
等价于
struct mutex*** mutex;mutex = {**,**,**,...};
说白了这就是在尝试给结构体变量进行整体赋值,我们讲c时说过,结构体变量是不能够整体赋值的,所以写法是错误的。如果你想给结构体变量赋值的话,只能一个一个的给结构体成员赋值来实现。其实我们调用pthread_mutex_init函数来初始化互斥锁时,这个函数设置初始值的方式,就是给mutex这个结构体变量的成员一个一个的赋值来实现的。
所以说:
调用pthread_mutex_init函数来给mutex设置初始值时,实现的本质是结构体赋值。
使用pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER方式给mutex设置初始值时,实现的本质是结构体初始化。
代码演示
结合2种初始化互斥锁的方式。代码中我两种都用了,这种没问题,互斥锁大不了重复初始化一次,不影响
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 12 13 #define SECON_PTH_NUMS 2 //次线程数量 14 #define PTHEXIT -1 15 16 17 /* 传递给线程的参数 */ 18 typedef struct pthread_arg 19 { 20 pthread_t tid;//存放线程tid 21 int pthno;//我自己定义的编号 22 int fd;//文件描述符 23 }ptharg; 24 25 26 27 struct gloable_va 28 { 29 ptharg pth_arg[SECON_PTH_NUMS];//结构体数组,每个元素会被当做参数传递给对应的次线程 30 int pth_exit_flg[SECON_PTH_NUMS];//每个元素存放对应编号线程的退出状态 31 pthread_attr_t attr;//存放线程新属性 32 pthread_mutex_t mutex;//互斥锁 33 }glbva = {.mutex = PTHREAD_MUTEX_INITIALIZER}; 34 35 void print_err(char *str, int line, int err_no) 36 { 37 printf("%d, %s:%s", line, str, strerror(err_no)); 38 exit(-1); 39 } 40 41 /* 线程退出处理函数 */ 42 void pth_exit_deal(void *arg) 43 { 44 pthread_t tid = ((ptharg *)arg)->tid; 45 46 printf("!!! pthread %lu exit\n", tid); 47 } 48 49 void *pth_fun(void *pth_arg) 50 { 51 int fd = ((ptharg *)pth_arg)->fd; 52 int pthno = ((ptharg *)pth_arg)->pthno; 53 pthread_t tid = ((ptharg *)pth_arg)->tid; 54 55 //pthread_detach(pthread_self());//线程把自己分离出去 56 57 //注册线程退出处理函数 58 pthread_cleanup_push(pth_exit_deal, pth_arg); 59 60 printf("pthno=%d, pthread_id=%lu\n", pthno, tid); 61 62 while(1) 63 { 64 pthread_mutex_lock(&glbva.mutex);//加锁 65 write(fd, "hello ", 6); 66 write(fd, "world\n", 6); 67 //检测退出状态 68 if(glbva.pth_exit_flg[pthno] == PTHEXIT) break; 69 pthread_mutex_unlock(&glbva.mutex);//解锁 70 } 71 72 73 pthread_cleanup_pop(!0); 74 return NULL; 75 pthread_exit((void *)10); 76 } 77 78 void signal_fun(int signo) 79 { 80 if(SIGALRM == signo) 81 { 82 int i = 0; 83 for(i=0; i
有关PTHREAD_MUTEX_INITIALIZER宏
实际上除了这个宏外,还有两个宏,分别是:
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
PTHREAD_MUTEX_INITIALIZER:快锁
快速互斥锁(或叫阻塞互斥锁),简称快锁。快锁的特点是:
①加锁不成功是会阻塞,如果不想阻塞必须使用pthread_mutex_trylock来加锁,而不是pthread_mutex_lock。
②对于同一把快锁来说,不能多次加锁,否者会出错
③已经解锁的快锁也不能再次解锁,否者会出错
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:检错互斥锁
使用pthread_mutex_lock加锁时,如果加锁不成功不会阻塞,会直接出错返回。加锁不成功就直接错误返回,所以才被称为“检错互斥锁”。
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:递归互斥锁。
特点:
①同一把锁可多次枷锁,每加一次锁,加锁次数就会加1
②解锁时,解锁的顺序刚好与加锁顺序相反,每解锁一次,加锁次数就会减1。
正是由于可以重复的加锁和解锁,所以才被称为递归加锁。
pthread_mutex_init(&mutex, NULL)设置是什么锁
当第二个参数为NULL时,默认设置的是快锁。如果你想通过pthread_mutex_init函数,将mutex初始化出“检错锁”和“递归锁”的话,我们必须通过第二个参数进行相应的属性设置来实现。这种实现方法比较麻烦。
如果你真想使用“检错锁”和“递归锁”,建议还是使用直接初始化的方式,这样会更方便。
pthread_mutex_t mutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
线程信号量
进程信号量与线程信号量
线程的信号量与进程的信号量几乎完全相同,只不过一个是给进程用的,另一个是给线程用的。我们使用进程信号量时,我们自己往往还需要二次封装,线程的信号量函数则不需要,直接就可以使用,所以线程的信号量使用起来更加容易,应该说使用难度非常低。
二值信号量和多值信号量
对于线程信号量来说,也分为二值信号量和多值信号量,同样的我们这里只讲二值信号量。使用二值信号量时,往往用来实现“互斥”和“同步”。如果想实现互斥的话,更多的还是使用前面讲的互斥锁来实现,因为线程互斥锁提供了更多可自供选择的功能,比如可以设置为“检错锁”、“递归锁”等。如果你只是想实现简单互斥的话,不管是使用线程互斥锁的“快锁”来实现,还是使用线程信号量来实现,最终所实现的互斥效果都是一样的。
信号量的使用步骤
①定义信号量集合
(a)用于互斥时,集合中只需要一个信号量。
(b)用于同步时,有几个线程需要同步,集合中就需要包含几个信号量
②初始化集合中的每个信号量
设置初始值,二值信号量的初始值要么是0、要么是1。
(a)如果是用于互斥,基本都是设置为1
(b)如果是用于同步,看具体情况
③p、v操作
p操作:信号量值-1
V操作:信号量值+1
④进程结束时,删除线程信号量集合
初始化信号量的函数
原型
#includeint sem_init(sem_t *sem, int pshared, unsigned int value);
功能
初始化线程信号量集合中的某个信号量,给它设置一个初始值。
参数
sem:信号量集合中的某个信号量
信号量集合需要我们自己定义,比如:sem_t sem[3]
线程信号量集合其实就是一个数组,数组每个元素就是一个信号量。
sem[0]:第一个信号量
sem[1]:第二个信号量
sem[2]:第三个信号量
sem_init(&sem[0], int pshared, unsigned int value);
线程信号量集合其实就是自定义的一个数组,而进程信号量集合则是通过semget函数创建的。我们只要把数组定义为全局变量,所有的线程即可共享使用,不像进程信号量,需要semid才能实现共享操作。
pshared:
0:给线程使用
!0:可以给进程使用
不过对于进程来说,我们更多的还是使用进程信号量,因为线程信号量用到进程上时,存在一些不稳定的情况。
value:初始化值。对于二值信号量来说,要么是1,要么是0。
返回值
成功返回0,失败返回-1,errno被设置。注意信号量的错误号不是返回的,而是设置到errno中。
PV操作函数
原型
#includeint sem_wait(sem_t *sem);//阻塞P操作int sem_post(sem_t *sem);//V操作
功能
sem_wait:
阻塞p操作集合中某个信号量,值-1。如果能够p操作成功最好,否则就阻塞直到p操作操作成功为止。
sem_wait的兄弟函数
int sem_trywait(sem_t *sem):不阻塞
如果能够p操作就p操作,如果不能p操作就出错返回,不会阻塞。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
可以设置阻塞时间,如果能够p操作就p操作,不能就阻塞,如果在设置的时间内好没有p操作成功就是出错返回,不再阻塞。
sem_post:
对某个信号量进行v操作,v操作不存在阻塞问题。v操作成功后,信号量的值会+1
参数
sem:p操作的某个信号量。比如:sem_wait(&sem[0]);
返回值
适用于2个函数,成功返回0,失败返回-1,errno被设置。
删除信号量函数
原型
#includeint sem_destroy(sem_t *sem);
功能
删除某个信号量,把所有信号量都删除后,信号量集合就被销毁。这与删除进程信号量集合有所不同,对于进程信号量集合来说,只要删除一个信号量,整个集合即被删除,但是对于线程信号量来说,需要一个一个的删除,当所有信号量都删除完后,集合才被删除完毕。
参数
sem:信号量集合中某个信号量
返回值
成功返回0,失败返回-1,errno被设置。
代码演示
使用信号量实现互斥
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 13 #define SECON_PTH_NUMS 2 //次线程数量 14 #define SEM_NUMS 1 //集合中信号量数量 15 16 /* 传递给线程的参数 */ 17 typedef struct pthread_arg 18 { 19 pthread_t tid;//存放线程tid 20 int pthno;//我自己定义的编号 21 int fd;//文件描述符 22 }ptharg; 23 24 struct gloable_va 25 { 26 ptharg pth_arg[SECON_PTH_NUMS];//结构体数组,每个元素会被当做参数传递给对应的次线程 27 sem_t sem[SEM_NUMS]; 28 29 }glbva; 30 31 void print_err(char *str, int line, int err_no) 32 { 33 printf("%d, %s:%s", line, str, strerror(err_no)); 34 exit(-1); 35 } 36 37 void *pth_fun(void *pth_arg) 38 { 39 int fd = ((ptharg *)pth_arg)->fd; 40 41 while(1) 42 { 43 sem_wait(&glbva.sem[0]); 44 write(fd, "hello ", 6); 45 write(fd, "world\n", 6); 46 sem_post(&glbva.sem[0]); 47 } 48 49 return NULL; 50 } 51 52 void signal_fun(int signo) 53 { 54 int i = 0; 55 int ret = 0; 56 57 for(i=0; i
使用信号量实现同步
比如有三个线程(1主线程,2个次线程),分别打印333333、222222、111111,使用同步让他们顺序的打印111111、222222、333333。
使用进程信号量实现进程同步时,有多少个进程需要同步,集合中就需要包含几个信号量。同样的,使用线程信号量实现同步时,有几个线程需要同步,集合中就需要包含几个信号量。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 13 #define SECON_PTH_NUMS 2 //次线程数量 14 #define SEM_NUMS (SECON_PTH_NUMS + 1) //集合中信号量数量 15 16 /* 传递给线程的参数 */ 17 typedef struct pthread_arg 18 { 19 pthread_t tid;//存放线程tid 20 int pthno;//我自己定义的编号 21 int fd;//文件描述符 22 }ptharg; 23 24 struct gloable_va 25 { 26 ptharg pth_arg[SECON_PTH_NUMS];//结构体数组,每个元素会被当做参数传递给对应的次线程 27 sem_t sem[SEM_NUMS]; 28 29 }glbva; 30 31 void print_err(char *str, int line, int err_no) 32 { 33 printf("%d, %s:%s", line, str, strerror(err_no)); 34 exit(-1); 35 } 36 37 void *pth_fun1(void *pth_arg) 38 { 39 while(1) 40 { 41 sem_wait(&glbva.sem[0]); 42 printf("111111\n"); 43 sleep(1); 44 sem_post(&glbva.sem[1]); 45 } 46 47 return NULL; 48 } 49 50 void *pth_fun2(void *pth_arg) 51 { 52 while(1) 53 { 54 sem_wait(&glbva.sem[1]); 55 printf("222222\n"); 56 sleep(1); 57 sem_post(&glbva.sem[2]); 58 } 59 60 return NULL; 61 } 62 63 void signal_fun(int signo) 64 { 65 int i = 0; 66 int ret = 0; 67 68 for(i=0; i
条件变量
线程配合工作的例子
eg:主线程对va变量循环+1,次线程发现va==5时,打印va的值并将va清0,如果va的值!=5就什么都不做
采用最笨的实现方法:次线程循环检测va的值,然后做出相应的响应。代码如下
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 12 #define SECON_PTH_NUMS 1 //次线程数量13 int va=0;14 15 void print_err(char *str, int line, int err_no)16 {17 printf("%d, %s:%s", line, str, strerror(err_no));18 exit(-1);19 }20 21 void *pth_fun(void *pth_arg)22 { 23 while(1)24 {25 if(va==5)26 {27 printf("va=%d\n",va);28 va=0;29 }30 }31 return NULL;32 }33 34 void signal_fun(int signo)35 {36 exit(0);37 }38 39 int main(void)40 {41 int i = 0;42 int ret = 0;43 pthread_t tid;44 ret = pthread_create(&tid, NULL, pth_fun, NULL);45 if(ret != 0) print_err("pthread_create fail", __LINE__, ret);46 while(1)47 {48 va=va+1;sleep(1);49 }50 return 0;51 }
这种循环检测的方法虽然简单,但是存在很大的问题,那就是当va不满足时,次线程会一直在不停的循环检测,cpu执行次线程的while时其实是在空转,白白浪费cpu的资源。
最好的方式是,当va条件不满足时就应该让次线程休眠(阻塞),等主线程将va准备好时,主动通知次线程,将它唤醒,像这样的解决方式,我们就可以使用条件变量来实现。
条件变量的作用
多线程配合工作时,当线程检测到某条件不满足时就休眠,直到别的线程将条件准备好,然后通过条件变量将其唤醒。条件变量需要在互斥锁的配合下才能工作。
条件变量的使用步骤
①定义一个条件变量(全局变量)。由于条件变量需要互斥锁的配合,所以还需要定义一个线程互斥锁。
②初始化条件变量
③使用条件变量
④删除条件变量,也需要把互斥锁删除。
初始化条件变量函数
原型
#includeint pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能
初始化条件变量,与互斥锁的初始化类似。
也可以直接初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//与互斥锁的初始化的原理是一样的
参数
cond:条件变量
attr:用于设置条件变量的属性,设置为NULL,表示使用默认属性
返回值
成功返回0,失败返回非零错误号
等待条件变量函数
原型
#includeint pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
功能
检测条件变量cond,如果cond没有被设置,表示条件还不满足,别人还没有对cond进行设置,此时pthread_cond_wait会休眠(阻塞),直到别的线程设置cond表示条件准备好后,才会被唤醒。
参数
cond:条件变量
mutex:和条件变量配合使用的互斥锁
返回值
成功返回0,失败返回非零错误号
兄弟函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond, \ pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
多了第三个参数,用于设置阻塞时间,如果条件不满足时休眠(阻塞),但是不会一直休眠,当时间超时后,如果cond还没有被设置,函数不再休眠。
设置条件变量函数
原型
#includeint pthread_cond_signal(pthread_cond_t *cond);
功能
当线程将某个数据准备好时,就可以调用该函数去设置cond,表示条件准备好了,pthread_cond_wait检测到cond被设置后就不再休眠(被唤醒),线程继续运行,使用别的线程准备好的数据来做事。当调用pthread_cond_wait函数等待条件满足的线程只有一个时,就是用pthread_cond_signal来唤醒,如果说有好多线程都调用pthread_cond_wait在等待时,使用int pthread_cond_broadcast(pthread_cond_t *cond);它可以将所有调用pthread_cond_wait而休眠的线程都唤醒。
参数
cond:条件变量
返回值
成功返回0,失败返回非零错误号
删除条件变量函数
原型
#includeint pthread_cond_destroy(pthread_cond_t *cond);
功能
删除条件变量
参数
cond:条件变量
返回值
成功返回0,失败返回非零错误号
代码演示
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 12 13 #define SECON_PTH_NUMS 1 //次线程数量14 15 int va = 0;16 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;17 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;18 19 20 void print_err(char *str, int line, int err_no)21 {22 printf("%d, %s:%s", line, str, strerror(err_no));23 exit(-1);24 }25 26 void signal_fun(int signo)27 {28 if(SIGINT == signo)29 {30 pthread_cond_destroy(&cond);31 pthread_mutex_destroy(&mutex);32 exit(0);33 }34 else if(SIGQUIT == signo)35 {36 printf("%lu\n", pthread_self());37 }38 }39 40 void *pth_fun(void *pth_arg)41 { 42 while(1)43 {44 pthread_mutex_lock(&mutex);45 //之所以将mutex传递给该函数,是因为害怕休眠后导致锁没有解开,46 //使得其他线程不能使用这个互斥锁,把mutex传递给该函数的目的47 //就是希望该函数如果检查cond没有被设置而休眠时,将Mutex解锁,48 //让其它线程能够使用这个锁49 pthread_cond_wait(&cond, &mutex);50 printf("va = %d\n", va);51 va = 0;52 pthread_mutex_unlock(&mutex);53 }54 55 return NULL;56 }57 58 59 int main(void)60 {61 int i = 0;62 int ret = 0;63 pthread_t tid;64 65 signal(SIGINT, signal_fun);66 67 //初始化条件变量 68 ret = pthread_cond_init(&cond, NULL);69 if(ret != 0) print_err("pthread_cond_init fail", __LINE__, ret);70 71 72 ret = pthread_create(&tid, NULL, pth_fun, NULL);73 if(ret != 0) print_err("pthread_create fail", __LINE__, ret);74 75 76 printf("main %lu\n", pthread_self());77 while(1)78 { 79 pthread_mutex_lock(&mutex);80 va = va + 1;81 82 if(va == 5)83 {84 pthread_cond_signal(&cond);85 } 86 pthread_mutex_unlock(&mutex);87 88 sleep(1);89 }90 91 return 0;92 }