C++20부터 latch and barrier 가 필요한 이유?
2023.11.28 - [L C++] - [Concurrency] std::jthread - join 없이 사용하기📌
C++ 20부터 join을 알아서 해주는 jthread가 생겼다. 하지만 기존 thread를 대체하기에는 아직 문제가 있는데 어떤 문제가 있는지 아래 의사코드(pseudocode)에서 확인해 보자.
main()
{
std::thread()
together_working()
std::join()
after_working()
}
: thread와 동시에 together_working이 실행되고, 스레드 종료 이후에 after_working이 실행되었던 것을
main()
{
std::jthread()
together_working()
after_working()
}
jthread로 변경하면 대기 없이 thread와 동시에 togerther_working, after_working 순으로 실행된다. (멈추게 할 수 있는 방법이 없다.) 주 스레드 종료 전 다른 스레드가 종료되는 걸 기다리기 위해 join이 존재하지만 사용하다 보면 이렇듯 (주 목적 외에) 다른 목적으로도 활용되기에 대안이 필요하다.
그래서 C++20에서는 jthread와 함께 스레드 조정 메커니즘 Latches과 Barriers를 제공하고 있다.
latch와 barrier는 카운팅 기반의 간단한 동기화 도구이며 하나씩 알아보자
이 도구는 내부에 condition_variable와 mutex로 동작하기에 이 도구가 없어도 condition_variable로 구현은 가능하다.
std::latch
since c++20
latch를 번역하면 '걸쇠'라는 뜻으로 걸쇠처럼 원하는 조건에서 스레드를 차단할 수 있다.
출처: 위키백과
File:Öffnen eines Riegels.gif - Wikimedia Commons
No higher resolution available.
commons.wikimedia.org
기존 thread 동작을 latch를 이용해서 join처럼(스레드가 끝날 때까지 기다리는 코드) 변경해 보자.
예제 1-1
kim, lee, park의 work가 각각의 thread로 시작하고 thread가 종료되면 go home 로그를 찍는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <latch>
void foo(const std::string& name)
{
std::cout << "start work : " << name << std::endl;
std::cout << "finish work : " << name << std::endl;
}
int main()
{
std::thread t1(foo, "kim"),
t2(foo, "lee"),
t3(foo, "park");
t1.join();
t2.join();
t3.join();
std::cout << "go home" << std::endl;
}
결과
start work : kim
start work : park
finish work : park
finish work : kim
start work : lee
finish work : lee
go home
이 코드를 latch를 이용하여 수정해 보자.
예제 1-2
latch를 이용하여 예제 1-1과 동일하게 동작하게 하는 프로그램 코드이다.
#include <iostream>
#include <thread>
//1.
#include <latch>
//2.
std::latch complete_latch(3);
void foo(const std::string& name)
{
std::cout << "start work : " << name << std::endl;
std::cout << "finish work : " << name << std::endl;
//4.
complete_latch.count_down();
}
int main()
{
//std::thread t1(foo, "kim"),
// t2(foo, "lee"),
// t3(foo, "park");
std::jthread t1(foo, "kim"),
t2(foo, "lee"),
t3(foo, "park");
//t1.join();
//t2.join();
//t3.join();
// 3.
complete_latch.wait();
std::cout << "go home" << std::endl;
}
결과
start work : kim
start work : park
finish work : park
finish work : kim
start work : lee
finish work : lee
go home
foo 함수를 kim, lee, park 각각 스레드로 실행시키고 wait() 함수에서 대기를 하다 count_down() 함수에서 latch 카운터를 '1'씩 차감하고 latch 카운터 수(3) 만큼 호출이 되면(count ==0) 대기가 풀려서 wait() 아래 줄부터 실행이 된다.
코드리뷰
1. #include <latch>
별도의 클래스로 되어 있기에 include를 해야 한다.
2. std::latch complete_latch(3);
숫자를 입력하면 latch 내부적으로 '3'을 보관한다.
3. complete_latch.wait();
join의 역할은 아니지만 정의된 latch 카운트가 '0'이 될 때까지 대기한다.
4. complete_latch.count_down();
count_down 함수가 호출될 때마다 latch 카운트를 '1' 씩 차감한다.
멀티 스레드 내에서 실행되는 함수의 동작 싱크 시키기
멀티 스레드 내에서 실행되는 함수의 동작 싱크가 필요할 때가 있다. 이 때는 다음과 같이 사용하면 된다.
예제 2
3개(kim, lee, park)의 스레드로 실행되는 함수 foo 내부 동작을 싱크 시키기 위한 프로그램 코드이다.
#include <iostream>
#include <latch>
#include <thread>
std::latch sync_point(3);
//std::latch sync_point{ 3 }; //동일한 코드
void foo(std::string name)
{
std::cout << "start work : " << name << std::endl;
std::cout << "finish work : " << name << std::endl;
sync_point.count_down();
sync_point.wait();
// sync_point.arrive_and_wait();
std::cout << "go home : " << name << std::endl;
}
int main()
{
std::jthread t1(foo, "kim"), t2(foo, "lee"), t3(foo, "park");
}
결과
start work : kim
start work : park
finish work : park
start work : lee
finish work : lee
finish work : kim
go home : kim
go home : park
go home : lee
latch 카운터로 정의한 수(3)에서 각 스레드 별로 count_down()로 '1'을 차감하고 wait() 함수에서 대기를 하다 3번째로 count_down()가 호출될 때 일괄적으로 wait()이 풀려서 go home 로그는 이후에 같이 실행되는 것을 확인할 수 있다.
위 코드는 대기와 카운터 감소 함수가 나눠져 있는데 arrive_and_wait() 함수는 두 가지를 한 번에 하는 함수로 위 코드를 아래와 같이 수정하면 된다.
//as-is
sync_point.count_down();
sync_point.wait();
// sync_point.arrive_and_wait();
//to-be
// sync_point.count_down();
// sync_point.wait();
sync_point.arrive_and_wait();
std::barrier
since c++ 20
barrier는 여러 번 사용가능한 latch이다.
만약 멀티 스레드 내에서 실행되는 함수의 동작 싱크가 한 번이 아닌 여러 번 해야 한다면 latch는 불가능하고 barrier를 사용해야 한다.
그래서 latch는 일회용 스레드 장벽, barrier는 재사용 가능한 스레드 장벽이다.
예제 3
예제 2처럼 함수 foo 내부 동작을 싱크 시키기 위해 스레드를 멈추게 할 수 있는데 여러 번 싱크를 맞출 수 있게 barrier를 사용한 프로그램 코드이다.
#include <iostream>
#include <mutex>
#include <thread>
//1.
#include <barrier>
// 3.
void arrive_sync_point() noexcept
{
std::cout << "synchronize!" << std::endl;
}
// 2.
//std::barrier sync_point(3);
std::barrier sync_point(3, arrive_sync_point );
void foo(std::string name)
{
std::cout << "start work : " << name << std::endl;
std::cout << "finish work : " << name << std::endl;
sync_point.arrive_and_wait();
std::cout << "have dinner : " << name << std::endl;
//4.
// latch에서 arrive_and_wait 하면 대기가 풀리지 않는다.
sync_point.arrive_and_wait();
std::cout << "go home : " << name << std::endl;
}
int main()
{
std::jthread t1(foo, "kim"), t2(foo, "lee"), t3(foo, "park");
}
결과
start work : kim
start work : park
finish work : park
start work : lee
finish work : lee
finish work : kim
synchronize!
have dinner : kim
have dinner : park
have dinner : lee
synchronize!
go home : lee
go home : park
go home : kim
synchronize! 로그가 찍혀 있는 곳을 보면 start/finish work -> synchronize, have dinner -> synchronize, go home으로 스레드 별 두 번 싱크가 맞춰진 것을 확인할 수 있다.
코드리뷰
1. #include <barrier>
별도의 탬플릿 클래스로 되어 있기에 include를 해야 한다.
2. std::barrier sync_point(3, arrive_sync_point );
barrier가 템플릿 클래스인 이유가 두 번째 인자로 받는 함수가 템플릿으로 되어 있기 때문이고, latch처럼 아래와 같이 카운터 숫자만 정의도 가능하다.
std::barrier sync_point(3);
두 번째 인자는 카운터가 '0'이 될 때 호출이 된다.
3. void arrive_sync_point() noexcept
barrier에 2번째 인자로 전달되는 함수는 대기가 풀릴 때 호출 되며 예외가 없는 함수만 가능하다. (noexcept 필요)
4. sync_point.arrive_and_wait();
첫 번째 arrive_and_wait()에서 카운터가 '0'이 되어 대기가 풀리면, 카운터가 다시 '3'이 되어 다음 arrive_and_wait()에서 설정된 카운터만큼 호출될 때까지 대기를 하게 된다. ( 만약 위 예제에서 latch를 사용한다면 두 번째 arrive_and_wait에서 대기가 풀리지 않음)
ps. 회사에서는 C++20을 사용하지 않아서 jthread를 정리할 때 본 문제를 고려하지 못했는데 C++20 이상을 지원하는 환경을 사용하는 분들께 도움이 되었으면 한다.
참고
* 본 글에 예제 코드는 코드누리 교육을 받으면서 제공된 샘플코드를 활용하였습니다.
'L C++ > Concurrency' 카테고리의 다른 글
[Concurrency] 동기화 기본 요소(synchronization primitive) 정리 (0) | 2024.01.19 |
---|---|
[Concurrency] ❇ std::atomic ❇의 이해 (0) | 2024.01.16 |
[Concurrency] mutex의 lock/unlock 관리 도구 소개 (2/2) (2) | 2024.01.02 |
[Concurrency] mutex의 lock/unlock 관리 도구 소개 (1/2) (4) | 2023.12.29 |
[Concurrency] 다양한 mutex 소개 (2) | 2023.12.19 |