반응형
병행 컴퓨팅 (concurrency)
- 여러 개의 계산들을 연속적(하나씩 일을 마치는 것)으로가 아닌, 병행 처리하는 것을 말합니다.
- 병행 시스템은 다른 계산들이 모두 끝날 때까지 기다리지 않고 계산을 진행할 수 있는 환경을 말하며, 즉 하나 이상의 계산은 동시에 진행이 가능합니다.
- 프로그램 논리 구조 상에서 연산들 간의 의존 관계가 많을수록 병렬화가 어려워지고,
반대로, 다른 연산의 결과와 관계없이 독립적으로 수행할 수 있는 구조가 많을수록 병렬화가 매우 쉬워집니다.- 쓰레드: CPU 코어에서 돌아가는 프로그램 단위. 한 프로세스 내에 있는 쓰레드끼리는 메모리 교환 가능.
- 프로세스: 최소 한 개 쓰레드로 이루어져 있으며, 여러 개의 쓰레드로 구성(멀티 쓰레드). 프로세스끼리 메모리를 공유하지 않음.
std::thread
- 이전에는 C++ 표준에 쓰레드가 없어서, 각 플랫폼 마다 다른 구현을 사용해야만 했습니다.
- 예를 들어서 윈도우즈에서는 CreateThread 로 쓰레드를 만들지만 리눅스에서는 pthread_create 로 만듭니다.
- 하지만 C++ 11 에서부터 표준에 thread가 추가되면서, 쓰레드 사용이 매우 편리해졌습니다.
- 인자가 없는 함수, 인자를 가지는 함수, 람다 함수 등을 이용하여 쓰레드를 생성할 수 있습니다.
- 참고로 리눅스에서 컴파일 하는 분은 컴파일 옵션에 -pthread 를 추가로 넣어야 합니다.
#include "spdlog/fmt/fmt.h"
#include <thread>
void func1()
{
fmt::print("[T1] I will be dead\n");
}
void func2(int const i)
{
fmt::print("[T2] I will be dead after {} seconds\n", i);
std::this_thread::yield();
std::this_thread::sleep_for(std::chrono::seconds(i));
fmt::print("[T2] now I am dead\n", i);
}
int main()
{
std::thread t1(func1);
std::thread t2(func2, 5);
std::thread t3([]()
{ fmt::print("[T3] I am lambda thread\n"); });
t1.join();
t2.join();
t3.join();
return 0;
}
- join 은, 해당하는 쓰레드들이 실행을 종료하면 리턴하는 함수 입니다.
따라서 t1.join() 의 경우 t1 이 종료하기 전 까지 리턴하지 않습니다. - detach 는 말 해당 쓰레드를 실행 시킨 후, 잊어버리는 것 이라 생각하시면 됩니다.
대신 쓰레드는 알아서 백그라운드에서 돌아가게 됩니다.- C++ 표준에 따르면, join 되거나 detach 되지 않는 쓰레드들의 소멸자가 호출된다면 예외를 발생시키도록 명시되어 있습니다.
+ std::cout, printf
std::cout << "쓰레드 " << hex << this_id << " 에서 " << dec << *start << " 부터 "
<< *(end - 1) << " 까지 계산한 결과 : " << sum << std::endl;
- std::cout
- << 를 실행하는 과정 중간 중간에 계속 실행되는 쓰레드들이 바뀌면서 결과적으로 메세지가 뒤섞여서 나타나게 됩니다.
- 따라서, std::cout 의 경우 std::cout << A; 이렇게 선언하여, A 의 내용이 출력되는 동안 중간에 다른 쓰레드가 내용을 출력할 수 없게 해야합니다.
- 그 사이에 컨텍스트 스위치가 되더라도 다른 쓰레드가 내용을 출력할 수 없습니다.
- printf
- "..." 안에 있는 문자열을 출력할 때, 컨텍스트 스위치가 되더라도 다른 쓰레드들이 그 사이에 메세지를 넣지 못하게 막습니다.
std::mutex, lock
- 영어의 상호 배제 (mutual exclusion) 라는 단어에서 따온 단어 입니다.
- m.lock() 은 뮤텍스 m의 사용권한을 갖는 것이며, 한 번에 한 쓰레드에서만 m 의 사용 권한을 갖습니다.
- 이때 m 을 소유한 쓰레드가 m.unlock() 을 통해 사용권한을 반환할 수 있습니다.
- 이때 m.lock() 과 m.unlock() 사이에 한 쓰레드만이 실행할 수 있는 코드 부분을 임계 영역(critical section) 이라고 부릅니다.
#include <iostream>
#include <mutex> // mutex 를 사용하기 위해 필요
#include <thread>
#include <vector>
void worker(int& result, std::mutex& m) {
for (int i = 0; i < 10000; i++) {
m.lock();
result += 1;
m.unlock();
}
}
int main() {
int counter = 0;
std::mutex m; // 우리의 mutex 객체
std::vector<std::thread> workers;
for (int i = 0; i < 4; i++) {
// std::ref() 는 특정 타입을 참조하는 객체를 만들며, &와 다르게 타입만 같다면 참조대상 교체 가능.
// 주로 thread의 인자 또는 bind의 인자로 넘겨줄 때 사용함.
workers.push_back(std::thread(worker, std::ref(counter), std::ref(m)));
}
for (int i = 0; i < 4; i++) {
workers[i].join();
}
std::cout << "Counter 최종 값 : " << counter << std::endl;
}
- 이때 뮤텍스를 취득한 쓰레드가 unlock 을 하지 않는다면? 다른 모든 쓰레드들이 기다리게 되므로 '데드락(deadlock) 상태'가 됩니다.따라서 취득한 뮤텍스는 반드시 반환해야 합니다.
- 이는 포인터를 반드시 delete 해야하는 것과 유사하며, unique_ptr과 비슷한 해제패턴(lock_guard)으로 처리할 수 있습니다.
- lock_guard 객체는 뮤텍스를 인자로 받아서 생성하면, lock_guard 가 소멸될 때 알아서 lock 했던 뮤텍스를 unlock 하게 됩니다.
std::mutex g_mutex;
// 락을 걸어야 할 장소
{ // scope 한정으로 락 범위를 좁힌다.
std::lock_guard<std::mutex> lock(g_mutex);
// 락이 필요한 연산
}
- 이 외의 데드락 상황을 피하기 위한 가이드라인
- 중첩된 Lock 을 사용하는 것을 피해라
- Lock 을 소유하고 있을 때 유저 코드를 호출하는 것을 피해라
- Lock 들을 언제나 정해진 순서로 획득해라
+ lock 사용을 위한 표준 클래스
- std::lock_guard
- std::unique_lock
- std::shared_lock
생산자(Producer) 와 소비자(Consumer) 패턴
- 생산자의 경우, 무언가 처리할 일을 받아오는 쓰레드를 의미합니다.
- 소비자의 경우, 받은 일을 처리하는 쓰레드를 의미합니다.
- TODO...(복잡쓰...)
쓰레드 간 통신
- conditional_variable, atomic, future 등을 통해 쓰레드 간 통신, 쓰레드 간 리턴값 반환 등을 할 수 있습니다.
참고
- [wiki] 병행 컴퓨팅: https://ko.wikipedia.org/wiki/%EB%B3%91%ED%96%89_%EC%BB%B4%ED%93%A8%ED%8C%85?tableofcontents=0
- [씹어먹는 C++] 동시에 실행을 시킨다고? - C++ 쓰레드(thread): https://modoocode.com/269
- [씹어먹는 C++] C++ 뮤텍스(mutex) 와 조건 변수(condition variable): https://modoocode.com/270
- [네이버 블로그] std::ref: https://m.blog.naver.com/pkk1113/221711759046
반응형
'Study: Software(SW) > SW: Language' 카테고리의 다른 글
[C++] 객체지향 프로그래밍: static, explicit, mutable, default, delete... (0) | 2022.06.22 |
---|---|
[C++] C++ Json 라이브러리 성능비교(벤치마크): JsonCpp, Nlohmann/json... (0) | 2022.06.16 |
[C++] 초심자가 자주 하는 C++ 실수 (0) | 2022.06.15 |
[C++] 알고리즘 (#include <algorithm>): 있는 거 가져다 쓰자. (0) | 2022.06.15 |
[C++] 템플릿 메타 프로그래밍 (Template Meta-programming) (0) | 2022.06.14 |