C++多线程(笔记二)

一、共享数据问题

1、多个线程只读,是安全稳定
2、多个线程同时写,或者既有读线程,也有写线程,若不加处理,就会出错

处理方法:
读的时候不能写,写的时候不能读
有两种具体实现方法:
①引入互斥量的概念,每次读写数据前都进行加锁保护,处理完数据后解锁
②引入原子操作的概念,定义某个变量为原子类型,每次进行一元运算符运算时,都能确保当前运算不被打断。

互斥锁的特点:
能对一段代码片段进行保护,操作灵活可变,但加锁解锁有一定时间开销。
原子操作特点:
仅适用于保护某个变量的一元运算符运算,但相对来讲额外的开销小,一般只适用于计数。

A. 互斥锁的用法:

1、mutex的成员函数lock(),unlock()
声明了一个mutex变量a后,可以用a.lock(),a.unlock()进行加锁解锁,加锁和解锁的次数必须相等。加锁期间能保证当前线程的操作不会被打断。

2、lock_guard

一个用类实现的,包装好了的锁
声明一个mutex变量a后,可以初始化一个类模板,例:lock_guard<mutex> obj(a);obj对象在被初始化的时候自动加锁,能在离开当前作用域后,自动析构解锁

3、unique_lock

也是一个用类实现的,包装好的锁,但相比lock_guard 功能更多更灵活,没有额外参数的情况下,效果和lock_guard相同。(unique_lock <mutex> obj(a);)

第二参数可以是:
①adopt_lock(表示互斥量已被lock,无需再次加锁,就是说在用之前这个锁一定是已经被锁了的,这个参数lock_guard也是可以用的)
②try_to_lock(尝试去锁,如果没锁成功也会返回,不会卡死在那,然后可用owns_lock()得到是否上锁的信息)

unique_lock<mutex> obj(mute,try_to_lock);
if (obj.owns_lock()) 
    {/*如果锁上了要怎么做…*/}
else 
    {/*没锁上也可以干别的事*/}

③defer_lock(用一个还没上锁的mutex变量初始化一个对象,自己可以在后续代码段中的某个位置加锁,而离开作用域时,也能帮助我们解锁,当然我们也能提前手动a.unlock()解锁)

        mutex a;
	unique_lock<mutex> obj(a,defer_lock);
	/* 一些代码 */
	a.lock();//也可结合条件判断语句使用a.try_lock() ,若成功锁上能返回true,否则返回false 

unique_lock还有一个成员函数release(),可返回他所管理的mutex对象指针,并释放所有权

一般来讲,锁住的代码越少,效率越高

使用多个互斥锁可能出现的问题:死锁

有两个线程(A和B),有两个锁(c和d),A锁了c,还想要d的锁进行下一步操作,但这时B锁了d,但是想要c进行下一步操作。于是彼此互相锁死。

死锁的一般避免方案:

1、保证两个互斥锁的上锁顺序一致
2、或用lock()这个函数模板,进行同时上锁。(只有当每个锁都是可锁的状态,才会真正一次性上锁)

例:
	mutex a;
	mutex b;
	lock(a, b);
	/*  ...  */
	a.unlock();
	b.unlock();
	
/*也可在lock(a,b)后用,以省去解锁步骤(adopt_lock参数表示,该锁已锁,不重复上锁,只在析构时,执行解锁)
	lock_guard<mutex> obj1(a,adopt_lock);
	lock_guard<mutex> obj2(b, adopt_lock);
*/

B.原子操作

保证对某个变量进行一元操作符运算的时候,能够不被打断,只需将该变量通过atomic这个类模板声明即可,效率比互斥锁高

class text_class
{
public:
	atomic<int> count;
	text_class():count(0){}
	void WriteAval()
	{
		for (int i = 0; i < 100000; ++i)
		{
			++(count);
		}
	}
};
//main函数里
	text_class B;
	thread th1(&text_class::WriteAval, ref(B));
	thread th2(&text_class::WriteAval, ref(B));
	th1.join();
	th2.join();
	cout << B.count << endl;

带其它功能的互斥锁

1、 recursive_mutex (可重复加锁的互斥量)

如果某个线程需要对同一个锁进行多次加锁,那么可以用recursive_mutex代替mutex去声明互斥量,加了多少次,还是得解锁多少次

2、timed_mutex( 带超时的互斥量 )

①用try_lock_for成员函数,参数是等待的时间,等一段时间,若成功拿到就锁上并返回true,反之返回false

timed_mutex a;
	chrono::microseconds timeout(100);
	if (a.try_lock_for(timeout))//如果在规定时间内拿到了锁
	{
		/*  一波操作 */
		a.unlock();//解锁
	}
	else
	{
		//没拿到锁
	}

②用try_lock_until成员函数,参数是时间点,要是到了这个时间点,成功拿到锁,就锁上并返回true,没拿到返回false

timed_mutex a;
	chrono::microseconds timeout(100);
	if (a.try_lock_until(chrono::steady_clock::now()+timeout))//如果在规定时间内拿到了锁
	{
		/*  一波操作 */
		a.unlock();//解锁
	}
	else
	{
		//没拿到锁
	}

条件变量condition_variable,wait(),notify_one()

A,B两个线程
A线程往下执行是需要条件的,
B线程可以提供一个这样的条件(不一定是一对一的关系,也可能B执行了一次,A就能拿去执行好几次了,也可能需要B执行好几次后,A才满足条件执行一次)
那么当A不满足条件的时候,就不应该再跑A线程(浪费资源),而应处于等待状态,让B去跑

class Data_
{
	queue<int> MsgQueue;
	mutex mute;
	condition_variable my_con;
public:
	void GetMsg()
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> obj(mute);
			my_con.wait(obj, [this]
			{
				if (MsgQueue.empty())
					return false;//如果空了的话就直接等待,并且解锁,这
                            //个线程就不要再跑了,等到被唤醒时,再重新加锁,往下执行
				return true;//如果没空,那就读出,继续该进程
		});
		cout << "读出" << MsgQueue.front() << endl;//到了这里就证明,队列没空,可读消息
		MsgQueue.pop();
		}
	}
	void SaveMsh()
	{
		for (int i = 0; i < 10000; ++i)
		{
			unique_lock<mutex> obj(mute);
			MsgQueue.push(i);
			cout << "装入" << i << endl;
			my_con.notify_one();//我已经加入元素,可以开始公平竞争(唤醒)
		}//不一定每次notify_one()时,另一个线程都在等待,也可能人家在干别的事
	}
};
//main函数里
Data_ var;
	thread obj1(&Data_::GetMsg, &var);
	thread obj2(&Data_::SaveMsh, &var);
	obj1.join();
	obj2.join()

如果允许唤醒多个线程的话,可以用notify_all()

二、其它

当我们使用单例设计模式的时候,如果我们把单例类的变量的初始化放在了线程里面(一般不推荐这样做),我们就需要确保这个初始化只会执行一次,实现的手段有以下方式:

①双重条件判断,在高效的同时,确保初始化只能被执行一次
比如说返回单类中某个指针,先判断是否该指针为空,若为空,则先加锁,再判断是否为空,如果为空就初始化该指针。如果不为空就返回此时的值。

②使用call_once(),能保证该函数只会被调用一次

        once_flag g_flag;//当然这个是放在大家都能访问到的地方
        /*  ...   */
	call_once(g_flag,函数名 )

浅谈线程池:
当线程数由请求所决定,就不能简单地根据请求而创建线程。应该要程序启动的时候就把一定数量的线程创建好,放在池子里,需要用的时候就拿一个,用完放回去,以便下次调用,这种循环利用线程的方式就是线程池
一般来讲线程的创建数量,两千就是极限,一般建议200~300个,但具体情况应该具体分析,如果要调用某些api,并且api有推荐使用的线程数量,就应该根据它来。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注