百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

c++ 疑难杂症(13) call_once c++常见问题解决

yuyutoo 2024-10-31 16:41 2 浏览 0 评论

0.问题

如果一些功能在使用之前, 需要先初始化, 但是又不能重复初始化, 那么应该怎样处理。

  • 在程序入口处, 直接初始化
int main() {
    //初始化操作..
    //...
}

优势是简单明了, 没有多线程之类的困扰。

有时候这不是很好的选择, 如要求功能使用之前, 才初始化;

有可能是生命周期都不使用这个功能, 初始化了可能会占用些资源或影响之类的。

  • 使用静态变量
void init_once() {
    
    static std::atomic_bool _a(false);
    if (std::atomic_exchange(&_a, true)) {
        return;
    }
    //初始化操作
    std::cout << "init_once" << std::endl;
}

void init_once2() {
    
    static bool _init(false);
    if (_init) {
        return;
    }

    static std::mutex _m;
    std::lock_guard<std::mutex> lock(_m);
    if (_init) {
        return;
    }
    //初始化操作
    //成功之后,设置_init为true;
    std::cout << "init_once" << std::endl;
    _init = true;
}

void init_once3() {

    class A {
    public:
        A() {
            //初始化操作
            std::cout << "init_once" << std::endl;
        }
    };
    static A _a;
}

init_once() 有个问题, 如一个线程还在初始化中(比较耗时的), 下一个线程就直接返回了,调用功能,那会出错的。

init_once2() 就线程安全了,保证在初始化完成之前,线程都在等待中,只是实现不大优雅。

那么STL 应该会给我们准备些什么吧, 毕竟都c++20了。

经查找, 还真是有准备, 在c++11 时就引入了 std::call_once, 来学习学习....

1.标准文档

//在标头 `<mutex>` 定义
//(C++11 起)

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

准确执行一次可调用对象 f,即使同时从多个线程调用。

准确地说:

  • 如果在调用 std::call_once 的时刻,flag 指示 f 已经调用过,那么 std::call_once 会立即返回(称这种对 std::call_once 的调用为消极)。
  • 否则,std::call_once 会调用 *INVOKE*(std::forward<Callable>(f), std::forward<Args>(args)...)。与 std::thread 的构造函数或 std::async 不同,不会移动或复制参数,因为不需要转移它们到另一执行线程(称这种对 std::call_once 的调用为积极)。

同一 flag 上的所有积极??调用组成单独全序,它们由零或多个异常??调用后随一个返回??调用组成。该顺序中,每个积极??调用的结尾同步于下个积极??调用。

返回??调用的返回同步于同一 flag 上的所有消极??调用:这表示保证所有对 std::call_once 的同时调用都观察到积极??调用所做的任何副效应,而无需额外同步。

参数

flag

对象,对于它只有一个函数得到执行

f

要调用的可调用对象

args...

传递给函数的参数

异常

  • 如果有任何条件阻止对 std::call_once 的调用按规定执行,那么就会抛出 std::system_error
  • 任何 f 抛出的异常

注解

如果对 std::call_once 进行并发调用时分别传递不同的函数 f ,那么哪个 f 将被执行是不明确的。被选中的函数会在与之对应的 std::call_once 的被调用线程中执行。

由于函数局域静态对象的初始化在多线程调用下也保证只触发一次,这可能比使用 std::call_once 的等价代码更为高效。

此函数在 POSIX 中类似 pthread_once

示例

#include <iostream>
#include <mutex>
#include <thread>
 
std::once_flag flag1, flag2;
 
void simple_do_once()
{
    std::call_once(flag1, [](){ std::cout << "简单样例:调用一次\n"; });
}
 
void may_throw_function(bool do_throw)
{
    if (do_throw)
    {
        std::cout << "抛出:call_once 会重试\n"; // 这会出现不止一次
        throw std::exception();
    }
    std::cout << "没有抛出,call_once 不会再重试\n"; // 保证一次
}
 
void do_once(bool do_throw)
{
    try
    {
        std::call_once(flag2, may_throw_function, do_throw);
    }
    catch (...) {}
}
 
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();
 
    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();
}

可能的输出:

简单样例:调用一次
抛出:call_once 会重试
抛出:call_once 会重试
抛出:call_once 会重试
没有抛出,call_once 不会再重试

2.探索

通过查看linux/windows平台的实现, 大概是:

  • linux 平台 内部封装使用了 pthread_once

pthread_once 是 POSIX 线程库(pthreads)中的一个函数,它提供了一种机制来确保一个特定的初始化函数在一个进程中的多个线程中只被执行一次。这对于需要初始化一些资源(如内存分配、文件打开、锁初始化等)并且这些资源只需要被初始化一次的场景非常有用。

pthread_once 函数定义如下:

pthread_once 是 POSIX 线程库(pthreads)中的一个函数,它提供了一种机制来确保一个特定的初始化函数在一个进程中的多个线程中只被执行一次。这对于需要初始化一些资源(如内存分配、文件打开、锁初始化等)并且这些资源只需要被初始化一次的场景非常有用。

pthread_once 函数定义如下:

#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

参数解释

once_control:是一个 pthread_once_t 类型的指针,它指向一个控制变量,用于跟踪初始化函数是否已经执行过。这个变量应该在调用 pthread_once 之前被初始化为 PTHREAD_ONCE_INIT

init_routine:是一个没有参数且返回类型为 void 的函数指针,该函数包含了需要执行的初始化代码。

pthread_once 的行为是这样的:

如果 init_routine 还没有被调用过(即初始化还没有完成),则 pthread_once 会调用 init_routine,并且确保它只被调用一次,即使在多个线程中同时调用 pthread_once

如果 init_routine 已经被调用过了(即初始化已经完成),则 pthread_once 不会再次调用它。

这个机制是线程安全的,因此它可以在多线程环境中安全地使用,而无需额外的同步机制。

下面是一个简单的示例,展示了如何使用 pthread_once 来确保一个函数只被初始化一次:

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

pthread_once_t once_control = PTHREAD_ONCE_INIT;

void initialize() {
    printf("Initializing...\n");
    // 执行一些初始化代码...
}

void *thread_function(void *arg) {
    // 在多个线程中调用 pthread_once
    pthread_once(&once_control, initialize);

    // 执行线程的其他任务...
    printf("Thread running...\n");
    return NULL;
}

int main() {
    pthread_t threads[5];
    int rc;
    int i;

    for (i = 0; i < 5; i++) {
        rc = pthread_create(&threads[i], NULL, thread_function, NULL);
        if (rc) {
            printf("Error: return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }

    // 等待所有线程完成
    for (i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

在这个示例中,即使我们创建了 5 个线程,并且每个线程都调用了 pthread_once 来初始化 initialize 函数,initialize 函数也只会被执行一次。

  • windows平台 内部使用了 InitOnceBeginInitialize / InitOnceComplete
//https://learn.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-initoncebegininitialize
//开始一次性初始化
BOOL InitOnceBeginInitialize(
  [in, out]       LPINIT_ONCE lpInitOnce,
  [in]            DWORD       dwFlags,
  [out]           PBOOL       fPending,
  [out, optional] LPVOID      *lpContext
);

//https://learn.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-initoncecomplete
//完成从 InitOnceBeginInitialize 函数开始的一次性初始化。
BOOL InitOnceComplete(
  [in, out]      LPINIT_ONCE lpInitOnce,
  [in]           DWORD       dwFlags,
  [in, optional] LPVOID      lpContext
);

c++ 疑难杂症(3) 模板特化

c++ 疑难杂症(2) std::move

c++ 疑难杂症(6) std::map

c++ 疑难杂症(5) std::pair

c++ 疑难杂症(7) std::tuple

c++ 疑难杂症(1) std::thread

c++ 疑难杂症(9) std::array

c++ 疑难杂症(4) std:vector

c++ 疑难杂症(8) std::multimap

c++ 疑难杂症(13) allocator

c++ 疑难杂症(11) std::forward_list

c++ 疑难杂症(10) std::initializer_list

c++ 疑难杂症(12) unordered_map

相关推荐

史上最全的浏览器兼容性问题和解决方案

微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...

平面设计基础知识_平面设计基础知识实验收获与总结
平面设计基础知识_平面设计基础知识实验收获与总结

CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...

2025-02-21 16:01 yuyutoo

写作排版简单三步就行-工具篇_作文排版模板

和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...

写一个2048的游戏_2048小游戏功能实现

1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...

今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……

  最近的天气暖和得让很多小伙伴们喊“热”!!!  昨天的气温到底升得有多高呢?你家有没有榜上有名?...

CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式

之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...

柠檬科技肖勃飞:大数据风控助力信用社会建设

...

你的自我界限够强大吗?_你的自我界限够强大吗英文

我的结果:A、该设立新的界限...

行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?

行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...

让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华

去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...

2025-02-21 16:00 yuyutoo

今年国家综合性消防救援队伍计划招录消防员15000名

记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...

一起盘点最新 Chrome v133 的5大主流特性 ?

1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...

竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使

style="text-align:center;"data-mce-style="text-align:...

学物理能做什么?_学物理能做什么 卢昌海

作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...

你不知道的关于这只眯眼兔的6个小秘密
你不知道的关于这只眯眼兔的6个小秘密

在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...

2025-02-21 16:00 yuyutoo

取消回复欢迎 发表评论: