C++多线程之互斥锁与死锁_第1页
C++多线程之互斥锁与死锁_第2页
C++多线程之互斥锁与死锁_第3页
C++多线程之互斥锁与死锁_第4页
C++多线程之互斥锁与死锁_第5页
已阅读5页,还剩8页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第C++多线程之互斥锁与死锁目录1.前言2.互斥锁2.1互斥锁的特点2.2互斥锁的使用2.3std::lock_guard3.死锁3.1死锁的含义3.2死锁的例子3.3死锁的解决方法

1.前言

比如说我们现在以一个list容器来模仿一个消息队列,当消息来临时插入list的尾部,当读取消息时就把头部的消息读出来并且删除这条消息。在代码中就以两个线程分别实现消息写入和消息读取的功能,如下:

classmsgList

private:

listintmylist;//用list模仿一个消息队列

public:

voidWriteList()//向消息队列中写入消息(以i作为消息)

for(inti=0;i100000;i++)

cout"Write:"iendl;

mylist.push_back(i);

return;

voidReadList()//从消息队列中读取并取出消息

for(inti=0;i100000;i++)

if(!mylist.empty())

cout"Read:"mylist.front()endl;

mylist.pop_front();

else

cout"MessageListisempty!"endl;

intmain()

msgListmlist;

threadpread(msgList::ReadList,mlist);//读线程

threadpwrite(msgList::WriteList,mlist);//写线程

//等待线程结束

pread.join();

pwrite.join();

return0;

}

这段程序在运行过程中,大部分时间是正常的,但是也会出现如下不稳定的情况:

为什么会出现这种情况呢?

这是因为消息队列对于读线程和写线程来说是共享的,这时就会出现两种特殊的情况:读线程的读取操作还没有结束,线程上下文就切换到了写线程中;或者写线程的写入操作还没有结束,线程上下文切换就到了读线程中,这两种情况都反映了读写冲突,从而出现了以上错误。

要想解决这个问题,最显然最直接的方法就是将读写操作分离开来,读的时候不允许写,写的时候不允许读,这样,才能实现线程安全的读和写。说形象一点,就是在进行读操作时,就对共享资源进行加锁,禁止其他线程访问,其他线程要访问就得等到读线程解锁才行,就像上厕所一样,一次只能上一个人,其他人必须得等他上完了再上。这样,就有了互斥锁的概念。

2.互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。比如说,同一个文件,可能一个线程会对其进行写操作,而另一个线程需要对这个文件进行读操作,可想而知,如果写线程还没有写结束,而此时读线程开始了,或者读线程还没有读结束而写线程开始了,那么最终的结果显然会是混乱的。为了保护共享资源,在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁(lock)和解锁(unlock)。

2.1互斥锁的特点

1.原子性:把一个互斥量锁定为一个原子操作,这意味着如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;

2.唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

3.非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

2.2互斥锁的使用

根据前面我们可以知道,互斥锁主要就是用来保护共享资源的,在C++11中,互斥锁封装在mutex类中,通过调用类成员函数lock()和unlock()来实现加锁和解锁。值得注意的是,加锁和解锁,必须成对使用,这也是比较好理解的。除此之外,互斥量的使用时机,就以开篇程序为例,我们要保护的共享资源当然就是消息队列list了,那么互斥锁应该加在哪里呢?

可能想的比较简单一点:就直接把锁加在函数最前面不就好了么?如下所示:

classmsgList

private:

listintmylist;//用list模仿一个消息队列

mutexmtx;//创建互斥锁对象

public:

voidWriteList()//向消息队列中写入消息(以i作为消息)

mtx.lock();

for(inti=0;i100000;i++)

cout"Write:"iendl;

mylist.push_back(i);

mtx.unlock();

return;

//.......

};

不过如果这样加锁的话,要等写线程完全执行结束才能开始读线程,读写线程变成了串行执行,这就违背了线程并发性的特点了。正确的加锁方式应当是在执行写操作的具体部分加锁,如下所示:

classmsgList

private:

listintmylist;//用list模仿一个消息队列

mutexmtx;//创建互斥锁对象

public:

voidWriteList()//向消息队列中写入消息(以i作为消息)

for(inti=0;i100000;i++)

mtx.lock();

cout"Write:"iendl;

mylist.push_back(i);

mtx.unlock();

return;

//.......

};

这样,才能真正的实现读写互不干扰。

下面再举一个更为直观的例子,创建两个线程同时对list进行写操作:

classmsgList

private:

listintmylist;

mutexm;

inti=0;

public:

voidWriteList()

while(i1000)

mylist.push_back(i++);

return;

voidshowList()

for(autop=mylist.begin();p!=mylist.end();p++)

cout(*p)"";

coutendl;

cout"sizeoflist:"mylist.size()endl;

return;

intmain()

msgListmlist;

threadpwrite0(msgList::WriteList,mlist);

threadpwrite1(msgList::WriteList,mlist);

pwrite0.join();

pwrite1.join();

cout"threadsend!"endl;

mlist.showList();//子线程结束后主线程打印list

return0;

}

这里用两个线程来写list,并且最终在主线程中调用了showList()来输出list的size和所有元素,我们先来看下输出情况:

根据结果可以看到,这里有很多问题:实际输出的元素个数和size不符,输出的元素也并不是连续的,这都是多个线程同时更新list所造成的情况。这种情况下,运行结果是无法预料的,每次都可能不一样。这就是线程不安全所引发的问题,我们加上锁再来看看:

classmsgList

private:

listintmylist;

mutexm;

inti=0;

public:

voidWriteList()

while(i1000)

m.lock();//加锁

mylist.push_back(i++);

m.unlock();//解锁

return;

//......

};

这样加锁就正确了吗?我们再多运行几次看看:

数字都是连续的,但是个数却多了一个(出现的几率还是比较小),这又是什么原因造成的呢?还是两个线程的问题,假设要插入1000个数,循环条件就是while(i1000),当i=999的时候两个写线程都可以进入while循环,此时如果pwrite0线程拿到了lock(),那么pwrite1线程就只能一直等待,pwrite0线程继续往下执行,使得i变成了1000,此时,对于pwrite0线程来说,它就必须退出循环了。而此时的pwrite1在哪里呢?还等在lock()的地方,pwrite0线程unlock()后,pwrite1成功lock(),此时i=1000,但是pwrite1却还没有执行完此次循环,因此向list中插入1000,此时退出的i的值为1001,这也就造成了实际输出为1001个数的情况。

为了避免这个问题,一个简单的办法就是在lock()之后再加上一个判断,判断i是否依旧满足while的条件,如下:

voidWriteList()

while(i10000)

m.lock();

if(i=10000)

m.unlock();//退出之前必须先解锁

break;

mylist.push_back(i++);

m.unlock();

return;

}

为什么这里要在break前面加一个unlock()呢?原因就在于:如果break前面没有unlock(),一旦i符合了if的条件,就直接break了,此时就没法unlock(),程序就会报错:

可以发现,这种错误是比较难发现的,特别是像这样程序中出现了分支的情况,很容易就使得程序实际运行时lock()了却没有unclock()。为了解决这一问题,就有了std::lock_guard。

2.3std::lock_guard

简单来理解的话,lock_guard就是一个类,它会在其构造函数中加锁,而在析构函数中解锁,也就是说,只要创建一个lock_guard的对象,就相当于lock()了,而该对象析构时,就自动调用unlock()了。

就以上述程序为例,直接改写为:

voidWriteList()

while(i10000)

lock_guardmutexguard(m);//创建lock_guard的类对象guard,用互斥量m来构造

//m.lock();

if(i=10000)

//m.unlock();//由于有了guard,这里就无需unlock()了

break;

mylist.push_back(i++);

//m.unlock();

return;

}

这里主要有两个需要注意的地方:第一、原先的lock()和unlock()都不用了;第二、if中的break前面也不用再调用unlock()了。这都是因为对象guard在lock_guard一句处构造出来,同时就调用了lock(),当退出while时,guard析构,析构时就调用了unlock()。(局部对象的生命周期就是创建该对象时离其最近的大括号的范围{})

3.死锁

3.1死锁的含义

死锁是什么意思呢?举个例子,我和你手里都拽着对方家门的钥匙,我说:“你不把我的锁还来,我就不把你的锁给你!”,你一听不乐意了,也说:“你不把我的锁还来,我也不把你的锁给你!”就这样,我们两个人互相拿着对方的锁又等着对方先把锁拿来,然后就只能一直等着等着等着......最终谁也拿不到自己的锁,这就是死锁。

显然,死锁是发生在至少两个锁之间的,也就是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行,当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

3.2死锁的例子

mutexm0,m1;

inti=0;

voidfun0()

while(i100)

lock_guardmutexg0(m0);//线程0加锁0

lock_guardmutexg1(m1);//线程0加锁1

cout"thread0running..."endl;

return;

voidfun1()

while(i100)

lock_guardmutexg1(m1);//线程1加锁1

lock_guardmutexg0(m0);//线程1加锁0

cout"thread1running..."iendl;

return;

intmain()

threadp0(fun0);

threadp1(fun1);

p0.join();

p1.join();

return0;

}

我们来看下运行结果:

这就出现了死锁。产生的原因就是因为在线程0中,先加锁0,再加锁1;在线程1中,先加锁1,再加锁0;如果两个线程之一能够完整执行的话,那自然是没有问题的,但是如果某个时刻,线程0中刚加锁0,就上下文切换到线程1,此时线程1就加锁1,然后此时两个线程都想向下执行的话,线程1就必须等待线程0解锁0,线程0就必须等待线程1解锁1,就这样两个线程都一直阻塞着,形成了死锁。

3.3死锁的解决方法

①按顺序加锁

以上述例程来说,就是线程0和线程1的加锁顺序保持一致,如下所示:

mutexm0,m1;

inti=0;

voidfun0()

while(i100)

lock_guardmutexg0(m0);//线程0加锁0

lock_guardmutexg1(m1);//线程0加锁1

cout"thread0running..."endl;

return;

voidfun1()

while(i100)

lock_guardmutexg0(m0);//线程1加锁0

lock_guardmutexg1(m1);//线程1加锁1

cout"thread1running..."iendl;

return;

intmain()

threadp0(fun0);

threadp1(fun1);

p0.join();

p1.join();

return0;

}

在这种情况下,两个线

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论