《C++并发编程实战》读书笔记(3):并发操作的同步
1、条件变量
当线程需要等待特定事件发生、或是某个条件成立时,可以使用条件变量std::condition_variable,它在标准库头文件<condition_variable>内声明。
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while (more_data_to_prepare())
{
const data_chunk 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; }
}
}
wait()会先在内部调用lambda函数判断条件是否成立,若条件成立则wait()返回,否则解锁互斥并让当前线程进入等待状态。当其它线程调用notify_one()时,当前调用wait()的线程被唤醒,重新获取互斥锁并查验条件,若条件成立则wait()返回(互斥仍被锁住),否则解锁互斥并继续等待。
wait()函数的第二个参数可以传入lambda函数,也可以传入普通函数或可调用对象,也可以不传。
notify_one()唤醒正在等待当前条件的线程中的一个,如果没有线程在等待,则函数不执行任何操作,如果正在等待的线程多于一个,则唤醒的线程是不确定的。notify_all()唤醒正在等待当前条件的所有线程,如果没有正在等待的线程,则函数不执行任何操作。
2、使用future等待一次性事件发生
C++标准程序库有两种future,分别由两个类模板实现,即std::future<>和std::shared_future<>,它们的声明位于头文件<future>内。
2.1、从后台任务返回值
由于std::thread没有提供直接回传结果的方法,所以我们使用函数模板std::async()来解决这个问题。std::async()以异步方式启动任务,并返回一个std::future对象,运行函数一旦完成,其返回值就由该对象持有。在std::future对象上调用get()方法时,当前线程就会阻塞,直到std::future准备妥当并返回异步线程的结果。std::future模拟了对异步结果的独占行为,get()仅能被有效调用一次,调用时会对目标值进行移动操作。
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << "The answer is " << the_answer.get() << std::endl;
}
在调用std::async()时,它可以接收附加参数进而传递给任务函数作为其参数,此方式与std::thread的构造函数相同。更多启动异步线程的方法可参考下面的例程:
struct X
{
void foo(int, const std::string&);
std::string bar(const std::string&);
};
X x;
auto f1 = std::async(&X::foo, &x, 42, "hello"); // 调用p->foo(42, "hello"),p是指向x的指针
auto f2 = std::async(&X::bar, x, "goodbye"); // 调用tmpx.bar("goodbye"), tmpx是x的拷贝副本
struct Y
{
double operator()(double);
};
Y y;
auto f3 = std::async(Y(), 3.141); // 调用tmpy(3.141),tmpy是由Y()生成的匿名变量
auto f4 = std::async(std::ref(y), 2.718); // 调用y(2.718)
X baz(X&);
std::async(baz, std::ref(x)); // 调用baz(x)
我们还能为std::async()补充一个std::launch类型的参数,来指定采用哪种方式运行:std::launch::deferred指定在当前线程上延后调用任务函数,等到在future上调用了wait()或get(),任务函数才会执行;std::launch::async指定必须开启专属的线程,在其上运行任务函数。该参数的还可以是std::launch::deferred | std::launch::async,表示由std::async()的实现自行选择运行方式,这也是这项参数的默认值。
auto f6 = std::async(std::launch::async, Y(), 1.2); // 在新线程上执行
auto f7 = std::async(std::launch::deferred, baz, std::ref(x)); // 在wait()或get()调用时执行
auto f8 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x)); // 交由实现自行选择执行方式
auto f9 = std::async(baz, std::ref(x));
f7.wait(); // 调用延迟函数
2.2、关联future实例和任务
std::packaged_task<>连结future对象与函数(或可调用对象,下同)。std::packaged_task<>对象在执行任务时,会调用关联的函数,把返回值保存为future的内部数据,并令future准备就绪。若一项庞杂的操作能分解为多个子任务,则可以把它们分别包装到多个std::packaged_task<>实例之中,再传递给任务调度器或线程池,这就隐藏了细节,使任务抽象化,让调度器得以专注处理std::packaged_task<>实例,无需纠缠于形形色色的任务函数。
std::packaged_task<>是类模板,其模板参数是函数签名(例如void()表示一个函数,不接收参数,也没有返回值),传入的函数必须与之相符,即它应接收指定类型的参数,返回值也必须可以转换成指定类型。这些类型不必严格匹配,若某函数接收int类型参数并返回float值,则可以为其构建std::packaged_task<double(double)>的实例,因为对应的类型可以隐式转换。
std::packaged_task<>具有成员函数get_future(),它返回std::future<>实例,该future的特化类型取决于函数签名指定的返回值。std::packaged_task<>还具备函数调用操作符,它的参数取决于函数签名的参数列表。
std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()
{
while (!gui_shutdown_message_received())
{
get_and_process_gui_message();
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
if (tasks.empty()) { continue; }
task = std::move(tasks.front());
tasks.pop_front();
}
task();
}
}
std::thread gui_bg_thread(gui_thread);
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
std::packaged_task<void()> task(f);
std::future<void> res = task.get_future();
std::lock_guard<std::mutex> lk(m);
tasks.push_back(std::move(task));
return res;
}
2.3、创建std::promise
有些任务无法以简单的函数调用表达出来,还有一些任务的执行结果可能来自多个部分的代码,这时可以借助std::promise显式地异步求值。配对的std::promise和std::future可以实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的std::promise设定关联的值,使future准备就绪。
若需从给定的std::promise实例获取关联的std::future对象,调用前者的成员函数get_future()即可,这与std::package_task一样。promise的值通过成员函数set_value()设置,只要设置好,future即准备就绪,凭借它就能获取该值。如果std::promise在被销毁时仍未曾设置值,保存的数据则由异常代替。
void f(std::promise<int> ps)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
ps.set_value(42);
}
int main()
{
std::promise<int> ps;
std::future<int> ft = ps.get_future();
std::thread t(f, std::move(ps));
int val = ft.get();
std::cout << val << std::endl;
t.join();
}
2.4、将异常保存到future中
若经由std::async()调用的函数抛出异常,则会被保存到future中,future随之进入就绪状态,等到其成员函数get()被调用,存储在内的异常即被重新抛出。std::packaged_task也是同理,若包装的任务函数在执行时抛出异常,则也会被保存到future中,只要调用get(),该异常就会被再次抛出。自然而然,std::promise也具有同样的功能,它通过成员函数显式调用实现。假如我们不想保存值,而想保存异常,就不应调用set_value(),而应调用成员函数set_exception()。
2.5、多个线程一起等待
若我们在多个线程上访问同一个std::future对象,而不采取额外的同步措施,将引发数据竞争并导致未定义的行为。std::future仅能移动构造和移动赋值,而std::shared_future的实例则能复制出副本。但即便改用std::shared_future,同一个对象的成员函数却依然没有同步,若我们从多个线程访问同一个对象,首选方式是:向每个线程传递std::shared_future对象的副本,它们为各线程独有,这些副本就作为各线程的内部数据,由标准库正确地同步,可以安全地访问。
future和promise都具备成员函数valid(),用于判别异步状态是否有效。std::shared_future的实例依据std::future的实例构造而得,前者所指向的异步状态由后者决定。因为std::future对象独占异步状态,所以若要按默认方式构造std::shared_future对象,则须用std::move向其默认构造函数传递归属权。
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());
std::shared_future<int> sf(std::move(f));
assert(!f.valid());
assert(sf.valid());
std::future具有成员函数share(),直接创建新的std::shared_future对象,并向它转移归属权。
std::promise<std::map<SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator> p;
auto sf = p.get_future().share();
3、限时等待
有两种超时机制可供选择:一是延迟超时,线程根据指定的时长而继续等待;二是绝对超时,在某个特定时间点来临之前,线程一直等待。大部分等待函数都有变体,专门处理这两种机制的超时。处理延迟超时的函数变体以_for为后缀,而处理绝对超时的函数变体以_until为后缀。例如std::condition_variable的成员函数wait_for()和wait_until()。
《C++并发编程实战》读书笔记(3):并发操作的同步的更多相关文章
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- 《java并发编程实战》笔记
<java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为: Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...
- Java并发编程实践读书笔记(2)多线程基础组件
同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...
- Java多线程编程实战读书笔记(一)
多线程的基础概念本人在学习多线程的时候发现一本书——java多线程编程实战指南.整理了一下书中的概念制作成了思维导图的形式.按照书中的章节整理,并添加一些个人的理解.
- Java并发编程实践读书笔记(5) 线程池的使用
Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...
- Java并发编程实践(读书笔记) 任务执行(未完)
任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元. 任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...
- Java并发编程艺术读书笔记
1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...
- Java并发编程实践读书笔记(1)线程安全性和对象的共享
2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...
- 《Java并发编程实战》笔记-Happens-Before规则
Happens-Before规则 程序顺序规则.如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行. 监视器锁规则.在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行. v ...
随机推荐
- 15-1 OOP概述
目录 核心思想 继承 动态绑定 核心思想 面向对象程序设计(object-oriented programming)的核心思想是 封装:类的接口和实现分离 继承:定义相似的类型并对相似关系建模 动态绑 ...
- Linux 日志管理基础
目录 基本介绍 日志的存放 存放目录与存放内容 举例说明 日志管理服务: rsyslogd 功能与配置 检查自启动 配置文件 /etc/rsyslog.conf 修改配置文件 基本介绍 日志文件是重要 ...
- virsh的基本使用
virsh基础命令 1.查看运行的虚拟机 virsh list 查看所有的虚拟机(关闭和运行的,不包括摧毁的) virsh list --all 2..启动虚拟机 virsh start 虚拟机名称 ...
- 零基础入门Hadoop:IntelliJ IDEA远程连接服务器中Hadoop运行WordCount
今天我们来聊一聊大数据,作为一个Hadoop的新手,我也并不敢深入探讨复杂的底层原理.因此,这篇文章的重点更多是从实际操作和入门实践的角度出发,带领大家一起了解大数据应用的基本过程.我们将通过一个经典 ...
- JESD79-5C_v1.30-2024 JEDEC DDR5 SOLID STATE TECHNOLOGY ASSOCIATION 最新内存技术规范
JESD79-5C_v1.30-2024 JEDEC DDR5 SOLID STATE TECHNOLOGY ASSOCIATION 最新DDR5内存技术规范 JEDEC 技术协会公布新 DDR5 ...
- 读书笔记-C#8.0本质论-01
1. IL代码入门 1.1 示例1 namespace ConsoleApp1; internal static class Program { internal static void Main(s ...
- Netty+Spring Boot 加持,解锁高性能 Web 应用
MiniTomcat(https://github.com/daichangya/MiniTomcat) 这个项目是一个基于Netty的Java Web服务器,它提供了从简单HTTP服务器到集成Spr ...
- 鸿蒙NEXT开发案例:温度转换
[引言] 温度是日常生活中常见的物理量,但不同国家和地区可能使用不同的温度单位,如摄氏度(Celsius).华氏度(Fahrenheit).开尔文(Kelvin).兰氏度(Rankine)和列氏度(R ...
- confd+Nacos实现nginx配置文件管理
场景: 由于公司内部站点保护的需求, 将部分的站点添加白名单, 这边的操作是在nginx配置文件中添加如下代码 allow 127.0.0.1: deny all; 但随之问题也出现了, 需要添加一个 ...
- Redis为什么这么快之IO多路复用
情景复现 面试官:Redis为什么这么快? 我:1. 基于内存 2. 高效数据结构 3. 单线程 4. IO多路复用 面试官:那你讲讲Redis的IO多路复用模型是什么. 我:哦,嗯,啊,呀...IO ...