返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++多线程std::call_once的使用
  • 790
分享到

C++多线程std::call_once的使用

2024-04-02 19:04:59 790人浏览 八月长安
摘要

在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。很多时候我们为了初始化某些数据会写出如下代码,这些代码在单

多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。很多时候我们为了初始化某些数据会写出如下代码,这些代码在单线程中是没有任何问题的,但是在多线程中就会出现不可预知的问题。

bool initialized = false;
void foo() {
    if (!initialized) {
        do_initialize ();  //1
        initialized = true;
    }
}

为了解决上述多线程中出现的资源竞争导致的数据不一致问题,我们大多数的处理方法就是使用互斥来处理。只要上面①处进行保护,这样共享数据对于并发访问就是安全的。如下:

bool initialized = false;
std::mutex resource_mutex;

void foo() {
    std::unique_lock<std::mutex> lk(resource_mutex);  // 所有线程在此序列化 
    if(!initialized) {
        do_initialize ();  // 只有初始化过程需要保护 
    }
    initialized = true;
    lk.unlock();
    // do other;
}

但是,为了确保数据源已经初始化,每个线程都必须等待互斥量。为此,还有人想到使用“双重检查锁模式”的办法来提高效率,如下:

bool initialized = false;
std::mutex resource_mutex;

void foo() {
    if(!initialized) {  // 1
        std::unique_lock<std::mutex> lk(resource_mutex);  // 2 所有线程在此序列化 
        if(!initialized) {
            do_initialize ();  // 3 只有初始化过程需要保护 
        }
        initialized = true;
    }
    // do other;  // 4
}

第一次读取变量initialized时不需要获取锁①,并且只有在initialized为false时才需要获取锁。然后,当获取锁之后,会再检查一次initialized变量② (这就是双重检查的部分),避免另一线程在第一次检查后再做初始化,并且让当前线程获取锁。

但是上面这种情况也存在一定的风险,具体可以查阅著名的《c++和双重检查锁定模式(DCLP)的风险》。

对此,C++标准委员会也认为条件竞争的处理很重要,所以C++标准库提供了更好的处理方法:使用std::call_once函数来处理,其定义在头文件#include<mutex>中。std::call_once函数配合std::once_flag可以实现:多个线程同时调用某个函数,它可以保证多个线程对该函数只调用一次。它的定义如下:

struct once_flag
{
    constexpr once_flag() noexcept;
    once_flag(const once_flag&) = delete;
    once_flag& operator=(const once_flag&) = delete;
};

template<class Callable, class ...Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);

他接受的第一个参数类型为std::once_flag,它只用默认构造函数构造,不能拷贝不能移动,表示函数的一种内在状态。后面两个参数很好理解,第一个传入的是一个Callable。Callable简单来说就是可调用的东西,大家熟悉的有函数、函数对象(重载了operator()的类)、std::function和函数指针,C++11新标准中还有std::bindlambda(可以查看我的上一篇文章)。最后一个参数就是你要传入的参数。 在使用的时候我们只需要定义一个non-local的std::once_flag(非函数局部作用域内的),在调用时传入参数即可,如下所示:

#include <iOStream>
#include <thread>
#include <mutex>
 
std::once_flag flag1;
void simple_do_once() {
    std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}
 
int main() {
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();
}

call_once保证函数func只被执行一次,如果有多个线程同时执行函数func调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)——不会直接返回,直到活动线程对func调用结束才返回。对于所有调用函数func的并发线程,数据可见性都是同步的(一致的)。

但是,如果活动线程在执行func时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行func,依此类推。一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。(实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程)。

std::call_once在签名设计时也很好地考虑到了参数传递的开销问题,可以看到,不管是Callable还是Args,都使用了&&作为形参。他使用了一个template中的reference fold(我前面的文章也有介绍过),简单分析:

  • 如果传入的是一个右值,那么Args将会被推断为Args
  • 如果传入的是一个const左值,那么Args将会被推断为const Args&
  • 如果传入的是一个non-const的左值,那么Args将会被推断为Args&

也就是说,不管你传入的参数是什么,最终到达std::call_once内部时,都会是参数的引用(右值引用或者左值引用),所以说是零拷贝的。那么还有一步呢,我们还得把参数传到可调用对象里面执行我们要执行的函数,这一步同样做到了零拷贝,这里用到了另一个标准库的技术std::forward(我前面的文章也有介绍过)。

如下,如果在函数执行中抛出了异常,那么会有另一个在once_flag上等待的线程会执行。

#include <iostream>
#include <thread>
#include <mutex>
 
std::once_flag flag;
inline void may_throw_function(bool do_throw) {
    // only one instance of this function can be run simultaneously
    if (do_throw) {
        std::cout << "throw\n"; // this message may be printed from 0 to 3 times
        // if function exits via exception, another function selected
        throw std::exception();
    }

    std::cout << "once\n"; // printed exactly once, it's guaranteed that
    // there are no messages after it
}
 
inline void do_once(bool do_throw) {
    try {
        std::call_once(flag, may_throw_function, do_throw);
    } catch (...) {
    }
}
 
int main() {
    std::thread t1(do_once, true);
    std::thread t2(do_once, true);
    std::thread t3(do_once, false);
    std::thread t4(do_once, true);
 
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

std::call_once 也可以用在类中:

#include <iostream>
#include <mutex>
#include <thread>

class A {
 public:
  void f() {
    std::call_once(flag_, &A::print, this);
    std::cout << 2;
  }

 private:
  void print() { std::cout << 1; }

 private:
  std::once_flag flag_;
};

int main() {
  A a;
  std::thread t1{&A::f, &a};
  std::thread t2{&A::f, &a};
  t1.join();
  t2.join();
}  // 122

还有一种初始化过程中潜存着条件竞争:static 局部变量在声明后就完成了初始化,这存在潜在的 race condition,如果多线程的控制流同时到达 static 局部变量的声明处,即使变量已在一个线程中初始化,其他线程并不知晓,仍会对其尝试初始化。很多在不支持C++11标准的编译器上,在实践过程中,这样的条件竞争是确实存在的,为此,C++11 规定,如果 static 局部变量正在初始化,线程到达此处时,将等待其完成,从而避免了 race condition,只有一个全局实例时,对于C++11,可以直接用 static 而不需要 std::call_once,也就是说,在只需要一个全局实例情况下,可以成为std::call_once的替代方案,典型的就是单例模式了:

template <typename T>
class Singleton {
 public:
  static T& Instance();
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

 private:
  Singleton() = default;
  ~Singleton() = default;
};

template <typename T>
T& Singleton<T>::Instance() {
  static T instance;
  return instance;
}

今天的内容就到这里了。

参考:

std::call_once - C++中文 - api参考文档 (apiref.com)

到此这篇关于C++多线程std::call_once的使用的文章就介绍到这了,更多相关C++ std::call_once内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: C++多线程std::call_once的使用

本文链接: https://lsjlt.com/news/142233.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • C++多线程std::call_once的使用
    在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。很多时候我们为了初始化某些数据会写出如下代码,这些代码在单...
    99+
    2024-04-02
  • C++中多线程std::call_once怎么用
    这篇文章主要介绍了C++中多线程std::call_once怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些...
    99+
    2023-06-29
  • C++中std::thread线程怎么使用
    本篇内容主要讲解“C++中std::thread线程怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++中std::thread线程怎么使用”吧!1:std::thread的基本用法最简...
    99+
    2023-07-04
  • C++中std::thread线程用法
    目录1:std::thread的基本用法2:std:: thread常用的成员函数 3:建立新 thread执行类别中的函数 4:建立新 thread 执行 la...
    99+
    2023-01-08
    C++ std::thread线程 C++ std::thread
  • C++11 std::function和std::bind如何使用
    这篇文章主要介绍了C++11 std::function和std::bind如何使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++11 std::function和std::bind如...
    99+
    2023-07-05
  • C++的std::visit如何使用
    这篇文章主要介绍“C++的std::visit如何使用”,在日常操作中,相信很多人在C++的std::visit如何使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C++的std::visit如何使用”的疑...
    99+
    2023-06-29
  • C++的std::any怎么使用
    这篇文章主要介绍了C++的std::any怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++的std::any怎么使用文章都会有所收获,下面我们一起来看看吧。    一般来说,c+...
    99+
    2023-06-29
  • C++编程之 std::forward使用例子
    std::forward 是一个 C++11 中的模板函数,其主要作用是在模板函数或模板类中,将一个参数以“原样”(forward)的方式转发给另一个函数。通...
    99+
    2023-03-19
    C++ std::forward C++ std::forward使用
  • C++ std::thread 使用方法
    目录一、std::thread的构造和析构二、std::thread的成员函数三、线程间的通信四、线程的异常处理五、总结总结:C++是一种高级编程语言,被广泛用于开发高性能、大规模、...
    99+
    2023-03-19
    C++ std::thread使用 C++ std::thread
  • C++ std::thread怎么使用
    这篇文章主要介绍了C++ std::thread怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++ std::thread怎么使用文章都会有所收获,下面我们一起来看看吧。C++是一...
    99+
    2023-07-05
  • C++std::shared_mutex读写锁的使用
    目录0.前言1.认识std::shared_mutex2.实例演示0.前言 读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。C...
    99+
    2024-04-02
  • C++中std::conditional的使用说明
    目录std::conditional的使用具体用法具体理解为以下显示了更多的例子利用std::conditional实现变量的多类型std::conditional的使用 今天在项目...
    99+
    2024-04-02
  • C# 使用CancellationTokenSource取消多线程
    目录几点关键问题解释:同时取消多个线程:有时间我们在使用多线程的时候,需要取消线程的执行,可以使用CancellationTokenSource来取消对Task开辟多线程的取消 如下...
    99+
    2024-04-02
  • c++中的std有多少函数
    截至 c++23,标准库中约有 2000 个函数。这些函数分布在不同的头文件中,例如 用于算法, 用于字符串处理, 用于容器, 用于输入/输出, 用于时间和日期。 C++ 中 std...
    99+
    2024-05-01
    c++ 标准库
  • C#如何使用多线程中的lock
    小编给大家分享一下C#如何使用多线程中的lock,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!经常碰到同时需要对某个数据进行操作,或者对某个文件进行读写操作,对于...
    99+
    2023-06-17
  • C# 使用多线程的几种方式
    在C#中,有几种方式可以使用多线程:1. 使用Thread类:可以创建一个新线程并在其中执行指定的方法。可以使用Thread类来启动...
    99+
    2023-09-15
    C#
  • C#中多线程Tread的使用(推荐)
    首先是概念,什么是线程? 线程是操作系统分配CPU时间的基本单元,在一个进程中可以有多个线程同时执行代码。 谈一谈什么是进程? 简单的说,一个正在运行的应用程序可以视为一个进程,进程...
    99+
    2022-11-13
    C#多线程使用 C#多线程 Tread
  • C++中std::conditional如何使用
    本篇内容介绍了“C++中std::conditional如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!std::conditiona...
    99+
    2023-07-02
  • C#多线程编程Task如何使用
    这篇“C#多线程编程Task如何使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“C#多线程编程Task如何使用”文章吧。一...
    99+
    2023-06-29
  • C++编程之std::forward使用代码分析
    今天小编给大家分享一下C++编程之std::forward使用代码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。std:...
    99+
    2023-07-05
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作