중복으로 초기화를 해야 하는 경우 보통 어떻게 하시나요?
저는 이럴 경우 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
'L C++ > Concurrency' 카테고리의 다른 글
[Concurrency] mutex의 lock/unlock 관리 도구 소개 (1/2) (4) | 2023.12.29 |
---|---|
[Concurrency] 다양한 mutex 소개 (2) | 2023.12.19 |
[Concurrency] thread_local - thread 전용 변수 ✔ (0) | 2023.12.05 |
[Concurrency] std::jthread - join 없이 사용하기📌 (0) | 2023.11.28 |
[Concurrency] std::async - 기존 함수 그대로 thread에 적용(2) (2) | 2023.11.21 |