Linux环境下编程(二)——线程的同步

来源:互联网 发布:java 启动main方法 编辑:程序博客网 时间:2024/05/29 07:25


上一节讲了基本的线程的创建、使用的方法,但是假如我们想要编写一个多线程程序还是有些问题需要处理。

既然提到了线程可以共享部分资源,那么在多个线程同时修改一段相同的内存空间时,会不会出现问题?就比如说,公司有两个boss,他们都可以让我办事,在同一个时间段内,a Boss让我一起开个讨论会,b Boss让我去楼下给他带个外卖。。。 这时,我到底是该买外卖呢,还是开会呢?在同一时间内,我只能做一件事情,如果a boss先让我去开会了,在开会结束前我肯定不能突然跑去给b boss带外卖。如何处理这个冲突,这就涉及到线程同步的问题了。


线程同步有几种方式:

1、互斥锁(互斥量)

可以通过使用pthread的互斥接口来实现线程的同步,互斥量(mutex)本质上是一把锁,可以对互斥量进行加锁,线程访问公共资源前之前对互斥量进行加锁,如果加锁成功则开始访问数据,当访问结束后,由该线程进行解锁,在解锁之后其它的线程才可以访问该资源。

注意:a. 不能锁上加锁,就是同一线程对被它自己锁住的互斥量再加锁,这样会造成死锁。

            b.各个线程需要按照一定顺序对互斥量进行加锁,这样可以避免死锁。

死锁的概念:

是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

对于第一次接触死锁的人来说这个概念很模糊,所以我来打一个比方,这样就很好理解了。

俗话说的好,世上有七颗龙珠1、2、3、4、5 、6、7 (公共资源),而集齐七颗龙珠便可以召唤神龙,于是孙悟空(线程a)集齐了1、2、3 三颗,大魔王(线程b)集齐了4、5、6、7四颗,这个时候孙悟空需要大魔王手上的四颗龙珠才能集齐七颗召唤神龙,而大魔王需要孙悟空手上的三颗,所以他们谁也不会把龙珠给对方,于是谁都没办法集齐七颗龙珠,他们大眼瞪小眼互相看了一辈子,永远都没有召唤出神龙(死锁)。

那么说说死锁产生的四个必要条件。

1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。(类似于孙悟空持有了1 2 3龙珠,那么大魔王此刻就不能用那三颗,同一颗龙珠,只能一个人用,用完了才释放)

2、请求和保持等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。(类似于孙悟空已经有了1 2 3 龙珠,但是它还需要剩下的4 5 6 7 颗龙族,并且在得到其它龙珠之前他是不会把已有的3颗龙珠释放掉的)

3、不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。(类似于不能召唤神龙还有一个原因就是孙悟空干不掉大魔王,假使他把其它龙珠抢来了,然后召唤出了神龙,那么七颗龙珠就会释放,重新被别人获得)

4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。(类似于。。。好吧,就不打比方了)

互斥锁接口:

初始化:

在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化,对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init。对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.

<pre name="code" class="cpp">#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁、解锁

#include<pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex)  锁住互斥量mutexint pthread_mutex_trylock(pthread_mutex_t *mutex) 试图锁住互斥量mutex,如果锁不住则返回ebusyint pthread_mutex_unlock(pthread_mutex_t *mutex) 解锁若成功返回0,否则返回错误编号

实例说明

#include<pthread.h>#include<stdio.h>#include<stdlib.h>int share = 1000;pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;typedef void* (func)(void *);void error_msg(int ret, char *msg){    printf("%s: %d", msg, strerror(ret));}void Pthread_create(pthread_t* tid, pthread_attr_t* attr,func* f, void* arg){    int ret;    if ( (ret = pthread_create(tid, attr,f , NULL )) != 0){        error_msg(ret, "create pthread faild");        exit(0);    }}void Pthread_join(pthread_t tid, void **thread_return){    int ret;    if ( (ret = pthread_join(tid, thread_return)) != 0){        error_msg(ret, "join faild");        exit(0);    }}void* pthread_instance(void* args){    int i = 0;//    pthread_mutex_lock(&mu);   <span style="color:#FF0000;"> for( i = 0; i < 1000; i++)</span>    {        if ( 1001 == share )            sleep(1);        ++share;    }//    pthread_mutex_unlock(&mu);}void* pthread_instance2(void* args){    int i = 0;    while ( 1000 == share );//    pthread_mutex_lock(&mu);    <span style="color:#FF0000;">if ( 2000 == share )</span>        share -= 100;//    pthread_mutex_unlock(&mu);}int main(void){    pthread_t tid1, tid2;    Pthread_create(&tid1, NULL, pthread_instance, NULL);    Pthread_create(&tid2, NULL, pthread_instance2, NULL);    Pthread_join(tid1, NULL);    Pthread_join(tid2, NULL);    printf("the share is %d\n", share);}

全局变量share是在线程之间共享的,第一次不加锁。有两个线程,线程1的红色语句(49行。。。)在线程2的红色语句(36行。。。)前执行(通过线程2红色语句前的while循环来保证),执行了++share后线程1开始休眠一秒,这个时候线程2开始执行红色语句,结果判断失败,线程2结束,线程1继续执行剩余语句,线程2结束,主线程打印结果。

在没有加锁的情况下,线程1和线程2同时对share进行操作。

运行结果:



现在把注释去掉,将线程1中的++share循环部分用互斥量mu锁住,然后在线程2中将 if( 2000 == share ){...}这段代码也用同样的互斥量mu锁住。这样就会造成一个效果:在线程1中for循环结束前,线程2会一直等待线程1解锁才会进行接下来的操作,他们不会同时对share变量进行运算。

运行结果:



2、信号量

信号量提供了一种很方便的方法来确保对共享变量的互斥访问。基本思想史将每个共享变量或者一组相关的共享变量)与一个信号量s(初始1的非负整数全局变量)联系起来,然后用p(s)和v(s)操作将相应的临界区包围起来。

P(s):如果s不为0,那么s减1,并且立即返回(执行接下来的操作)。如果s为0,那么就挂起这个线程(不改变s的值)。

V(s):将s+1。 如果最开始s为0,那么s+1后v操作会重启等待s变为非0的线程之一,让它进行p操作。

如何理解?

既然上面先学习了互斥量,那么信号量理解起来也变得简单了很多,p操作和v操作和lock和unlock一样都是成对出现,保护他们之间的资源。区别在哪里呢?首先,p操作和v操作可以出现在不同的线程之中(也可以出现在同一线程中,就是这么任性),而lock和unlock是同一线程锁和解锁;其次,信号量可以允许多个线程同时访问同一资源,但是限制了访问上线。

嗯,说说接口吧

#include<semaphore.h>int sem_init(sem_t *sem, 0, unsigned, int value);int sem_wait(sem_t *s);int sem_post(sem_t *s);若成功返回0,若出错返回-1

sem_init 将信号量sem初始化为value。sem_wait和sem_post分别对应p操作和v操作。

典型的pv操作实例:

公交车——售票员

定义两个信号量  driver(初始值0) conductor(初始值0)

如何理解?

p v 操作其实也可以是线程间通信的一种:

p操作简单理解就是请求访问权限:司机能不能开车就要通过p(driver)来询问售票员线程,如果dirver为0,那么司机就被挂起,等待售票员关门;售票员能不能开门就要通过p(conductor)来询问司机线程,如果conductor为0,则售票员不能开门,等待司机停车。

v操作简单理解就是赋予权限: 售票员关了门之后v(driver)告诉司机,你可以开车了;司机停车之后v(conductor)告诉售票员,你可以开门了。

代码实现。。。。

自己来吧,理解了上面的思想,依葫芦画瓢,把上面的公交车实例的两个线程实现以下应该没什么问题,大半夜的就不做重复劳动了。

其它实例

生产者、消费者问题:

生产者线程  <-------------------> 仓库 <---------------------->消费者线程

简单来讲:1     我们用 size变量来定义仓库大小,假如size容量为10,但是仓库的容量变换需要用互斥锁进行锁住,上面互斥量里讲过(也可以在同一线程里把lock和unlock换成p(lock),v(lock),实际换汤不换药)。一个仓库放一个物品。

                    2    生产者p(剩余空间)来询问仓库是否有剩余空间? 若有,则p成功,剩余空间-1,v(剩余物品),剩余物品+1;若没有,等待消费者线程。

                    3    消费者p(剩余物品)来询问仓库是否有剩余物品?若有,则p成功,剩余物品-1,v(剩余空间),剩余空间+1;若没有,等待生产者线程。

至于其它简单模型,一通百通。


线程安全

一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确结果。

几类线程不安全的函数:

1、不保护共享变量(上面所讲,不给共享变量加锁)

2、保持跨越多个调用的状态的函数。(通俗点将,就是这个函数里面使用了全局变量,在单线程程序里面每次调用它的结果都依赖于前一次调用的结果(全局变量在调用后会改变并且保存下来),所以这种函数应用到多线程里就更不可预见了)

3、返回制造向静态变量的指针的函数。例如gethostbyname将计算结果放在static变量中,然后返回一个指向这个变量的指针。多线程中,这个static结果会被另外一个线程悄然改变

4、调用以上三种函数的函数。

0 0
原创粉丝点击