C++11 多线程(中)

发布时间:2023年12月25日

六.死锁问题std::lock(),adopt_lock

????????接着上面的继续讨论死锁,死锁从根本上多个进程争夺资源引起的循环等待问题,是由程序的执行顺序和资源的分配所导致的。修改上面的类来进行测试,需要增加一个互斥量;只有两个或两个以上的互斥量才会造成死锁问题,对这两个互斥量进行不同顺序的锁定就会发生死锁。修改后的代码如下,新增一个互斥量,在in函数中先对mymutex2进行加锁,再对mymutex1进行加锁,在out函数中,加锁的方式相反,对于解锁的顺序不会有影响,谁先解锁都行

class Test
{
private:
	list<int>l1;
	mutex mymutex1;
	mutex mymutex2;
public:
	void in()//往容器里写入数据
{
	for (int i = 0; i < 100000; i++)
	{
		cout << "in执行,插入数据 " << i << endl;
		mymutex2.lock();
		mymutex1.lock();//加锁
		l1.push_back(i);
		mymutex1.unlock();//解锁
		mymutex2.unlock();
	}
}
	bool isGet(int &command)
	{
		mymutex1.lock();
		mymutex2.lock();
		if (!l1.empty())
		{
			 command = l1.front();
			l1.pop_front();
			//cout << "取出一个元素 " << command << endl;
			mymutex1.unlock();
			mymutex2.unlock();
			return true;
		}
		mymutex1.unlock();
		mymutex2.unlock();
		return false;
	}
	void out()//从容器里面读出数据
	{
		int command = 0;
		for(int i=0;i<100000;i++)
		{
			bool result = isGet(command);
			if (result)
			{
				cout << "out执行了,取出一个元素" << command << endl;
			}
			else
			{
				cout << "l1为空" <<i<< endl;
			
			}
		}
	}
};

int main()
{
	Test t;
	thread mythread2(&Test::out, &(t));
	thread mythread1(&Test::in, &(t));

	mythread1.join();
	mythread2.join();
	
	return 0;

}

运行结果如下:

????????程序会卡死在这

????????下面看看死锁的解决方式:

????????死锁的一般解决方式就是直接改变互斥量上锁的顺序,一致的上锁顺序就不会出现死锁,对于上面的死锁问题,只要in和out函数同时对mymutex1或者mymutex2加锁就可以解决这里的死锁问题。

????????对于这种同时对两个互斥量或者两个以上的互斥量进行上锁,我们可以采std::lock(),这里的lock不是mutex的成员函数,是一个单独的函数,这一点要区分,它可以同时对多个互斥量进行加锁,比较方便使用,对上面的代码进行修改,用std::lock(),来替换两个单独的lock。

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "in执行,插入数据 " << i << endl;
			lock(mymutex1, mymutex2);
			l1.push_back(i);
			mymutex1.unlock();//解锁
			mymutex2.unlock();
		}
	}

????????这里只对in函数进行修改,其他的都不进行改动,程序可以正常运行

????????lock()内的两个互斥量,对顺序没有要求。写到这里可能就会有人觉得,lock可以被替换掉,那么unlock能不能被替换呢,答案是肯定的。之前我们介绍过lock_guard<>(),这里就来进一步介绍一个lock_guard<>()的第二个参数 adapt_lock,adopt_lock 就是一种标记,表示这个互斥量已经被加锁,在结合lock_guard<>()的特性,lock_guard<>()就只用对互斥量进行解锁,也就是说使用adopt_lock 必须先对互斥量进行加锁。下面我们对in函数进行简单修改

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "in执行,插入数据 " << i << endl;
			lock(mymutex1, mymutex2);
			lock_guard<mutex> guard1(mymutex1, adopt_lock);
			lock_guard<mutex> guard2(mymutex2, adopt_lock);
			l1.push_back(i);
			
		}
	}

这样程序也能够安全运行:

七.unique_guard<>()

????????unique_guard<>()可以取代lock_gaurd<>(),unique_guard<>()可能会占用更多的内存,但是unique_guard<>()的使用会比lock_guard<>()更灵活,上面我们说到过lock_guard<>()的第二个参数adopt_lock,当然unique_guard<>()也支持这个参数,和lock_guard<>()的作用是一样的,下面我们就来看看unique_guard<>()的不同的第二个参数

(1)try_to_lock

????????try_to_lock表示尝试着去加锁,因此在使用之前不能对互斥量加锁,否则就会发生错误,我们可以通过unique_guard<>()的成员函数owns_lock()来判断是否拿到锁,下面看测试

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			
			unique_lock<mutex> guard1(mymutex1,try_to_lock);
			if (guard1.owns_lock())//检查是否拿到锁
			{
				cout << "in执行,插入数据 " << i << endl;
                l1.push_back(i);
			}
			else
			{
				cout << "in函数执行,没拿到锁" << endl;
			}
		}
	}

????????修改in函数,如果成功拿到了锁,owns_lock()会返回true,否则返回false

? ? ? ?我们可以看出,大多数情况还是能拿到锁的,只有少数情况没有拿到锁,在没拿到锁的情况下,程序也不会阻塞

(2)defer_lock

????????defer_lock是始化一个没有被加锁的互斥量,使用defer_lock之后还能引出unique_lock<>()的几个成员函数,这几个成员函数就能很好的体现其灵活性,注意在使用unique_lock<>()之前不能对互斥量进行加锁。下面来了解一下其成员函数

<1>lock()

? ? ? ? 这个lock就和mutex的成员函数很像了,可以说就是一样的,只有在defer_lock的前提下才能使用该lock()

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			unique_lock<mutex> guard1(mymutex1,defer_lock);
			guard1.lock();//调用lock()
			cout << "in执行,插入数据 " << i << endl;
			l1.push_back(i);
			
		}
	}

这里可以没有用unlock,因为unique_lock<>()自身是可以解锁的

<2>unlock()

? ? ? ? 这个的使用就是在程序中我们可能需要临时对互斥量先进行解锁,执行一段非共享的代码,然后再对互斥量进行加锁,继续执行共享代码,大概就是下面的这种用法:

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			unique_lock<mutex> guard1(mymutex1,defer_lock);
			guard1.lock();//调用lock()
			//执行一段共享代码

			guard1.unlock();
			//执行非共享代码

			guard1.lock();
			cout << "in执行,插入数据 " << i << endl;
			l1.push_back(i);
			guard1.unlock();//此处的unlock可以省略
		}
	}
<3>try_lock()

? ? ? ? try_lock()的用法和上面的unique_lock<>()的第二个参数try_to_lcok基本都一样,试着去拿锁,拿到了返回true,没拿到返回false,如果没拿到,程序也不会阻塞,它会去干点别的

????????在这打一个断点观察发现,try_lock()有时候也会有拿不到锁的情况

<4>release()

? ? ? ? 该成员函数的作用是将互斥量和unique_guard<>()解除绑定,函数自身返回mutex型指针,解绑了之后就需要自己进行解锁,否则程序就会出错。

	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			unique_lock<mutex> guard1(mymutex1, defer_lock);
			guard1.lock();
			mutex *ptr=guard1.release();//解除互斥量和unique_lock()的绑定
			cout << "in执行,插入数据 " << i << endl;
			l1.push_back(i);
			ptr->unlock();//前面已经解除绑定,此处需要自己解锁

		}
	}

我们打断点观察guard1和ptr的值的变化 ,记住guard1的初始值,然后我们接着往下运行

????????我们发现guard1已经空了,ptr中的值变成了guard1的值,也就是说已经成功解除绑定,后面就需要自己手动解锁了,因此后面调用了ptr->unlcok()。

? ? ? ? 讨论完unique_lock<>()的参数和成员函数之后,让我们继续来了解一下unique_lock<>()的所有权转移,所有权就是unique_lock<>()是和mutex绑定在一起的,我们不能对其所有权进行赋值,只能转移其所有权:

????????对所有权进行复制会报错,这个就和独占的智能指针非常类似,只能转移所有权,不能复制所有权。下面看看如果进行所有权的转移:

????????这里用到的第一种方式就是移动语义move,打个断点观察发现,guard1成功的转移到guard2中了,下面看看第二种方式通过成员函数来返回:

	unique_lock<mutex> re_move()
	{
		unique_lock<mutex> guard(mymutex1);
		return guard;
	}
	void in()//往容器里写入数据
	{
		for (int i = 0; i < 100000; i++)
		{
			unique_lock<mutex> gaurd1 = re_move();
			cout << "in执行,插入数据 " << i << endl;
			l1.push_back(i);

		}
	}

????????我们在test的成员函数中加入这个函数,直接返回一个临时对象,这里能返回成功是因为返回的这个临时对象会触发unique_lock<>()的移动构造函数。

八.单例设计模式和call_once()

? ? ? ? 这里涉及到设计模式相关的内容,设计模式是一种很高效的代码,对于第一次见会感觉它很难理解。下面就让我们来了解一下单例设计模式:单例设计模式就是单例类,我们对比一下一个普通的类,可以创建任意多个对象,但是在实际开发中,我们有时候并不需要创建那么多的对象,甚至创建一个对象就够了,这时候就需要我们的单例类出现了,单例类就是只能创建一个类对象,下面我们来了解一下如何来写一个单例类:

class CAS
{
private:
	CAS() {};//构造函数私有化
private:
	static CAS* my_cas;
public:
	static CAS* Get_cas()
	{
		if (my_cas == nullptr)
		{
			my_cas = new CAS();
		}
		return my_cas;
	}
	void func()
	{
		cout << "单例类" << endl;
	}
};
CAS* CAS::my_cas = nullptr;//静态成员变量初始化

????????我们定义一个静态成员指针,在类外将其初始化,并将其设置为私有化成员;我们把它的构造函数私有化,把创建类对象的方式封装在一个静态成员函数中,这就就可以通过调用这个成员函数来创建类对象,这个成员函数的这种写法只能产生一个类对象,下面看测试:

????????我们在函数体上创建了两个类对象指针,打个断点观察一下,发现这两个指针实际上是指向同一块内存的,所以我们还是只创建了一个类指针。

????????我们通过类名是无法创建类对象的,只有调用那个函数接口才行。注意这里的静态成员变量,需要类内定义,类外初始化,还有那个静态成员函数在调用时,需要在前面加上类名。对于上面的这个单例类,其实它是不完美的,因为在类中有指针,我们并没有手动释放,这样会造成内存泄漏,下面对该单例类进行修改:

class CAS
{
private:
	CAS() {};//构造函数私有化
private:
	static CAS* my_cas;
public:
	static CAS* Get_cas()
	{
		if (my_cas == nullptr)
		{
			my_cas = new CAS();
			static Recycle a;
		}
		return my_cas;
	}
	class Recycle
	{
	public:
		~Recycle()
		{
			if (CAS::my_cas)
			{
				cout << "析构" << endl;
				delete CAS::my_cas;
				CAS::my_cas = nullptr;
			}
		}
	};
	void func()
	{
		cout << "单例类" << endl;
	}
};

????????通过在CAS类中再定义一个类来专门析构CAS类中的指针,(这个写法我也第一次见,确实牛),在创建CAS成员指针的时候顺便创建一个回收类的静态变量,这样就能够保证最后一定会被析构。

????????打个断点观察一下,这里析构的指针和创建的是一样的

????????这样来说这个单例类的析构函数已经完成了。

????????下面来了解一下单例类在实际应用中会出现的问题,如果我们同时用Get_cas()函数创建多个线程,当这些线程在运行时,有一种情况就是多个线程同时都进入if(my_cas==nullptr)这个判断中,因为是同时进入这里的,因此没有线程已经创建了CAS指针,那么导致的结果就是多个线程都会创建一个CAS指针对象,这与我们单例类的初衷相违背。下面看看如何解决这个问题。

????????我们之前已经了解了unique_guard<>(),当然这里放一个unique_guard<>()是可以的,但是这不是一种高效的写法,因为当每一个线程执行到Get_cas()函数时,都会加锁,这样一来会加无数次锁,拉低效率,下面的就是这种写法

????????下面我们来看看另一个写法,我们在这个这个加锁上面在加一个条件判断,判断一下my_cas 是否为空,然后再加锁,分析一下这个写法,发现只有在多个线程第一次创建CAS指针对象的时候才会进行加锁,因为此时my_cas初始化为空,也就是说只会加锁这一次,这样就可以大大提高程序的效率。

static CAS* Get_cas()
{
	if (my_cas == nullptr)
	{
		unique_lock<mutex> guard(mymutex);
		if (my_cas == nullptr)
		{
			my_cas = new CAS();
			static Recycle a;
		}
	}
	return my_cas;
}

?????????这种用两个if 来判断的叫做双重锁定或者是双重检查,是一种高效的写法。其实还有另外 一种方式也能用来解决这个问题,就是call_once():

? ? ? ? call_once()能够让函数只执行一次,其第一个参数传入的是一个once_flag 标记,来标记函数是否已经被访问过,第二个参数是你要求只能执行一次的函数,当两个线程都执行到call_once()时,只会有一个线程能够继续执行,而另一个线程则等待,然后通过flag 来判断是否能够执行这个函数。下面对Get_cas函数进行修改:

????????将创建CAS指针对象单独拿出来作为一个新的函数create_cas,在原来的函数中只是返回CAS指针和调用call_once()函数

	static void create_cas()
	{
		my_cas = new CAS();
		cout << "create_cas执行了" << endl;
		static Recycle a;
	}
	static CAS* Get_cas()
	{
		
		call_once(flag, create_cas);
		cout << "Get_cas函数执行了" << endl;
		return my_cas;
	}

????????我们定义一个全局的互斥量和一个once_flag 标记;将标记和create_cas 函数传入call_once()。下面创建两个线程来验证create_cas()函数是否只是被执行了一次:

void mythread()
{
	cout << "我创建的线程开始执行了" << endl;
	CAS* cas = CAS::Get_cas();
	cout << "我创建的线程执行完毕" << endl;
}
int main()
{
	thread mythread1(mythread);
	thread mythread2(mythread);
	mythread1.join();
	mythread2.join();
	return 0;
}

? ? ? ? 如果cout << "create_cas执行了" << endl;该行语句只执行一次说明create_cas()函数只被执行了一次。

????????以上就是call_once()的写法,其实对于解决这个问题,更建议上面呢双重检查法,毕竟它只需要锁一次,而call_once()函数每次都会对其进行判断,感觉效率会低于双重检查那种写法。

文章来源:https://blog.csdn.net/qq_66157971/article/details/132644409
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。