这一节主要讲讲线程同步的方式,C++ 11中提供了丰富的线程同步元语,如condition_variable,futrue,std::packaged_task<>,std::promise,std::async等,本节后续内容将就这些话题进行阐述。

1. Lambda表达式

lambda表达式是C++ 11提供的新特性,在高级语言当中,该语法特性已经得到了普遍的支持。lambda函数能够大大简化代码复杂度(语法糖:利于理解具体的功能),避免实现调用对象。能为等待函数,例如 std::condition_variable提供很好谓词函数,其语义可以用来快速的表示可访问的变量,而非使用类中函数来对成员变量进行捕获。在开始讲线程同步的内容之前,我想简单讲讲lambda表达式,主要是后面在写示例代码的时候会使用到lambda表达式的知识,就算是做个知识的提前铺垫吧。

lambda表达书以方括号开头,所有操作语义都在一对大括号中{ },并以()结尾,


[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下

  1. capture list:捕获外部变量列表
  2. params list:形参列表
  3. mutable指示符:用来说用是否可以修改捕获的变量
  4. exception:异常设定
  5. return type:返回类型
  6. function body:函数体

此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:

序号 格式
1 [capture list] (params list) -> return type {function body}
2 [capture list] (params list) {function body}
3 [capture list] {function body}

其中:

  • 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值。
  • 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型: (1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定; (2):如果function body中没有return语句,则返回值为void类型。
  • 格式3中省略了参数列表,类似普通函数中的无参函数。

例 1 使用了lambda表达式对以一个verctor容器中的所有元素进行的打印.

std::vector<int> data={1,2,3,4,5,6,7,8,9};
std::for_each(data.begin(),data.end(),[](int i){std::cout<<i<<"\n";});

例2 容器比较函数

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std; bool cmp(int a, int b)
{
return a < b;
} int main()
{
vector<int> myvec{ , , , , , };
vector<int> lbvec(myvec); sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
cout << "predicate function:" << endl;
for (int it : myvec)
cout << it << ' ';
cout << endl; sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; }); // Lambda表达式
cout << "lambda expression:" << endl;
for (int it : lbvec)
cout << it << ' ';
}

在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。

 

Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,主要有值捕获、引用捕获、隐式捕获三种。

捕获形式 说明
[] 不捕获任何外部变量
[变量名, …] 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
[this] 以值的形式捕获this指针
[=] 以值的形式捕获所有外部变量
[&] 以引用形式捕获所有外部变量
[=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
[&, x] 变量x以值的形式捕获,其余变量以引用形式捕获

举例:

#include <iostream>
using namespace std; int main()
{
int a = ;
auto f = [a] { cout << a << endl; }; // 值捕获外部本地变量a
f(); // 输出:123 //或通过“函数体”后面的‘()’传入参数
auto x = [](int a){cout << a << endl; return ; }(); // 显示传入外部变量值(值传递)给lambda表达式,输出123 auto refFunction = [&a] { cout << a << endl; }; // 通过lambda表达式定义函数,引用捕获外部变量
a = ;
refFunction(); //调用lambda函数,输出:321 auto fvalue = [=] { cout << a << endl; }; // 隐式值捕获外部所有变量
fvalue(); // 输出:321 auto fref = [&] { cout << a << endl; }; // 隐式引用捕获外部所有变量
a = ;
fref(); // 输出:321
}

 2. std::condition_variable

std::condition_variable可以被多个thread都可以访问的并等待条件达成,如果条件未达成,则访问线程阻塞;反之,如果条件达成,可以在达成的线程内调用std::condition_variable.notify_one()或std::condition_variable.notify_all来唤醒一个或全部等待线程。等待线程收到通知,就知道条件满足,就可以继续执行了。std::condition_variable需要和std::mutex,std::unique_lock一起配合来使用

 #include <mutex>
#include <condition_variable>
#include <thread>
#include <queue> bool more_data_to_prepare()
{
return false;
} struct data_chunk
{}; data_chunk prepare_data()
{
return data_chunk();
} void process(data_chunk&)
{} bool is_last_chunk(data_chunk&)
{
return true;
} std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond; void data_preparation_thread()
{
while(more_data_to_prepare())
{
data_chunk const data=prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
} void data_processing_thread()
{
while(true)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[]{return !data_queue.empty();});
data_chunk data=data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if(is_last_chunk(data))
break;
}
} int main()
{
std::thread t1(data_preparation_thread);
std::thread t2(data_processing_thread); t1.join();
t2.join();
}

示例代码演示了一个生产者data_preparation_thread和消费者data_processing_thread两个线程之间的同步。

1. 生产者线程data_preparation_thread循环进行消息的准备,在37行往队列中插入一条记录后,通过38行的std::condition_variable实例唤醒一个等待线程。

2. 消费者线程data_processing_thread在47行检查通过std::condition_variable.wait中的 lambda表达式检查队列是否为空,如果队列为空,wait()函数将解锁互斥量,并且将这个线程(上段提到的处理数据的线程)置于阻塞或等待状态。直到生产者线程data_preparation_thread有数据push到队列中后,通过std::condition_variable.notify_one()在38行进行通知唤醒。一旦被唤醒,消费者线程会再次获取互斥锁,并且对条件再次检查,并在条件满足的情况下,继续持有锁,并48行和以下部分的代码。

3. std::condition_variable.wait等待条件满足是直接与等待条件相关的,而与通知到达无关,通知到达了,只是唤醒等待线程重新锁定共享数据,检查条件满足没有。wait的第一个参数为对共享数据进行保护的锁,只有在锁定状态下,wait才会去检查wait条件是否达成。

 线程安全队列代码示例:

#include <queue>
#include <mutex>
#include <condition_variable>
#include <memory> template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut;
std::queue<std::shared_ptr<T> > data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{} void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
value=std::move(*data_queue.front());
data_queue.pop();
} bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return false;
value=std::move(*data_queue.front());
data_queue.pop();
} std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
std::shared_ptr<T> res=data_queue.front();
data_queue.pop();
return res;
} std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res=data_queue.front();
data_queue.pop();
return res;
} bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
} void push(T new_value)
{
std::shared_ptr<T> data(
std::make_shared<T>(std::move(new_value)));
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
} };

3. std::future<T>

std::future是一个异步结果获得的对象,将任务与一个future对象关联,任务做完后就可以通过std::future对象得到结果,当我们需要时就可以随时去从std::future对象取得我们期待的结果.  异步需要执行的任务是通过provider来设定的,通过在定义provider的时候,将future与某个task关联在一起,就能通过future异步取得执行结果(假设fut为future对象,通过ful.get()取得结果)了。future也是一个模板类future<T>,T表示future返回结果的类型。

通过thread在执行的任务是没有反获值的,如果需要获取线程执行的返回值,就需要通过future。std::future是一次性等待任务,只是执行一次,然后结果会返回给future,如果还来就需要重新设置。std:condition_variable是可以重复执行的。

3.1 std::async()

std::async启动一个异步任务并会返回一个std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个futrue对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果。
std::async的参数格式为std::async(parameA,paramB,paramC,paramD),参数说明如下:

a) paramA是执行的方式,std::launch::deferred表示延迟执行,即在future上调用get()或者wait()才开始执行; std::launch::async表示立即启动一个新线程执行。   此参数忽略,如果忽略表示由具体实现选择方式。
b) paramB是需要执行任务的函数名。              此参数为必选参数
c) paramC表示,调用需要执行任务的函数的对象。   此参数为可选参数,因为可能不是一个类的成员函数,所以可以直接调用
d) paramD表示函数的参数,所以D可以有很多项。

如:
#include <future>
#include <iostream>
#include <string>
using namespace std; int find_the_answer_to_ltuae()
{
return ;
} void do_something_in_main_thread()
{
std::cout<<"I'm in main thead\n";
} struct X
{
void foo(int inumber,std::string const& svalue)
{
cout<<"struct X inumber:"<<inumber<<" svalue:"<<svalue<<endl;
}
std::string bar(std::string const& svalue)
{
cout<<"struct X svalue:" <<svalue<<endl;
}
}; X x;
auto f1=std::async(&X::foo,&x,,"hello");
auto f2=std::async(&X::bar,x,"goodbye"); struct Y
{
double operator()(double value)
{
std::cout<<"struct Y :"<<value<<std::endl;
}
};
Y y;
auto f3=std::async(Y(),);
auto f4=std::async(std::ref(y),); auto f6=std::async(std::launch::async,Y(),); // 在新线程上执行
auto f7=std::async(std::launch::deferred,Y(),); // 在wait()或get()调用时执行
// f7.wait() //// 调用延迟函数 int main()
{
std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
do_something_in_main_thread();
std::cout<<"The answer is "<<the_answer.get()<<std::endl;
}

输出如下:

lenmom@M1701:~/workspace/open-source$ ./a.out
struct X svalue:goodbyestruct Y :
struct X inumber: svalue:hello struct Y :
struct Y :
I'm in main thead
The answer is

3.2 std::packaged_task<>
std::packaged_task<>将一个可调用对象(即需要执行的任务)或函数和一个future封装在一起的可调用对象。当构造一个 std::packaged_task<> 实例时,必须传入一个函数或可调用对象,这个函数或可调用的对象需要能接收指定的参数和返回可转换为指定返回类型的值。类型可以不完全匹配;比如使用float f(int )函数,来构建 std::packaged_task<double(double)> 的实例,因为在这里,类型可以隐式转换。使用std::packaged_task关联的std::future对象保存的数据类型是可调对象的返回结果类型,如示例函数的返回结果类型是int,那么声明为std::future<int>,而不是std::future<int(int)。代码示例:

#include <iostream>
#include <type_traits>
#include <future>
#include <thread> using namespace std;
int main()
{
std::packaged_task<int()> task([]() {
std::this_thread::sleep_for(std::chrono::seconds(5));// 线程睡眠5s
return ; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future(); auto r = f1.get();// 线程外阻塞等待
std::cout << r << std::endl;

    t1.join();
return ;
}

3.3 std::promise<>

std::promise用来包装一个值将数据和future绑定起来,为获取线程函数中的某个值提供便利,需要显示设置promise的返回值,取值是间接通过promise内部提供的future来获取的,也就是说promise的层次比future高。

#include <iostream>
#include <type_traits>
#include <future>
#include <thread> using namespace std;
int main()
{
std::promise<int> promiseParam;
std::thread t([](std::promise<int>& p)
{
std::this_thread::sleep_for(std::chrono::seconds());// 线程睡眠10s
p.set_value_at_thread_exit();//
}, std::ref(promiseParam));
std::future<int> futureParam = promiseParam.get_future(); auto r = futureParam.get();// 线程外阻塞等待
std::cout << r << std::endl;
 
    t.join()
return ;
}

C++并发编成 03 线程同步的更多相关文章

  1. Python并发编程-进程 线程 同步锁 线程死锁和递归锁

    进程是最小的资源单位,线程是最小的执行单位 一.进程 进程:就是一个程序在一个数据集上的一次动态执行过程. 进程由三部分组成: 1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成 2.数据 ...

  2. Java提高班(三)并发中的线程同步与锁

    乐观锁.悲观锁.公平锁.自旋锁.偏向锁.轻量级锁.重量级锁.锁膨胀...难理解?不存的!来,话不多说,带你飙车. 上一篇介绍了线程池的使用,在享受线程池带给我们的性能优势之外,似乎也带来了另一个问题: ...

  3. java并发编程:线程同步和锁

    一.锁的原理 java中每个对象都有一个内置锁.当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this)有关的锁.获得一个对象的锁也称为获取锁,当程序运 ...

  4. [03] 线程同步 synchronized

    1.线程同步概述 线程之间有可能共享一些资源,比如内存.文件.数据库等.多个线程同时读写同一份共享资源时,就可能引起冲突,所以引入了线程的"同步"机制. 所谓同步,就是说线程要有先 ...

  5. java并发编程基础——线程同步

    线程同步 一.线程安全问题 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安 ...

  6. 第40天学习打卡(静态代理 Lambda表达式 线程状态 线程同步 同步方法)

    静态代理  package com.kuang.demo03; //静态代理模式总结 //真实对象和代理对象都要实现同一个接口 //代理对象要代理真实角色 //好处:  //代理对象可以做很多真实对象 ...

  7. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  8. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  9. C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题

    (补充:初始化FileStream时使用包含文件共享属性(System.IO.FileShare)的构造函数比使用自定义线程锁更为安全和高效,更多内容可点击参阅) 在开发程序的过程中,难免少不了写入错 ...

随机推荐

  1. Python学习(004)-字典{}

    特点: 无序状态 键唯一   不可变类型:字符串.整型.元组 可变类型:列表.字典   字典创建 第一种: dic1={','sex':'man'} print(dic1['name']) ----- ...

  2. Dll封装dll,并且调用该封装的dll

    按照图1,2,3创建一个默认的(可以导出符号的dll项目) 默认创建的,很好地给我们说明了怎么导出 变量,导出函数,导出类 注意这里加入你要导出的函数的声明 WIN32PROJECT1_API int ...

  3. ADB命令行工具使用

    Putty工具连接Android设备 下载链接:https://github.com/sztupy/adbputty/downloads 如上图所示:在HostName中输入transport-usb ...

  4. 升级 macOS Mojave 后部分软件 (如 VS Code) 字体变虚的解决方法

    目前有些朋友的设备可能还是“非 Retina” 显示器,那这样如果升级到 Mojave 后你会发现文字不清晰了,这是因为 Mojave 默认关闭了文字次像素渲染字体,你需要在终端里执行: defaul ...

  5. 玩转TypeScript(引言&文章目录) --初看TypeScript.

    JavaScript过去一直被当作一种玩具语言存在,直到2005年以后,这门语言又开始活跃并可以说是火爆,而且随着浏览器版本的不断升级和完善,各种DOM之间的兼容性已经渐渐的被各种技术解决了,比如经典 ...

  6. spring读取propertyes 新方法

    <context:property-placeholder location="classpath:mysql.properties"/> <util:prope ...

  7. Qemu编译qemu-system-arm

    /********************************************************************************* * Qemu编译qemu-syst ...

  8. zookeeper 官方文档——综述

      Zookeeper: 一个分布式应用的分布式协调服务   zookeeper 是一个分布式的,开源的协调服务框架,服务于分布式应用程序.   它暴露了一系列基础操作服务,因此,分布式应用能够基于这 ...

  9. ConfigUtil读取配置文件

    package utils; import java.util.ResourceBundle; public class ConfigUtil { private static ResourceBun ...

  10. CentOS7使用打开关闭防火墙与端口

    systemctl是CentOS7的服务管理工具中主要的工具,它融合之前service和chkconfig的功能于一体. 启动一个服务:systemctl start firewalld.servic ...