一些函数看似其本身是线程安全的,但是当多个函数同时执行时出现了非线程安全。一个例子是多个函数在多线程下同时执行时临界区相互重叠
Requset向Inventory注册,然后Invetory存有Request对象的指针。当Request对象析构自身时向Inventory中移除指针,此时若析构停住,然后Inventory持有该Request指针做一些操作,将访问到Request这个半成品对象。且为了保证每个函数线程安全采用锁保护,最后多个函数同时执行导致死锁。
#include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<set> using namespace std; class Request; class Inventory{ public: void add(Request* req){//添加Request pthread_mutex_lock(&mutex); requests.insert(req); pthread_mutex_unlock(&mutex); } void remove(Request* req){//移除Request pthread_mutex_lock(&mutex); requests.erase(req); pthread_mutex_unlock(&mutex); } void printALL();//打印所有Request Inventory(){ pthread_mutex_init(&mutex,NULL); } private: pthread_mutex_t mutex;//保护成员 set<Request*> requests;//关于Request的容器 }; Inventory g_inventory; class Request{ public: void process(){//将自己添加到inventory pthread_mutex_lock(&mutex); g_inventory.add(this); pthread_mutex_unlock(&mutex); } ~Request() __attribute__ ((noinline)){ pthread_mutex_lock(&mutex);//###2### sleep(1);//###1###这里是为了处于析构状态中,下面的###1###出现死锁 g_inventory.remove(this);//###2### pthread_mutex_unlock(&mutex); } void print(){// __attribute__ ((noninline)){//告诉编译器此函数非内联 pthread_mutex_lock(&mutex); cout<<"Request print()"<<endl; pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; void Inventory::printALL(){ pthread_mutex_lock(&mutex); for(set<Request*>::const_iterator it=requests.begin();it!=requests.end();it++){ (*it)->print();//###1###当Request处于析构中时,这里出现死锁 } cout<<"Inventory::printALL() unlocked"<<endl; pthread_mutex_unlock(&mutex); } void* threadFun(void* arg){ Request* req=new Request; req->process(); delete req; } int main(){ pthread_t pid; if(pthread_create(&pid,NULL,threadFun,NULL)<0) cout<<"pthread_create error"<<endl; usleep(500*1000);//###1###这里休眠是为了让Request添加到inventory g_inventory.printALL(); pthread_join(pid,NULL); return 0; }
将Request::remove()移除临界区,交换###2###处的两句位置,没有解决析构竞态,当Request::remove()后处于正在析构中,刚好inventory::printALL()访问到这个半废品对象...
死锁出现的原因是无法确定对方的生命周期出现析构竞态,采用shared_ptr/weak_ptr管理对象的生命周期。
#include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<set> #include<boost/shared_ptr.hpp> #include<boost/weak_ptr.hpp> #include<boost/enable_shared_from_this.hpp> using namespace std; using namespace boost; class Request; class Inventory{ public: void add(shared_ptr<Request> a){ weak_ptr<Request> one(a); pthread_mutex_lock(&mutex); requests.insert(one); pthread_mutex_unlock(&mutex); } void remove(shared_ptr<Request> &a){ weak_ptr<Request> one(a); pthread_mutex_lock(&mutex); requests.erase(one); pthread_mutex_unlock(&mutex); } void printALL(); Inventory(){ pthread_mutex_init(&mutex,NULL); } private: pthread_mutex_t mutex; set<weak_ptr<Request> > requests;//持有所有Request对象的weak_ptr }; Inventory g_inventory; class Request:public enable_shared_from_this<Request>{ public: void process(){ pthread_mutex_lock(&mutex); g_inventory.add(shared_from_this());//shared_from_this()将this指针转换为shared_ptr指针 pthread_mutex_unlock(&mutex); } ~Request() __attribute__ ((noinline)){//告诉编译器此函数非内联 //pthread_mutex_lock(&mutex); //sleep(1); //g_inventory.remove(a); //pthread_mutex_unlock(&mutex); cout<<"~Request()"<<endl; } void print(){// __attribute__ ((noninline)){ pthread_mutex_lock(&mutex); cout<<"Request print()"<<endl; pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; void Inventory::printALL(){ shared_ptr<Request> one; pthread_mutex_lock(&mutex); for(set<weak_ptr<Request> >::iterator it=requests.begin();it!=requests.end();it++){ one=it->lock();//尝试提升weak_ptr,若提升成功则Request对象还存在,否则不存在。注意这个提升操作是线程安全的,这点至关重要 if(one){ one->print(); cout<<"lock success"<<endl; } else{ requests.erase(it); //it--;//原本以为set会像vector的erase会返回一个迭代器那么需要先自减再自增这样才完全遍历容器,可是set不是那样,可见set底层应该是类似链表这样的数据结构实现的 cout<<"lock failure"<<endl; } } cout<<"Inventory::printALL() unlocked"<<endl; pthread_mutex_unlock(&mutex); } void* threadFun(void* arg){ shared_ptr<Request> one(new Request);//###2###采用shared_ptr管理Request对象生命周期 one->process(); sleep(1);//###4###注释掉该句则:Requset对象析构了,Inventory::printALL()无打印对象 //delete req; } int main(){ pthread_t pid; if(pthread_create(&pid,NULL,threadFun,NULL)<0) cout<<"pthread_create error"<<endl; usleep(500*1000); g_inventory.printALL(); pthread_join(pid,NULL); return 0; }
Request print()
lock success
Inventory::printALL() unlocked
~Request()
###4###注释掉的输出:
~Request()
lock failure
注:这种方式存在轻微的内存泄露,当Request对象析构后,Inventory不会立即将相应的weak_ptr从容器中删除,而是到下一次遍历容器时才会删除(延迟删除)。
将print()移除printALL()的临界区,采用写时拷贝技术。一个简单的方法是:在Inventory::printALL()内用一个临时容器temp在临界区内复制requests_全部元素,然后temp在临界区外执行遍历操作。这样显然拷贝整个容器非上策。还有个方法是:采用shared_ptr管理set容器。
Inventory::add(),Inventory::remove()可以看成针对容器set的写者,而Inventory::printALL()看成是set的读者。采用生产者消费者模型,写者要操作时采用写时拷贝,读者始终在set的一个副本上读取。并将Request析构临界区内的remove操作移出临界区,采用一个函数执行该操作,这样Request对象析构前需要显式调用此函数。
#include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<assert.h> #include<set> #include<boost/shared_ptr.hpp> #include<boost/weak_ptr.hpp> #include<boost/enable_shared_from_this.hpp> using namespace std; using namespace boost; class Request; class Inventory{ public: void add(shared_ptr<Request> a){//向Inventory添加一个Request....写者 weak_ptr<Request> one(a); pthread_mutex_lock(&mutex); if(!requests.unique()){//采用写时拷贝,若requests有其它读者,则拷贝 requests.reset(new set<weak_ptr<Request> >(*requests));//强制将requests计数置为1,而读者读取的是读者局部的shared_ptr cout<<"add() copy on write"<<endl; } assert(requests.unique());//判定requests有且仅有一个写者 requests->insert(one);//添加 pthread_mutex_unlock(&mutex); } void remove(shared_ptr<Request> a){//向Inventory移除一个Request...写者 weak_ptr<Request> one(a); pthread_mutex_lock(&mutex); if(!requests.unique()){//采用写时拷贝 requests.reset(new set<weak_ptr<Request> >(*requests)); cout<<"remove() copy on write"<<endl; } assert(requests.unique()); requests->erase(one);//移除 pthread_mutex_unlock(&mutex); } void printALL(); Inventory(){ pthread_mutex_init(&mutex,NULL); requests.reset(new set<weak_ptr<Request> >); } private: pthread_mutex_t mutex; shared_ptr<set<weak_ptr<Request> > > requests;//采用shared_ptr管理set容器,set元素是管理Request的weak_ptr }; Inventory g_inventory; class Request:public enable_shared_from_this<Request>{ public: void process(){ pthread_mutex_lock(&mutex); g_inventory.add(shared_from_this());//shared_from_this()将this指针转换为shared_ptr指针 pthread_mutex_unlock(&mutex); } void cancel(){//Request对象必须显示调用此函数才能析构 pthread_mutex_lock(&mutex); g_inventory.remove(shared_from_this()); pthread_mutex_unlock(&mutex); } ~Request() __attribute__ ((noinline)){//告诉编译器此函数非内联 cout<<"~Request()"<<endl; } void print(){// __attribute__ ((noninline)){ pthread_mutex_lock(&mutex); cout<<"Request print()"<<endl; pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; void Inventory::printALL(){//打印Request对象...读者 shared_ptr<set<weak_ptr<Request> > > temp; { pthread_mutex_lock(&mutex); temp=requests;//读者采用局部变量temp保存requests副本,然后从temp中读取Request对象 pthread_mutex_unlock(&mutex); } shared_ptr<Request> one; for(set<weak_ptr<Request> >::iterator it=(*temp).begin();it!=(*temp).end();it++){ one=it->lock(); if(one){ one->print(); cout<<"lock success"<<endl; } else{//这一步没什么意义了...采用weak_ptr<Request>作为set容器元素是由于在前面一个程序基础上改的,可以用shared_ptr<Request>作为set元素,不过weak_ptr<Request>有个好处是如果忘记了显式调用Request::cancel(),那么容器也就存放些weak_ptr,不至于使Request对象滞留内存,导致内存泄露.... temp->erase(it); //it--; cout<<"lock failure"<<endl; } } cout<<"Inventory::printALL() unlocked"<<endl; } void* threadFun(void* arg){ shared_ptr<Request> one(new Request); one->process(); sleep(1);//###1### one->cancel();//Request对象析构前需显示调用此函数进行解注册 } int main(){ pthread_t pid; if(pthread_create(&pid,NULL,threadFun,NULL)<0) cout<<"pthread_create error"<<endl; usleep(500*1000); g_inventory.printALL(); pthread_join(pid,NULL); return 0; }
Request print()
lock success
Inventory::printALL() unlocked
~Request()
###1###注释掉程序输出://当读者printALL时Request对象已经析构了,set容器为空
~Request()
Inventory::printALL() unlocked