一、共享数据问题
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有推荐使用的线程数量,就应该根据它来。