博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
IPC——线程资源保护:互斥锁、信号量、条件变量
阅读量:6648 次
发布时间:2019-06-25

本文共 14042 字,大约阅读时间需要 46 分钟。

资源保护

进程的资源保护

对于进程来说,由于每个进程空间是完全独立的,相互间不可能篡改对方进程空间的数据,所以进程空间内部的数据(资源)保护的非常到位,不需要加什么额外的保护机制。只有当它们共享操作第三方资源时才会涉及到资源保护问题,比如共享操作第三方文件(或者共享内存)的数据时,才会使用到进程信号量这样的资源保护机制。我们在讲进程IPC的时候就说过,虽然进程信号量被划到“IPC”中,但是进程信号量的作用实际上是借助通信来实现资源(数据)保护。对于进程来说,因为进程空间的独立性,因此进程资源的保护很到位,反倒是进程间共享数据很困难,因此OS提供了管道、消息队列等进程间通信机制。

线程的资源保护

对于线程来说,由于进程内部的所有线程共享进程空间,因此线程间使用全局变量即可实现数据共享,数据通信的实现非常容易,不过数据共享越是容易,数据相互篡改的危险性就越高,因此对于线程来说,需要重点考虑如何保护资源(数据),防止相互篡改。

总结

进程:进程空间天然是独立的,因此进程间资源的保护是天然的(现成的),需要重点关心的进程间的通信

线程:多线程天然的共享进程空间,因此线程数据共享是天然的(现成的),需要重点关心的是资源的保护

线程的资源保护机制

C线程的资源保护机制有:互斥锁、信号量、条件变量

互斥锁

互斥锁的作用就是用来实现互斥的。原理同进程信号量那里的互斥。

互斥锁使用的步骤

①定义一个互斥锁(变量)

②初始化互斥锁:预设互斥锁的初始值

③加锁解锁

④进程退出时销毁互斥锁

初始化互斥锁的函数

原型 

#include 
int 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,所以这个函数不需要进行出错处理。

加锁解锁函数

原型 

#include 
int 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,失败返回错误号。

销毁互斥锁函数

原型 

#include 
int 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 #include 
2 #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
View Code

 

有关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

④进程结束时,删除线程信号量集合

初始化信号量的函数

原型 

#include 
int 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操作函数

原型 

#include 
int 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被设置。

删除信号量函数

原型 

#include 
int sem_destroy(sem_t *sem); 

功能

删除某个信号量,把所有信号量都删除后,信号量集合就被销毁。这与删除进程信号量集合有所不同,对于进程信号量集合来说,只要删除一个信号量,整个集合即被删除,但是对于线程信号量来说,需要一个一个的删除,当所有信号量都删除完后,集合才被删除完毕。

参数

sem:信号量集合中某个信号量

返回值

成功返回0,失败返回-1,errno被设置。

代码演示

使用信号量实现互斥

1 #include 
2 #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
View Code

使用信号量实现同步

比如有三个线程(1主线程,2个次线程),分别打印333333、222222、111111,使用同步让他们顺序的打印111111、222222、333333。

使用进程信号量实现进程同步时,有多少个进程需要同步,集合中就需要包含几个信号量。同样的,使用线程信号量实现同步时,有几个线程需要同步,集合中就需要包含几个信号量。

1 #include 
2 #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
View Code

条件变量

线程配合工作的例子 

eg:主线程对va变量循环+1,次线程发现va==5时,打印va的值并将va清0,如果va的值!=5就什么都不做

采用最笨的实现方法:次线程循环检测va的值,然后做出相应的响应。代码如下

1 #include 
2 #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 }
View Code

这种循环检测的方法虽然简单,但是存在很大的问题,那就是当va不满足时,次线程会一直在不停的循环检测,cpu执行次线程的while时其实是在空转,白白浪费cpu的资源。

最好的方式是,当va条件不满足时就应该让次线程休眠(阻塞),等主线程将va准备好时,主动通知次线程,将它唤醒,像这样的解决方式,我们就可以使用条件变量来实现。

条件变量的作用

多线程配合工作时,当线程检测到某条件不满足时就休眠,直到别的线程将条件准备好,然后通过条件变量将其唤醒。条件变量需要在互斥锁的配合下才能工作。

条件变量的使用步骤

①定义一个条件变量(全局变量)。由于条件变量需要互斥锁的配合,所以还需要定义一个线程互斥锁。

②初始化条件变量

③使用条件变量

④删除条件变量,也需要把互斥锁删除。

初始化条件变量函数

原型 

#include 
int 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,失败返回非零错误号

等待条件变量函数

原型 

#include 
int 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);
View Code

多了第三个参数,用于设置阻塞时间,如果条件不满足时休眠(阻塞),但是不会一直休眠,当时间超时后,如果cond还没有被设置,函数不再休眠。

设置条件变量函数

原型 

#include 
int 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,失败返回非零错误号

删除条件变量函数

原型 

#include 
int pthread_cond_destroy(pthread_cond_t *cond); 

功能

删除条件变量

参数

cond:条件变量

返回值

成功返回0,失败返回非零错误号

代码演示

1 #include 
2 #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 }
View Code

 

 

 

转载于:https://www.cnblogs.com/kelamoyujuzhen/p/9439873.html

你可能感兴趣的文章