SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++/Concurrency

[Concurrency] std::call_once - 중복 초기화 해소 기술

보리남편 김 주부 2023. 12. 12. 09:00

중복으로 초기화를 해야 하는 경우 보통 어떻게 하시나요?

저는 이럴 경우 flag를 둬서 두 번 호출하게 하지 않거나, 기존 함수를 한 번만 호출되는 함수와 두 번 호출되는 함수로 분리하는 방식으로 수정했었습니다. 여러분은 어떻게 하시나요?

 

생각해 봅시다.

예를 들어 아래 foo 함수 내에서 init 그리고 work 함수를 호출하는 foo를 멀티 스레드로 동작시키는 경우를 생각해 보자.

foo -> init -> work

아래와 같이 실행이 될 텐데

Thread 1 : foo -> init -> work
Thread 2 : foo -> init -> work

예제 1
foo를 다수의 스레드로 실행하여 스레드 수만큼 init이 실행되는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;

void work()
{
    std::cout << "work" << std::endl;
    std::this_thread::sleep_for(1s);
}

void init(int a, double d)
{
    std::cout << "init" << std::endl;
    std::this_thread::sleep_for(2s);
}
void foo()
{
    std::cout << "start foo" << std::endl;
    init(10, 3.4);
    work();
    std::cout << "finish foo" << std::endl;
}
int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
    t1.join();
    t2.join();
}

 

결과
start foostart foo
init

init
work
work
finish foofinish foo

 

위 예제처럼 init 이 중복으로 실행이 된다.

 

init 함수는 이름처럼 work를 하기 전에 한 번만 실행하면 되는 경우라고 했을 때 보통 어떻게 수정하면 될까?


 

보통은 아래처럼 함수 자체를 수정하여 스레드로 실행하기 편하게 수정할 것이다.

foo -> init  -> Thread 1 : work
                   -> Thread 2 : work

 

std::call_once() ☜(゚ヮ゚☜)

하지만 내부 구현 상황에 따라 foo에서 스레드로 실행해야 할 때가 있을 수 있다. 이처럼 여러 개의 스레드로 실행되지만 '초기화 작업은 한 번만 수행'되기 원할 때 std::call_once()를 사용하면 된다.

 

예제 2
예제 1을 std::call_once()를 이용해서 init이 한 번만 호출되게 만든 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;

std::once_flag flag; //1. 전역 변수

void init(int a, double d)
{
    std::cout << "init" << std::endl;
    std::this_thread::sleep_for(2s);
}

void work()
{
    std::cout << "work" << std::endl;
    std::this_thread::sleep_for(1s);
}

void foo()
{
    std::cout << "start foo" << std::endl;

    // init(10, 3.4);     
    std::call_once(flag, init, 10, 3.4);

    work();
    std::cout << "finish foo" << std::endl;
}


int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
    t1.join();
    t2.join();
}

 

결과
start foostart foo

init
work
work
finish foofinish foo

 

결과에서 보시다시피 이렇게 호출되는 함수는 스레드가 몇 개가 있던 최초 1회만 호출되며 처음 도착한 스레드에 의해서 호출되고 나중에 도착한 스레드는 init 종료 시까지 대기한다.

중요코드 설명

std::once_flag init_flag; // 전역변수
std::call_once를 위한 구조체이며  “복사 와 이동을 모두 삭제(=delete)” 되어 있다.
_EXPORT_STD struct once_flag { // opaque data structure for call_once()
    constexpr once_flag() noexcept : _Opaque(nullptr) {}

    once_flag(const once_flag&)            = delete;
    once_flag& operator=(const once_flag&) = delete;

    void* _Opaque;
};

 

함수의 delete 선언에 대해 알고 싶다면
참고 : https://progtrend.blogspot.com/2017/03/deleted-functions.html

 

std::call_once(init_flag, init, 10, 3.4);
<mutex> 헤더 필요
std::once_flag를 첫 번째 인자로 넘기고, 한 번만 실행할 함수명을 인자와 함께 넘긴다.

이처럼 좋은 표준 기술을 통해 중복 초기화 문제를 깔끔하게 해결해 나가자.

 

참고


* 본 글에 예제 코드는 코드누리 교육을 받으면서 제공된 샘플코드를 활용하였습니다.

728x90