《C++并发编程实战》读书笔记(3):并发操作的同步

这篇具有很好参考价值的文章主要介绍了《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::promisestd::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()文章来源地址https://www.toymoban.com/news/detail-694008.html

到了这里,关于《C++并发编程实战》读书笔记(3):并发操作的同步的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++并发编程 -3.同步并发操作

    本文介绍如何使用条件变量控制并发的同步操作、C++ 并发三剑客,函数式编程 1.概念         C++条件变量(condition variable)是一种多线程编程中常用的同步机制,用于线程间的通信和协调。它允许一个或多个线程等待某个条件的发生,当条件满足时,线程被唤醒并继续执

    2024年02月22日
    浏览(32)
  • java并发编程之美第五章读书笔记

    CopyOnWriteArrayList 线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)进行的,也就是写时复制策略 类图 每一个对象里面有一个array数组进行存放具体的元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改,这里只要记得ReentrantLock是独占锁

    2024年02月03日
    浏览(32)
  • 《C++高级编程》读书笔记(七:内存管理)

    1、参考引用 C++高级编程(第4版,C++17标准)马克·葛瑞格尔 2、建议先看《21天学通C++》 这本书入门,笔记链接如下 21天学通C++读书笔记(文章链接汇总) 1. 使用动态内存 1.1 如何描绘内存 在本书中,内存单元表示为一个带有标签的框,该标签表示这个内存对应的变量名,方

    2024年02月08日
    浏览(71)
  • C++并发操作解密:轻松搞定数据同步

      概述: 在C++中,通过互斥锁解决并发数据同步问题。定义共享数据和互斥锁,编写线程函数,使用互斥锁确保操作的原子性。主函数中创建并启动线程,保障线程安全。实例源代码演示了简单而有效的同步机制。 在C++中解决并发操作时的数据同步问题通常需要使用互斥锁

    2024年02月04日
    浏览(23)
  • 《Java并发编程实战》课程笔记(九)

    信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。 在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。 init():设置计数器的初始值。 down():计

    2024年02月07日
    浏览(24)
  • 《Java并发编程实战》课程笔记(二)

    在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。 因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。 多核

    2024年02月06日
    浏览(25)
  • 《Java并发编程实战》课程笔记(十二)

    对账系统的业务简化后: 首先用户通过在线商城下单,会生成电子订单,保存在订单库; 之后物流会生成派送单给用户发货,派送单保存在派送单库。 为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单。 目前对账系统的处理逻辑是首先查询订单,然后

    2024年02月08日
    浏览(30)
  • C++ 并发编程实战 第二章 线程管控

    线程通过构建 std::thread 对象而自动启动 ,该对象指明线程要运行的任务。 对应复杂的任务,可以使用函数对象。 一旦启动了线程,我们就需明确是要等待它结束(与之汇合 join() ),还是任由它独自运行(与之分离 detach() ) ❗❗❗ 同一个线程的 .join() 方法不能被重复调用

    2023年04月08日
    浏览(25)
  • C++并发编程 | 原子操作std::atomic

    目录 1、原子操作std::atomic相关概念 2、不加锁情况 3、加锁情况  4、原子操作 5、总结 原子操作: 更小的代码片段,并且该片段必定是连续执行的,不可分割。 1.1 原子操作std::atomic与互斥量的区别 1) 互斥量 :类模板,保护一段共享代码段,可以是一段代码,也可以是一个

    2023年04月26日
    浏览(27)
  • c++并发编程实战-第3章 在线程间共享数据

    多线程之间共享数据,最大的问题便是数据竞争导致的异常问题。多个线程操作同一块资源,如果不做任何限制,那么一定会发生错误。例如: 输出: 显然,上面的输出结果存在问题。出现错误的原因可能是: 某一时刻, th1线程获得CPU时间片,将g_nResource从100增加至200后时

    2024年02月08日
    浏览(27)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包