接着上次的的测试函数继续往下看,我们仔细分析一下 isGet这个函数,就可以发现它的unique_lock<>()执行次数非常多,每次进入到 isGet 这个函数就会加锁,这样会拉低程序的运行效率,下面提供两个解决方案:
(1)双重锁定(双重检查)
上一次我们在写单例类的时候已经说到过这个问题,双重检查会大大减少锁定的次数,提高程序的效率。
bool isGet(int &command)
{
if (!l1.empty())//双重检查
{
unique_lock guard(mymutex);
if (!l1.empty())
{
command = l1.front();
l1.pop_front();
return true;
}
}
return false;
}
先对 l1 进行判空操作,如果l1是空,则就不会加锁,这样就会大大减少加锁的次数从而提高程序效率 。
(2)使用条件变量condition_varibale
condition_varibale 是一个类,使用它的wait()和notify_one()成员函数可以解决上述问题,wait()的第一个参数要与互斥量绑定,第二个参数是一个可调用对象 ,我们更多用lambda表示式来实现,当第二个参数返回false时,wait()是将拿到的锁释放掉,并将该线程卡在当前行,使其进入阻塞状态,只能在另一个线程中使用notify_one()才能将其唤醒;当第二个参数返回true的时,wait()直接返回,程序继续向下执行;当没有第二个参数时,效果和第二个参数返回false一样,等待另一个线程使用notify_one()将其唤醒。下面看我们是如何对int 和out这两个函数进行修改。
void out()
{
int command = 0;
while (1)
{
unique_lock guard(mymutex);
my_cond.wait(guard, [this]() {//lambda 表达式,this表示要调用类的对象(l1)
if (l1.empty())return false;
return true;
});//lambda表达式
command = l1.front();
cout << "out函数执行了,取出一个元素" << command << endl;
l1.pop_front();
guard.unlock();
}
}
对于out函数,我们修改后的代码如上,我们使用了wait(),并且我们用lambda表达式作为其第二个参数,其中的this表示要调用本类中的成员,此处就是要对l1进行判空,如果l1为空,那么我们就会将这个线程阻塞在wait()这行,out线程中的锁也因此会被释放,然后等待in线程中的notify_one()函数来将其唤醒。这里最后有一个unlock(),就是当线程完成它要完成互斥事务后,就可以对其进行提前解锁,来提高程序的效率,这也是unique_lock<>()的灵活性的体现,可以随时加锁解锁。
void in()
{
for (int i = 0; i < 100000; i++)
{
my_cond.notify_one();
unique_lock guard(mymutex);
cout << "in 函数执行了,插入一个元素" << i << endl;
l1.push_back(i);
}
}
对于in函数,我们只需要加入一个notify_one()即可,专门用来唤醒out里面的wait().
wait()函数会一直试着去拿锁,如果wait()函数拿到锁,如果其有第二个参数,并且第二个参数返回true,那么流程就会继续往下执行,此时在该线程中互斥量是被加锁的,直到该线程执行完毕,互斥量才会被解锁,第二个参数返回false,则线程就会被阻塞到改行;如果没有第二个参数,流程也会继续走下去。
wait()和notity_one()函数的一些误解:这个两个线程并不是和表面上看上去的那样,in线程执行一次,out线程执行一次,实际上是在每一次in线程执行了notify_one()之后,in线程的unique_lock开始和out线程的wait开始抢夺这个互斥锁,谁抢夺成功了,那么那个线程就可以继续往下执行,所以说这个抢夺过程可能out线程的wait函数很久都抢不到一次互斥锁,因此这两个线程并不是依次执行的。如果out函数在对互斥量进行解锁之后还有其他的代码,而此时in线程执行到notify_one函数这里,这里并不会对wait起到唤醒作用,因为out线程在干别的事情,当且仅当out线程卡在wait这里,in线程执行notify_one才能够起到唤醒的作用,因此并不是每一次notify_one都能唤醒wait。
运行结果很好的说明了notify_one和wait不是依次执行的。
上面我们谈到的notify_one一次只能唤醒一个线程,但是在实际的操作中,可能会有多个线程需要被唤醒,这时候就需要引入notify_all()来唤醒所有的处于wait状态的线程。我们使用out再创建一个线程,那么在执行的时候就会让两个out线程都陷入wait状态,此时如果使用notify_one只能唤醒其中一个,另一个继续等待,因此同时需要将in函数中的notify_one改成notify_all,将两个wait都唤醒,但是此处只有一个互斥量,因此执行的时候还是只有一个wait能拿到锁,
async是一个函数模板,用来启动一个异步任务,最后返回一个future对象,这个future对象里面含有线程入口函数的返回值,可以通过future的get成员函数得到返回值,get()函数会让程序卡在当前行直到线程执行完毕返回一个结果,主线程才能继续往下运行,当然在使用这两个类模板时候需要引入头文件
int mythread()
{
cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
chrono::microseconds dura(5000);//休息5s
this_thread::sleep_for(dura);
cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
return 6;
}
int main()
{
cout << "主线程" << this_thread::get_id() << endl;
future result = async(mythread);
cout << "**************" << endl;
cout << result.get() << endl;//程序会卡在这一行
cout << "Hello World" << endl;
return 0;
}
首先写一个线程入口函数,让其在运行的时候休息5s,便于观察,在主线程中我们通过asyns创建一个异步线程,用future来创建一个对象收其返回值,在通过get()函数返回拿到的结果,如果get()函数没有拿到返回值,那么就会卡在这里知道子线程执行结束。
同时future对象也有一个wait()函数,其作用只是让主线程卡在这里等待,并不带回返回值,和thread里面的join()比较像。既然get()函数的功能如此强大,那么能否多次调用呢?答案是否定的
我们在这里调用了两次get()函数,发现程序崩溃了,因此get()函数只能调用一次。
下面我们把这个线程入口函数修改成类的成员函数,复习成员函数作线程入口函数如何创建线程:
class TEST
{
public:
int mythread(int val)
{
cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
chrono::microseconds dura(5000);//休息5s
this_thread::sleep_for(dura);
cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
return 6;
}
};
int main()
{
TEST T;
int val = 5;
cout << "主线程" << this_thread::get_id() << endl;
future result = async(&TEST::mythread,ref(T),val);
cout << "**************" << endl;
cout << result.get() << endl;//程序会卡在这一行
cout << "Hello World" << endl;
return 0;
}
最重要的还是要注意成员函数作线程入口的创建方式,还有里面一些传参的方式。写到这里,有一个好奇的问题就是如果这个主线程里面既没有wait()有没有get()那么这个程序会如何执行呢,下面就让我们继续来测试一下:
观察运行结果,我们发现即使没有wait()和get()函数,线程也是可以创建的 ,但是观察这个执行顺序不难发现,子线程是在主线程执行完才开始创建的。
下面让我们来了解一下async的参数问题:在async的第一个参数中有一个枚举类型,launch,其包括下面俩成员(1)deferred (2)async ,首先介绍一下deferred这个,它的作用是推迟线程的创建,直到wait和get时才创建子线程,那么如果没有wait和get又会怎样呢,接着往下看;async参数就是直接在async函数这里创建线程,可见async函数的第一个参数默认使用的就是async。
这里没有wait和get,我们使用了launch::deferred,子线程没有创建,运行结果里面没有子线程相关信息。
通过对比wait和get的运行结果我们会发现,主线程id和子线程id是一样的,子线程是在主线程里被调用的,也就是说子线程和主线程是同一个线程,子线程压根就没有被创建 。下面再来测试一下async参数:
通过观察三种情况下的运行结果发现,无论有没有wait还是get都会创建子线程。
通过调试发现,确实是在future这里创建的子线程。说完这些下面让我们来看一下packaged_task.
packaged_task是一个类模板,把各种可调用对象包装起来,方便作为线程入口函数;下面举个例子看一下。我们重新将线程入口函数写成一个普通的函数,用packaged_task将其打包:
int main()
{
cout << "主线程" << this_thread::get_id() << endl;
packaged_task pthread(mythread);//将线程入口函数打包
thread p1(ref(pthread), 1);//第一个参数是打包的线程入口函数,第二个参数就是线程入口函数的参数
p1.join();
futureresult = pthread.get_future();
cout << result.get() << endl;
cout << "Hello World" << endl;
return 0;
}
main函数里面的package_tack的写法需要注意,里面的 int(int)表示的是一个函数的返回值是int,其有一个int的参数,用packaged_task将线程入口函数打包,再通过thread来创建线程,通过packaged_tack的get_future()函数来将线程的返回值和future创建的对象result绑定,再通过future的get函数拿到返回值。
上面我们说到packaged_task可以将可调用对象进行绑定,那么我们就可以将上面的绑定换一种写法,我们可以使用lambda表达式来进行绑定。
packaged_task pthread([](int val) {
cout << "mythread 开始执行,线程ID :" << this_thread::get_id() << endl;
chrono::microseconds dura(5000);//休息5s
this_thread::sleep_for(dura);
cout << "mythread 执行完毕 线程ID:" << this_thread::get_id() << endl;
return 6;
});//将线程入口函数打包
其他的都不需要修改,只需要将mythread改成lambda表达式就行。
程序一样可以正常运行。packaged_task本身也是一个可调用对象,当然此时就不会创建线程,就是一个函数调用。
pthread(100);
futureresult = pthread.get_future();
cout << result.get() << endl;
cout << "Hello World" << endl;
我们直接对打包好的线程进行传参调用,可以看到这里的线程id是相同的,所以这里就是一个函数调用。还有packaged_task放到容器中的写法:
vector>pack;
pack.push_back(move(pthread));
packaged_taskp;
auto it = pack.begin();
p= move(*it);
pack.erase(it);
p(123);
//pthread(100);
futureresult = p.get_future();
cout << result.get() << endl;
cout << "Hello World" << endl;
先创建一个packaged_task类型的容器,将刚才的打包好的可调用对象存入容器中,这里需要用到移动语义,不然会报异常,再将pthread移动到一个新的packaged_task对象中,在调用它;总之,写法有很多种,需要大家自己去探索。
最后再来看一下 promise ,promise也是一个类模板,我们能够在一个 线程中给它赋值,然后在另外一个线程中取出这个值。
void accumalate(promise& temp, int calc)
{
int sum = 1;
for (int i = 0; i < calc; i++)
{
sum += calc;
}
temp.set_value(sum);
}
这是我们创建的线程入口函数,传入的第一个参数是一个promise对象,用来存放线程函数最后的返回结果,在这个函数里,我们简单的做一个加法运算,最后通过promise的set_value函数将最终的运算结果和promise创建的对象temp绑定。
promise my_promsie;
thread mythread(accumalate, ref(my_promsie), 10);
mythread.join();
futureful = my_promsie.get_future();
cout << ful.get() << endl;
cout << "Hello word" << endl ;
在主函数里面,我们先创建一个promise对象用来作线程入口函数的参数,在通过thread创建线程,传入参数,将future和promise绑定,通过future的fet函数拿到最终的结果。
注意:get依旧只能调用一次 。上面的写法是在主线程中拿到运算结果,我们还可以在新创建的线程中拿到运算结果。
void get_result(future& temf)
{
cout << "在线程中得到运算结果:" << temf.get() << endl;
}
重新在写一个线程入口函数,这个函数里面只拿到它的运算结果,线程入口函数传入的是future对象,对于主函数,作以下更改:
thread mythread2(get_result, ref(ful));
mythread2.join();
cout << "Hello word" << endl ;
需要将上面创建的future对象传入,然后不用写get函数,因为在新线程中已经get过了,再次get会出错,最终也会 得到一个正确的结果:
这个promise配合future就可以实现线程的运算结果在线程之间传递。
前面介绍了很多多线程相关的函数和一些相关的写法及用法,并不是所有的都必须掌握,在实际的开发中,只要能够用最简洁的代码写出高效的程序才是最重要的,不要把简单的问题复杂化,用好自己最熟悉的即可,遇到一些复杂的只要能看懂就行。
注:文章内容参考《C++新经典》