顶部左侧内容
百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 在线教程 > 正文

C++ 多线程编程系列 | call_once确保多线程执行一次初始化

gosiye 2024-08-26 13:55 4 浏览 0 评论

5、std::call_once() (C++11)

std::call_once() 函数保证 f 只会被调用一次,即使在多线程环境也是如此。

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

比如下面的例子

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

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();
}

只会有一句打印

Simple example: called once

首先是std::call_once() 函数的第一个参数 std::once_flag,在 Linux 平台下,其实就是封装了 pthread_once_t 数据结构。

/// mutex
  struct once_flag
  {
    constexpr once_flag() noexcept = default;

    /// Deleted copy constructor
    once_flag(const once_flag&) = delete;
    /// Deleted assignment operator
    once_flag& operator=(const once_flag&) = delete;

  private:
    // For gthreads targets a pthread_once_t is used with pthread_once, but
    // for most targets this doesn't work correctly for exceptional executions.
    __gthread_once_t _M_once = __GTHREAD_ONCE_INIT;

    struct _Prepare_execution;

    template<typename _Callable, typename... _Args>
      friend void
      call_once(once_flag& __once, _Callable&& __f, _Args&&... __args);
  };

std::once_flag::_Prepare_execution 使用 RAII 机制,设置 __once_callable 和 __once_call 两个变量。这两个变量是 TLS 变量,每个线程独享,某个线程修改不会影响到其他线程。

/// mutex
  extern __thread void* __once_callable;
  extern __thread void (*__once_call)();

  // RAII type to set up state for pthread_once call.
  struct once_flag::_Prepare_execution
  {
    template<typename _Callable>
      explicit
      _Prepare_execution(_Callable& __c)
      {
    // Store address in thread-local pointer:
    __once_callable = std::__addressof(__c);
    // Trampoline function to invoke the closure via thread-local pointer:
    __once_call = [] { (*static_cast<_Callable*>(__once_callable))(); };
      }

    ~_Prepare_execution()
    {
      // PR libstdc++/82481
      __once_callable = nullptr;
      __once_call = nullptr;
    }

    _Prepare_execution(const _Prepare_execution&) = delete;
    _Prepare_execution& operator=(const _Prepare_execution&) = delete;
  };

__once_callable 和 __once_call 的定义在 mutex.cc 这个文件里,另外 __once_proxy() 函数就是调用 __once_call 指向的函数。

/// mutex.cc
  __thread void* __once_callable;
  __thread void (*__once_call)();

  extern "C" void __once_proxy()
  {
    // The caller stored a function pointer in __once_call. If it requires
    // any state, it gets it from __once_callable.
    __once_call();
  }

std::call_once() 函数的定义就比较简单

  • 首先将传入的 f 和 args 用 lambda 表达式封装成 callable
  • 用 callable 初始化一个 once_flag::_Prepare_execution 对象
  • 最后调用 pthread_once() 函数,执行 __once_proxy() 函数

从前面的分析,__once_proxy() 函数最终执行用户传入的 f 和 args。

/// mutex
  template<typename _Callable, typename... _Args>
    void
    call_once(once_flag& __once, _Callable&& __f, _Args&&... __args)
    {
      // Closure type that runs the function
      auto __callable = [&] {
      std::__invoke(std::forward<_Callable>(__f),
            std::forward<_Args>(__args)...);
      };

      once_flag::_Prepare_execution __exec(__callable);

      // XXX pthread_once does not reset the flag if an exception is thrown.
      if (int __e = __gthread_once(&__once._M_once, &__once_proxy))
    __throw_system_error(__e);
    }

std::call_once() 有一个 BUG,会一直阻塞调用线程。按照官方说法,如果抛出异常,后续调用可以继续执行。

If that invocation throws an exception, it is propagated to the caller of std::call_once, and flag is not flipped so that another call will be attempted (such a call to std::call_once is known as exceptional??).

使用 WSL(Debian) GCC12.2 测试,如果抛出异常,下一次执行将一直阻塞。比如下面也是官方的例子

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

std::once_flag flag2;

void may_throw_function(bool do_throw) {
  if (do_throw) {
    // this may appear more than once
    std::cout << "throw: call_once will retry\n";
    throw std::exception();
  }
  // guaranteed once
  std::cout << "Did not throw, call_once will not attempt again\n";
}

void do_once(bool do_throw) {
  try {
    std::call_once(flag2, 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();
}

只会打印一句,然后程序阻塞

throw: call_once will retry

原因出在 pthread_once() 函数上,pthread_once() 在抛出异常时,不会清理 flag 的值

#include <pthread.h>
#include <stdio.h>

pthread_once_t flag_ = PTHREAD_ONCE_INIT; // 0

int call_count = 0;
extern "C" void func_() {
  printf("Inside func_ call_count %d\n", call_count);
  if (++call_count < 2)
    throw 0; // exception
}
int main() {
  printf("Before calling call_once flag_: %d\n", *(int*)&flag_);
  try {
    pthread_once(&flag_, func_);
  } catch (...) {
    printf("Inside catch all excepton flag_: %d\n", *(int*)&flag_);
    // flag_ = PTHREAD_ONCE_INIT;
  }
  printf("before the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
  pthread_once(&flag_, func_);
  printf("after the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
}

阻塞在第二次调用 pthread_once() 函数上,输出为:

Before calling call_once flag_: 0
Inside func_ call_count 0
Inside catch all excepton flag_: 1
before the 2nd call to call_once flag_: 1

可以看到,第二次调用 pthread_once() 函数时,flag 的值为 1,而不是 0。另外,如果不抛出异常,flag 的值应该是多少呢?

#include <pthread.h>
#include <stdio.h>

pthread_once_t flag_ = PTHREAD_ONCE_INIT; // 0

int call_count = 0;
extern "C" void func_() {
  printf("Inside func_ call_count %d\n", call_count);
  // if (++call_count < 2)
  //   throw 0; // exception
}
int main() {
  printf("Before calling call_once flag_: %d\n", *(int*)&flag_);
  try {
    pthread_once(&flag_, func_);
  } catch (...) {
    printf("Inside catch all excepton flag_: %d\n", *(int*)&flag_);
    // flag_ = PTHREAD_ONCE_INIT;
  }
  printf("before the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
  pthread_once(&flag_, func_);
  printf("after the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
}

可以看到,将抛出异常注释掉,flag 最后是 2。

Before calling call_once flag_: 0
Inside func_ call_count 0
before the 2nd call to call_once flag_: 2
after the 2nd call to call_once flag_: 2

所以,使用 std::call_once() 时,不能有异常抛出。

相关推荐

全球最大的H5网站模板库(h5页面模板下载)

当今社会,互联网迅猛发展,在网络营销中,客户往往通过企业的网站建设留下对该企业的第一印象,一个优秀的企业网站已成为企业发展的重要纽带,嗨创H5,拥有国内外一流的技术团队,潜心专研网站建设6年,是全球最...

wordpress集团公司网站模板:XSgr(wordpress建站公司)

小兽wordpress推出一款高端集团公司主题,打造高品质官网。高端是一种态度和坚持,因为我坚信贴合产品及品牌理念的高端深度定制才能最大化地呈现企业的务实严谨与产品的专业品质相比,某种程度上讲–...

私心推荐,小编酷爱的五款高逼格网站模板

建站宝盒的网站模板上千套之多,各有各的风格色彩,但是,弱水三千,小编我却只取一瓢饮,在这上千套模板之中,小编酷爱的网站模板有五套,让小编私心推荐一下吧!1、茶叶贸易公司网站模板小编对这款网站模板可是一...

「书讯」政府网站用户行为研究与应用

《政府网站用户行为研究与应用》作者:刘合翔著出版日期:2018年6月开本:16开出版社:经济管理出版社小编推荐《政府网站用户行为研究与应用》的主题是关于政府网站用户行为的特征规律及其在政府网站优...

免费服务器-搭建模板网站的操作流程(图文版)

之前发文《创业者的官网:如何搭建免费云服务器及操作面板(图文版)》,因为做了视频才发现,创业者对视频的需求,远远低于对图文解说的需求。因此,补充图文教程,不清楚的看官们,可以直接看视频版本进行细部学...

快收藏这些高逼格H5网站模板吧,不绕弯子直接下载

上面这些响应式H5网站是不是很炫酷,比起那些“在线一键生成”是不是好太多了?关键是,那些一键制作都不会开放源码给你,自定义性也很局限。不过说到底还是难看。今天笔者推荐大家一个模板网站,全都是高质量的响...

如何开发网站建设管理系统模板(如何开发网站建设管理系统模板图片)

根据用户网站需求文档设计美工图,并设计数据库结构,让网站开发人员可以更多地关注前台美工,先对照美工图,编写静态HTML页面,按网站建设管理系统模板语法,修改编写好的静态HTML页面,运行。不再需要对...

C语言的数据类型介绍(c语言的数据类型介绍是什么)

在计算机系统中,数据是放在内存中的,数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别,那么0001000该理解为数字8呢,还是图像中某个像素的颜色...

C 语言格式化输出函数中常用的格式符号

在之前介绍输入输出函数的文章中,有提到格式化输入输出函数都有包含一种特殊的符号——格式符号。那篇文章中关于格式符号也只是一笔带过,没有进行深入挖掘。本篇文章主要对输出函数(printf)中的一些常用格...

C#中的类型转换(c#数据转换类)

计算机存储的基本单位:字节我们知道一个字节(Byte)有8个比特(bit)构成,比特是存储的最小单位,表示0和1,但为什么计算机存储的基本单位是字节,而不是比特呢?假设我们要存储数字3(二进制:11...

Java8中String内存空间占用分析(电脑里下载的文件怎样删除才不会占用内存空间)

1.前言分析之前,简单回顾一下对象的内存分布。在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三部分:对象头、实例数据和对齐填充。对象头包含两部分内容:MarkWord和类型指针。实例数据...

「每日C语言」数据类型大小和取值范围

对于c语言来说,数据类型是一个很重要的概念和知识点,它涉及到的是内存的空间,这在和硬件交互的时候是非常重要的。K&R给出了7个数据类型相关的关键字,分别是:int、long、short、uns...

【c语言学习笔记】数据类型(c语言里面的数据类型)

c语言学习笔记,欢迎大家能在评论区提出我学习错误的地方方便我进行改正~在计算机中,计算机用二进制来储存数据,在c语言中有许多的数据类型用来存储数据,当然不同的数据类型所用的内存占用也不一样,下面就来用...

关于MySQL varchar类型最大值,原来一直都理解错了

我是架构精进之路,点击上方“关注”,坚持每天为你分享技术干货,私信我回复“01”,送你一份程序员成长进阶大礼包。写在前面关于MySQLvarchar字段类型的最大值计算,也许我们一直都理解错误了,...

C语言数据类型的转换(c语言数据类型的转换方式)

类型转换在C语言程序中,经常需要对不同类型的数据进行运算,为了解决数据类型不一致的问题,需要对数据的类型进行转换。例如一个浮点数和一个整数相加,必须先将两个数转换成同一类型。C语言程序中的类型...

取消回复欢迎 发表评论: