C , C++, C#

[C/C++] Thread

vhxpffltm 2020. 1. 19. 16:55

이번 시간에는 '쓰레드' 라고 하는것에 대해 알아본다. 이 개념은 '운영체제'를 들어봤다면 공부해봤을 내용이다. C/C++은 속도가 빠르기 때문에 하드웨어 내부에서 자주 사용하는 언어이다. 

 

시작하기에 앞서 잠깐 운영체제 내용을 간단하게 살펴보고 가자.

 

CPU 는 한 프로그램을 통째로 쭉 실행시키는 것이 아니라, 이 프로그램 조금, 저 프로그램 조금씩 골라서 차례를 돌며 실행시킨다. 이 CPU가 프로세스를 실행시킨다. CPU는 한 번에 한개의 연산을 수행한다.

하지만 컨텍스트 스위칭(Context Swithing)을 통해 여러가지 일들을 한써번에 할 수 있다.

CPU 는 그냥 운영체제가 처리하라고 시키는 명령어들을 실행할 뿐, 어떤 프로그램을 실행시키고, 얼마 동안 실행 시키고, 또 다음에 무슨 프로그램으로 스위치 할지는 운영체제의 스케쥴러(scheduler)로 결정된다.

이 CPU 코어에서 돌아가는 프로그램 단위를 쓰레드 (thread) 라고 한다. 즉, CPU 의 코어 하나에서는 한 번에 한 개의 쓰레드의 명령을 실행시키게 된다.

쓰레드와 프로세스의 가장 큰 차이점은 프로세스들은 서로 메모리를 공유하지 않는다.

한 프로세스 안에 쓰레드 1 과 쓰레드 2 가 있다면, 서로 같은 메모리를 공유하며 예를들어, 쓰레드 1 과 쓰레드 2 가 같은 변수에 값에 접근할 수 있습니다.

최근에는 멀티 코어 CPU를 제공하며 이 CPU에서는 여러개의 코어에 각기 다른 쓰레드들이 들어가 동시에 여러개의 쓰레드들을 효율적으로 실행할 수 있다. 예를들어 Core 1, Core 2, Core 3 등 여러개의 코어안에 인터넷, 음악감상, 한글 등의 프로그램이 각기 다르게 여러개류 실행 할 수 있다.  

멀티 쓰레드를 사용하였을 경우 CPU 에 코어가 10 개가 있어서 각 쓰레드들이 동시에 실행될 수 만 있다면, 각 쓰레드에서 덧셈은 1000 초가 걸리고, 마지막으로 다 합칠 때 10 초가 걸려서 총 1010 초가 걸리게 된다. 싱글 쓰레드의 경우보다 속도가 무려 10 배가 향상된다.

이렇게, 어떠한 작업을 여러개의 다른 쓰레드를 이용해서 좀 더 빠르게 수행하는 것을 병렬화(parallelize) 라고 한다. 이 외에도 대기시간 등의 CPU 시간을 낭비하지 않고 효율적으로 처리할 수 있다.

 

Thread 사용하기

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<iostream>
#include<thread>
#include<stdio.h>
 
using namespace std;
 
void f1() { for(auto i=0;i<10;i++)cout << "Thread 1  시작!!!\n";}
void f2() { for (auto i = 0; i < 10; i++)cout << "Thread 2  시작!!!\n"; }
void f3() { for (auto i = 0; i < 10; i++)cout << "Thread 3  시작!!!\n"; }
 
int main()
{
    cout << thread::hardware_concurrency() << "concurrent threads are supported\n"// 얼마나 많은 스레드가 동시에 실행될 수 있는지 출력
 
    thread t1(f1);
    thread t2(f2);
    thread t3(f3);  // thread를 시작, {f,x}와 같은 표현식으로 객체를 만들 때 개로운 스래드에 의해 f(x)의 호출이 가능
 
    t1.join();
    t2.join();  //join은 스레드들이 자유롭게 실행되고 나서 작업이 끝나면 스레드를 다시 정지, 스레드가 종료하면 리턴함
    t3.detach();//detach는 스레드 인스턴스가 소멸하고 나서도 3번째 스레드를 진행하도록 알려줌
 
    //thread를 실행하기에 앞서 join이나 detach 하지 않으면 스레드 소멸자가 호출한 뒤 예외를 발생한다. 
    //하나의 스레드가 다른 스레드 객체에서 x.join()을 호출하면 스레드 x가 반환될 때까지 해당 스레드는 멈추게됨.
    //x.detach()로 스레드가 종료할 때까지 해당 스레드를 계속 살아있게 한다. join, detach 아무거나 상관은 없는데 사용은 해야한다.
    //main함수가 반환되면 전체 앱이 종료되고 detach된 스레드가 완료되기 전에 끝이 난다.
    //여기서는 스레드 상로 배치 순서, 우선순위 중요도 등의 세부사항은 무시한다.
    cout << "Thread joined.\n";
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 
 

위 코드를 실행할때마다 결과가 달리 나온다. 쓰레드 1이 실행되는 도중 2, 3이 실행되는 모습을 볼 수 있다. 각 스레드들이 서로 다른 스레드상에서 실행되며 CPU코어에 어떻게 할당되고 어떤 순서로 스케줄링 할지는 그때그때 상황에 따라 다르기 때문에 우리가 예측할 수 없다.

 

join() 은, 해당하는 쓰레드들이 실행을 종료하면 리턴하는 함수이며 t1.join() 의 경우 t1 이 종료하기 전 까지 리턴하지 않는다. 다만 join() 이나 detach() 되지 않은 스레드들이 소멸자가 호출되면 예외를 발생하도록 하니 둘중 하나를 사용해야 한다.

 

detach()는 스레드를 사용 후 잊어버리며 스레드는 백그라운드로 들어간다. 쓰레드를 detach() 하면 메인 함수는 더 이상 스레드들이 종료될때까지 기다리지 않게 된다.

 

 

이제 다음의 코드를 보자

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
#include<thread>
#include<stdio.h>
 
using namespace std;
 
void worker(vector<int>::iterator start, vector<int>::iterator end,int* result) {
    int sum = 0;
    for (auto itr = start; itr < end++itr) sum += *itr;
    *result = sum;
    // 쓰레드의 id 를 구한다.
    thread::id this_id = std::this_thread::get_id();
    printf("쓰레드 %p 에서 %d 부터 %d 까지 계산한 결과 : %d \n", this_id, *start,*(end - 1), sum);
    //cout을 사용하면 << 시 스레가 바뀌면서 메세지가 뒤바껴서 나올수 있음 
}
 
int main()
{
    int ans = 0;
    vector<int> v;
    vector<int> sum(4);
    vector<thread> t;
    for (int i = 0; i < 10000; i++) v.push_back(i);
    for (int i = 0; i < 4; i++) {
        t.push_back(thread{worker,v.begin() + i * 2500,v.begin() + (i + 1* 2500,&sum[i] });
    }// 첫 번째 스레드는 0~2499 두번째는 2500~4999 ... callable의 개념으로 호출
    for (int i = 0; i < 4; i++) t[i].join();
    for (int i = 0; i < 4; i++) ans += sum[i];
    cout << "전체 합 : " << ans;
    return 0;
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 

 

먼저, worker 함수는 덧셈을 수행할 데이터의 시작점과 끝점을 받아서 해당 범위 내의 원소들을 모두 더한 후, 그 결과를 result 에 저장한다. 벡터의 이터레이터를 사용한다.

쓰레드는 리턴값 이란것이 없기 때문에 만일 어떠한 결과를 반환하고 싶다면 포인터의 형태(int *result)  로 전달하면 된다.

 

thread 생성자의 첫번째 인자로 함수(Callable) 를 전달하고, 이어서 해당 함수에 전달할 인자들을 써주면 된다.  

 

thread::get_id  함수를 통해서 현재 내가 돌아가고 있는 쓰레드의 ID값을 알 수 있다. 결과는 아래와 같다.

 

 

Referencr

https://modoocode.com/269

C++17 STL 프로그래밍

 

 

 

 

 

 

 

 

 

 

 

'C , C++, C#' 카테고리의 다른 글

[C/C++] mutex 응용과 condition_value  (0) 2020.03.01
[C/C++] Mutex  (0) 2020.02.22
[C/C++] Callable, std::function  (0) 2020.01.14
[C/C++] weak_ptr  (0) 2020.01.09
[C/C++] Shared_ptr  (0) 2020.01.06