#!if 문서명2 != null
, [[라이브러리]]
#!if 문서명3 != null
, [[스레드(컴퓨터 용어)]]
#!if 문서명4 != null
, [[멀티코어 프로세서]]
#!if 문서명5 != null
, [[멀티태스킹(컴퓨터 공학)]]
#!if 문서명6 != null
, [[]]
1. 개요2. 기본적인 사용방법
2.1. std::thread2.2. 예제 1: 스레드 클래스의 생성과 제거2.3. 예제 2: 외부 의존성이 없는 인자 전달2.4. 예제 3: 병렬 파일 입출력2.5. 예제 4: 외부 의존성이 있는 인자 전달
3. 다중 스레드 프로그래밍 기법4. 메모리 일관성 문제5. 임계 영역과 경쟁 상태6. 교착 상태7. std::jthread7.1. 예제 7: std::jthread와 std::stop_token 연계
8. 메모리와 성능의 연관성9. 동시성 자료구조10. 원자적 연산 (Atomic Operations)11. 참고 사항12. 외부 링크13. 둘러보기1. 개요
||<-12><tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1><rowcolor=#d7d7d7,#a1a1a1>
<thread>
||<bgcolor=#20b580> | |||||||||||
| |||||||||||
스레드(컴퓨터 용어)와 일대일 대응되는 클래스. 함자와 인자를 전달하면 스레드를 생성하고 실행한다. | |||||||||||
<bgcolor=#20b580> | |||||||||||
| C++20 | ||||||||||
std::thread 에서 확장된 기능을 제공하는 클래스. 생성자와 소멸자에서 다른 기능을 제공한다. | |||||||||||
<bgcolor=#20b580> |
<thread>
C++11부터 존재한 모듈
<thread>
는 클래스 std::thread
, std::jthread
와 관련 유틸리티를 제공한다.자세히는 스레드(컴퓨터 용어)를 생성하고 관리하는 방법을 제공한다. 이 문서에서는 단순히 스레드에 관한 예제 외에도 다중 스레드 프로그래밍 방법론에 대한 이론을 소개하겠다.
std::thread
와 std::jthread
는 컴퓨터 운영체제의 스레드와 일대일 대응되는 클래스로써 운영체제 자원을 소유한다. 생성된 스레드는 main
함수가 있는 주 스레드(Main Thread)와는 별도로, 동시에 실행될 수 있다. 그리고 각 스레드는 주 스레드와는 병렬적으로 진행되며, 서로간에 영향을 거의 끼치지 못한다. 대신 모든 스레드는 전역 변수와 힙 등의 자원을 다 같이 쓴다. 곧 프로그램에서 자동으로 할당되지 않고 정적 혹은 동적으로 할당된 객체를 공유한다. 그리고 다수의 스레드에서 함께 사용하는 자원을 공유 자원이라고 칭한다.만약 막대한 양의 작업이 필요할 때 스레드마다 작업을 분산시킬 수 있다면 소요 시간을 극적으로 줄일 수 있다. 분업의 행태를 안다면 스레드를 어디에 활용할 수 있는지 떠올릴 수 있을 것이다. 가령 긴 문자열 안에서 단어 하나를 찾아야 한다면 이 문자열을 반으로 쪼개 두 스레드에서 각각 찾는 방법이 있다.
멀티코어 프로그래밍과 다중 스레드 프로그래밍의 차이는 현대 운영체제에선 거의 구별되지 않는다. 단일코어 CPU에선 일어나지 않는 캐시 메모리 누락, 캐시 메모리 워드 잘림 등 메모리 관련 문제가 있으나 다행히도 멀티코어 프로세서와 비슷한 기법으로 C++ 코드 상에서는 별다른 차이없는 방법으로 해결할 수 있다.
다중 스레드 프로그래밍의 어려움은 정말 많은데 일단 상기한 메모리와 성능 문제가 있고, 평소엔 관심도 없었을 운영체제와 CPU 구조에 대해서도 알아야 한다. 알기만 하는게 아니라 내부가 어떻게 돌아가는지 원리도 알아두어야 한다. 비유하자면 스마트폰을 쓰는 사람에서 벗어나 스마트폰의 하드웨어를 조립하고 소프트웨어를 만들고, 컴파일하고, 설치하고, 직접 사용도 하는 것과 같다. 다중 스레드를 쓰려고 하면 단순히 코딩 노하우만으로 이해하기는 불가능한 장벽이 도사리고 있어서 C++의 코드를 작성하는 건 부차적인 문제고 이를 응용하기 위해선 컴퓨터가 어떻게 동작하는지 알아야 한다는 말이다.
예를 들어서 하드웨어 지연 때문에 명령어의 순서가 바뀌거나 누락될 수 있고, 분기 예측 결과에 따라 성능이 천차만별이며 명령어의 순서가 바뀔 수 있고 성능에 큰 영향을 끼친다. 비순차적 실행 때문에 CPU에서 명령어의 순서가 바뀔 수 있으며, 컴파일러의 최적화에 의해 명령어의 실행 여부와 순서가 바뀔 수 있다.
volatile
등 평소에 쓰지 않는 기능과 객체 수명도 숙지해야 한다.2. 기본적인 사용방법
2.1. std::thread
||<tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1><rowcolor=#d7d7d7,#a1a1a1>
||
#!if head_comment!=null
{{{#57A64A,#5EBD46 }}}
#!if head_lnb!=null
##======================================= include and import
[br]
#!if import!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{import }}}}}}'''{{{#C8865E {{{<>}}}}}}{{{;}}}
#!if include!=null
##======================================= the keyword at head (can be `template`)
{{{#include <>}}}
#!if (head_keyword_available = head_keyword != null)
##======================================= template parameters begin
'''{{{#DodgerBlue,#CornFlowerBlue {{{class}}}}}}'''
#!if template_available = (template_p0 != null || template_v0 != null || template_p1 != null || template_v1 != null || template_p2 != null || template_v2 != null || template_p3 != null || template_v3 != null)
#!if template_available || template_last_label != null
##======================================= template parameter 0
##======================================= template parameter 0 concept
{{{<}}}{{{#!if template_concept0_available = (template_cpt0 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept0_params_available = (template_concept0_p0 != null || template_concept0_p1 != null || template_concept0_p2 != null || template_concept0_p3 != null)
{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{<}}}}}}{{{#!if template_concept0_params_available
##======================================= template parameter 0 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p0 != null || template_concept0_v0 != null) && (template_concept0_p1 != null || template_concept0_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p1 != null || template_concept0_v1 != null) && (template_concept0_p2 != null || template_concept0_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p2 != null || template_concept0_v2 != null) && (template_concept0_p3 != null || template_concept0_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept0_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept0_available
{{{ }}}}}}{{{#!if template_p0_available = (template_p0 != null)
{{{#!if !template_concept0_available
##======================================= template parameter 0 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v0_available = (template_v0 != null)
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}
##======================================= template parameter 0 finished
}}}{{{#!if (template_p0_available || template_v0_available) && (template_p1 != null || template_v1 != null)
##======================================= template parameter 1
{{{, }}}}}}{{{#!if template_concept1_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept1_params_available = (template_concept1_p0 != null || template_concept1_p1 != null || template_concept1_p2 != null || template_concept1_p3 != null)
{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{<}}}}}}{{{#!if template_concept1_params_available
##======================================= template parameter 1 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p0 != null || template_concept1_v0 != null) && (template_concept1_p1 != null || template_concept1_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p1 != null || template_concept1_v1 != null) && (template_concept1_p2 != null || template_concept1_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p2 != null || template_concept1_v2 != null) && (template_concept1_p3 != null || template_concept1_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept1_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept1_available
{{{ }}}}}}{{{#!if template_p1_available = (template_p1 != null)
{{{#!if !template_concept1_available
##======================================= template parameter 1 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}}}}{{{#!if template_v1_available = (template_v1 != null)
##======================================= template parameter 1 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if (template_p1_available || template_v1_available) && (template_p2 != null || template_v2 != null)
##======================================= template parameter 2
{{{, }}}}}}{{{#!if template_concept2_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept2_params_available = (template_concept2_p0 != null || template_concept2_p1 != null || template_concept2_p2 != null || template_concept2_p3 != null)
{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{<}}}}}}{{{#!if template_concept2_params_available
##======================================= template parameter 2 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p0 != null || template_concept2_v0 != null) && (template_concept2_p1 != null || template_concept2_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p1 != null || template_concept2_v1 != null) && (template_concept2_p2 != null || template_concept2_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p2 != null || template_concept2_v2 != null) && (template_concept2_p3 != null || template_concept2_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept2_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept2_available
{{{ }}}}}}{{{#!if template_p2_available = (template_p2 != null)
{{{#!if !template_concept2_available
##======================================= template parameter 2 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v2_available = (template_v2 != null)
##======================================= template parameter 2 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if template_available && template_last_label != null
##======================================= template finished
{{{, }}}}}}{{{#!if template_last_label != null
{{{...}}}}}}{{{#!if template_available || template_last_label != null
{{{>}}}}}}{{{#!if do_template_linebreak = (template_lnb != null)
##======================================= template linebreak
[br]}}}{{{#!if do_template_linebreak ;nbsp
}}}
#!if head_keyword_available!=null
{{{ }}}
#!if fn_attribute!=null
##======================================= contents
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnk!=null
[[C++/문법/특성#attr1|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnb!=null
[br]
#!if fn_attribute_lnb!=null ;nbsp
#!if kw1!=null
'''{{{#569cd6,#CornFlowerBlue {{{contexpr}}}}}}'''{{{#!if kw1_post!=null
{{{_kwp1}}}}}}{{{#!if kw1_post==null
{{{ }}}}}}
#!if kw2!=null
'''{{{#569cd6,#CornFlowerBlue {{{long long}}}}}}'''{{{#!if kw2post!=null
{{{&&}}}}}}{{{#!if kw2_post==null
{{{ }}}}}}
#!if cls_attribute!=null
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr2]]}}}}}}]]{{{ }}}
#!if cls_attribute_lnk!=null
[[C++/문법/특성#attr2|{{{#a8a8a8 {{{[[attr2]] }}}}}}]]
#!if ns1!=null
##======================================= namespaces
'''{{{#58fafe {{{std}}}}}}'''
#!if ns2!=null
{{{::}}}'''{{{#58fafe {{{chrono}}}}}}'''
#!if ns1!=null
{{{::}}}
#!if pre1_t!=null
##======================================= types at the front
{{{#4ec9b0,#6fdbba {{{thread }}}}}}
#!if pre2_t!=null
{{{::}}}{{{#4ec9b0,#6fdbba {{{duration }}}}}}
#!if pre_e!=null
{{{::}}}{{{#f0f068 {{{enum }}}}}}
#!if pre_post!=null
{{{_pre}}}
#!if pre_lnb!=null
[br]
#!if do_pre_linebreak = (pre_lnb != null)
##======================================= pre-body things finished
#!if (!do_pre_linebreak && !do_template_linebreak && head_keyword_available) && (body_v != null || body_f != null || body_mv != null || body_mf != null || body_post != null)
{{{ }}}
#!if body_v!=null
##======================================= identifiers of variable and function
{{{#a8a8a8,#a1a1a1 {{{val}}}}}}
#!if body_mv!=null
{{{#ffffff {{{mem_val}}}}}}
#!if body_f!=null
{{{#f87a7a {{{fun}}}}}}
#!if body_mf!=null
{{{#f0a962 {{{mem_fn}}}}}}
#!if body_post!=null
##======================================= argument 1
{{{}}}
#!if body_tmpopen!=null
{{{<}}}
#!if body_bopen!=null
{{{(}}}
#!if arg1_concept!=null
'''{{{#4ec9b0,#6fdbba {{{concept1}}}}}}'''{{{#!if arg1_concept_params!=null
{{{<}}}{{{#!if arg1_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg1_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg1_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg1_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg1_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg1_t!=null
{{{#4ec9b0,#6fdbba {{{type1}}}}}}
#!if arg1_t_post!=null
{{{&}}}
#!if arg1_param != null && (arg1_kw != null || arg1_t_kw != null || arg1_t != null)
{{{ }}}
#!if arg1_param!=null
{{{#bcdce6 {{{param1}}}}}}
#!if arg2_concept != null || arg2_kw != null || arg2_t_kw != null || arg2_t != null || arg2_param != null
{{{, }}}
#!if arg2_concept!=null
##======================================= argument 2
'''{{{#4ec9b0,#6fdbba {{{concept2}}}}}}'''{{{#!if arg2_concept_params!=null
{{{<}}}{{{#!if arg2_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg2_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg2_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg2_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg2_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg2_t!=null
{{{#4ec9b0,#6fdbba {{{type2}}}}}}
#!if arg2_t_post!=null
{{{&}}}
#!if arg2_param!=null && (arg2_kw != null || arg2_t_kw != null || arg2_t != null)
{{{ }}}
#!if arg2_param!=null
{{{#bcdce6 {{{param2}}}}}}
#!if arg3_concept != null || arg3_kw != null || arg3_t_kw != null || arg3_t != null || arg3_param != null
{{{, }}}
#!if arg3_concept!=null
##======================================= argument 3
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg3_concept_params!=null
{{{<}}}{{{#!if arg3_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg3_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg3_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg3_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg3_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg3_t!=null
{{{#4ec9b0,#6fdbba {{{type3}}}}}}
#!if arg3_t_post!=null
{{{&}}}
#!if arg3_param!=null && (arg3_kw != null || arg3_t_kw != null || arg3_t != null)
{{{ }}}
#!if arg3_param!=null
{{{#bcdce6 {{{param3}}}}}}
#!if arg4_concept != null || arg4_kw != null || arg4_t_kw != null || arg4_t != null || arg4_param != null
{{{, }}}
#!if arg4_concept!=null
##======================================= argument4
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg4_concept_params!=null
{{{<}}}{{{#!if arg4_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg4_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg4_concept_tparam4!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg4_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg4_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg4_t!=null
{{{#4ec9b0,#6fdbba {{{type4}}}}}}
#!if arg4_t_post!=null
{{{&}}}
#!if arg4_param!=null && (arg4_kw != null || arg4_t_kw != null || arg4_t != null)
{{{ }}}
#!if arg4_param!=null
{{{#bcdce6 {{{param4}}}}}}
#!if arg5_param!=null
##======================================= argument5, argument6
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg6_param != null
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg_last_dots != null
{{{, ...}}}
#!if body_bclose!=null
##=======================================
{{{)}}}
#!if body_lnb!=null
[br]
#!if body_lnb!=null
{{{ }}}
#!if body_spec1!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{ const}}}}}}'''
#!if body_spec1_paren != null
{{{(}}}
#!if body_spec1_ref!=null
{{{&}}}
#!if body_spec2!=null
{{{#!if body_spec1!=null && body_spec1_paren == null
{{{ }}}}}}'''{{{#DodgerBlue,#CornFlowerBlue {{{noexcept}}}}}}'''
#!if body_spec2_paren != null
{{{(}}}
#!if body_spec2_label != null
{{{...}}}
#!if body_spec2_paren != null
{{{)}}}
#!if body_spec1_paren != null
{{{)}}}
#!if body_tmpclose!=null
##======================================= last labels
{{{>}}}
#!if label_last!=null
{{{}}}
#!if last!=null
{{{;}}}
||
<bgcolor=#20b580> |
|
빈 스레드 클래스의 인스턴스를 생성한다. 아무 스레드도 만들지 않고 실행하지 않는다. |
<bgcolor=#20b580> |
|
스레드를 하나 생성하고, 실행가능한 객체 `Functor` 를 그 스레드에서 실행한다. 이때 인자를 추가로 전달할 수 있다.C++23 이전에는 복사로 인자를 전달했다. C++23 부터는 완벽한 전달을 수행한다. |
<bgcolor=#20b580> |
std::thread
std::thread
[1]는 스레드를 하나 생성하고 곧바로 실행하는 기능을 가지고 있다. 스레드의 실행은 하나의 실행가능한 객체(Invocable Object)를 통해 이루어진다. 실행가능한 객체에는 함수, 람다 표현식, ()
연산자를 오버로딩한 함자 클래스[2]가 있다.스레드라는 운영체제 자원을 갖고 있기 때문에 이동만 가능하며 복사할 수 없다. 생성자에서는 실행가능한 객체를 받아 스레드를 생성하고, 소멸자에선 갖고 있는 스레드를 파괴한다.
스레드가 생성되는 순서와 실행되는 순서는 일정하지 않다. 심지어 최초로 스레드 클래스 인스턴스를 여러개 만든 순간부터 순서가 달라질 수 있다.
2.2. 예제 1: 스레드 클래스의 생성과 제거
#!syntax cpp
import <thread>;
먼저 스레드를 쓰기 위해서는 스레드 모듈이 필요하다. 앞으로 모듈이 추가로 필요할 때를 빼면 굳이 명시하지 않겠다.#!syntax cpp
import <chrono>;
import <utility>;
본격적으로 쓰기 위해서는 <chrono>
, <utility>
도 있는 게 좋다.#!syntax cpp
import <print>;
void Worker()
{
using namespace std::chrono_literals;
std::print("Worker started");
// 1초 대기
std::this_thread::sleep_for(1s);
std::print("Worker finished");
}
int main()
{
std::thread worker(Worker);
std::println("Waiting the worker...");
worker.join();
}
상기 코드는 주 스레드(Main Thread)에서 1초간 기다리는 스레드를 하나 만드는 예제다. 이때 스레드 클래스의 생성자에 함수 `Worker`
를 전달했다. 또한 worker.join();
으로 멤버 함수 join
을 실행시켜줬다.||<tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><color=#a8a8a8,#a1a1a1><(>
||
멤버 함수 #!if head_comment!=null
{{{#57A64A,#5EBD46 }}}
#!if head_lnb!=null
##======================================= include and import
[br]
#!if import!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{import }}}}}}'''{{{#C8865E {{{<>}}}}}}{{{;}}}
#!if include!=null
##======================================= the keyword at head (can be `template`)
{{{#include <>}}}
#!if (head_keyword_available = head_keyword != null)
##======================================= template parameters begin
'''{{{#DodgerBlue,#CornFlowerBlue {{{}}}}}}'''
#!if template_available = (template_p0 != null || template_v0 != null || template_p1 != null || template_v1 != null || template_p2 != null || template_v2 != null || template_p3 != null || template_v3 != null)
#!if template_available || template_last_label != null
##======================================= template parameter 0
##======================================= template parameter 0 concept
{{{<}}}{{{#!if template_concept0_available = (template_cpt0 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept0_params_available = (template_concept0_p0 != null || template_concept0_p1 != null || template_concept0_p2 != null || template_concept0_p3 != null)
{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{<}}}}}}{{{#!if template_concept0_params_available
##======================================= template parameter 0 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p0 != null || template_concept0_v0 != null) && (template_concept0_p1 != null || template_concept0_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p1 != null || template_concept0_v1 != null) && (template_concept0_p2 != null || template_concept0_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p2 != null || template_concept0_v2 != null) && (template_concept0_p3 != null || template_concept0_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept0_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept0_available
{{{ }}}}}}{{{#!if template_p0_available = (template_p0 != null)
{{{#!if !template_concept0_available
##======================================= template parameter 0 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v0_available = (template_v0 != null)
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}
##======================================= template parameter 0 finished
}}}{{{#!if (template_p0_available || template_v0_available) && (template_p1 != null || template_v1 != null)
##======================================= template parameter 1
{{{, }}}}}}{{{#!if template_concept1_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept1_params_available = (template_concept1_p0 != null || template_concept1_p1 != null || template_concept1_p2 != null || template_concept1_p3 != null)
{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{<}}}}}}{{{#!if template_concept1_params_available
##======================================= template parameter 1 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p0 != null || template_concept1_v0 != null) && (template_concept1_p1 != null || template_concept1_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p1 != null || template_concept1_v1 != null) && (template_concept1_p2 != null || template_concept1_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p2 != null || template_concept1_v2 != null) && (template_concept1_p3 != null || template_concept1_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept1_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept1_available
{{{ }}}}}}{{{#!if template_p1_available = (template_p1 != null)
{{{#!if !template_concept1_available
##======================================= template parameter 1 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}}}}{{{#!if template_v1_available = (template_v1 != null)
##======================================= template parameter 1 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if (template_p1_available || template_v1_available) && (template_p2 != null || template_v2 != null)
##======================================= template parameter 2
{{{, }}}}}}{{{#!if template_concept2_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept2_params_available = (template_concept2_p0 != null || template_concept2_p1 != null || template_concept2_p2 != null || template_concept2_p3 != null)
{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{<}}}}}}{{{#!if template_concept2_params_available
##======================================= template parameter 2 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p0 != null || template_concept2_v0 != null) && (template_concept2_p1 != null || template_concept2_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p1 != null || template_concept2_v1 != null) && (template_concept2_p2 != null || template_concept2_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p2 != null || template_concept2_v2 != null) && (template_concept2_p3 != null || template_concept2_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept2_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept2_available
{{{ }}}}}}{{{#!if template_p2_available = (template_p2 != null)
{{{#!if !template_concept2_available
##======================================= template parameter 2 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v2_available = (template_v2 != null)
##======================================= template parameter 2 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if template_available && template_last_label != null
##======================================= template finished
{{{, }}}}}}{{{#!if template_last_label != null
{{{...}}}}}}{{{#!if template_available || template_last_label != null
{{{>}}}}}}{{{#!if do_template_linebreak = (template_lnb != null)
##======================================= template linebreak
[br]}}}{{{#!if do_template_linebreak ;nbsp
}}}
#!if head_keyword_available!=null
{{{ }}}
#!if fn_attribute!=null
##======================================= contents
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnk!=null
[[C++/문법/특성#attr1|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnb!=null
[br]
#!if fn_attribute_lnb!=null ;nbsp
#!if kw1!=null
'''{{{#569cd6,#CornFlowerBlue {{{void}}}}}}'''{{{#!if kw1_post!=null
{{{_kwp1}}}}}}{{{#!if kw1_post==null
{{{ }}}}}}
#!if kw2!=null
'''{{{#569cd6,#CornFlowerBlue {{{long long}}}}}}'''{{{#!if kw2post!=null
{{{&&}}}}}}{{{#!if kw2_post==null
{{{ }}}}}}
#!if cls_attribute!=null
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr2]]}}}}}}]]{{{ }}}
#!if cls_attribute_lnk!=null
[[C++/문법/특성#attr2|{{{#a8a8a8 {{{[[attr2]] }}}}}}]]
#!if ns1!=null
##======================================= namespaces
'''{{{#58fafe {{{std}}}}}}'''
#!if ns2!=null
{{{::}}}'''{{{#58fafe {{{chrono}}}}}}'''
#!if ns1!=null
{{{::}}}
#!if pre1_t!=null
##======================================= types at the front
{{{#4ec9b0,#6fdbba {{{system_clock }}}}}}
#!if pre2_t!=null
{{{::}}}{{{#4ec9b0,#6fdbba {{{duration }}}}}}
#!if pre_e!=null
{{{::}}}{{{#f0f068 {{{enum }}}}}}
#!if pre_post!=null
{{{_pre}}}
#!if pre_lnb!=null
[br]
#!if do_pre_linebreak = (pre_lnb != null)
##======================================= pre-body things finished
#!if (!do_pre_linebreak && !do_template_linebreak && head_keyword_available) && (body_v != null || body_f != null || body_mv != null || body_mf != null || body_post != null)
{{{ }}}
#!if body_v!=null
##======================================= identifiers of variable and function
{{{#a8a8a8,#a1a1a1 {{{val}}}}}}
#!if body_mv!=null
{{{#ffffff {{{mem_val}}}}}}
#!if body_f!=null
{{{#f87a7a {{{fun}}}}}}
#!if body_mf!=null
{{{#f0a962 {{{join}}}}}}
#!if body_post!=null
##======================================= argument 1
{{{}}}
#!if body_tmpopen!=null
{{{<}}}
#!if body_bopen!=null
{{{(}}}
#!if arg1_concept!=null
'''{{{#4ec9b0,#6fdbba {{{concept1}}}}}}'''{{{#!if arg1_concept_params!=null
{{{<}}}{{{#!if arg1_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg1_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg1_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg1_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg1_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg1_t!=null
{{{#4ec9b0,#6fdbba {{{type1}}}}}}
#!if arg1_t_post!=null
{{{&}}}
#!if arg1_param != null && (arg1_kw != null || arg1_t_kw != null || arg1_t != null)
{{{ }}}
#!if arg1_param!=null
{{{#bcdce6 {{{param1}}}}}}
#!if arg2_concept != null || arg2_kw != null || arg2_t_kw != null || arg2_t != null || arg2_param != null
{{{, }}}
#!if arg2_concept!=null
##======================================= argument 2
'''{{{#4ec9b0,#6fdbba {{{concept2}}}}}}'''{{{#!if arg2_concept_params!=null
{{{<}}}{{{#!if arg2_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg2_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg2_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg2_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg2_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg2_t!=null
{{{#4ec9b0,#6fdbba {{{type2}}}}}}
#!if arg2_t_post!=null
{{{&}}}
#!if arg2_param!=null && (arg2_kw != null || arg2_t_kw != null || arg2_t != null)
{{{ }}}
#!if arg2_param!=null
{{{#bcdce6 {{{param2}}}}}}
#!if arg3_concept != null || arg3_kw != null || arg3_t_kw != null || arg3_t != null || arg3_param != null
{{{, }}}
#!if arg3_concept!=null
##======================================= argument 3
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg3_concept_params!=null
{{{<}}}{{{#!if arg3_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg3_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg3_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg3_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg3_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg3_t!=null
{{{#4ec9b0,#6fdbba {{{type3}}}}}}
#!if arg3_t_post!=null
{{{&}}}
#!if arg3_param!=null && (arg3_kw != null || arg3_t_kw != null || arg3_t != null)
{{{ }}}
#!if arg3_param!=null
{{{#bcdce6 {{{param3}}}}}}
#!if arg4_concept != null || arg4_kw != null || arg4_t_kw != null || arg4_t != null || arg4_param != null
{{{, }}}
#!if arg4_concept!=null
##======================================= argument4
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg4_concept_params!=null
{{{<}}}{{{#!if arg4_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg4_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg4_concept_tparam4!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg4_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg4_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg4_t!=null
{{{#4ec9b0,#6fdbba {{{type4}}}}}}
#!if arg4_t_post!=null
{{{&}}}
#!if arg4_param!=null && (arg4_kw != null || arg4_t_kw != null || arg4_t != null)
{{{ }}}
#!if arg4_param!=null
{{{#bcdce6 {{{param4}}}}}}
#!if arg5_param!=null
##======================================= argument5, argument6
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg6_param != null
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg_last_dots != null
{{{, ...}}}
#!if body_bclose!=null
##=======================================
{{{)}}}
#!if body_lnb!=null
[br]
#!if body_lnb!=null
{{{ }}}
#!if body_spec1!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{ const}}}}}}'''
#!if body_spec1_paren != null
{{{(}}}
#!if body_spec1_ref!=null
{{{&}}}
#!if body_spec2!=null
{{{#!if body_spec1!=null && body_spec1_paren == null
{{{ }}}}}}'''{{{#DodgerBlue,#CornFlowerBlue {{{noexcept}}}}}}'''
#!if body_spec2_paren != null
{{{(}}}
#!if body_spec2_label != null
{{{...}}}
#!if body_spec2_paren != null
{{{)}}}
#!if body_spec1_paren != null
{{{)}}}
#!if body_tmpclose!=null
##======================================= last labels
{{{>}}}
#!if label_last!=null
{{{}}}
#!if last!=null
{{{;}}}
||
|
join
은 스레드 클래스의 상태를 정리하고 종료를 기다리는 멤버 함수다. 기다린다는 말은 join
이 실행된 현재 스레드를 대기 상태(Block)로 만든다는 뜻이다.만약 스레드 클래스가 빈 클래스이거나, 스레드가 실행중이 아니거나 [3], 올바른 스레드 상태가 아니면 예외가 발생한다. 예외는
std::system_error
가 발생하며 상세 원인은 std::system_error
의 멤버 함수 code()
로 std::errc
를 읽어 파악할 수 있다.#!syntax cpp
void Worker()
{
while (true) {}
}
만약 무한 반복문이 있을 경우 스레드가 종료되지 않아 join
도 영원히 끝나지 않는다. 이를 해결하는 방법은 후술한다.join
의 의미는 가지치기(Fork 또는 Branching)한 스레드가 프로세스의 주 루틴으로 다시 합쳐진다는 의미다.2.3. 예제 2: 외부 의존성이 없는 인자 전달
스레드의 함자에 인자를 전달하는 방법은 크게 두 분류로 나눌 수 있다. 바로 외부 의존성이 있는 것과 없는 것이다. 외부 의존성이란 스레드 바깥의 상태를 참조한다는 뜻이다. 상태는 변수나 상수 따위의 데이터를 말한다.일반적으로 스레드 내부에선 외부의 상태를 바로 알 수 없다. 스레드가 생성된 이후의 상태는 정적 데이터 내지는 동적 할당된 데이터가 아니면 접근한 수가 없다. 이때 스레드의 매개변수를 통해 데이터를 읽을 수 있는 창구를 만들어주고 스레드는 이를 통해 외부와 소통하는 방법이 보편적이다. 창구를 통해 외부에서 상태를 전달해주면 그제서야 스레드 내부에서 정보 갱신이 이루어지는 것이다. 물론 전역 변수를 쓰면 편하기는 하지만 꼬이면 디버그가 까다롭다.
스레드를 크게 정보를 생산하는 생산자와 이용하는 소비자로 구분하는 디자인 패턴이 흔히 사용된다. 그러나 이 구분은 절대적인 것이 아니며 다만 스레드가 필요한 상황에 따라 적절한 동기화가 필요하다.
#!syntax cpp
import <print>;
void Worker1()
{
std::println("Worker1 said nothing.");
}
void Worker2(int code)
{
std::println("Worker2 said {}", code);
}
int main()
{
// 'Worker1 said nothing.' 출력
std::thread worker1(Worker1);
// `worker1`의 종료 대기
worker1.join();
// 'Worker2 said 70.' 출력
std::thread worker2(Worker2, 70);
// `worker2`의 종료 대기
worker2.join();
}
상기 코드는 단순한 값에 의한 전달을 수행하는 스레드를 보여주고 있다.- <C++ 예제 보기>
#!syntax cpp import <print>; // 서명이 void Worker(int value) 이어도 상관없음. void Worker(int&& value) { using namespace std::chrono_literals; std::this_thread::sleep_for(1s); value = 3; } int main() { int origin = 0; std::thread worker(Worker, static_cast<int&&>(origin)); std::println("Waiting the worker..."); worker.join(); // 0 출력 std::println("origin is {}.", origin); std::println("End"); }
&&
)의 경우 스레드에 전달할 함자에 &&
매개변수를 쓰든 안쓰든 문제는 없다. 우측값 참조자는 임시값을 강제하기 위한 문법이며 만약 사용자가 값을 이동시켜야만 하는 사실을 알고 있다면 필수가 아니다. 멤버 함수 삭제를 통해 컴파일 단계에서 이동을 강제한다는 선택지도 있으니 말이다. 매개변수에 &&
를 명시하지 않아도 사용자가 이동 연산을 해주면, 알아서 스레드 내부로 값이 이동된다.2.4. 예제 3: 병렬 파일 입출력
우리가 첫번째로 알아볼 실제 활용법은 다수의 파일을 읽고 쓰는 예제다.<fstream>
모듈을 사용할 예정이다.📄 OldConfig.txt
🧾 Setting.json
🧾 LastSessionInfo.json
📄 DebugLog.txt
🧾 Setting.json
🧾 LastSessionInfo.json
📄 DebugLog.txt
OldConfig.txt
의 내용을 나머지 세 파일로 분할하고자 한다.OldConfig.txt
[Session]
SessionName=CPP_THREADING
SessionID=fe954d9f-18d1-4232-b715-4afa6d3ccbbe
LastOpenedDate=2025-08-01
SessionPermission=Guest
SessionDebugState=Stable
SessionCount=6;
SessionLastStage=Matching;
[Option]
Volume=10
EnvironmentVolume=5
UiVolume=0
Resolution=1600x1200
Fullscreen=0
OldConfig.txt
의 내용물은 위와 같다.- <C++ 예제 보기>
#!syntax cpp import <string>; import <fstream>; import <sstream>; int main() { std::fstream oldcfg("OldConfig.txt", std::ios_base::in); std::fstream settings("Settings.json"); std::fstream session("LastSessionInfo.json"); std::fstream debuglog("DebugLog.txt"); settings << "{\n"; session << "{\n"; std::string content; content.reserve(256); while (oldcfg >> content) { if (content.starts_with("SessionName")) { session << std::quoted("SessionName") << " : " << content.substr(12) << ",\n"; } else { // ... } } settings << "}\n"; session << "}\n"; oldcfg.close(); settings.close(); session.close(); debuglog.close(); }
OldConfig.txt
를 한줄씩 읽어서 조건문으로 분류해서 쓰는 것이다. 이게 간단하긴 하나 좀 더 체계적으로 코드를 써보자. 세개의 스레드를 만들고 전역 변수로 선언한 std::fstream
을 세 스레드에서 읽는 방식으로 구현할 수 있다.- <C++ 예제 보기>
#!syntax cpp // 전역 변수 std::fstream oldcfg; void SettingsWorker(); void SessionWorker(); void DebugLogWorker(); int main() { std::println("시작"); oldcfg.open("OldConfig.txt", std::ios_base::in); if (!oldcfg.is_open()) { std::println("오류! 예전 구성 파일을 열 수 없습니다."); return 0; } std::println("스레드 생성 중..."); std::thread settingsworker(SettingsWorker); std::thread sessionworker(SessionWorker); std::thread dbglogworker(DebugLogWorker); std::println("파일 쓰는 중..."); settingsworker.join(); sessionworker.join(); dbglogworker.join(); std::println("쓰기 작업 완료됨."); oldcfg.close(); std::println("종료"); return 0; }
- <C++ 예제 보기>
#!syntax cpp void SettingsWorker() { std::fstream settings("Settings.json", std::ios_base::out | std::ios_base::trunc); if (!settings.is_open()) { std::println("오류! 설정 파일을 열 수 없습니다."); return 0; } // 쓰기 작업... settings.close(); }
`SettingsWorker`
의 구현은 위와 같다.- <C++ 예제 보기>
#!syntax cpp void SessionWorker() { std::fstream session("LastSessionInfo.json", std::ios_base::out | std::ios_base::trunc); if (!session.is_open()) { std::println("오류! 세션 기록 파일을 열 수 없습니다."); return 0; } // 쓰기 작업... session.close(); }
`SessionWorker`
의 구현은 위와 같다.- <C++ 예제 보기>
#!syntax cpp void DebugLogWorker() { std::fstream debuglog("DebugLog.txt", std::ios_base::out | std::ios_base::trunc); if (!debuglog.is_open()) { std::println("오류! 디버그 로그 파일을 열 수 없습니다."); return 0; } // 쓰기 작업... debuglog.close(); }
`DebugLogWorker`
의 구현은 위와 같다.보면 알겠지만 맨 처음에
OldConfig.txt
파일이 잘 열린지만 확인하면 이후는 각 스레드에서 알아서 진행한다. 각 함수마다 파일 하나씩을 책임지는 구조다. 파일 하나에서 문제가 생겨도 다른 작업에 영향이 가는 걸 피할 수 있다. 그리고 이 모든 작업은 병렬적으로 이루어지므로 기나긴 IO 작업시간을 줄일 수 있다.2.5. 예제 4: 외부 의존성이 있는 인자 전달
#!syntax cpp
import <functional>; // std::ref, std::cref
void Worker(int& ref)
{
using namespace std::chrono_literals;
ref *= 20;
std::this_thread::sleep_for(1s);
}
int main()
{
int value = 40;
std::thread worker(Worker, std::ref(value));
//'`value`는 40.' 출력
std::println("`value`는 {}.", value);
std::println("작업자 대기 중...");
worker.join();
//'`value`는 800.' 출력
std::println("`value`는 {}.", value);
std::println("종료");
}
좌측값 참조자를 스레드에 전달하기 위해서는 std::ref
와 std::cref
함수를 써야 한다. std::ref
는 lvalue T
를 인자로 받아 std::reference_wrapper<T>
를 반환한다. std::cref
는 lvalue T
를 인자로 받아 std::reference_wrapper<const T>
를 반환한다.||<-12><tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1><rowcolor=#d7d7d7,#a1a1a1>
std::reference_wrapper<T>
||
| |||||||||||
<colcolor=#f0f0f8,#e1e1e1>좌측값 참조자 래퍼[4] 클래스
| |||||||||||
<bgcolor=#20b580> | |||||||||||
| |||||||||||
| |||||||||||
자료형 T 의 좌측값 참조자를 반환한다. | |||||||||||
<bgcolor=#20b580> |
std::reference_wrapper<T>
는 복사가 불가능했던 좌측값 참조자의 문제를 해결하기 위해 고안된 클래스다. 이 클래스는 복사 생성과 복사 대입이 자유롭다.#!syntax cpp
void Worker(int* ptr)
{
using namespace std::chrono_literals;
// 3초 대기
std::this_thread::sleep_for(3s);
delete ptr;
/* 런타임 오류! 메모리 참조 위반! */
// *ptr = 5;
}
int main()
{
int* memory = new int(0);
std::thread worker(Worker, memory);
std::println("작업자 대기 중...");
worker.join();
// `memory` 해제됨.
// nullptr가 아닌 어떤 주소 출력
std::println("`memory`는 {}.", reinterpret_cast<void*>(ptr));
// 쓰레기 값 출력
// 컴파일러에 따라 디버그 모드에서 메모리 커럽션 감지를 해주기도 함.
std::println("`*memory`는 {}.", *memory);
std::println("종료");
}
외부에서 인자를 통해 참조되는 객체들의 수명을 유념해야 한다. 예를 들어서 인자로 포인터를 전달했는데 다른 스레드에서 메모리가 해제되버리면, 포인터 자체는 어떤 주소가 복사된 값이므로 해제된 사실을 알 수 없다. 만약 빈번하게 수정될 동적할당 메모리를 써야 한다면 스마트 포인터를 써야 한다. 그리고 상기 예제에서 오류가 일어나는 경우는 바로 같은 스레드에서 메모리의 잘못된 접근이 일어났을 때란 걸 알 수 있다. 다른 스레드에서 발생한 메모리의 수정은 현재 스레드에서 바로 알 수가 없다.이 문제는 좌측값 참조자를 써도 일어난다. 왜냐하면 좌측값 참조자 역시 하드 링크가 아니며 내부적으론 포인터를 쓰기 때문이다. 참조하는 변수가 지역 변수라거나 해서 소멸해버리면 그대로 참조 대상 소실(Reference Dangling)이 일어난다.
3. 다중 스레드 프로그래밍 기법
다음 내용을 보기 전에 우리가 왜 다중 스레드 프로그래밍을 배우는지 돌아볼 필요가 있다. 다중 스레드 프로그래밍은 사실 반드시 필요한 존재가 아니다. 앞서 살펴본 예제는 너무 간단하지만 사실 더 큰 규모의 코드에서도 필요성이 크게 느껴지지 않는 것도 사실이다. 단적으로 말해서 코드를 실행했는데 CPU 점유율 100%가 아니면 굳이 다중 스레드를 이용할 필요가 없다는 말이다. 그럼에도 불구하고 다중 스레드 프로그래밍은 성능을 올리는 유일한 방법이 되고 말았다. 순수 CPU 성능의 효용 한계가 찾아오고 있다는 말이다. 오래전 무어의 법칙을 되물을 필요도 없다. 최근에는 반도체 미세공정에 따른 성능 증가율도 점점 한계에 다다르고 있다.앞으로 배울 내용은 그래서 최신 기술로 어떻게 고성능을 달성할 수 있는지에 관한 것이다. 여기에는 이미 확정적인 한계 법칙이 존재한다. 암달의 법칙은 효율화된 일부 작업이 전체 작업에서 차지하는 비율에 따라 성능 증가의 폭이 정해진다는 법칙이다. 일단 우리가 앞서 본 예제는 딱히 데이터 의존성도 없고 철저하게 분업화할 수 있었다. 그러나 이는 오히려 특수한 사례에 해당한다. 실제로 다중 스레드가 필요한 상황은 온갖 데이터와 함수가 뒤섞인 상황이 대부분이다. 우리는 그런 환경에서 데이터 사이의 관계를 최대한 분리하고 분할할 수 있는 작업의 단위를 정해야 한다.
이때 앞서 살펴본 파일 쓰기 예제와 같이, 각 스레드가 고정된 역할을 가지면 이질성(Heterogeneous, 헤테로지니어스) 다중 스레드라고 칭한다. 이질성 다중 스레드 기법은 스레드마다 일일히 역할을 부여하고, 역할이 끝나면 스레드는 사라진다. 한편 모든 스레드가 동일한 역할을 가지만 동질성(Homogeneous, 호모지니어스) 다중 스레드라고 칭한다. 오해하면 안되는 것이 동일한 역할이란 코드 구현의 분할 없이 공통의 코드로 통일하는 것을 말한다. 동질성 다중 스레드에선 어떤 사건(Event, 이벤트)이 발생하면 모든 스레드들 중 하나가 이 사건을 받아 해결하고 결과를 반환한다. 다수의 스레드를 관리하는 스레드 풀(Thread Pool) 구현에 이용된다.
암달의 법칙은 우리가 다중 스레드를 구현했을 때 얼마만큼의 성능 이득이 있는지를 보여준다. 가령 앞서 살펴본 파일 쓰기 예제는 이질성 다중 스레드 기법을 썼는데 각 스레드가 별개의 역할을 하므로 각각 3분의 1 지분을 차지한다고 말할 수 있다. 스레드 작업이 실패하거나 오류가 뜨는 경우는 제외하고 말이다. 여기서 각 파일에 필요한 작업량이나 다른 데이터를 또 참조해야 한다든지의 이유로 스레드 간의 작업량 차이가 발생하면 마찬가지로 비율이 달라질 것이다.
이질성 다중 스레드 기법은 코드 진행을 파악하기는 상대적으로 수월하다. 그러나 스레드를 사용하는 큰 이유인 병렬 작업의 효율이 매우 떨어지며 병목 현상 등 성능 문제를 해결하기 힘들다. 각각의 스레드는 결국 단일 스레드로 돌아가는 것과 다르지 않기 때문이다. 그리고 다중 스레드 환경에서 가장 큰 병목 구간은 메모리 작업과 사용자 IO 작업인데, 이를 분할하는 게 이질성 다중 스레드 기법에선 어렵다.
사실 문제점을 많이 나열했지만 이질성 스레드 기법의 장점도 많다. 우선 구현이 쉽고, 동기화는 작업이 끝나기를 대기하는 것으로 충분하다. 그리고 가변적인 동작이 필요없고 필요한 작업만 끝내면 그만이라면 이질성 스레드 기법이 정답이다. 그리고 그런 작업을 여러개의 독립된 작업으로 쪼갤 수 있으면 동질성 스레드 기법보다 성능이 잘 나온다. 컴파일러의 최적화의 도움을 받을 수 있다는 장점도 매우 크다. 이 정도 수준까지 왔으면 결국 프로그래머 하기 나름이며 목적에 따라 다르게 구현하면 된다. 이질성 스레드 기법은 웹 분야에서 쓰이는 일회용 스레드와도 비슷하다. 몇몇 클라우드 서비스에서 찾아볼 수 있는 일회성 요청과 응답, PHP의 작동 방식이나 HTTP의 통신 구조가 적절한 예시다. 그외에 코루틴도 이질성 스레드 기법의 좋은 대안이다. 그 외엔 언리얼 엔진 5는 일회용 스레드는 아니고, 오디오, 게임 로직, 렌더링 등으로 역할을 나누어 대기 방식의 동기화를 쓰는 이질성 스레드 기법으로 구현되었다. 다만 이에 대해서는 평이 갈리는데 이에 대해서는 해당 항목 참고.
동질성 스레드 기법의 문제점은 객체 지향 프로그래밍의 단점이 다중 스레드 상에 그대로 투영되는 식으로 나타난다. 어떻게든 작업을 나누고 줄이기 위해 클래스를 많이 쓰게 되는데[5] 프로그램의 구조가 점점 얽히고 설켜 코드 가독성은 저 멀리 간다. 작업 단위라도 잘 분류해놨다면 유지보수는 그럭저럭 가능하지만 코드를 처음부터 쓴 사람이 아니면 유지보수가 어렵다는 맹점도 있다. 또한 필연적으로 어디에 스레드 풀 등을 관할하는 관리자 클래스를 만들게 되는데 거기가 최적화의 최대 접전 구역이 되는 경우가 많다.
다음 문단부터는 문제점과 이를 해결하는 기법을 소개하고 마지막엔 응용법을 소개하는 식으로 진행한다.
4. 메모리 일관성 문제
#!if 리스트 == null
{{{#!html 휘발성 자료형 한정자}}}에 대한 내용은 {{{#!if 같은문서1 == null
[[C++/문법]] 문서{{{#!if (문단1 == null) == (앵커1 == null)
를}}}{{{#!if (문단1 == null) != (앵커1 == null)
의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[C++/문법#s-|]]번 문단을}}}{{{#!if 문단1 == null & 앵커1 != null
[[C++/문법#volatile|volatile]] 부분을}}}{{{#!if 설명2 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서2 == null
[[]] 문서{{{#!if (문단2 == null) == (앵커2 == null)
를}}}{{{#!if (문단2 == null) != (앵커2 == null)
의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단을}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분을}}}}}}{{{#!if 설명3 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서3 == null
[[]] 문서{{{#!if (문단3 == null) == (앵커3 == null)
를}}}{{{#!if (문단3 == null) != (앵커3 == null)
의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단을}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분을}}}}}}{{{#!if 설명4 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서4 == null
[[]] 문서{{{#!if (문단4 == null) == (앵커4 == null)
를}}}{{{#!if (문단4 == null) != (앵커4 == null)
의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단을}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분을}}}}}}{{{#!if 설명5 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서5 == null
[[]] 문서{{{#!if (문단5 == null) == (앵커5 == null)
를}}}{{{#!if (문단5 == null) != (앵커5 == null)
의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단을}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분을}}}}}}{{{#!if 설명6 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서6 == null
[[]] 문서{{{#!if (문단6 == null) == (앵커6 == null)
를}}}{{{#!if (문단6 == null) != (앵커6 == null)
의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단을}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분을}}}}}}{{{#!if 설명7 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서7 == null
[[]] 문서{{{#!if (문단7 == null) == (앵커7 == null)
를}}}{{{#!if (문단7 == null) != (앵커7 == null)
의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단을}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분을}}}}}}{{{#!if 설명8 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서8 == null
[[]] 문서{{{#!if (문단8 == null) == (앵커8 == null)
를}}}{{{#!if (문단8 == null) != (앵커8 == null)
의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단을}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분을}}}}}}{{{#!if 설명9 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서9 == null
[[]] 문서{{{#!if (문단9 == null) == (앵커9 == null)
를}}}{{{#!if (문단9 == null) != (앵커9 == null)
의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단을}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분을}}}}}}{{{#!if 설명10 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서10 == null
[[]] 문서{{{#!if (문단10 == null) == (앵커10 == null)
를}}}{{{#!if (문단10 == null) != (앵커10 == null)
의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단을}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분을}}}}}}
#!if 리스트 != null
한시적 넘겨주기로 연결되는 다른 문서에 대한 내용은 아래 문서를
참고하십시오.#!if 리스트 != null
{{{#!if 문서명1 != null
* {{{#!if 설명1 != null
휘발성 자료형 한정자: }}}{{{#!if 같은문서1 == null
[[C++/문법]] {{{#!if (문단1 == null) != (앵커1 == null)
문서의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[C++/문법#s-|]]번 문단}}}{{{#!if 문단1 == null & 앵커1 != null
[[C++/문법#volatile|volatile]] 부분}}}}}}{{{#!if 문서명2 != null
* {{{#!if 설명2 != null
: }}}{{{#!if 같은문서2 == null
[[]] {{{#!if (문단2 == null) != (앵커2 == null)
문서의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분}}}}}}{{{#!if 문서명3 != null
* {{{#!if 설명3 != null
: }}}{{{#!if 같은문서3 == null
[[]] {{{#!if (문단3 == null) != (앵커3 == null)
문서의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분}}}}}}{{{#!if 문서명4 != null
* {{{#!if 설명4 != null
: }}}{{{#!if 같은문서4 == null
[[]] {{{#!if (문단4 == null) != (앵커4 == null)
문서의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분}}}}}}{{{#!if 문서명5 != null
* {{{#!if 설명5 != null
: }}}{{{#!if 같은문서5 == null
[[]] {{{#!if (문단5 == null) != (앵커5 == null)
문서의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분}}}}}}{{{#!if 문서명6 != null
* {{{#!if 설명6 != null
: }}}{{{#!if 같은문서6 == null
[[]] {{{#!if (문단6 == null) != (앵커6 == null)
문서의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분}}}}}}{{{#!if 문서명7 != null
* {{{#!if 설명7 != null
: }}}{{{#!if 같은문서7 == null
[[]] {{{#!if (문단7 == null) != (앵커7 == null)
문서의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분}}}}}}{{{#!if 문서명8 != null
* {{{#!if 설명8 != null
: }}}{{{#!if 같은문서8 == null
[[]] {{{#!if (문단8 == null) != (앵커8 == null)
문서의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분}}}}}}{{{#!if 문서명9 != null
* {{{#!if 설명9 != null
: }}}{{{#!if 같은문서9 == null
[[]] {{{#!if (문단9 == null) != (앵커9 == null)
문서의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분}}}}}}{{{#!if 문서명10 != null
* {{{#!if 설명10 != null
: }}}{{{#!if 같은문서10 == null
[[]] {{{#!if (문단10 == null) != (앵커10 == null)
문서의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분}}}}}}
이 문단에서는 다중 스레드 환경에서 발생하는 메모리 관련 문제를 살펴보고자 한다. 먼저 컴파일러와 CPU의 최적화에 의해 발생하는 문제를 살펴보자. 메모리는 다수의 스레드에 의해 읽히거나 수정될 수 있다. 그런데 이 과정에서 스레드마다 읽는 정보가 다를 수도 있고, 못 읽을 수도 있고, 아예 안 읽을 수도 있다. 예제를 통해 이유를 알아보자.
#!syntax cpp
/*volatile*/ int a;
void Worker()
{
a = 0; // 릴리스 모드에서 이 구문은 삭제됨.
a = 10; // 릴리스 모드에서 이 구문은 삭제됨.
a = 20; // 최적화 여하에 따라 `a`의 선언 부분으로 코드가 옮겨질 수도 있음.
}
int main()
{
Worker();
// 릴리스 모드에서는 항상 a = 20임.
}
최적화 옵션을 켜면 상기 코드의 변수 접근 문장들이 삭제된다. 상기 예제는 여러번 연속으로 a
에 대입하고 있는데 최적화를 켜면 마지막 구문만 남는다. 이 문제는 저 변수 사이에 다른 작업이 수행될 때도 발생할 수 있다. 만약 a = 10;
과 a = 20;
사이에 어떤 시간이 걸리는 작업을 수행하고 있어도, 그 작업이 변수 a
를 참조하지 않으면 컴파일러 는 a = 10;
구문은 없애도 되는 것으로 판단한다.휘발성 (Volatile)
이를 방지하기 위해서는
volatile
을 사용해야 한다. volatile
이 아닌 변수는 무조건 최종 결과 이전의 연산 과정이 생략된다. 우리는 컴파일러가 로직을 깨버리는 걸 막기 위해서 공유 자원에 volatile
을 써야 한다. 허나 volatile
이 만능은 아니다.#!syntax cpp
volatile bool ready = false;
volatile int value;
void Producer()
{
while (!ready);
std::println("`value`는 {}.", value);
}
void Consumer()
{
std::println("정수를 입력해주세요.");
std::cin >> value;
ready = true;
}
int main()
{
std::println("프로그램 시작");
std::thread pro(Producer);
std::thread con(Consumer);
std::println("작업자 대기 중...");
pro.join();
con.join();
std::println("프로그램 종료");
}
상기 프로그램에서 ready = true;
구문의 실행 순서가 바뀔 수 있다. 혹은 while (!ready);
구문이 최적화되어 무한 반복문으로 바뀔 수 있다. 원인은 두가지다. 첫번째는 두 스레드에서 참조되는 ready
는 분명 정수를 입력하기 전까지 건드리지 못하게 막는 게 의도된 사항이다. 허나 컴파일러는 이 사실을 알지 못하고 최적화를 수행해버린다.#!syntax cpp
volatile bool ready;
void Worker()
{
ready = false;
// 단일 스레드 환경에서는 이 사이에 `ready`에 접근하는 다른 명령어가 오지 않음,
// 릴리스 모드에서 최적화의 대상
while (false == ready) // while (true)와 같은 무한 반복문으로 변환됨.
{
std::this_thread::yield();
}
}
void main()
{
std::println("시작");
std::thread worker(Worker);
std::println("작업 완료 대기 중...");
ready = true;
std::println("작업자 대기 중...");
worker.join();
std::println("종료");
}
상기 코드는 릴리스 모드에서(또는 컴파일러의 최적화 옵션을 켜고) 실행하면 실행이 멈춘다. 왜 그럴까? 스레드 `Worker`
안의 while
문이 최적화되면서 전역 변수 ready
를 검사하는 구문이 사라진다. 스레드 안에서 `ready`
는 항상 false
로 보인다 (Visible). 주 스레드에서 ready
를 true
로 만들지만, 다른 스레드에서는 이를 관측할 수 없다 (Invisible).단일 스레드 환경이라면 함수
`Worker`
안에서 메모리 읽기/쓰기의 순서가 지켜진다. 그런데 다중 스레드 환경에서는 ready = false;
와 while
문 사이에 다른 명령어가 배치될 수 있다. 최적화의 기준은 항상 단일 스레드 기준으로 수행되기에 컴파일러는 이를 알지 못하고 무한 반복문으로 바꿔버리는 것이다.다음은 CPU의 비순차적 실행(Out of Order)에 의해 발생하는 문제를 알아보자. 파이프라인에서 나온 명령어는 한번에 한번 실행된다는 점도 알아두어야 한다. 사용자의 눈에는 동시에 실행되는 것처럼 보이지만, 실제로 메모리에 영향을 끼치는 명령어는 한번에 하나씩이다.
#!syntax cpp
volatile int a, b;
// 여기서 b = 1;이 먼저 실행될 수도 있음.
// 두 변수 사이에 데이터 의존성이 없기 때문임.
a = 1;
b = 1;
비순차적 실행에 의하여 파이프라인에 들어가고 나오는 순서가 바뀔 수 있다. 코드로 전달한 명령어가 우리 기준을 따르지 않고 무작위로 실행된다는 건 우리가 CPU의 다음 실행 순서를 예측하기가 매우 어렵다는 말이 된다. 하지만 이 기능은 현대 CPU에서 고성능 달성의 핵심 중의 핵심 요소이므로 제외할 수도 없다.- <C++ 예제 보기>
#!syntax cpp volatile int a, b; void Worker1() { a = 1; a = ++b; a = (a++) + b; ++a; } void Worker2() { b = 1; b -= a; ++b; b = b * 2; b += a; } void Worker3() { for (int i = 0; i < 10; ++i) { std::println("a: {}, b: {}", *(const_cast<int*>(&a)), *(const_cast<int*>(&b))); } } int main() { std::println("시작"); std::thread worker1(Worker1); std::thread worker2(Worker2); std::thread worker3(Worker3); worker3.join(); worker1.join(); worker2.join(); std::println("종료"); return 0; }
`a`
와 `b`
를 읽고 쓰는 순서가 고정되지 않기 때문이다. Worker1
과 Worker2
의 연산 구문의 최적화는 volatile
에 의해 방지된다. 그럼에도 불구하고 잘못된 결과를 보여준다.변수에 적용된
volatile
은 다중 스레드 환경에서 동기화 기능을 하지 못한다. volatile
은 읽은 메모리가 항상 갱신된다는 보장의 역할이며 다른 스레드 사이의 통신 기능을 하지 않는다.5. 임계 영역과 경쟁 상태
#!if 리스트 == null
{{{#!html 이 문단}}}에 대한 내용은 {{{#!if 같은문서1 == null
[[뮤텍스]] 문서{{{#!if (문단1 == null) == (앵커1 == null)
를}}}{{{#!if (문단1 == null) != (앵커1 == null)
의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[뮤텍스#s-|]]번 문단을}}}{{{#!if 문단1 == null & 앵커1 != null
[[뮤텍스#임계 구역(Critical Section)|임계 구역(Critical Section)]] 부분을}}}{{{#!if 설명2 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서2 == null
[[]] 문서{{{#!if (문단2 == null) == (앵커2 == null)
를}}}{{{#!if (문단2 == null) != (앵커2 == null)
의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단을}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분을}}}}}}{{{#!if 설명3 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서3 == null
[[]] 문서{{{#!if (문단3 == null) == (앵커3 == null)
를}}}{{{#!if (문단3 == null) != (앵커3 == null)
의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단을}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분을}}}}}}{{{#!if 설명4 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서4 == null
[[]] 문서{{{#!if (문단4 == null) == (앵커4 == null)
를}}}{{{#!if (문단4 == null) != (앵커4 == null)
의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단을}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분을}}}}}}{{{#!if 설명5 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서5 == null
[[]] 문서{{{#!if (문단5 == null) == (앵커5 == null)
를}}}{{{#!if (문단5 == null) != (앵커5 == null)
의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단을}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분을}}}}}}{{{#!if 설명6 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서6 == null
[[]] 문서{{{#!if (문단6 == null) == (앵커6 == null)
를}}}{{{#!if (문단6 == null) != (앵커6 == null)
의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단을}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분을}}}}}}{{{#!if 설명7 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서7 == null
[[]] 문서{{{#!if (문단7 == null) == (앵커7 == null)
를}}}{{{#!if (문단7 == null) != (앵커7 == null)
의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단을}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분을}}}}}}{{{#!if 설명8 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서8 == null
[[]] 문서{{{#!if (문단8 == null) == (앵커8 == null)
를}}}{{{#!if (문단8 == null) != (앵커8 == null)
의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단을}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분을}}}}}}{{{#!if 설명9 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서9 == null
[[]] 문서{{{#!if (문단9 == null) == (앵커9 == null)
를}}}{{{#!if (문단9 == null) != (앵커9 == null)
의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단을}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분을}}}}}}{{{#!if 설명10 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서10 == null
[[]] 문서{{{#!if (문단10 == null) == (앵커10 == null)
를}}}{{{#!if (문단10 == null) != (앵커10 == null)
의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단을}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분을}}}}}}
#!if 리스트 != null
한시적 넘겨주기로 연결되는 다른 문서에 대한 내용은 아래 문서를
참고하십시오.#!if 리스트 != null
{{{#!if 문서명1 != null
* {{{#!if 설명1 != null
이 문단: }}}{{{#!if 같은문서1 == null
[[뮤텍스]] {{{#!if (문단1 == null) != (앵커1 == null)
문서의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[뮤텍스#s-|]]번 문단}}}{{{#!if 문단1 == null & 앵커1 != null
[[뮤텍스#임계 구역(Critical Section)|임계 구역(Critical Section)]] 부분}}}}}}{{{#!if 문서명2 != null
* {{{#!if 설명2 != null
: }}}{{{#!if 같은문서2 == null
[[]] {{{#!if (문단2 == null) != (앵커2 == null)
문서의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분}}}}}}{{{#!if 문서명3 != null
* {{{#!if 설명3 != null
: }}}{{{#!if 같은문서3 == null
[[]] {{{#!if (문단3 == null) != (앵커3 == null)
문서의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분}}}}}}{{{#!if 문서명4 != null
* {{{#!if 설명4 != null
: }}}{{{#!if 같은문서4 == null
[[]] {{{#!if (문단4 == null) != (앵커4 == null)
문서의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분}}}}}}{{{#!if 문서명5 != null
* {{{#!if 설명5 != null
: }}}{{{#!if 같은문서5 == null
[[]] {{{#!if (문단5 == null) != (앵커5 == null)
문서의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분}}}}}}{{{#!if 문서명6 != null
* {{{#!if 설명6 != null
: }}}{{{#!if 같은문서6 == null
[[]] {{{#!if (문단6 == null) != (앵커6 == null)
문서의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분}}}}}}{{{#!if 문서명7 != null
* {{{#!if 설명7 != null
: }}}{{{#!if 같은문서7 == null
[[]] {{{#!if (문단7 == null) != (앵커7 == null)
문서의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분}}}}}}{{{#!if 문서명8 != null
* {{{#!if 설명8 != null
: }}}{{{#!if 같은문서8 == null
[[]] {{{#!if (문단8 == null) != (앵커8 == null)
문서의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분}}}}}}{{{#!if 문서명9 != null
* {{{#!if 설명9 != null
: }}}{{{#!if 같은문서9 == null
[[]] {{{#!if (문단9 == null) != (앵커9 == null)
문서의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분}}}}}}{{{#!if 문서명10 != null
* {{{#!if 설명10 != null
: }}}{{{#!if 같은문서10 == null
[[]] {{{#!if (문단10 == null) != (앵커10 == null)
문서의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분}}}}}}
임계 영역과 경쟁 상태 (Critical Section and Race Condition)
이 문단에서는 다수의 스레드가 어떤 자원에 동시에 접근할 때 발생하는 문제를 알아보겠다.
#!syntax cpp
// 전역 변수
volatile long long summary = 0;
void Worker()
{
for (long long i = 0; i < 5'000'000'000; ++i)
{
summary += 1;
}
}
int main()
{
std::thread worker1(Worker);
std::thread worker2(Worker);
std::println("작업자 대기 중...");
worker1.join();
worker2.join();
// "합계 = (무작위 숫자)" 출력
// 대개 100억은 안나온다.
long long temp = summary;
std::println("합계 = {}", temp);
}
상기 코드는 두 스레드에서 하나의 전역 변수가 증감 연산을 실행하는 예제다. 그러나 실행 결과를 보면 틀리게 나온다는 걸 알 수 있다. 왜 일까? 앞 문단에서 언급했던 사항을 돌아볼 필요가 있다. 메모리에 영향을 끼치는 명령어는 한번에 하나씩이라고 했는데, 정확히는 쓰기(Write) 동작이 한번에 하나씩 실행된다는 뜻이다. 우리가 쓴 C++ 코드가 기계어에 일대일 대응이 되지 않는 사실도 알아야 한다. 우리가 작성한 summary += 1;
구문은 사실 아래의 기계 명령어와 같다. 1. RAM 또는 캐시 메모리 상의 `summary` 의 값을 버스를 통해 레지스터로 옮기기. (읽기, 문맥 전환) |
2. 레지스터에 1 더하기 (가산기) |
3. 레지스터의 값을 메모리로 옮기기. (쓰기, 문맥 전환) |
<rowcolor=#d7d7d7,#a1a1a1>`worker1` | `worker2` |
`summary` 읽기 → 10 읽음. | 레지스터에 더하기 → 9. |
레지스터에 더하기 → 11. | `summary` 쓰기 → 9 씀. |
`summary` 쓰기 → 11 씀. | `summary` 읽기 → 11 읽음. |
`summary` 읽기 → 11 읽음. | 레지스터에 더하기 → 11. |
레지스터에 더하기 → 12. | `summary` 쓰기 → 12 씀. |
이하 반복... |
다수의 스레드에 의해 공유 자원에 다수의 쓰기 작업이 발생할 때 발생하는 정보의 어긋남을 경쟁 상태(Race Condition)라고 한다.
다수의 쓰기 작업이 발생하면 안되는 공유 자원을 임계 영역(Critical Section)이라고 한다.
이제 이 문제를 해결하는 방법을 알아보자.
5.1. 예제 5: 상호 배제 - std::mutex
#!if 리스트 == null
{{{#!html 이 용어}}}에 대한 내용은 {{{#!if 같은문서1 == null
[[뮤텍스]] 문서{{{#!if (문단1 == null) == (앵커1 == null)
를}}}{{{#!if (문단1 == null) != (앵커1 == null)
의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[뮤텍스#s-|]]번 문단을}}}{{{#!if 문단1 == null & 앵커1 != null
[[뮤텍스#|]] 부분을}}}{{{#!if 설명2 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서2 == null
[[]] 문서{{{#!if (문단2 == null) == (앵커2 == null)
를}}}{{{#!if (문단2 == null) != (앵커2 == null)
의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단을}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분을}}}}}}{{{#!if 설명3 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서3 == null
[[]] 문서{{{#!if (문단3 == null) == (앵커3 == null)
를}}}{{{#!if (문단3 == null) != (앵커3 == null)
의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단을}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분을}}}}}}{{{#!if 설명4 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서4 == null
[[]] 문서{{{#!if (문단4 == null) == (앵커4 == null)
를}}}{{{#!if (문단4 == null) != (앵커4 == null)
의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단을}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분을}}}}}}{{{#!if 설명5 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서5 == null
[[]] 문서{{{#!if (문단5 == null) == (앵커5 == null)
를}}}{{{#!if (문단5 == null) != (앵커5 == null)
의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단을}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분을}}}}}}{{{#!if 설명6 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서6 == null
[[]] 문서{{{#!if (문단6 == null) == (앵커6 == null)
를}}}{{{#!if (문단6 == null) != (앵커6 == null)
의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단을}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분을}}}}}}{{{#!if 설명7 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서7 == null
[[]] 문서{{{#!if (문단7 == null) == (앵커7 == null)
를}}}{{{#!if (문단7 == null) != (앵커7 == null)
의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단을}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분을}}}}}}{{{#!if 설명8 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서8 == null
[[]] 문서{{{#!if (문단8 == null) == (앵커8 == null)
를}}}{{{#!if (문단8 == null) != (앵커8 == null)
의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단을}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분을}}}}}}{{{#!if 설명9 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서9 == null
[[]] 문서{{{#!if (문단9 == null) == (앵커9 == null)
를}}}{{{#!if (문단9 == null) != (앵커9 == null)
의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단을}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분을}}}}}}{{{#!if 설명10 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서10 == null
[[]] 문서{{{#!if (문단10 == null) == (앵커10 == null)
를}}}{{{#!if (문단10 == null) != (앵커10 == null)
의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단을}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분을}}}}}}
#!if 리스트 != null
한시적 넘겨주기로 연결되는 다른 문서에 대한 내용은 아래 문서를
참고하십시오.#!if 리스트 != null
{{{#!if 문서명1 != null
* {{{#!if 설명1 != null
이 용어: }}}{{{#!if 같은문서1 == null
[[뮤텍스]] {{{#!if (문단1 == null) != (앵커1 == null)
문서의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[뮤텍스#s-|]]번 문단}}}{{{#!if 문단1 == null & 앵커1 != null
[[뮤텍스#|]] 부분}}}}}}{{{#!if 문서명2 != null
* {{{#!if 설명2 != null
: }}}{{{#!if 같은문서2 == null
[[]] {{{#!if (문단2 == null) != (앵커2 == null)
문서의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분}}}}}}{{{#!if 문서명3 != null
* {{{#!if 설명3 != null
: }}}{{{#!if 같은문서3 == null
[[]] {{{#!if (문단3 == null) != (앵커3 == null)
문서의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분}}}}}}{{{#!if 문서명4 != null
* {{{#!if 설명4 != null
: }}}{{{#!if 같은문서4 == null
[[]] {{{#!if (문단4 == null) != (앵커4 == null)
문서의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분}}}}}}{{{#!if 문서명5 != null
* {{{#!if 설명5 != null
: }}}{{{#!if 같은문서5 == null
[[]] {{{#!if (문단5 == null) != (앵커5 == null)
문서의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분}}}}}}{{{#!if 문서명6 != null
* {{{#!if 설명6 != null
: }}}{{{#!if 같은문서6 == null
[[]] {{{#!if (문단6 == null) != (앵커6 == null)
문서의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분}}}}}}{{{#!if 문서명7 != null
* {{{#!if 설명7 != null
: }}}{{{#!if 같은문서7 == null
[[]] {{{#!if (문단7 == null) != (앵커7 == null)
문서의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분}}}}}}{{{#!if 문서명8 != null
* {{{#!if 설명8 != null
: }}}{{{#!if 같은문서8 == null
[[]] {{{#!if (문단8 == null) != (앵커8 == null)
문서의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분}}}}}}{{{#!if 문서명9 != null
* {{{#!if 설명9 != null
: }}}{{{#!if 같은문서9 == null
[[]] {{{#!if (문단9 == null) != (앵커9 == null)
문서의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분}}}}}}{{{#!if 문서명10 != null
* {{{#!if 설명10 != null
: }}}{{{#!if 같은문서10 == null
[[]] {{{#!if (문단10 == null) != (앵커10 == null)
문서의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분}}}}}}
std::mutex
[7]는 상호 배제(Mutual Exclusive) 개념을 구현한 클래스다. 이 클래스는 잠기거나 잠금 해제될 수 있다. 잠기는 동안에 접근한 메모리 영역은 다른 스레드에서 접근하지 못하도록 보호받는다. 여기서 보호받는다는 말을 짚고 넘어갈 필요가 있다. 뮤텍스는 배타적 소유권으로 보호받는다. 뮤텍스를 잠그는 것은 뮤텍스의 소유권을 현재 스레드로 옮기는 것이다. 그리고 이전에 잠긴 동안에 잠그는 시도를 하면 소유권을 넘기지 않고 현재 스레드를 대기 상태로 만든다 [8]. 즉, 잠근 영역 안에 있는 메모리를 귀신같이 찝어내서 다른 스레드에서 자동으로 접근을 막는 게 아니다! 사용자가 알아서 잠금이 필요한 영역을 분석하고 뮤텍스를 배치해야 한다.여러 개 만든 뮤텍스는 잠그는 것 자체는 누가 언제 잠겨있든 아니든 상관없이 수행할 수 있다. 허나 이렇게 잠근 뮤텍스는 반드시 잠금을 풀어줘야 한다. 왜냐하면 뮤텍스가 잠겨있으면 운영체제 단에서 스핀 락(Spin Lock)을 걸어두고 스레드를 대기시킨다. 그런데
join
내지는 스레드의 종료는 스레드의 수행이 모두 끝나야 가능한데 스레드는 실행이 멈춘 상태이므로 영원히 끝나지 않는다. main
함수가 종료되는 시점에 프로그램(프로세스)도 종료되면서 프로세스가 소유한 스레드들도 모두 종료되기는 하다. 그러나 이는 비정상적인 종료로 취급되며 로직 상으로 무언가 꼬인 상황일 확률이 크다. 스레드 클래스가 하던 어떤 작업이 제대로 끝나지 않았다는 증표일 수도 있다.5.1.1. 성긴 동기화
성긴 동기화 (Coarse-grained Synchronization)#!syntax cpp
volatile long long summary = 0;
std::mutex globalLock;
void Worker()
{
for (long long i = 0; i < 5'000'000'000; ++i)
{
globalLock.lock();
summary += 1;
globalLock.unlock();
}
}
int main()
{
std::thread worker1(Worker);
std::thread worker2(Worker);
std::println("작업자 대기 중...");
worker1.join();
worker2.join();
// "합계 = 10000000000" 출력
long long temp = summary;
std::println("합계 = {}", temp);
}
뮤텍스의 멤버 함수 lock
를 써서 잠그고, unlock
를 써서 잠금 해제할 수 있다. 상기 코드는 100억번의 for문을 실행하면서 뮤텍스와 함께 전역 변수의 값을 증분시키고 있다. 결과도 올바르게 나온다.허나 실제 활용 중에 복잡한 코드와 함께 뮤텍스도 빈번하게 상태가 변하는 것은 성능에 치명적인 악영향을 부른다. 실행해보면 시간이 매우 오래걸리는 걸 알 수 있다. 비록 결과는 틀리게 나오지만 제일 위쪽에 뮤텍스를 쓰지 않은 합계 코드는 컴퓨터 성능에 따라 다르지만 보통 1~3분 정도 걸린다. 뮤텍스를 쓴 코드는 10분 넘게 걸릴 수도 있다 [9]. 왜냐하면 뮤텍스의 소유권 선점은 전적으로 운영체제 호출을 통해 이루어지기 때문이다. 뮤텍스 내부의 플래그를 검사하고, 소유권을 확인하고, 문맥 전환 등등의 동작을 수억번 하면 당연히 성능이 안나올 수 밖에 없다.
성긴 동기화는 성능이 불균일하고, 구현도 조잡한 방식으로 작성되었다는 뜻이다. 다음 구현을 보면서 개선점을 알아보자.
5.1.2. 고운 동기화
고운 동기화 (Fine-grained Synchronization)#!syntax cpp
volatile long long summary = 0;
std::mutex globalLock;
void Worker_v2()
{
globalLock.lock();
for (long long i = 0; i < 5'000'000'000; ++i)
{
summary += 1;
}
globalLock.unlock();
}
void Worker_v3()
{
auto local_sum = []()
{
long long result = 0;
for (long long i = 0; i < 5'000'000'000; ++i)
{
result += 1;
}
return result;
};
const long long result = local_sum();
globalLock.lock();
summary += result;
globalLock.unlock();
}
int main()
{
std::thread worker1(Worker_v2);
std::thread worker2(Worker_v3);
std::println("작업자 대기 중...");
worker1.join();
worker2.join();
// "합계 = 10000000000" 출력
long long temp = summary;
std::println("합계 = {}", temp);
}
상기 코드는 윗 문단의 예제의 성능을 개선시킨 코드다.`Worker_v2`
는 뮤텍스가 잠기는 시점을 반복문의 바깥으로 빼내어 뮤텍스가 잠기는 횟수를 줄인 함수다. 운영체제 호출을 줄여 문맥 전환을 획기적으로 감소시켰다. 그러나 이 예제에도 한계점이 있다. 이 코드는 병렬성이 오히려 성긴 반복문보다 떨어진다. 어쨌든 하나의 스레드가 뮤텍스와 `summary`
변수를 일정 시간 고정 점유하고 있기 때문이다.`Worker_v3`
는 뮤텍스가 필요한 지점을 축소하고 실제 잠기는 시간을 줄인 함수다. 다시 말하자면 임계 영역에 접근하는 코드와 그렇지 않은 코드를 분리하고 뮤텍스를 배치한 예시다. 우리가 이 코드를 작성하는 최종 목적은 두 스레드의 협업을 통해 숫자 100억을 구하는 것이다. 이는 두 스레드가 각각 50억 씩 맡아서 더하는 작업으로 쪼갤 수 있다. 더하고 나면 다시 원래 변수에 50억 씩 두번 더하면 100억이 된다. 그런데 이 과정을 천천히 보면 앞서 살펴본 변수에 더하는 기계어 명령어와 비슷한 걸 알 수 있다. 1. 변수(`summary` )를 레지스터로 읽는다. |
2. 레지스터에 50억을 더한다. |
3. 변수(`summary` )에 레지스터의 값을 쓴다. |
1. 레지스터A에 50억을 더한다 → 2. 레지스터B에 레지스터A의 값을 더한다 → 3. 변수를 읽는다 → 4. 변수에 레지스터B의 값을 쓴다
`summary`
에 더하는 순간만이 임계 영역이기에 이 부분만 뮤텍스가 막아주면 된다. 어떻게든 문맥 전환의 빈도를 줄인다면 성능도 올리고 높은 병렬성도 달성할 수 있는 것이다.5.1.3. 벤치마크
- <C++ 예제 보기>
#!syntax cpp constexpr long long summary_target = 100'000'000LL; // 10억 volatile long long summary = 0; std::mutex globalLock; thread_local long long count = 0; void Worker_v1(long long number) { count = summary_target / number; for (long long i = 0; i < count; ++i) { globalLock.lock(); summary += 1; globalLock.unlock(); } } void Worker_v2(long long number) { count = summary_target / number; globalLock.lock(); for (long long i = 0; i < count; ++i) { summary += 1; } globalLock.unlock(); } void Worker_v3(long long number) { count = summary_target / number; auto local_sum = [=]() { long long result = 0; for (long long i = 0; i < count; ++i) { result += 1; } return result; }; const long long result = local_sum(); globalLock.lock(); summary += result; globalLock.unlock(); } int main() { constexpr long long bm_max_number = 16; using Functor_t = void(*)(long long); constexpr std::array<Functor_t, 3> functors{ Worker_v1, Worker_v2, Worker_v3 }; std::vector<std::thread> workers; workers.reserve(bm_max_number); std::chrono::system_clock::time_point before_tm, after_tm; std::println("시작"); // 최대 16개의 스레드까지 벤치마크 수행 for (long long n = 1; n <= bm_max_number; n *= 2) { std::println("========================"); // 세 종류의 작업 함수 열거 for (long long j = 0; j < 3; ++j) { auto& functor = functors[j]; const auto functor_id = j + 1; std::println("[Worker_v{}] 스레드 개수 {}에서 작업 시작하는 중...", functor_id, n); before_tm = std::chrono::system_clock::now(); // 작업자 스레드 생성 for (long long i = 0; i < n; ++i) { workers.emplace_back(functor, n); } // 작업자 스레드 종료 std::println("[Worker_v{}] 작업자 대기 중...", functor_id); // *이 출력 함수도 벤치마크에 영향을 끼침. for (long long i = 0; i < n; ++i) { auto& worker = workers[i]; worker.join(); } std::println("[Worker_v{}] 스레드 개수 {}에서 작업 완료 (합계: {})", functor_id, n, summary); after_tm = std::chrono::system_clock::now(); auto duration = after_tm - before_tm; std::println("[Worker_v{}] 스레드 개수 {}에서 걸린 시간: {}", functor_id, n, duration.count()); workers.clear(); summary = 0; } std::println("========================"); } std::println("벤치마크 종료"); }
- <실행 결과 보기>
시작 ======================== [Worker_v1] 스레드 개수 1에서 작업 시작하는 중... [Worker_v1] 작업자 대기 중... [Worker_v1] 스레드 개수 1에서 작업 완료 (합계: 100000000) [Worker_v1] 스레드 개수 1에서 걸린 시간: 111454737 [Worker_v2] 스레드 개수 1에서 작업 시작하는 중... [Worker_v2] 작업자 대기 중... [Worker_v2] 스레드 개수 1에서 작업 완료 (합계: 100000000) [Worker_v2] 스레드 개수 1에서 걸린 시간: 5422907 [Worker_v3] 스레드 개수 1에서 작업 시작하는 중... [Worker_v3] 작업자 대기 중... [Worker_v3] 스레드 개수 1에서 작업 완료 (합계: 100000000) [Worker_v3] 스레드 개수 1에서 걸린 시간: 5746908 ======================== ======================== [Worker_v1] 스레드 개수 2에서 작업 시작하는 중... [Worker_v1] 작업자 대기 중... [Worker_v1] 스레드 개수 2에서 작업 완료 (합계: 100000000) [Worker_v1] 스레드 개수 2에서 걸린 시간: 122256458 [Worker_v2] 스레드 개수 2에서 작업 시작하는 중... [Worker_v2] 작업자 대기 중... [Worker_v2] 스레드 개수 2에서 작업 완료 (합계: 100000000) [Worker_v2] 스레드 개수 2에서 걸린 시간: 5364200 [Worker_v3] 스레드 개수 2에서 작업 시작하는 중... [Worker_v3] 작업자 대기 중... [Worker_v3] 스레드 개수 2에서 작업 완료 (합계: 100000000) [Worker_v3] 스레드 개수 2에서 걸린 시간: 2749176 ======================== ======================== [Worker_v1] 스레드 개수 4에서 작업 시작하는 중... [Worker_v1] 작업자 대기 중... [Worker_v1] 스레드 개수 4에서 작업 완료 (합계: 100000000) [Worker_v1] 스레드 개수 4에서 걸린 시간: 113969307 [Worker_v2] 스레드 개수 4에서 작업 시작하는 중... [Worker_v2] 작업자 대기 중... [Worker_v2] 스레드 개수 4에서 작업 완료 (합계: 100000000) [Worker_v2] 스레드 개수 4에서 걸린 시간: 5338313 [Worker_v3] 스레드 개수 4에서 작업 시작하는 중... [Worker_v3] 작업자 대기 중... [Worker_v3] 스레드 개수 4에서 작업 완료 (합계: 100000000) [Worker_v3] 스레드 개수 4에서 걸린 시간: 1996369 ======================== ======================== [Worker_v1] 스레드 개수 8에서 작업 시작하는 중... [Worker_v1] 작업자 대기 중... [Worker_v1] 스레드 개수 8에서 작업 완료 (합계: 100000000) [Worker_v1] 스레드 개수 8에서 걸린 시간: 114418716 [Worker_v2] 스레드 개수 8에서 작업 시작하는 중... [Worker_v2] 작업자 대기 중... [Worker_v2] 스레드 개수 8에서 작업 완료 (합계: 100000000) [Worker_v2] 스레드 개수 8에서 걸린 시간: 5145375 [Worker_v3] 스레드 개수 8에서 작업 시작하는 중... [Worker_v3] 작업자 대기 중... [Worker_v3] 스레드 개수 8에서 작업 완료 (합계: 100000000) [Worker_v3] 스레드 개수 8에서 걸린 시간: 1957579 ======================== ======================== [Worker_v1] 스레드 개수 16에서 작업 시작하는 중... [Worker_v1] 작업자 대기 중... [Worker_v1] 스레드 개수 16에서 작업 완료 (합계: 100000000) [Worker_v1] 스레드 개수 16에서 걸린 시간: 117675779 [Worker_v2] 스레드 개수 16에서 작업 시작하는 중... [Worker_v2] 작업자 대기 중... [Worker_v2] 스레드 개수 16에서 작업 완료 (합계: 100000000) [Worker_v2] 스레드 개수 16에서 걸린 시간: 4439445 [Worker_v3] 스레드 개수 16에서 작업 시작하는 중... [Worker_v3] 작업자 대기 중... [Worker_v3] 스레드 개수 16에서 작업 완료 (합계: 100000000) [Worker_v3] 스레드 개수 16에서 걸린 시간: 2103307 ======================== 벤치마크 종료
||<tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1><rowcolor=#d7d7d7,#a1a1a1> 스레드 개수 ||
프로세서가 4개 뿐이라 최적화의 혜택을 잘 보진 못한다[10]. 그러나 명백한 성능 개선점이 있음을 알 수 있다. 우리가 처음 만든 `Worker_v1`
|| `Worker_v2`
|| `Worker_v3`
||1개 | 111453737 | 5422907 | 5746908 | ||||||||
2개 | 122256458 | 5364200 | 2749176 | ||||||||
4개 | 113969307 | 5338313 | 1996369 | ||||||||
8개 | 114418716 | 5145375 | 1957579 | ||||||||
16개 | 117675779 | 4439445 | 2103307 | ||||||||
<bgcolor=#20b580> |
`Worker_v1`
는 쓰지 못할 지경이고, `Worker_v2`
는 무려 200분의 1의 시간만 걸린다. `Worker_v3`
는 스레드가 1개면 차이가 없지만 개수를 늘릴 수록 큰 성능 향상이 있음을 알 수 있다. 마지막 16개의 경우 성능이 오히려 하락하는데, 왜냐하면 두가지 이유가 있다. 첫번째는 프로세스가 4개이고, 하이퍼스레딩으로 스레드 8개 까지가 CPU가 감당할 수 있는 성능 효용의 한계다. 두번째는 스레드의 개수가 16개쯤 되면 스레드를 여러개 만들고 실행했다가 join
으로 종료하는 과정 자체의 오버헤드도 무시할 수 없기 때문이다.이렇게 임계 영역을 잘 구별하는 일이 중요함을 알아보았다.
5.2. 예제 6: 콘솔 출력 동기화
#!syntax cpp
void Worker()
{
std::cout << "작업자 " << std::this_thread::get_id() << " 시작..." << std::endl;
for (int i = 0; i < 5; ++i)
{
std::cout << "작업자 " << std::this_thread::get_id() << " 작업 중...(" << (i + 1) << "/5)" << std::endl;
}
}
int main()
{
std::thread worker1(Worker);
std::thread worker2(Worker);
std::thread worker3(Worker);
std::thread worker4(Worker);
std::println("작업자 대기 중...");
worker1.join();
worker2.join();
worker3.join();
worker4.join();
std::println("모든 작업자 종료.");
}
상기 코드는 네개의 스레드를 생성하고 각 스레드에서 5번의 작업 메시지를 띄우는 예제다. 이 예제에서 임계 영역은 std::cout
이다.- <실행 결과 보기>
작업자 대기 중... 작업자 24920 시작...작업자 27660 시작... 작업자 27660 작업 중...(1/5) 작업자 27660 작업 중...(2/5) 작업자 27660 작업 중...(3/5) 작업자 27660 작업 중...(4/5) 작업자 27660 작업 중...(5/5) 작업자 4136 시작... 작업자 4136 작업 중...(1/5) 작업자 4136 작업 중...(2/5) 작업자 4136 작업 중...(3/5) 작업자 4136 작업 중...(4/5) 작업자 4136 작업 중...(5/5) 작업자 작업자 29164 시작... 작업자 29164 작업 중...(1/5) 작업자 29164 작업 중...(2/5) 작업자 29164 작업 중...(3/5) 작업자 29164 작업 중...(4/5) 작업자 29164 작업 중...(5/5) 24920 작업 중...(1/5) 작업자 24920 작업 중...(2/5) 작업자 24920 작업 중...(3/5) 작업자 24920 작업 중...(4/5) 작업자 24920 작업 중...(5/5) 모든 작업자 종료.
std::cout
에서 실행되는 <<
연산자가 실행되는 사이에 다른 스레드의 <<
연산자가 끼어들기 때문이다. 다행히 콘솔의 출력은 운영체제가 관리하므로 <<
로 전달한 출력 단위 하나는 쪼개지지 않지만, <<
연산 사이에 지연 시간 때문에 발생한다.#!syntax cpp
import <string_view>;
import <mutex>;
import <utility>;
import <iostream>;
import <strstream>;
// 전역 변수
std::mutex outputLock;
class LockedStream
{
public:
LockedStream() noexcept = default;
~LockedStream()
{
// 생성자에서 `outputLock` 잠금, 소멸자에서 `outputLock` 잠금 해제
std::lock_guard lock(outputLock);
std::cout << innerStream.str();
}
template<typename T>
LockedStream& operator<<(T&& value)
{
innerStream << std::forward<T>(value);
return *this;
}
LockedStream& operator<<(std::string_view str)
{
innerStream << str.data();
return *this;
}
LockedStream& operator<<(std::basic_ostream<char>& (*fun)(std::basic_ostream<char>&))
{
fun(innerStream);
return *this;
}
private:
std::stringstream innerStream;
};
void Worker()
{
LockedStream() << "작업자 " << std::this_thread::get_id() << " 시작..." << std::endl;
for (int i = 0; i < 5; ++i)
{
LockedStream() << "작업자 " << std::this_thread::get_id() << " 작업 중...(" << (i + 1) << "/5)" << std::endl;
}
}
std::stringstream
에 출력할 내용을 쌓아 놨다가 소멸자에서 뮤텍스를 쓰고 한번에 출력한다. 참고로 동일한 기능이 C++20의 <syncstream>
으로 추가되었다.- <실행 결과 보기>
작업자 대기 중... 작업자 23848 시작... 작업자 18284 시작... 작업자 18284 작업 중...(1/5) 작업자 18284 작업 중...(2/5) 작업자 23848 작업 중...(1/5) 작업자 23848 작업 중...(2/5) 작업자 23848 작업 중...(3/5) 작업자 23848 작업 중...(4/5) 작업자 23848 작업 중...(5/5) 작업자 27256 시작... 작업자 18284 작업 중...(3/5) 작업자 18284 작업 중...(4/5) 작업자 18284 작업 중...(5/5) 작업자 9376 시작... 작업자 9376 작업 중...(1/5) 작업자 9376 작업 중...(2/5) 작업자 27256 작업 중...(1/5) 작업자 27256 작업 중...(2/5) 작업자 27256 작업 중...(3/5) 작업자 27256 작업 중...(4/5) 작업자 27256 작업 중...(5/5) 작업자 9376 작업 중...(3/5) 작업자 9376 작업 중...(4/5) 작업자 9376 작업 중...(5/5) 모든 작업자 종료.
6. 교착 상태
#!if 리스트 == null
{{{#!html 이 항목의 배경 지식}}}에 대한 내용은 {{{#!if 같은문서1 == null
[[식사하는 철학자 문제]] 문서{{{#!if (문단1 == null) == (앵커1 == null)
를}}}{{{#!if (문단1 == null) != (앵커1 == null)
의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[식사하는 철학자 문제#s-|]]번 문단을}}}{{{#!if 문단1 == null & 앵커1 != null
[[식사하는 철학자 문제#|]] 부분을}}}{{{#!if 설명2 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서2 == null
[[]] 문서{{{#!if (문단2 == null) == (앵커2 == null)
를}}}{{{#!if (문단2 == null) != (앵커2 == null)
의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단을}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분을}}}}}}{{{#!if 설명3 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서3 == null
[[]] 문서{{{#!if (문단3 == null) == (앵커3 == null)
를}}}{{{#!if (문단3 == null) != (앵커3 == null)
의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단을}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분을}}}}}}{{{#!if 설명4 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서4 == null
[[]] 문서{{{#!if (문단4 == null) == (앵커4 == null)
를}}}{{{#!if (문단4 == null) != (앵커4 == null)
의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단을}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분을}}}}}}{{{#!if 설명5 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서5 == null
[[]] 문서{{{#!if (문단5 == null) == (앵커5 == null)
를}}}{{{#!if (문단5 == null) != (앵커5 == null)
의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단을}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분을}}}}}}{{{#!if 설명6 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서6 == null
[[]] 문서{{{#!if (문단6 == null) == (앵커6 == null)
를}}}{{{#!if (문단6 == null) != (앵커6 == null)
의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단을}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분을}}}}}}{{{#!if 설명7 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서7 == null
[[]] 문서{{{#!if (문단7 == null) == (앵커7 == null)
를}}}{{{#!if (문단7 == null) != (앵커7 == null)
의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단을}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분을}}}}}}{{{#!if 설명8 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서8 == null
[[]] 문서{{{#!if (문단8 == null) == (앵커8 == null)
를}}}{{{#!if (문단8 == null) != (앵커8 == null)
의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단을}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분을}}}}}}{{{#!if 설명9 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서9 == null
[[]] 문서{{{#!if (문단9 == null) == (앵커9 == null)
를}}}{{{#!if (문단9 == null) != (앵커9 == null)
의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단을}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분을}}}}}}{{{#!if 설명10 != null
, {{{#!html }}}에 대한 내용은 {{{#!if 같은문서10 == null
[[]] 문서{{{#!if (문단10 == null) == (앵커10 == null)
를}}}{{{#!if (문단10 == null) != (앵커10 == null)
의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단을}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분을}}}}}}
#!if 리스트 != null
한시적 넘겨주기로 연결되는 다른 문서에 대한 내용은 아래 문서를
참고하십시오.#!if 리스트 != null
{{{#!if 문서명1 != null
* {{{#!if 설명1 != null
이 항목의 배경 지식: }}}{{{#!if 같은문서1 == null
[[식사하는 철학자 문제]] {{{#!if (문단1 == null) != (앵커1 == null)
문서의 }}}}}}{{{#!if 문단1 != null & 앵커1 == null
[[식사하는 철학자 문제#s-|]]번 문단}}}{{{#!if 문단1 == null & 앵커1 != null
[[식사하는 철학자 문제#|]] 부분}}}}}}{{{#!if 문서명2 != null
* {{{#!if 설명2 != null
: }}}{{{#!if 같은문서2 == null
[[]] {{{#!if (문단2 == null) != (앵커2 == null)
문서의 }}}}}}{{{#!if 문단2 != null & 앵커2 == null
[[#s-|]]번 문단}}}{{{#!if 문단2 == null & 앵커2 != null
[[#|]] 부분}}}}}}{{{#!if 문서명3 != null
* {{{#!if 설명3 != null
: }}}{{{#!if 같은문서3 == null
[[]] {{{#!if (문단3 == null) != (앵커3 == null)
문서의 }}}}}}{{{#!if 문단3 != null & 앵커3 == null
[[#s-|]]번 문단}}}{{{#!if 문단3 == null & 앵커3 != null
[[#|]] 부분}}}}}}{{{#!if 문서명4 != null
* {{{#!if 설명4 != null
: }}}{{{#!if 같은문서4 == null
[[]] {{{#!if (문단4 == null) != (앵커4 == null)
문서의 }}}}}}{{{#!if 문단4 != null & 앵커4 == null
[[#s-|]]번 문단}}}{{{#!if 문단4 == null & 앵커4 != null
[[#|]] 부분}}}}}}{{{#!if 문서명5 != null
* {{{#!if 설명5 != null
: }}}{{{#!if 같은문서5 == null
[[]] {{{#!if (문단5 == null) != (앵커5 == null)
문서의 }}}}}}{{{#!if 문단5 != null & 앵커5 == null
[[#s-|]]번 문단}}}{{{#!if 문단5 == null & 앵커5 != null
[[#|]] 부분}}}}}}{{{#!if 문서명6 != null
* {{{#!if 설명6 != null
: }}}{{{#!if 같은문서6 == null
[[]] {{{#!if (문단6 == null) != (앵커6 == null)
문서의 }}}}}}{{{#!if 문단6 != null & 앵커6 == null
[[#s-|]]번 문단}}}{{{#!if 문단6 == null & 앵커6 != null
[[#|]] 부분}}}}}}{{{#!if 문서명7 != null
* {{{#!if 설명7 != null
: }}}{{{#!if 같은문서7 == null
[[]] {{{#!if (문단7 == null) != (앵커7 == null)
문서의 }}}}}}{{{#!if 문단7 != null & 앵커7 == null
[[#s-|]]번 문단}}}{{{#!if 문단7 == null & 앵커7 != null
[[#|]] 부분}}}}}}{{{#!if 문서명8 != null
* {{{#!if 설명8 != null
: }}}{{{#!if 같은문서8 == null
[[]] {{{#!if (문단8 == null) != (앵커8 == null)
문서의 }}}}}}{{{#!if 문단8 != null & 앵커8 == null
[[#s-|]]번 문단}}}{{{#!if 문단8 == null & 앵커8 != null
[[#|]] 부분}}}}}}{{{#!if 문서명9 != null
* {{{#!if 설명9 != null
: }}}{{{#!if 같은문서9 == null
[[]] {{{#!if (문단9 == null) != (앵커9 == null)
문서의 }}}}}}{{{#!if 문단9 != null & 앵커9 == null
[[#s-|]]번 문단}}}{{{#!if 문단9 == null & 앵커9 != null
[[#|]] 부분}}}}}}{{{#!if 문서명10 != null
* {{{#!if 설명10 != null
: }}}{{{#!if 같은문서10 == null
[[]] {{{#!if (문단10 == null) != (앵커10 == null)
문서의 }}}}}}{{{#!if 문단10 != null & 앵커10 == null
[[#s-|]]번 문단}}}{{{#!if 문단10 == null & 앵커10 != null
[[#|]] 부분}}}}}}
교착 상태 (Deadlock)
교착 상태는 다수의 잠금을 수행할 때 일어날 수 있는 동기화 문제 중 하나다. 가령 뮤텍스 사이에 상호 의존성이 있어 서로의 잠김 상태에 영향을 받을 때 발생할 가능성이 크다.
#!syntax cpp
std::mutex lock1;
std::mutex lock2;
void Worker1()
{
std::lock_guard locker1(lock1);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::lock_guard locker2(lock2);
}
void Worker2()
{
lock2.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
lock1.lock();
lock2.unlock();
lock1.unlock();
}
int main()
{
std::println("시작");
std::thread worker1(Worker1);
std::thread worker2(Worker2);
std::println("== Ctrl+C로 강제 종료 ==");
worker1.join();
worker2.join();
std::println("종료");
}
상기 코드는 교착 상태의 대표적인 예를 보여주고 있다. 락이 해제되는 코드를 넣었으나 worker1.join();
에서 진행이 멈춰버린다. 이유는 `Worker1`
에서 lock2
를 잠그고 싶은데, `Worker2`
에서 잠겨버렸기에 잠금이 풀리기를 기다리는 대기 상태가 된다. 그리고 `Worker2`
에서는 lock1
을 잠그고 싶어도 `Worker1`
에서 이미 잠갔기 때문에 마찬가지로 대기 상태가 된다. 곧 두 스레드 모두 정지 상태가 된다.이론을 설명하자면 뮤텍스가 잠기는 행위(
lock 멤버 함수
) 자체는 원자적으로(Atomically) 수행되지만 원자적인 연산을 모아놨다고 해서 그것도 원자적 실행을 의미하지 않는다. 상기 예제에서는 코드 상에서 두개의 뮤텍스가 순차적으로 잠기는 순서가 나타났다. 뮤텍스가 잠기는 동작은 파이프라인에서 순서가 섞이지 않는다. 뮤텍스의 잠금 순서는 컴파일러의 최적화, 운영체제의 스케줄링, CPU의 각종 최적화 기법에 영향을 받지 않고 반드시 실행됨을 강력하게 보장한다. 이는 동기화의 중심이 되는 뮤텍스가 줏대없이 의도하지 않은 동작을 하는 걸 막는 큰 장점이 있다. 허나 공유 자원[11]의 소유권이 배타적으로 선점되어서 차례를 기다려도 문제가 해소되지 않는다. 게다가 이 문제는 단일코어 CPU에서도 똑같은 사유로 일어난다.#!syntax cpp
std::mutex mtx1;
std::mutex mtx2;
// RAII 방식의 잠금
{
std::scoped_lock lock(mtx1, mtx2);
}
// 상동
{
std::lock(mtx1, mtx2);
// `std::adopt_lock`을 std::lock_guard, std::scoped_lock, std::unique_lock의 생성자에 전달하면 뮤텍스를 잠그지 않음.
std::lock_guard lock1(mtx1, std::adopt_lock);
std::lock_guard lock2(mtx2, std::adopt_lock);
}
이를 방지하는 방법도 여러 방식이 있다. 하나는 class std::scoped_lock
C++17와 std::lock(Mutex&...)
전역 함수를 써서 특수한 잠금 알고리즘을 쓰도록 지시할 수 있다. 또 하나는 임계 영역을 최대한 가려내어 최소한의 락을 쓰도록 잘 알아서 구현하는 방안이 있다. 마지막 하나는 후술할 원자적 메모리로 좀 더 유연한 공유 자원 접근 알고리즘을 구현할 수도 있다.#!syntax cpp
std::mutex lock;
void Worker_d1()
{
lock.lock();
// (1) 런타임 오류! std::system_error 예외 발생!
lock.lock();
}
void Worker_d2(std::thread* ptr)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
// (2) 런타임 오류! std::system_error 예외 발생!
ptr->join();
}
void Worker_wait()
{
std::this_thread::sleep_for(std::chrono::seconds(10));
}
int main()
{
std::println("시작");
std::thread worker1(Worker_d1);
auto worker2 = new std::thread();
*worker2 = std::thread(Worker_d2, worker2);
std::thread workerw(Worker_wait);
// (3) 런타임 오류! std::system_error 예외 발생!
std::thread worker3([&]() { workerw.join(); });
worker1.join();
worker2->join();
workerw.join();
worker3.join();
std::println("종료");
}
여담으로 자명하게 교착 상태를 일으키는 동작은 std::system_error
예외를 던지도록 표준에 명시되어 있다. 상기 코드에서는 한 스레드에서 하나의 뮤텍스를 두번 잠그는 행위와 스레드 루틴 안에서 자기 스레드를 join
하는 예시를 보여주고 있다.7. std::jthread
||<tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1>
||
#!if head_comment!=null
{{{#57A64A,#5EBD46 }}}
#!if head_lnb!=null
##======================================= include and import
[br]
#!if import!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{import }}}}}}'''{{{#C8865E {{{<>}}}}}}{{{;}}}
#!if include!=null
##======================================= the keyword at head (can be `template`)
{{{#include <>}}}
#!if (head_keyword_available = head_keyword != null)
##======================================= template parameters begin
'''{{{#DodgerBlue,#CornFlowerBlue {{{class}}}}}}'''
#!if template_available = (template_p0 != null || template_v0 != null || template_p1 != null || template_v1 != null || template_p2 != null || template_v2 != null || template_p3 != null || template_v3 != null)
#!if template_available || template_last_label != null
##======================================= template parameter 0
##======================================= template parameter 0 concept
{{{<}}}{{{#!if template_concept0_available = (template_cpt0 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept0_params_available = (template_concept0_p0 != null || template_concept0_p1 != null || template_concept0_p2 != null || template_concept0_p3 != null)
{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{<}}}}}}{{{#!if template_concept0_params_available
##======================================= template parameter 0 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p0 != null || template_concept0_v0 != null) && (template_concept0_p1 != null || template_concept0_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p1 != null || template_concept0_v1 != null) && (template_concept0_p2 != null || template_concept0_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept0_p2 != null || template_concept0_v2 != null) && (template_concept0_p3 != null || template_concept0_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept0_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept0_params_available || template_concept0_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept0_available
{{{ }}}}}}{{{#!if template_p0_available = (template_p0 != null)
{{{#!if !template_concept0_available
##======================================= template parameter 0 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v0_available = (template_v0 != null)
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}
##======================================= template parameter 0 finished
}}}{{{#!if (template_p0_available || template_v0_available) && (template_p1 != null || template_v1 != null)
##======================================= template parameter 1
{{{, }}}}}}{{{#!if template_concept1_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept1_params_available = (template_concept1_p0 != null || template_concept1_p1 != null || template_concept1_p2 != null || template_concept1_p3 != null)
{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{<}}}}}}{{{#!if template_concept1_params_available
##======================================= template parameter 1 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p0 != null || template_concept1_v0 != null) && (template_concept1_p1 != null || template_concept1_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p1 != null || template_concept1_v1 != null) && (template_concept1_p2 != null || template_concept1_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept1_p2 != null || template_concept1_v2 != null) && (template_concept1_p3 != null || template_concept1_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept1_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept1_params_available || template_concept1_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept1_available
{{{ }}}}}}{{{#!if template_p1_available = (template_p1 != null)
{{{#!if !template_concept1_available
##======================================= template parameter 1 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}}}}{{{#!if template_v1_available = (template_v1 != null)
##======================================= template parameter 1 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if (template_p1_available || template_v1_available) && (template_p2 != null || template_v2 != null)
##======================================= template parameter 2
{{{, }}}}}}{{{#!if template_concept2_available = (template_cpt1 != null)
'''{{{#4ec9b0,#6fdbba {{{ }}}}}}'''}}}{{{#!if template_concept2_params_available = (template_concept2_p0 != null || template_concept2_p1 != null || template_concept2_p2 != null || template_concept2_p3 != null)
{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{<}}}}}}{{{#!if template_concept2_params_available
##======================================= template parameter 2 concept's parameters (type or value)
{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p0 != null || template_concept2_v0 != null) && (template_concept2_p1 != null || template_concept2_v1 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p1 != null || template_concept2_v1 != null) && (template_concept2_p2 != null || template_concept2_v2 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if (template_concept2_p2 != null || template_concept2_v2 != null) && (template_concept2_p3 != null || template_concept2_v3 != null)
{{{, }}}}}}{{{#4ec9b0,#6fdbba {{{}}}}}}'''{{{#ffffff {{{}}}}}}'''{{{#!if template_concept2_last_label != null
{{{,...}}}}}}}}}{{{#!if template_concept2_params_available || template_concept2_last_label != null
{{{>}}}}}}}}}{{{#!if template_concept2_available
{{{ }}}}}}{{{#!if template_p2_available = (template_p2 != null)
{{{#!if !template_concept2_available
##======================================= template parameter 2 contents
'''{{{#569cd6,#CornFlowerBlue {{{typename}}}}}}'''}}}{{{}}}{{{#4ec9b0,#6fdbba {{{ }}}}}}
}}}{{{#!if template_v2_available = (template_v2 != null)
##======================================= template parameter 2 finished
{{{#4ec9b0,#6fdbba {{{}}}}}}{{{}}}{{{#ffffff '''{{{ }}}'''}}}}}}{{{#!if template_available && template_last_label != null
##======================================= template finished
{{{, }}}}}}{{{#!if template_last_label != null
{{{...}}}}}}{{{#!if template_available || template_last_label != null
{{{>}}}}}}{{{#!if do_template_linebreak = (template_lnb != null)
##======================================= template linebreak
[br]}}}{{{#!if do_template_linebreak ;nbsp
}}}
#!if head_keyword_available!=null
{{{ }}}
#!if fn_attribute!=null
##======================================= contents
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnk!=null
[[C++/문법/특성#attr1|{{{#a8a8a8 {{{[[attr1]]}}}}}}]]
#!if fn_attribute_lnb!=null
[br]
#!if fn_attribute_lnb!=null ;nbsp
#!if kw1!=null
'''{{{#569cd6,#CornFlowerBlue {{{contexpr}}}}}}'''{{{#!if kw1_post!=null
{{{_kwp1}}}}}}{{{#!if kw1_post==null
{{{ }}}}}}
#!if kw2!=null
'''{{{#569cd6,#CornFlowerBlue {{{long long}}}}}}'''{{{#!if kw2post!=null
{{{&&}}}}}}{{{#!if kw2_post==null
{{{ }}}}}}
#!if cls_attribute!=null
[[C++/문법/특성|{{{#a8a8a8 {{{[[attr2]]}}}}}}]]{{{ }}}
#!if cls_attribute_lnk!=null
[[C++/문법/특성#attr2|{{{#a8a8a8 {{{[[attr2]] }}}}}}]]
#!if ns1!=null
##======================================= namespaces
'''{{{#58fafe {{{std}}}}}}'''
#!if ns2!=null
{{{::}}}'''{{{#58fafe {{{chrono}}}}}}'''
#!if ns1!=null
{{{::}}}
#!if pre1_t!=null
##======================================= types at the front
{{{#4ec9b0,#6fdbba {{{jthread }}}}}}
#!if pre2_t!=null
{{{::}}}{{{#4ec9b0,#6fdbba {{{duration }}}}}}
#!if pre_e!=null
{{{::}}}{{{#f0f068 {{{enum }}}}}}
#!if pre_post!=null
{{{_pre}}}
#!if pre_lnb!=null
[br]
#!if do_pre_linebreak = (pre_lnb != null)
##======================================= pre-body things finished
#!if (!do_pre_linebreak && !do_template_linebreak && head_keyword_available) && (body_v != null || body_f != null || body_mv != null || body_mf != null || body_post != null)
{{{ }}}
#!if body_v!=null
##======================================= identifiers of variable and function
{{{#a8a8a8,#a1a1a1 {{{val}}}}}}
#!if body_mv!=null
{{{#ffffff {{{mem_val}}}}}}
#!if body_f!=null
{{{#f87a7a {{{fun}}}}}}
#!if body_mf!=null
{{{#f0a962 {{{mem_fn}}}}}}
#!if body_post!=null
##======================================= argument 1
{{{}}}
#!if body_tmpopen!=null
{{{<}}}
#!if body_bopen!=null
{{{(}}}
#!if arg1_concept!=null
'''{{{#4ec9b0,#6fdbba {{{concept1}}}}}}'''{{{#!if arg1_concept_params!=null
{{{<}}}{{{#!if arg1_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg1_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg1_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg1_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg1_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg1_t!=null
{{{#4ec9b0,#6fdbba {{{type1}}}}}}
#!if arg1_t_post!=null
{{{&}}}
#!if arg1_param != null && (arg1_kw != null || arg1_t_kw != null || arg1_t != null)
{{{ }}}
#!if arg1_param!=null
{{{#bcdce6 {{{param1}}}}}}
#!if arg2_concept != null || arg2_kw != null || arg2_t_kw != null || arg2_t != null || arg2_param != null
{{{, }}}
#!if arg2_concept!=null
##======================================= argument 2
'''{{{#4ec9b0,#6fdbba {{{concept2}}}}}}'''{{{#!if arg2_concept_params!=null
{{{<}}}{{{#!if arg2_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg2_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg2_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg2_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg2_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg2_t!=null
{{{#4ec9b0,#6fdbba {{{type2}}}}}}
#!if arg2_t_post!=null
{{{&}}}
#!if arg2_param!=null && (arg2_kw != null || arg2_t_kw != null || arg2_t != null)
{{{ }}}
#!if arg2_param!=null
{{{#bcdce6 {{{param2}}}}}}
#!if arg3_concept != null || arg3_kw != null || arg3_t_kw != null || arg3_t != null || arg3_param != null
{{{, }}}
#!if arg3_concept!=null
##======================================= argument 3
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg3_concept_params!=null
{{{<}}}{{{#!if arg3_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg3_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg3_concept_tparam3!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg3_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg3_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg3_t!=null
{{{#4ec9b0,#6fdbba {{{type3}}}}}}
#!if arg3_t_post!=null
{{{&}}}
#!if arg3_param!=null && (arg3_kw != null || arg3_t_kw != null || arg3_t != null)
{{{ }}}
#!if arg3_param!=null
{{{#bcdce6 {{{param3}}}}}}
#!if arg4_concept != null || arg4_kw != null || arg4_t_kw != null || arg4_t != null || arg4_param != null
{{{, }}}
#!if arg4_concept!=null
##======================================= argument4
'''{{{#4ec9b0,#6fdbba {{{concept3}}}}}}'''{{{#!if arg4_concept_params!=null
{{{<}}}{{{#!if arg4_concept_tparam1!=null
{{{#4ec9b0,#6fdbba {{{T1}}}}}}}}}{{{#!if arg4_concept_tparam2!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T2}}}}}}}}}{{{#!if arg4_concept_tparam4!=null
{{{, }}}{{{#4ec9b0,#6fdbba {{{T3}}}}}}}}}{{{>}}}}}}{{{ }}}
#!if arg4_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{const }}}}}}'''
#!if arg4_t_kw!=null
'''{{{#569cd6,#CornFlowerBlue {{{int }}}}}}'''
#!if arg4_t!=null
{{{#4ec9b0,#6fdbba {{{type4}}}}}}
#!if arg4_t_post!=null
{{{&}}}
#!if arg4_param!=null && (arg4_kw != null || arg4_t_kw != null || arg4_t != null)
{{{ }}}
#!if arg4_param!=null
{{{#bcdce6 {{{param4}}}}}}
#!if arg5_param!=null
##======================================= argument5, argument6
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg6_param != null
{{{, }}}{{{#bcdce6 {{{param5}}}}}}
#!if arg_last_dots != null
{{{, ...}}}
#!if body_bclose!=null
##=======================================
{{{)}}}
#!if body_lnb!=null
[br]
#!if body_lnb!=null
{{{ }}}
#!if body_spec1!=null
'''{{{#DodgerBlue,#CornFlowerBlue {{{ const}}}}}}'''
#!if body_spec1_paren != null
{{{(}}}
#!if body_spec1_ref!=null
{{{&}}}
#!if body_spec2!=null
{{{#!if body_spec1!=null && body_spec1_paren == null
{{{ }}}}}}'''{{{#DodgerBlue,#CornFlowerBlue {{{noexcept}}}}}}'''
#!if body_spec2_paren != null
{{{(}}}
#!if body_spec2_label != null
{{{...}}}
#!if body_spec2_paren != null
{{{)}}}
#!if body_spec1_paren != null
{{{)}}}
#!if body_tmpclose!=null
##======================================= last labels
{{{>}}}
#!if label_last!=null
{{{}}}
#!if last!=null
{{{;}}}
||
|
빈 조인 스레드 클래스의 인스턴스를 생성한다. 아무 스레드도 만들지 않고 실행하지 않는다. |
|
스레드를 하나 생성하고, 실행가능한 객체 `Functor` 를 그 스레드에서 실행한다. 이때 함자의 첫번째 매개변수가 std::stop_token 을 받을 수 있다면 클래스 자신의 std::stop_source 로부터 멤버 함수 get_stop_source 를 실행해 std::stop_token 인스턴스를 전달한다.이는 선택 사항이며 std::thread 의 경우와 같이 평범하게 인자를 추가로 전달할 수도 있다. |
|
내부에 가지고 있는 std::stop_source 인스턴스를 공유한다. |
|
내부에 가지고 있는 std::stop_source 인스턴스로부터 std::stop_token 인스턴스를 공유한다. std::stop_token 으로 조인 스레드 클래스에 정지 요청이 왔는지 검사할 수 있다. |
|
내부에 가지고 있는 std::stop_source 를 통해 현재 조인 스레드 클래스의 정지를 요청한다. 공유된 모든 std::stop_token 를 통해 정지가 요청됐는지 알 수 있다. |
std::jthread
C++20
에서 도입된 개선된 스레드 클래스[12]. 멤버 함수 join
이 소멸자에서 자동으로 호출되므로 수동으로 호출할 필요가 없다. std::stop_token
, std::stop_source
, std::stop_callback
과 연동할 수 있다.7.1. 예제 7: std::jthread와 std::stop_token 연계
#!syntax cpp
import <string>;
import <iostream>;
import <stop_token>;
void Worker(std::stop_token cancellation_token)
{
while (!cancellation_token.stop_requested())
{
std::println("작업 중...");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
std::println("시작");
std::jthread worker(Worker);
std::string buffer;
buffer.reserve(1024);
std::cin >> buffer;
std::println("입력 받음: {}", buffer);
worker.request_stop();
std::println("종료 신호 보냄.");
std::println("종료");
// worker의 소멸자에서 join 호출
}
상기 코드는 단일 스레드로 정지 요청을 보내는 예시를 보여주고 있다. 작업자 스레드에서는 100밀리초 마다 문장을 출력하며 주 스레드에서는 입력을 받으면 작업자를 종료시킨다. 특별히 인자를 전달해주지 않아도 함자의 매개변수에 std::stop_token
이 있으면 조인 스레드 클래스가 자동으로 전달한다.8. 메모리와 성능의 연관성
#!if (문단 == null) == (앵커 == null)
를
#!if 문단 != null & 앵커 == null
의 [[캐시 메모리/성능 요소#s-|]]번 문단을
#!if 문단 == null & 앵커 != null
의 [[캐시 메모리/성능 요소#|]] 부분을
참고하십시오.캐시 적중 & 캐시 누락 (Cache Hit & Cache Miss)
캐시와 RAM 메모리 접근에 의한 지연은 다중 스레드 프로그래밍에서 해결하기 가장 까다로운 사항이다 [13]. 단일 스레드 환경에서 캐시 누락에 의한 성능 하락은 기껏 자료구조 정렬 정도에서나 체감할 수 있었다. 다중 스레드 환경에서는 각 스레드가 가지는 데이터는 최대한 서로간의 데이터 의존성이 없도록 설계된다. 그러지 않으면 동시성 프로그래밍을 다루거나 락을 써야 하는데, 메모리에 의한 성능 하락과는 비교도 안되는 어려움이 있기 때문이다. 그런데 이때 각 스레드가 가진 데이터와 접근하는 데이터 사이에 연관이 적다는 말은 CPU가 예측한 캐시 내용이 불일치할 가능성이 높다는 뜻도 된다. 결국 사용자가 어찌할 도리가 없는 성능 하락이 발생할 가능성이 커진다. 그래서 동시성 프로그래밍을 사용해야만 정확한 연산을 보장하면서 최대한의 성능을 이끌어 낼 수 있다.
9. 동시성 자료구조
기획과 이론적 한계, 실제 구현에 대한 단상.9.1. 예제 8: 큐
- <C++ 예제 보기>
#!syntax cpp struct WrapperConcurrentQueue { using value_type = int; WrapperConcurrentQueue() = default; void Push(value_type val) { std::lock_guard lock(myMutex); myData.push_back(val); } [[nodiscard]] std::optional<value_type> Pop() { std::lock_guard lock(myMutex); if (myData.empty()) { return std::nullopt; } value_type val = myData.back(); myData.pop_back(); return val; } bool TryPop(value_type& out) { std::lock_guard lock(myMutex); if (myData.empty()) { return false; } out = myData.back(); myData.pop_back(); return true; } // Unsafe [[nodiscard]] auto begin() { return myData.begin(); } // Unsafe [[nodiscard]] auto begin() const { return myData.begin(); } // Unsafe [[nodiscard]] auto end() { return myData.end(); } // Unsafe [[nodiscard]] auto end() const { return myData.end(); } // Unsafe [[nodiscard]] bool IsEmpty() const { return myData.empty(); } // Unsafe [[nodiscard]] size_t Size() const { return myData.size(); } void Clear() { std::lock_guard lock(myMutex); myData.clear(); } private: std::vector<value_type> myData; std::mutex myMutex; };
std::vector
의 래퍼로 구현한 동시성 큐다. 이 큐는 우리가 세부 동작을 제어할 수 없어서 동시성 프로그래밍에 적합하지 않다. 추후 무잠금 동기화 큐의 구현을 위해서는 별도의 자료구조를 구축해야 한다. 그래도 어떤 동작을 만들 수 있는지 파악할 수는 있다.#!syntax cpp
struct ConcurrentQueue
{
struct Node
{
int value;
Node* next;
}
Node myHead; // 임계 영역 (1)
Node myTail; // 임계 영역 (2)
// myHead.next 도 임계영역 (3)
};
우리가 구현할 큐는 노드끼리 연결된 연결 리스트 방식으로 구현할 예정이다. 큐에는 머리 노드와 꼬리 노드가 있다. 머리 노드부터 새로운 값이 순차적으로 추가되며 새로운 노드는 머리 노드에 연결된다. 값을 삭제할 때는 꼬리 노드부터 삭제한다. 이때 다수의 스레드에서 머리 노드와 꼬리 노드, 그리고 머리 노드의 다음 노드에 접근해서 수정할 수 있으므로 세 변수는 임계 영역이다.#!syntax cpp
mutable std::mutex headMutex;
mutable std::mutex tailMutex;
그런데 우리는 머리 노드와 꼬리 노드 둘 중 하나만 가지고는 큐에 값을 추가하거나 삭제할 수가 없다. 그래서 데이터 접근이 머리 노드와 꼬리 노드에 집중되는데 둘 다 선점해야만 큐를 수정할 수 있다. 뮤텍스로 둘 다 잠가야 한다. 허나 머리 노드와 꼬리 노드 각각에 뮤텍스를 따로 할당하는 방법은 사용할 수 없다. 왜냐하면 뮤텍스를 두 개 쓰면 두 노드에 대한 접근은 원자적으로 수행되겠지만, 앞서 살펴본 뮤텍스 예제에서 알 수 있듯이, 다수의 원자적 연산을 결합하더라도 그게 원자적 연산이 되지 못한다. 결국 머리 노드와 꼬리 노드를 같이 선점하는 구현 뿐만이 못한다. 어떻게 잘 임계 영역을 구별할 수 있지 않을까 싶지만 근본적인 해결책은 되지 못한다.9.1.1. 성긴 동기화
#!syntax cpp
struct NodeConcurrentQueue
{
using value_type = int;
struct Node
{
value_type value;
Node* volatile next = nullptr;
[[nodiscard]]
Node* GetNext() const noexcept
{
return next;
}
};
NodeConcurrentQueue() noexcept = default;
void Push(value_type val)
{
Node* node = new Node{ val };
NodeAllocFailsafe failsafe{ node };
std::lock_guard lock(myMutex);
if (myTail != nullptr)
{
myTail->next = node;
myTail = node;
}
else
{
myHead.next = node;
myTail = node;
}
failsafe.isSafe = true;
}
[[nodiscard]]
std::optional<value_type> Pop()
{
if (myHead.next == nullptr)
{
return std::nullopt;
}
std::scoped_lock lock(myMutex);
Node* volatile old = myHead.next;
value_type result = old->value;
myHead.next = old->next;
myTail = (nullptr == old->next) ? nullptr : myTail;
delete old;
return result;
}
bool TryPop(value_type& out)
{
if (myHead.next == nullptr)
{
return false;
}
std::scoped_lock lock(myMutex);
Node* volatile old = myHead.next;
out = old->value;
myHead.next = old->next;
myTail = (nullptr == old->next) ? nullptr : myTail;
delete old;
return true;
}
std::int64_t Reset()
{
std::int64_t cnt = 0;
std::scoped_lock lock(myMutex);
Node* target = myHead.next;
while (target != nullptr)
{
Node* next = target->next;
delete target;
target = next;
(void)++cnt;
}
myHead.next = nullptr;
myTail = nullptr;
return cnt;
}
[[nodiscard]]
Node* UnsafeBegin() const noexcept
{
return myHead.next;
}
[[nodiscard]]
Node* UnsafeEnd() const noexcept
{
return nullptr;
}
private:
struct NodeAllocFailsafe
{
Node* myTarget = nullptr;
bool isSafe = false;
~NodeAllocFailsafe() noexcept
{
if (!isSafe and myTarget != nullptr)
{
delete myTarget;
}
}
};
volatile Node myHead;
Node* volatile myTail = nullptr;
mutable std::mutex myMutex;
};
상기 코드는 노드 기반의 성긴 동기화 동시성 큐의 구현이다. 이 코드는 몇가지 멤버 함수가 빠져있는데, 동시성 큐 외부로 데이터를 노출할 수 밖에 없는 begin
과 end
내지는 size
멤버 함수를 구현할 수 없다. Unsafe 버전으로 구현할 수 있으나 큰 의미는 없다. 락은 동시성 큐 내부에 귀속되기 때문에 동시성 큐 외부에서는 힘을 쓰지 못한다. 가령 begin
안에서 뮤텍스를 잠그고 푸는 행위는 아무 의미가 없다. 이를 해결하려면 또다른 뮤텍스를 선언하고 동시성 큐와는 별도로 또 잠가야 한다.상기 예제에서는
new Node
구문을 미리 앞으로 빼서 RAII로 메모리 안전을 확보했다. 분명 동적 할당은 큰 오버헤드가 있는 부분이지만 나머지 임계 영역 구문에서 노드에 접근해야 하므로 노드에 접근할 때 뮤텍스를 뺄 수가 없다.9.1.2. 고운 동기화
#!syntax cpp
struct FineConcurrentQueue
{
using value_type = int;
struct Node
{
value_type value;
Node* next = nullptr;
std::mutex mutex;
};
constexpr FineConcurrentQueue()
: myHead(), myTail(new Node{})
{
myHead.next = myTail;
}
constexpr ~FineConcurrentQueue() noexcept
{
myHead.next = nullptr;
delete std::exchange(myTail, nullptr);
}
void Push(value_type val)
{
// 동적 할당 구문을 앞쪽에 배치
Node* newbie = new Node{ val };
NodeAllocFailsafe failsafe{ newbie };
// 머리 노드를 잠금.
std::unique_lock head_lock(myHead.mutex);
Node* prev = &myHead; // 머리 노드부터 시작하는 노드 포인터
Node* curr = prev->next; // 머리 노드의 다음 노드부터 시작하는 노드 포인터. 아무 원소가 없으면 꼬리 노드를 가리킴.
// 머리 노드의 다음 노드를 잠금.
std::unique_lock tail_lock(curr->mutex);
while (curr != myTail)
{
// 이전에 가리킨 노드의 잠금을 해제함.
// 아무 원소가 없으면 머리 노드가 해제됨.
head_lock = std::unique_lock(curr->mutex, std::defer_lock);
prev = curr;
curr = curr->next;
// 현재 가리키는 노드를 잠금.
// 아무 원소가 없으면 꼬리 노드가 잠김.
tail_lock = std::unique_lock(curr->mutex);
}
// 머리 노드 뒤쪽에 새로운 노드 연결
newbie->next = curr;
prev->next = newbie;
failsafe.isSafe = true;
// `head_lock`과 `tail_lock`은 소멸자에서 자동으로 잠금 해제됨.
}
[[nodiscard]]
std::optional<value_type> Pop()
{
std::lock_guard head_lock(myHead.mutex);
myHead.next->mutex.lock(); // `last` 선언 뒤에 놓으면 안됨. 머리 노드의 다음 노드가 도중에 수정될 수 있음.
Node* last = myHead.next;
if (last == myTail)
{
myHead.next->mutex.unlock();
return std::nullopt;
}
else
{
value_type result = last->value;
myHead.next = last->next;
last->mutex.unlock();
delete last;
return result;
}
}
bool TryPop(value_type& out)
{
return Pop().and_then([&out](value_type&& result) -> std::optional<bool>
{
out = result;
return true;
}
).value_or(false);
}
std::int64_t Reset()
{
std::int64_t cnt = 0;
Node* target = myHead.next;
while (target != myTail)
{
Node* next = target->next;
delete target;
target = next;
(void)++cnt;
}
myHead.next = myTail;
return cnt;
}
[[nodiscard]]
Node* UnsafeBegin() const noexcept
{
return myHead.next;
}
[[nodiscard]]
Node* UnsafeEnd() const noexcept
{
return myTail;
}
private:
struct NodeAllocFailsafe
{
Node* myTarget = nullptr;
bool isSafe = false;
~NodeAllocFailsafe() noexcept
{
if (!isSafe and myTarget != nullptr)
{
delete myTarget;
}
}
};
Node myHead;
Node* myTail;
};
상기 코드는 고운 동기화 동시성 큐의 구현을 보여주고 있다. 고운 동기화에서는 각 노드마다 따로 뮤텍스를 할당해서 병렬성을 향상 시켰다. 그러나 성능이 좋지 않다. 왜냐하면 한 두번 크게 잠그고 마는 성긴 동기화에 비해 고운 동기화는 뮤텍스를 계속 잠갔다 풀면서 운영체제 호출과 문맥 전환이 많이 일어나기 때문이다.9.1.3. 낙천적인 동기화
삽입할 위치를 찾을 때 계속 뮤텍스를 잠갔다 푸는 게 문제였다. 만약 뮤텍스를 쓰지 않으면 어떻게 되는지, 어떻게 구현할 수 있는지 알아보자.#!syntax cpp
9.1.4. 무잠금 & 무대기 동기화
무잠금 무대기 동기화 동시성 큐#!syntax cpp
import <optional>;
struct LockfreeConcurrentQueue
{
using value_type = int;
struct Node
{
value_type value;
Node* volatile next = nullptr;
};
constexpr LockfreeConcurrentQueue(Node* head = new Node{})
: myHead(head), myTail(head)
{}
void Push(value_type value)
{
Node* node = new Node{ .value = value };
NodeAllocFailsafe failsafe{ .myTarget = node };
while (true)
{
Node* last = myTail;
Node* next = last->next;
// 이 문장 이전에도 꼬리 노드가 변경되었을 수 있으므로 다시 확인
if (last != myTail) continue;
// 마지막 노드가 진짜 마지막 노드인지 단순 비교로 확인
if (nullptr == next)
{
Node* old = nullptr;
// `예전 꼬리 노드`의 다음 노드를 `새 노드`로 변경
// 이때 `예전 꼬리 노드`의 다음 노드가 nullptr이어야 성공함.
// 이 문장 직후 다른 스레드에서 `예전 꼬리 노드`에 접근하려고 하면 그때는 실패함.
if (Cas(&(last->next), old, node))
{
// `현재 꼬리 노드`를 `새 노드`로 변경
// 이때 다른 스레드에서 `현재 꼬리 노드`를 수정했다면 실패함.
// 왜냐하면 `예전 꼬리 노드`의 다음 노드를 수정한 이후에 `현재 꼬리 노드`가 수정될 수 있기 때문임.
// 하지만 `예전 꼬리 노드`와 `새 노드`가 연결되었으므로 메모리 누수는 발생하지 않음.
Cas(&myTail, last, node);
// 성공하면 종료
failsafe.isSafe = true;
return;
}
}
else
{
// 실패하면 상태 롤백, 이후 다시 시도
Cas(&myTail, last, next);
}
}
}
[[nodiscard]]
std::optional<value_type> Pop()
{
while (true)
{
/* `first` (머리 노드)와 `last` (꼬리 노드)를 획득함. */
Node* first = myHead;
Node* last = myTail;
// `꼬리 노드`를 읽어오는 도중에도 `머리 노드`가 수정될 수 있으므로 한번 더 확인함.
if (first != myHead)
{
continue;
}
/* `머리 노드`의 다음 노드를 획득함. */
Node* next = first->next;
if (next == nullptr)
{
return std::nullopt;
}
// 현재 `next`는 있는데 아직 `현재 꼬리 노드`가 전진을 하지 않은 상태라서 `myHead`와 같은 값을 가지고 있음.
if (first == last)
{
// `현재 꼬리 노드` 전진
// Push 함수에서 `현재 꼬리 노드`를 수정하는 중이므로 도와줌.
// 실패해도 문제 없는 구문임.
// 단지 Convoying (스레드 공회전)을 방지하는 목적의 문장임.
Cas(&myTail, last, next);
continue;
}
auto result = next->value;
// `기존 머리 노드`에서 다음 노드로 `머리 노드` 전진
if (!Cas(&myHead, first, next))
{
continue;
}
//
// 이 사이에 `next` 노드가 수정될 수도 있음.
//
// 왜냐하면 `현재 머리 노드`가 `next`로 바뀌었는데, 그럼 다른 스레드에서 `next`에 접근할 수 있다는 뜻이기 때문임.
// 다른 스레드에서 삭제될 노드를 쓰는 것을 방지하는 목적임.
first->next = nullptr;
// ABA 문제가 발생할 수 있으나 이 구현에서는 무시함.
delete first;
return result;
}
}
bool TryPop(value_type& out)
{
return Pop().and_then(
[&out](value_type val) noexcept -> std::optional<bool>
{
out = val;
return true;
}
).value_or(false);
}
std::int64_t Reset()
{
std::int64_t cnt = 0;
Node* it = myHead;
while (it != nullptr)
{
Node* next = it->next;
delete it;
it = next;
(void)++cnt;
}
auto one = new Node{};
myHead = one;
myTail = one;
return cnt - 1;
}
[[nodiscard]]
Node* UnsafeBegin() const noexcept
{
return myHead;
}
[[nodiscard]]
Node* UnsafeEnd() const noexcept
{
return nullptr;
}
private:
inline bool Cas(Node* volatile* target, Node* oldone, Node* newone)
{
return std::atomic_compare_exchange_strong(reinterpret_cast<volatile std::atomic<Node*>*>(target), &oldone, newone);
}
Node* volatile myHead;
Node* volatile myTail;
};
상기 코드는 원자적 메모리를 사용한 무잠금, 무대기 큐의 구현을 보여주고 있다.Push
멤버 함수에서 RAII를 사용해서 예외 안정성을 확보했다. 한편 노드를 삭제하는 Pop
멤버 함수에는 치명적인 문제가 하나 있다. 바로 운영체제의 메모리 재사용에 의해 일어나는 ABA 문제라는 버그가 있다. 꽤나 드물게 발생하지만 발생하면 무조건 프로그램이 터진다고 생각하면 된다. 일단 이 예제에서는 넘어가도록 한다.#!syntax cpp
thread_local std::vector<LockfreeConcurrentQueue::Node*> freeList;
이를 해결하는 방법 중 하나는 Hazard Pointer를 구현해야 한다. Hazard Pointer는 삭제하면 위험한 포인터 라는 뜻이다. 간단히 설명하자면 thread_local
로 스레드 마다 삭제 대기 리스트를 정의하고, 노드를 delete
하는 대신에 대기 리스트에 추가한다. 그리고 Hazard Pointer는 프로그램이 끝날 때 혹은 작업이 없을 때 스레드 마다의 삭제 대기 리스트를 정리한다. 마치 쓰레기 수집 기능을 우리가 직접 구현하는 것과 같다.C++26에서
<hazard_pointer>
모듈이 추가되었다. 이쪽 구현을 기다리는 게 속편할 수 있다.9.1.5. 벤치마크
- <C++ 예제 보기>
#!syntax cpp //#define BM_TARGET NodeConcurrentQueue //#define BM_TARGET OptimisticConcurrentQueue #define BM_TARGET FineConcurrentQueue BM_TARGET bm_queue; // 최대 16개의 스레드까지 벤치마크 수행 constexpr int bm_thread_max = 16; constexpr int bm_test_count = 30'000; void Worker(const int id, const int count) { std::println("==== 작업자 스레드 {} 시작 ====", id); for (int i = 0; i < count; ++i) { bm_queue.Push(i); } } int main() { std::println("= 벤치마크 시작 ="); for (int n = 1; n <= bm_thread_max; n *= 2) { std::println("== 스레드 개수: {}개 ==", n); std::vector<std::thread> workers; workers.reserve(n); const int each_count = bm_test_count / n; std::println("=== 작업 시작하는 중... ==="); const auto start_tm = std::chrono::high_resolution_clock::now(); for (int i = 0; i < n; ++i) { workers.emplace_back(Worker, i, each_count); } std::println("=== 작업자 대기 중... ==="); for (auto& worker : workers) { worker.join(); } const auto end_tm = std::chrono::high_resolution_clock::now(); const auto duration = end_tm - start_tm; std::println("\n=== 정리 중... ==="); int test_count = 0; for (auto it = bm_queue.UnsafeBegin(); it != bm_queue.UnsafeEnd(); it = it->next) { ++test_count; } const auto rm_count = bm_queue.Reset(); std::println("== 작업 완료: 추가된 값 {}, 삭제된 값 {} ==", test_count, rm_count); std::println("== 걸린 시간: {}ms ==\n", std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()); } std::println("= 벤치마크 종료 ="); }
||<tablewidth=80%><tablebordercolor=#20b580><tablebgcolor=#090f0a,#050b09><tablecolor=#a8a8a8,#c1c1c1><rowcolor=#d7d7d7,#a1a1a1> 스레드 개수 || 성긴 동기화 || 고운 동기화 || 낙천적 동기화 || 게으른 동기화 || 무잠금 & 무대기 동기화 ||
Surface Go 2에서 실행했다. 코어는 4개, 하이퍼스레딩의 한계는 8개다.1개 | 28783954 | 12861 | 0 | 0 | 0 | ||||||
2개 | 13389682 | 9447 | 0 | 0 | 0 | ||||||
4개 | 19634695 | 9220 | 0 | 0 | 0 | ||||||
8개 | 19675010 | 6447 | 0 | 0 | 0 | ||||||
16개 | 20077502 | 4881 | 0 | 0 | 0 | ||||||
<bgcolor=#20b580> |
9.2. 예제 9: 집합
이 문단에서는 고유한 정수로 관리되는 집합 자료구조를 구현하고자 한다. 집합의 특징은 각 값은 유일한 원소로 존재한다는 것이다. 즉 원소를 추가할 때 같은 원소가 이미 존재하면 추가할 수 없다. 그리고 삭제할 때도 원소가 존재해야 성공한다. 구현 방식은 여러가지가 있겠지만, 원소를 추가할 때 부터 정렬된 상태로 추가시키는 것이 좋다. 그렇지 않으면 원소를 추가할 때 검증이 필요한데, 집합의 처음부터 끝까지 순회해야하기 때문이다. 사용자가 실행한 함수를 아예 버릴 수도 없는 노릇이니 검증에 실패하면 다시 시작하는 식으로 반복해야 한다. 검증 시간이 길어지면 검증이 실패할 확률도 커지고 재시도가 늘어나면서 도달 성능이 떨어질 수 밖에 없다.9.2.1. 성긴 동기화
- <C++ 예제 보기>
#!syntax cpp struct CoarseConcurrentSet { using value_type = int; struct Node { value_type value; Node* next = nullptr; }; constexpr CoarseConcurrentSet() : myHead(), myMutex() {} std::optional<bool> Add(value_type key) { Node* newbie = new Node{ .value = key }; std::scoped_lock lock(myMutex); if (Contains(key)) return false; volatile Node* it = &myHead; do { Node* next = it->next; if (nullptr == next) { it->next = newbie; return true; } it = next; } while (nullptr != it); return std::nullopt; } bool Remove(value_type key) { std::scoped_lock lock(myMutex); volatile Node* it = &myHead; while (true) { Node* next = it->next; if (nullptr == next) { return false; } else if (next->value == key) { it->next = next->next; delete next; return true; } it = next; } return false; } [[nodiscard]] bool Contains(value_type key) const { Node* it = myHead.next; while (it != nullptr) { if (it->value == key) { return true; } it = it->next; } return false; } std::int64_t Reset() { std::scoped_lock lock(myMutex); std::int64_t cnt = 0; Node* it = myHead.next; while (it != nullptr) { Node* next = it->next; delete it; it = next; (void)++cnt; } myHead.next = nullptr; return cnt; } [[nodiscard]] Node* UnsafeBegin() const noexcept { return myHead.next; } [[nodiscard]] Node* UnsafeEnd() const noexcept { return nullptr; } private: volatile Node myHead; mutable std::mutex myMutex; };
9.2.2. 고운 동기화
- <C++ 예제 보기>
#!syntax cpp struct FineConcurrentSet { using value_type = int; struct Node { value_type value; Node* next = nullptr; mutable std::mutex mtx; }; constexpr FineConcurrentSet() : myHead(new Node{}) { } ~FineConcurrentSet() { delete myHead; } std::optional<bool> Add(value_type key) { Node* newbie = new Node{ .value = key }; if (Contains(key)) return false; Node* it = myHead; do { std::scoped_lock lock(it->mtx); Node* next = it->next; if (nullptr == next) { it->next = newbie; return true; } it = next; } while (nullptr != it); return std::nullopt; } bool Remove(value_type key) { Node* it = myHead; while (true) { std::scoped_lock lock(it->mtx); Node* next = it->next; if (nullptr == next) { return false; } else if (next->value == key) { it->next = next->next; delete next; return true; } it = next; } return false; } [[nodiscard]] bool Contains(value_type key) const { std::scoped_lock lock(myHead->mtx); Node* it = myHead->next; while (it != nullptr) { std::scoped_lock lock(it->mtx); if (it->value == key) { return true; } it = it->next; } return false; } std::int64_t Reset() { std::scoped_lock lock(myHead->mtx); std::int64_t cnt = 0; Node* it = myHead->next; while (it != nullptr) { Node* next = it->next; delete it; it = next; (void)++cnt; } myHead->next = nullptr; return cnt; } [[nodiscard]] Node* UnsafeBegin() const noexcept { return myHead->next; } [[nodiscard]] Node* UnsafeEnd() const noexcept { return nullptr; } private: Node *volatile myHead; };
9.2.3. 낙천적 동기화
- <C++ 예제 보기>
#!syntax cpp
9.2.4. 게으른 동기화
- <C++ 예제 보기>
#!syntax cpp
9.2.5. 벤치마크
- <C++ 예제 보기>
#!syntax cpp
10. 원자적 연산 (Atomic Operations)
#!if (문단 == null) == (앵커 == null)
를
#!if 문단 != null & 앵커 == null
의 [[C++/표준 라이브러리/atomic#s-|]]번 문단을
#!if 문단 == null & 앵커 != null
의 [[C++/표준 라이브러리/atomic#|]] 부분을
참고하십시오.11. 참고 사항
12. 외부 링크
- PDF: 2008년 중에 게시된 C++11의 초기 제안서 중 하나
- PDF: C++11의 표준 헤더 제안서
- C++ 공식 참고서
- TBB
==# 코드 요약 #==
#!syntax cpp
namespace std
{
class thread
{
public:
// class thread::id
class id;
using native_handle_type = /* implementation-defined */;
// construct/copy/destroy
thread() noexcept;
template<class F, class... Args>
explicit thread(F&& f, Args&&... args);
thread(const thread&) = delete;
thread(thread&&) noexcept;
thread& operator=(const thread&) = delete;
thread& operator=(thread&&) noexcept;
~thread();
// members
void swap(thread&) noexcept;
bool joinable() const noexcept;
void join();
void detach();
id get_id() const noexcept;
native_handle_type native_handle();
// static members
static unsigned int hardware_concurrency() noexcept;
};
class jthread
{
public:
// types
using id = thread::id;
using native_handle_type = thread::native_handle_type;
// constructors, move, and assignment
jthread() noexcept;
template<class F, class... Args>
explicit jthread(F&& f, Args&&... args);
jthread(const jthread&) = delete;
jthread(jthread&&) noexcept;
jthread& operator=(const jthread&) = delete;
jthread& operator=(jthread&&) noexcept;
~jthread();
// members
void swap(jthread&) noexcept;
bool joinable() const noexcept;
void join();
void detach();
id get_id() const noexcept;
native_handle_type native_handle();
// stop token handling
stop_source get_stop_source() noexcept;
stop_token get_stop_token() const noexcept;
bool request_stop() noexcept;
// specialized algorithms
friend void swap(jthread& lhs, jthread& rhs) noexcept;
// static members
static unsigned int hardware_concurrency() noexcept;
private:
stop_source ssource; // exposition only
};
class thread::id
{
public:
id() noexcept;
};
bool operator==(thread::id x, thread::id y) noexcept;
strong_ordering operator<=>(thread::id x, thread::id y) noexcept;
template<class CharT, class Traits>
basic_ostream<CharT, Traits>& operator<<(basic_ostream<CharT, Traits>& out, thread::id id);
template<class CharT>
struct formatter<thread::id, CharT>;
// hash support
template<class T>
struct hash;
template<>
struct hash<thread::id>;
}
13. 둘러보기
||<:><-12><width=90%><tablewidth=80%><tablebordercolor=#20b580><rowbgcolor=#090f0a,#050b09><rowcolor=#d7d7d7,#a1a1a1>C++||||
}}}
}}}||
}}}||
C언어와의 차이점 | 학습 자료 | 평가 | |||||||||||||
<bgcolor=#20b580> | |||||||||||||||
<rowcolor=#090912,#bebebf>C++ 문법 | |||||||||||||||
<bgcolor=#ffffff> | |||||||||||||||
main | 헤더 | 모듈 | |||||||||||||
함수 | 구조체 | 이름공간 | |||||||||||||
한정자 | 참조자 | 포인터 | |||||||||||||
클래스 | 값 범주론 | 특성 | |||||||||||||
auto | using | decltype | |||||||||||||
상수 표현식 | 람다 표현식 | 객체 이름 검색 | |||||||||||||
템플릿 | 템플릿 제약조건 | 메타 프로그래밍 | |||||||||||||
<bgcolor=#20b580> | |||||||||||||||
<rowcolor=#090912,#bebebf>C++ 버전 | |||||||||||||||
<bgcolor=#ffffff> | |||||||||||||||
C++26 | C++23 | C++20 | |||||||||||||
C++17 | C++14 | C++11 | |||||||||||||
C++03 | C++98 | C with Classes | |||||||||||||
<bgcolor=#20b580> | |||||||||||||||
<rowcolor=#090912,#bebebf>C++ 표준 라이브러리 | |||||||||||||||
<rowcolor=#090912,#bebebf>문서가 있는 모듈 목록 | |||||||||||||||
<bgcolor=#ffffff> | |||||||||||||||
#개요 | C++11 #개요 | <unordered_map> C++11 #개요 | |||||||||||||
C++20 #개요 | #개요 | #개요 | |||||||||||||
C++11 #개요 | C++11 #개요 | C++17 #개요 | |||||||||||||
#개요 | <string_view> C++17 #개요 | C++20 #개요 | |||||||||||||
C++11 #개요 | C++11 #개요 | C++11 #개요 | |||||||||||||
C++20 #개요 | C++23 #개요 | ||||||||||||||
<bgcolor=#20b580> | |||||||||||||||
<rowcolor=#090912,#bebebf>예제 목록 | |||||||||||||||
<bgcolor=#ffffff> | |||||||||||||||
{{{#!wiki style=""text-align: center, margin: 0 -10px" {{{#!folding [ 펼치기 · 접기 ] | 동기화 전략 1 임계 영역과 상호 배제 | 동기화 전략 2 원자적 연산과 메모리 장벽 | 동시성 자료 구조 1 큐 구현 | 동시성 자료 구조 2 집합 구현 | |||||||||||
함수 템플릿 일반화 프로그래밍 | 전이 참조 완벽한 매개변수 전달 | 튜플 구현 가변 클래스 템플릿 | 직렬화 함수 구현 템플릿 매개변수 묶음 | ||||||||||||
SFINAE 1 멤버 함수 검사 | SFINAE 2 자료형 태그 검사 | SFINAE 3 메타 데이터 | SFINAE 4 자료형 트레잇 | ||||||||||||
제약조건 1 개념 (Concept) | 제약조건 2 상속 여부 검사 | 제약조건 3 클래스 명세 파헤치기 | 제약조건 4 튜플로 함자 실행하기 | ||||||||||||
메타 프로그래밍 1 특수화 여부 검사 | 메타 프로그래밍 2 컴파일 시점 문자열 | 메타 프로그래밍 3 자료형 리스트 | 메타 프로그래밍 4 안전한 union |
}}}||
<bgcolor=#20b580> | |||||||||||||||
<rowcolor=#090912,#bebebf>외부 링크 | |||||||||||||||
<bgcolor=#ffffff> | |||||||||||||||
{{{#!wiki style=""text-align: center, margin: 0 -10px" | |||||||||||||||
<bgcolor=#20b580> | |||||||||||
<rowcolor=#090912,#bebebf>C++ |
[1] 이하 스레드 클래스[2] Function Object 또는 Functor[3] joinable의 반환값이 false가 된다[4] Wrapper, 직역하면 싸개[5] 이게 싫으면 소스 파일 하나에 모든 구현을 다 박아 넣어야 한다[6] 단일 코어에서 다중 스레드는 시분할로 동작하는데 이 예제에서는 다중 스레드의 경우와 사실 차이가 없다[7] 이하 뮤텍스[8] 내부적으로는 대기를 위해 운영체제 단에서 반복문을 실행(Spin Lock)한다[9] 다만 릴리스 모드라면 순식간에 끝날 수도 있다[10] 서버급 CPU에서는 더 많은 코어에서도 효용이 나온다[11] 뮤텍스도 공유 자원의 일종[12] 통칭 조인 스레드 클래스[13] 또한 동질성 스레드 기법의 가장 큰 약점이다