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

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

yuyutoo 2024-10-31 16:41 3 浏览 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

相关推荐

【Socket】解决UDP丢包问题

一、介绍UDP是一种不可靠的、无连接的、基于数据报的传输层协议。相比于TCP就比较简单,像写信一样,直接打包丢过去,就不用管了,而不用TCP这样的反复确认。所以UDP的优势就是速度快,开销小。但是随之...

深入学习IO多路复用select/poll/epoll实现原理

Linux服务器处理网络请求有三种机制,select、poll、epoll,本文打算深入学习下其实现原理。0.结论...

25-1-Python网络编程-基础概念

1-网络编程基础概念1-1-基本概念1-2-OSI七层网络模型OSI(开放系统互联)七层网络模型是国际标准化组织(ISO)提出的网络通信分层架构,用于描述计算机网络中数据传输的过程。...

Java NIO多路复用机制

NIO多路复用机制JavaNIO(Non-blockingI/O或NewI/O)是Java提供的用于执行非阻塞I/O操作的API,它极大地增强了Java在处理网络通信和文件系统访问方面的能力。N...

Python 网络编程完全指南:从零开始掌握 Socket 和网络工具

Python网络编程完全指南:从零开始掌握Socket和网络工具在现代应用开发中,网络编程是不可或缺的技能。Python提供了一系列高效的工具和库来处理网络通信、数据传输和协议操作。本指南将从...

Rust中的UDP编程:高效网络通信的实践指南

在实时性要求高、允许少量数据丢失的场景中,UDP(用户数据报协议)凭借其无连接、低延迟的特性成为理想选择。Rust语言凭借内存安全和高性能的特点,为UDP网络编程提供了强大的工具支持。本文将深入探讨如...

Python 网络编程的基础复习:理解Socket的作用

计算机网络的组成部分在逻辑上可以划分为这样的结构五层网络体系应用层:应用层是网络协议的最高层,解决的是具体应用问题...

25-2-Python网络编程-TCP 编程示例

2-TCP编程示例应用程序通常通过“套接字”(socket)向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通信。Python语言提供了两种访问网络服务的功能。...

linux下C++ socket网络编程——即时通信系统(含源码)

一:项目内容本项目使用C++实现一个具备服务器端和客户端即时通信且具有私聊功能的聊天室。目的是学习C++网络开发的基本概念,同时也可以熟悉下Linux下的C++程序编译和简单MakeFile编写二:需...

Python快速入门教程7:循环语句

一、循环语句简介循环语句用于重复执行一段代码块,直到满足特定条件为止。Python支持两种主要的循环结构:for循环和while循环。...

10分钟学会Socket通讯,学不会你打我

Socket通讯是软硬件直接常用的一种通讯方式,分为TCP和UDP通讯。在我的职业生涯中,有且仅用过一次UDP通讯。而TCP通讯系统却经常写,正好今天写了一个TCP通讯的软件。总结一下内容软件使用C#...

Python 高级编程之网络编程 Socket(六)

一、概述Python网络编程是指使用Python语言编写的网络应用程序。这种编程涉及到网络通信、套接字编程、协议解析等多种方面的知识。...

linux网络编程Socket之RST详解

产生RST的三个条件:1.目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;2.TCP想取消一个已有的连接;3.TCP接收到一个根本不存在的连接上的分节;现在模拟上面的三种情况:cl...

ABB机器人编程实用技巧,多项案例

...

Python中实现Socket通讯(附详细代码)

套接字(socket)是一种在计算机网络中进行进程间通信的方法,它允许不同主机上的程序通过网络相互通信。套接字是网络编程的基础,几乎所有的网络应用程序都使用某种形式的套接字来实现网络功能。套接字可以用...

取消回复欢迎 发表评论: