안녕하세요. 핑크코냥입니다.
오늘은 멀티 프로그래밍 정리 내용입니다. 긴글 주의 해주세요. ㅎㅎ;;
제가 멀티 프로그래밍 개념이 부족해서 업무 중 대화에서 이해 못한 적이 몇 번 있어서 한 번 쫙 정리 해보았습니다.

* 멀티스레드 프로그래밍
- 프로세서 유닛이 여러 개 장착된 컴퓨터 시스템에 중요한 기법.
- 시스템에 있는 여러 프로세서 유닛을 병렬로 사용가능.
* 시스템에 프로세서 유닛이 장착되는 방식은 다양함.
1. 독립적인 cpu center processor unit를 담은 프로세서 칩이 여러 개 달리는 방식.
2. 한 프로세서 칩 안에 코어core라 부르는 독립적인 cpu가 여러개 있는 방식.
3. 위 두개를 혼합하는 방식
- 위 와 같이 여러 개 달린 프로세서를 - 멀티코어 프로세서 multicore processor라 부름.
23.1 멀티스레드 프로그래밍 개념
- 여러 연산을 병렬로 함.
- cpu뿐만 아니라 gpu라 부르는 그래픽 카드용 프로세서도 병렬화 되어있음.
- 요즘 고성능 그래픽 카드는 코어가 4000개 이상.
* 멀티스레드가 필요한 이유는?
- 주어진 작업을 작은 문제들로 나눠서 각각을 멀티프로세서 시스템에서 병렬로 실행하면 성능 향상.
- 연산을 다른 관점에서 모듈화할 수 있음.
- but, 항상 독립 작업을 나눠서 병렬화 할 수 있는 것은 아님. 멀티스레드 프로그래밍을 하는데 어려운
부분은 병렬 알고리즘을 고안하는 것이다. 작업의 성격에 따라 구현 방식이 크게 달라짐.
또한 경쟁상태, 교착 상태(데드락), 테어링tearing, 잘못된 공유 false-sharing등 과 같은 문제가 발생하지
않게 만드는것도 쉽지 않음. 거의 아토믹, 명시적인 동기화 메커니즘으로 해결함.
23.1.1 경쟁상태
- 여러 스레드가 공유 리소스를 동시에 접근할 때 경쟁 상태가 발생 (메모리에 대란 경쟁상태 = 데이터 경쟁)
23.1.2 테어링 tearing
- 데이터 경쟁의 특수한 경우
- 읽기 테어링 torn read : 어떤 스레드가 메모리에 데이터 일부만 쓰고 나머지 부분을 미처 쓰지 못한 상태에서
다른 스레드가 이 데이터를 읽으면 두 스레드가 보는 값이 달라짐.
- 쓰기 테어링 torn write : 두 스레드가 데이터에 동시에 쓸 때 한 스레드는 그 데이터의 한 쪽 부분을 쓰고, 다른 스레드는
그 데이터의 다른 부분을 썼다면 각자 수행한 결과가 달라짐.
23.1.3 데드락
- 경쟁 상태를 막기 위해 상호 배제와 같은 동기화 기법을 적용하다 보면 발생하는 데드락(교착상태)
- 여러 스레드가 서로 상대방 작업이 끝날 때가지 동시에 기다리는 상태.
- 스레드가 공유 리소스에 접근하려면 접근 권한 요청을 해야함. - 뮤텍스
[리소스B]
↗ ↘
[스레드1] [스레드2]
↖ ↙
[리소스A]
- 이러한 상황이 발생하지 않게 하려면 모든 스레드가 일정한 순서로 리소스를 획득해야 함.
* [해결 방법]
- 리소스 접근 권한을 요청하는 작업에 시간제한을 검.
- 위에 방법보다 아예 발생하지 않도록하는 것이 베스트
- 접근 권한을 개별적으로 요청하지 않고, 상호배제의 std::lock( ) std:: try_lock( )과 같은 함수를 활용
23.1.4 잘못된 공유
- 캐시cache는 캐시 라인 cache line단위로 처리.
- 캐시라인에 데이터를 쓰려면 반드시 그 라인 전체에 락을 걸어야 함.
- 멀티스테드 코드를 실행할때 데이터 구조를 잘 만들지 않으면 캐시라인에 락을 거는 과정에서 성능이 떨어짐.
ex) 두 스레드가 두 가지 데이터 영역을 사용하는데 데이터가 같은 캐시 라인에 걸쳐 있는 경우
- 한 스레드가 데이터를 업데이트하면 캐시 라인 전체에 락을 걸어버려 다른 스레드는 기다려야함.
- 즉, 캐시라인에 걸쳐 있지 않도록 데이터 구조가 저정될 메모리 영역을 명시적으로 정렬하면 여러 스레드가
접근 할 때 대기하지 않게 만들어야 함.
C++17 <new>헤더 파일의 hardware_destructive_interference_size 동시에 접근하는 두 객체가 캐시
라인을 공유하지 않도록 최소한의 오프셋
23.2 스레드
23.2.1 함수 포인터로 스레드 만들기
- 윈도우 시스템의 CreateThread( ), _beginthread( ) pthreads라이브러리의 pthread_create( ) 스레드 함수는 매개변수를 하나만 받음.
- C++표준 std::thread클래스에서 사용하는 함수 매개변수를 원하는 개수만큼 받을 수 있음.
- thread 클래스 생성자는 가변 인수 템플릿 이기 때문에 인수 개수를 원하는 만큼 지정이 가능함.
- thread ( 스레드가 실행 할 함수, 스레드 구동되면서 실행할 함수에 전달하는 인수 개수)
- thread객체가 실행 가능한 상태에 있을때 조인 가능 joinable하다고 표현
- 디폴드로 생성된 thread객체는 조인 불가능 unjoinable
- 조인 가능한 thread객체를 제거하려면 먼저 그 객체의 join이나 detach 부터 호출
- join( ) 호출시 그 스레드는 블록 된다. :그 스레드가 작업이 끝날 때까지 기다림
- detach( )를 호출하면 thread 객체를 os내부의 스레드와 분리한다. 그래서 os 스레드는 독립적으로 실행됨.
- 조인 가능 상태의 thread객체를 제거하면 그 객체의 소멸자 std::terminate를 호출해서 모든 스레드뿐만 아니라 애플리케이션마저 종료.
- 스레드 함수에 전달한 인수는 항상 그 스레드의 내부 저장소에 복제된다. 인수를 레퍼런스로 전달하고 싶다면 <functional>에 정의된 std::ref나 cref를 사용함.
23.2.2 함수 객체로 스레드 만들기
- 장점: 함수 객체로 만들면 그 함수 객체의 클래스에 멤버 변수를 추가해서 원하는 방싱으로 초기화해서 사용 가능
- 클래스를 함수 객체로 만들려면 operator( )를 구현해야 함. - 18장
(1). 유니폼 초기화 thread t1
{Counter{1, 20}} * 중괄호
(2). 일반 변수처럼 네임드 인스턴스로 초기화
Counter c( 2, 12 ) <- 네임드 인스턴스
thread t2( c )
thread t2( ref(c) )
(3). 임시 객체를 사용
thread t3 (Counter(3, 10)) *소괄호
함수 객체 생성자가 매개변수를 받지 않을때는 후자와 같이 코드를 작성하면 에러가 발생한다.
- 함수 객체는 항상 스레드의 내부 저장소에 복제된다. 함수 객체의 인스턴스를 복제하지 않고 그 인스턴스에 대해 operator( )
을 호출하려면 <functional>헤더에 정의된 std::ref()나 cref()를 사용해서 인스턴스 레퍼런스로 전달해야한다.
23.2.3 람다 표현식으로 스레드 만들기
- 람다 굿임.
23.2.4 멤버 함수로 스레드 만들기
- 스레드에서 실행할 내용을 클래스의 멤버 함수로 지정할 수도 있다.
- thread t{&Request::process, &ref}
- 특정한 객체에 있는 메서드를 스레드로 분리해서 실행할 수 있다.
23.2.5 스레드 로컬 저장소 thread local storage
- 원하는 변수에 thread_local이란 키워드를 지정해서 스레드 로컬 저장소로 지원하면 스레드마다 이 변수를 복제해서
스레드가 없어질 때까지 유지한다. 이 변수는 스레드에서 한 번만 초기화 된다.
- thread_local 변수를 함수 스코프 안에서 선언하면 모든 스레드가 복제본을 따로 갖고 있고, 함수를 아무리 많이 호출하더라고
스레드마다 단 한 번만 초기화된다는 점을 제외하면 static으로 선언할 때와 똑같이 작동한다.
23.2.6 스레드 취소하기
- C++표준은 실행 중인 스레드를 다른 스레드에서 중단시키는 메커니즘을 제공하지 않음. 방법은 ?
1. 여러 스레드가 공통으로 따르는 통신 메커니즘을 제공하는것이다
2. 공유 변수를 활용하자
- 값을 전달 받은 스레드는 이 값을 주기적으로 확인하며 중단여부를 결정한다.
- 주의: 여러 스레드가 공유 변수에 접근하기 때문에 최소한 한 스레드는 그 변수에 값을 쓸수 있다. 따라서
이 변수를 아토믹이나 조건 변수로 만드는 것이 좋다.
23.2.7 스레드로 실행한 결과 얻기
- 결과를 담은 변수에 대한 포인터나 레퍼런스를 스레드로 전달해서 스레드마다 결과를 저장하게 만듦.
- 함수객체의 클래스 멤버 변수에 처리 결과를 저장했다가 나중에 스레드가 종료할때 그 값을 가져오는 것.
반드시 std::ref( )를 이용해서 함수 객체의 레퍼런스 thread생성자에 전달해야 함.
- future를 활용.
23.2.8 익셉션 복제와 다시 던지기
- 스레드에서 던진 익셉션은 그 스레드 안에서 처리해야 함.
- 던진 익셉션을 스레드 안에서 잡지 못하면 C++런타임은 std::terminate( )를 호출해서 애플리케이션 전체를 종료시킴.
- 한 스레드에서 던진 익셉션을 다른 스레드에서 잡을 수 없음.
- 익셉션 관련 함수를 표준 스레드 라이브러리에서 제공함.
이 함수는 std::exception, int, string, 커스텀 익셉션등에도 적용됨.
* exception_ptr current_exception( ) noexcept;
* [[noreturn]] void rethrow_exception (exception_ptr p);
* template<class E> exception_ptr make_exception_ptr(E e) noexcept;
23.3 아토믹 연산 라이브러리 <atomic>
- 아토믹 타입(atomic type)을 사용하면 동기화 기법을 적용하지 않고 읽기와 쓰기를 동시에 처리하는 아토믹 접근(atomic access)이 가능함.
- 아토믹 연산을 사용하지 않고 변수의 값을 증가시키면 스레드에 안전하지 않음.
- 컴파일러는 먼저 메모리에서 이 값을 읽고, 레지스터로 불러와서 값을 증가시킨 다음, 그 결과를 메모리에 다시 저장한다. 이 과정에서 메모리 영역을 다른 스레드가 건드리면 데이터 경쟁이 발생함.
- std::atomic 타입을 적용하면 'mutex' 뮤텍스 객체와 같은 동기화 기법을 따로 사용하지 않고도 스레드에 안전하게 만들수 있음.
- 아토믹 타입을 사용할 때는 동기화 메커니즘을 명시적으로 사용하지 않아도 된다. but, 특정타입에 대해 아토믹 연산으로 처리할때는 뮤텍스와 같은 동기화 메커니즘을 내부적으로 사용하기도 함.
- 연산을 아토믹 방식으로 처리하는 인스트럭션(instruction)을 타깃 하드웨어에서 제공하지 않을수 있음.
is_lock_free( )메서드를 호출해서 잠그지 않아도 되는지 (락 프리 lock- free) (~ㅇ.ㅇ)~ ♥ ~(ㅇ.ㅇ~)
즉 명시적으로 동기화 메커니즘을 사용하지 않고도 수행할수 있는지 확인함.
- atomic 클래스 템플릿은 정수 타입뿐만 아니라 다른 모든 종류의 타입에 대해서도 적용할 수 있음.
23.3.1 아토믹 타입 사용예
void increment(int& counter)
{
for(int i = 0; i< 100 ; ++i)
{
++count;
this_thread::sleep_for(1ms); //std::this_thread::sleep_for( )
//std::chrono::duration타입의 인수를 하나 받는다.
}
}
- increment( )함수를 실행하는 스레드를 여러개 뛰운다 가정
- counter변수 하나를 여러 스레드가 공유하게 된다.
- 아토믹, 스레드 동기화 메커니즘을 사용하지 않고 단순하게 구현하면 데이터 경쟁이 발생함.
int main ()
{
int count = 0;
vector<thread> threads;
for (int i = 0 ; i< 10; ++i)
{ threads.push_back(thread {increment, ref(counter)}); }
for (auto& t: threads) { t.join(); }
cout <<"Result =" <<counter <<endl;
}
- 아토믹을 사용하면 동기화 메커니즘을 따로 추가하지 않고도 스레드에 안전하고 데이터 경쟁이 발생하지 않게 만들수 있음.
- ++counter연산을 수행하는데 필요한 작업을 하나의 아토믹 트랜잭션으로 처리해서 중간에 다른 스레드가 개입 할 수 없기 때문임.
23.3.2 아토믹 연산
* bool atomic<T>::compare_exchange_strong(T& expected, T desired)
* atomic<T>::fetch_add( ) : 주어진 아토믹 타입의 현재 값을 가져와서 지정한 값만큼 증가시킨 다음, 중가시키기 전의 값을 리턴함.
- 정수형 아토믹: fetch_add( ), fetch_sub( ), fetch_and( ), fetch_or( ), fetch_xor( ), ++, --, +=, -=. &=. ^=, |=
- 포인터 아토믹: fetch_add( ), fetch_sub( ), ++, --, +=, -=
- 대부분 원하는 메모리 순서를 지정하는 매개변수를 추가로 받음.
23.4 상호 배제
- 부울, 정수를 비롯한 스칼라값은 아토믹 연산으로 충분.
- 복잡하게 구성된 데이터를 여러 스레드가 동시에 접근할때는 동기화 메커니즘을 사용함.
- 표준 라이브러리는 mutex, lock 클래스를 통해 상호 배제 메커니즘을 제공
23.4.1 mutex
- mutex는 상호 배제를 뜻하는 mutual exclusion의 줄임말임.
- C++표준은 [시간 제약이 없는 뮤텍스 non-timed mutex], [시간 제약이 있는 뮤텍스 timed mutex]클래스를 제공함.
(1). [시간 제약이 없는 뮤텍스 non-timed mutex]
- std::mutex -> header: <mutex>
- std::mutex는 소유권을 독점하는 기능을 제공하는 표준 뮤텍스 클래스다. 한 스레드만 가질수 있음.
다른 스레드가 이 뮤텍스를 소유하려면 lock( )을 호출하고 대기함.
try_lock( )호출하면 락 걸기에 실패해 곧바로 리턴함.
뮤텍스를 이미 확보한 스레드가 같은 뮤텍스에 대해 lock( ), try_lock( )을 또 호출하면 데드락 발생하므로 조심해야 함.
- std::recursive_mutex -> header: <mutex>
recursive_mutex를 확보한 스레드가 동일한 recursive_mutex에 대해 lock , try_lock을 또 호출할 수 있다.
- std::shard_mutex -> header: <shared_mutex>
공유 락 소유권 shard lock ownership
읽기-쓰기 락 reader-writer lock
독점 소유권 exclusive ownership
공유 소유권 shard ownership
독점 소유권 또는 쓰기락은 다른 쓰레드가 독점 소유권이나 공유 소유권을 가지고 있지 않을 때만 얻을 수 있다.
lock_shared( ), try_lock_shared(), unlock_shared()와 같은 공유 소유권 관련 메서드를 제공
(2). [시간 제약이 있는 뮤텍스 timed mutex]
- std::timed_mutex -> header: <mutex>
- std::recursive_timed_mutex -> header: <mutex>
- try_lock_for (rel_time)
- try_lock_until (abs_time)
- std::shard_timed_mutex -> header: <shared_mutex>
- try_lock_shared_for (rel_time)
- try_lock_shared_until (abs_time)
23.4.2 Lock
- RAII( Resource Acquisition Is Initialization )원칙이 적용되는 클래스
- 뮤텍스에 락을 정확히 걸거나 해제하는 작업을 쉽게 처리하게 해줌.
- lock 클래스의 소멸자는 확보했던 뮤텍스를 자동으로 해제시킴.
- C++표준에서는 std::lock_guard, std::unique_lock, std::shared_lock, std::scoped_lock (C++17에서 추가)
* lock_guard -> header: <mutex>
* std::unique_lock -> header: <mutex>
- 락을 선언하고 한참 뒤 실행될 때 락을 걸도록 지연시키는 고급 기능을 제공
* std::shared_lock -> header: <shared_mutex>
- 내부 공유 뮤텍스에 대해 공유 소유권에 관련된 메서드를 호출하는 다른 점이 있음.
* 한 번에 여러 개의 락을 동시에 걸기
- lock( ) 지정 - 락을 거는 순서는 알수 없다.
- 어느 하나의 뮤텍스 락에 대해 익셉션이 발생하면 이미 확보한 락에 대해 unlock( )을 호출한다.
- try_lock ( )
* scoped_lock -> header: <mutex>
- std::lock_guard와 비슷하지만 뮤텍스를 지정하는 인수 개수에 제한이없다.
- C++17에 추가된 생성자에 대한 템플릿 인수 추론 기능을 적용도 가능함.
scoped_lock locks(mut1, mut2);
23.4.3 std::call_once
- std::call_once와 once_flag를 함께 사용하면 once_flag에 대한 여러 스레드가 call_once를 호출하더라도 call_once의 인수로 지정한 함수나
메서드가 단 한번만 호출되게 할 수 있다. 지정한 함수가입셉션을 던지지 않을 때 이렇게 호출하는 것을 이펙티브call_once 호출이라고 부른다.
23.5 조건 변수 condition variable
- 다른 스레드가 조건을 설정하기 전이나 따로 지정한 시간이 경과하기 전까지 스레드의 실행을 멈추고 기다리게 할 수 있음.
- 스레드 통신을 구현할 수 있음.
- <condition_variable> std::condition_variable std::condition_variable_any
23.5.1 비정상적으로 깨어나기
- 조건 변수를 기다리는 스레드는 다른 스레드가 notify_one( )이나 contify_all( )을 호출할때까지 기다림.
그런데 이렇게 미리 지정된 시점에 다다르지 않았는데 비정상적으로 깨어날 수도 있음.
-프레디케이트를 인수로 받는 wait( ) 를 사용하여 이유를 검사해야함.
23.6 promise( std::promise )와 future( std::future )
- future를 사용하면 스레드의 실행 결과를 쉽게 받아올 수 있을 뿐만 아니라 익셉션을 다른 스레드로 전달해서
원하는 방식으로 처리할 수 있음.
- 스레드의 실행 결과를 promise에 담으면 future로 그 값을 가져올 수 있음.
- 채널에 비유하면 promise는 입력 포트, future는 출력 포트인 셈.
- set_exception()을 호출해서 익셉션을 promise에 저장할 수 있음.
- future의 가장 큰 장점은 스레드끼리 익셉션을 주고받는데 활용할 수 있다는 것.
- get( )을 여러 스레드에 대해 여러번 호출하고 싶다면 std::shared_future<T>를 사용함.
23.6.2 std::packaged_task
- std::promise를 명시적으로 사용하지 않고도 promise를 구현할 수 있음.
23.6.3 std::async 에이싱크, 어싱크
- 스레드로 계산하는 작업을 C++런타임으로 좀 더 제어하고 싶다면 std::async( )를 사용함.
- 함수를 스레드로 만들어 비동기식으로 구동함.
- 스레드를 따로 만들지 않고, 리턴된 future에 대해 get( )을 호출할때 동기식으로 함수를 실행함.
23.8 스레드 풀
- 프로그래밍을 구동할 때부터 종료할 때까지 스레드를 필요할 때마다 생성했다 삭제하는 식으로 구현하지 말고,
필요한 수만큼 스레드 풀 thread pool을 구성해도 됨.
- 주로 특정한 종류의 이벤트를 처리할 떄 이 기법을 적용함.
- 일반적으로 프로세서 코어 수만큼 스레드를 생성하는 것이 적절함.
※ 인스트럭션(instruction)은 컴퓨터에게 일을 시키는 단위로서, 컴퓨터가 알아들을 수 있는 기계어로 이루어져
있는 명령이다. 지시 또는 명령이라고 함.
출처: 전문가를 위한 C++
'👨🏻💻 programming > ◽ c, c++' 카테고리의 다른 글
(c++17) decltype, 로 스트링 리터럴, static, const, extern, mutable, constexpr, 타입 앨리어스, 스코프, 레퍼런스, 어트리튜트 (4) | 2022.08.11 |
---|---|
(c++17) 전처리 지시자, if-switch 이니셜라이저, __func__, 구조적 바인드 (0) | 2022.08.04 |
[c++]'전문가를 위한 C++17'을 공부하며 정리(ing...) (0) | 2022.05.16 |
[C++] 제네릭 알고리즘 모음 (0) | 2022.05.16 |
[C++] std::string_view 클래스 (0) | 2022.02.09 |
안 하는 것 보다 낫겠지
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!