C++ 멀티스레딩(multi-thread) 기본 개념: 세마포어(semaphore)와 while() loop를 사용한 프로세스 병렬 처리
멀티스레딩은 여러 실행 스레드가 단일 프로세스 내에서 동시에 실행될 수 있도록 하는 프로그래밍 개념입니다. 스레드는 프로세스 내에서 가장 작은 실행 단위이며 가볍고 독립적인 명령 시퀀
easycode.tistory.com
→ 해당 글과의 차별점
: 본 thread의 포스팅의 경우 총 5가지 thread로 확장했으며, 각 thread는 단일 반복이 아닌 16번의 반복 처리 리 과정을 거칩니다. 또한, header_v1.h 파일의 구조체를 통해 각 thread의 매개변수로 구조체를 선언했으며, 각 thread가 연산 처리 후 해당 구조체 변수에 저장할 수 있는 type으로 설계되었습니다.
Let's Go
스레딩은 단일 프로세스 내에서 여러 작업의 실행을 나타내는 컴퓨터 프로그래밍의 개념입니다. 스레드로 알려진 각 작업은 동일한 프로세스 내에서 동시에 실행되며 동일한 메모리와 리소스를 공유합니다. 스레드는 종종 단일 프로세스 내에서 여러 독립적인 작업을 수행하는 데 사용되어 프로그램의 전반적인 성능과 응답성을 향상합니다.
멀티스레딩은 단일 프로세스 내에서 또는 여러 프로세스에서 여러 스레드를 병렬로 실행하는 프로세스입니다. 멀티스레딩의 주요 이점은 여러 작업을 동시에 수행할 수 있어 시스템 리소스를 보다 효율적으로 사용하고 성능을 향상할 수 있다는 것입니다. 메인 스레드가 사용자 입력에 응답할 수 있는 상태로 유지되는 동안 다중 스레드를 사용하여 백그라운드에서 시간 소모적인 작업을 수행하는 데 다중 스레드를 사용할 수 있으므로 다중 스레드를 사용하여 프로그램의 응답성을 향상시킬 수도 있습니다.
많은 이미지 처리 작업이 계산 집약적이고 완료하는 데 오랜 시간이 걸릴 수 있기 때문에 이미지 처리에서 스레딩을 사용하는 것이 일반적입니다. 스레드를 사용하면 큰 이미지 처리를 병렬로 수행할 수 있는 더 작고 독립적인 작업으로 나눌 수 있으므로 처리 시간이 빨라지고 성능이 향상됩니다. 예를 들어 이미지 크기 조정, 이미지 필터링 및 이미지 압축은 여러 스레드를 사용하여 병렬로 수행할 수 있는 작업으로 상당한 성능 향상을 가져옵니다.
이러한 Thread 개념을 통해 다음과 같은 multi-thread를 만들고자 합니다. 아래의 포스팅도 함께 참고하면 이해하기 수월합니다.
→ multi-thread의 가장 기본 구조 : https://easycode.tistory.com/25
→ multi-thread와 TCP/IP 네트워크를 결합한 구조 : https://easycode.tistory.com/24
5개의 스레드를 생성하고 세마포어를 이용하여 스레드 간 동기화를 구현하는 프로그램입니다. 프로그램은 CreateSemaphore 함수로 5개의 세마포어를 초기화하는 것으로 시작합니다. 첫 번째 세마포어 sem1은 초기 카운트 0과 최대 카운트 16으로 초기화됩니다. 다른 세마포어 sem2, sem3, sem4 및 sem5에 대해서도 동일하게 수행됩니다. 5개의 스레드 각각은 특정 작업을 수행하며 현재 역할(Role in English) 주석으로 표시됩니다. 작업은 스레드 순서대로 수행되며 각 스레드는 자체 작업을 시작하기 전에 이전 스레드가 세마포어를 해제하기를 기다립니다. 또한 각 스레드는 작업을 수행하는 데 걸린 시간을 측정하고 출력합니다. 시간은 ctime 라이브러리의 clock() 함수를 사용하여 측정됩니다. 스레드를 생성한 후 main 함수는 각 스레드가 join() 함수로 완료될 때까지 기다립니다. 마지막으로 프로그램은 CloseHandle 함수를 사용하여 세마포어에 대한 핸들을 닫습니다.
[코드 설명]
이 코드에서 첫 번째 단계는 프로그램에 필요한 헤더를 포함하는 것입니다. 이러한 헤더에는 표준 입력 및 출력 라이브러리(iostream), 스레드 라이브러리(thread), Windows API 라이브러리(Windows.h), 표준 라이브러리(stdlib.h), 고정 너비 정수 유형용 헤더 파일(stdint)이 포함됩니다.. h), 시간 라이브러리(ctime) 및 표준 입력 및 출력 라이브러리(stdio.h).
다음으로 코드에는 "header_v1.h"라는 이름의 헤더 파일이 포함되어 있는데, 이 파일은 프로그램용으로 생성된 사용자 정의 헤더 파일로 보입니다.
그런 다음 코드는 값이 15인 "CNT"라는 상수를 정의합니다. 이 상수는 나중에 코드에서 각 스레드의 반복 횟수를 결정하는 데 사용됩니다. → 해당 부분이 KP(key point) : 영상처리로 비유한다면, 초당 10 frame 정도를 처리하고 싶은데 넉넉잡아 16개의 frame 데이터를 처리 할 수 있도록 각 thread의 반복 횟수를 결정하는 열쇠입니다.
그런 다음 코드는 네임스페이스를 "using namespace std"로 선언합니다. 즉, 표준 네임스페이스에 정의된 모든 식별자는 "std::" 접두사 없이 액세스 할 수 있습니다.
마지막으로 이 코드는 sem1, sem2, sem3, sem4 및 sem5라는 5개의 HANDLE 변수를 선언합니다. 이 변수는 프로그램에서 세마포어를 나타내는 데 사용됩니다. 세마포어는 여러 스레드에서 공유 리소스에 대한 액세스를 제어하는 데 사용되는 동기화 메커니즘입니다.
thread_1 함수는 별도의 스레드에 의해 실행될 함수입니다. 헤더 파일 header_v1.h에 정의된 MAIN_FORMAT 유형의 구조에 대한 포인터인 MAIN_FORMAT*를 인수로 사용합니다.
함수에서 지역 변수 thread_1_count가 선언되고 0으로 초기화됩니다. 이 변수는 스레드가 실행된 횟수를 추적하는 데 사용됩니다. 변수 runningtime은 스레드의 실행 시간을 저장하기 위해 선언됩니다.
스레드 실행의 시작 및 종료 시간은 변수 start_time 및 end_time에 각각 저장됩니다.
while 루프는 무기한 실행되며 각 반복에서 다음 작업을 수행합니다.
▶ 시작 시간(start)은 clock()을 사용하여 기록됩니다.
▶ 작업을 시뮬레이트하기 위해 this_thread::sleep_for(chrono::milliseconds(100))를 사용하여 스레드를 100밀리 초 동안 휴면 상태로 만듭니다. → Input signal이 100ms 정도 주기적으로 소모된다는 가정.
▶ 스레드가 수행하는 기능이 여기에서 실행되지만 현재 자리 표시자 주석 /* 스레드가 수행한 기능 추가 */로 주석 처리되어 있습니다.
▶ 종료 시간(end)은 clock()을 사용하여 기록됩니다.
▶ 실행 시간은 시작 시간(start)에서 종료 시간(end)을 빼서 계산하여 runningtime 변수에 저장합니다.
▶ thread_1_count는 1씩 증가합니다. → 수행하는 이유 : 각 count에 원하는 정보를 저장하기 위해.
▶ thread_1_count가 CNT(=15로 정의됨) 보다 크면 thread_1_count가 0으로 재설정됩니다. → 수행하는 이유 : 최종 count는 16번째 까지만 증가하도록 제한을 두기 때문에.(0부터 시작하니까 15까지.)
▶ ReleaseSemaphore 함수의 각 인수의 역할은 아래와 같습니다.
★ 첫 번째 인수가 sem1이고, ★ 두 번째 인수가 1인 상태에서 호출됩니다. 이 함수는 지정된 세마포 개체의 카운트를 증가시켜 대기 중인 스레드 중 하나가 진행될 수 있도록 합니다. ★ 세 번째 인수는 오류 코드가 필요하지 않으므로 NULL입니다. |
이 코드의 목적은 일부 작업을 수행하는 스레드를 시뮬레이트한 다음 세마포어를 해제하여 다른 스레드가 계속 진행하도록 신호를 보내는 것입니다. 아래의 그림에서 확인할 수 있듯이 thread_2, thread_3, thread_4, thread_5 모두 동일한 구성으로 이루어져 있습니다. (단, thread_1과 다른 점은 thread_1은 100ms 소유되도록 했지만, 나머지는 10ms으로 설정했습니다.)
(상단의 thread_1 설명과 동일로 인한 생략)
............
(상단의 thread_1 설명과 동일로 인한 생략)
............
(상단의 thread_1 설명과 동일로 인한 생략)
............
(상단의 thread_1 설명과 동일로 인한 생략)
............
다음은 main() 부분입니다.
주요 기능은 CreateSemaphore 기능을 사용하여 세마포 sem1, sem2, sem3 및 sem4를 초기화하는 것으로 시작합니다.
CreateSemaphore 함수는 새 세마포 개체를 만들고 핸들을 반환합니다. 이 함수는 다음 네 가지 인수를 사용합니다.
★ 첫번째 인수 : 하위 프로세스가 세마포 개체를 상속할 수 있는지 여부를 결정하는 보안 특성 구조에 대한 포인터입니다. → 이 인수가 NULL이면 핸들을 상속할 수 없습니다. ★ 두번째 인수 : 세마포 개체의 초기 개수입니다. 이 경우 초기 카운트는 0으로 설정됩니다. ★ 세번째 인수 : 세마포 개체의 최대 개수입니다. 이 경우 최대 개수는 16개로 설정됩니다. ★ 네번째 인수 : 세마포 개체의 이름을 지정하는 null로 끝나는 문자열에 대한 포인터입니다. → 이 인수가 NULL이면 이름 없이 세마포어 객체가 생성됩니다. |
세마포어가 초기화된 후 main 함수는 MAIN_FORMAT 유형의 main_data structure를 선언하고 memset()를 사용하여 해당 memory를 0으로 설정합니다.
그런 다음 main 함수는 스레드 생성자를 사용하여 5개의 스레드를 만듭니다. 생성자의 첫 번째 인수는 스레드가 실행할 함수입니다. 두 번째 인수는 main_data 구조에 대한 포인터입니다. 스레드는 thread_1, thread_2, thread_3, thread_4 및 thread_5 함수를 사용하여 생성됩니다.
t1.join(), t2.join(), t3.join(), t4.join() 및 t5.join() 함수는 기본 함수를 계속하기 전에 스레드가 실행을 완료할 때까지 기다립니다.
마지막으로 main 함수는 CloseHandle 함수를 사용하여 세마포어 핸들을 닫고 0을 반환합니다.
[결과]
각 thread가 병렬로 처리되는 모습을 볼 수 있습니다.
각 thread가 병렬로 처리되는 모습을 볼 수 있습니다.
각 thread가 병렬로 처리되는 모습을 볼 수 있습니다. 그러다가 CNT가 15일 때(→ 15 thread_5 처리가 끝나면) 다시 0으로 초기화 되면서 0 thread_1 부터 thread가 처리되는 것을 볼 수 있습니다.
예시 코드
[Basic Thread Full Code - BasicTrhead_v1.cpp]
/* 2023.03.14
This is a program that creates five threads and implements synchronization between them using semaphores.
The program starts by initializing five semaphores with the CreateSemaphore function.
The first semaphore sem1 is initialized with an initial count of 0 and a maximum count of 16 (it means 16 frames).
The same is done for the other semaphores sem2, sem3, sem4, and sem5.
Each of the five threads performs a specific task, which is currently indicated by the comment 역할 (Role in English).
The tasks are performed in the order of the threads, with each thread waiting for the previous thread to release a semaphore before starting its own task.
Each thread also measures and outputs the time it took to perform its task.
The time is measured using the clock() function from the ctime library.
After creating the threads, the main function waits for each of them to finish with the join() function.
Finally, the program closes the handles to the semaphores with the CloseHandle function.
*/
#include <iostream>
#include <thread>
#include <Windows.h>
#include <stdlib.h>
#include <stdint.h>
#include <ctime>
#include <stdio.h>
// generated Header
#include "header_v1.h"
// Define
#define CNT 15
// namespace
using namespace std;
HANDLE sem1, sem2, sem3, sem4, sem5;
void thread_1(MAIN_FORMAT* ts) {
// local variable
int thread_1_count = 0;
int runningtime = 0;
clock_t start_time;
clock_t end_time;
while (true) {
start_time = clock();
this_thread::sleep_for(chrono::milliseconds(100)); // Sleep for 100 milliseconds to simulate work
/*
Add function performed by the thread
*/
end_time = clock();
runningtime = end_time - start_time;
printf("%d thread_1 is worked..... Running Time : %d..... \n", thread_1_count, runningtime);
thread_1_count += 1;
if (thread_1_count > CNT)
{
thread_1_count = 0;
}
else {}
ReleaseSemaphore(sem1, 1, NULL);
}
}
void thread_2(MAIN_FORMAT* ts) {
int thread_2_count = 0;
int runningtime = 0;
clock_t start_time;
clock_t end_time;
while (TRUE) {
WaitForSingleObject(sem1, INFINITE);
start_time = clock();
this_thread::sleep_for(chrono::milliseconds(10)); // Sleep for 10 milliseconds to simulate work
/*
Add function performed by the thread
*/
end_time = clock();
runningtime = end_time - start_time;
printf("%d thread_2 is worked..... Running Time : %d..... \n", thread_2_count, runningtime);
thread_2_count += 1;
if (thread_2_count > CNT)
{
thread_2_count = 0;
}
else {}
ReleaseSemaphore(sem2, 1, NULL);
}
}
void thread_3(MAIN_FORMAT* ts) {
int thread_3_count = 0;
int runningtime = 0;
clock_t start_time;
clock_t end_time;
while (TRUE) {
WaitForSingleObject(sem2, INFINITE);
start_time = clock();
this_thread::sleep_for(chrono::milliseconds(10)); // Sleep for 10 milliseconds to simulate work
/*
Add function performed by the thread
*/
end_time = clock();
runningtime = end_time - start_time;
printf("%d thread_3 is worked..... Runnig Time : %d..... \n", thread_3_count, runningtime);
thread_3_count += 1;
if (thread_3_count > CNT)
{
thread_3_count = 0;
}
else {}
ReleaseSemaphore(sem3, 1, NULL);
}
}
void thread_4(MAIN_FORMAT* ts) {
int thread_4_count = 0;
int runningtime = 0;
clock_t start_time = 0;
clock_t end_time = 0;
while (TRUE) {
WaitForSingleObject(sem3, INFINITE);
start_time = clock();
this_thread::sleep_for(chrono::milliseconds(10)); // Sleep for 10 milliseconds to simulate work
/*
Add function performed by the thread
*/
end_time = clock();
runningtime = end_time - start_time;
printf("%d thread_4 is worked..... Running Time : %d..... \n", thread_4_count, runningtime);
thread_4_count += 1;
if (thread_4_count > CNT)
{
thread_4_count = 0;
}
else {}
ReleaseSemaphore(sem4, 1, NULL);
}
}
void thread_5(MAIN_FORMAT* ts) {
int thread_5_count = 0;
int runningtime = 0;
clock_t start_time;
clock_t end_time;
while (TRUE) {
WaitForSingleObject(sem4, INFINITE);
start_time = clock();
this_thread::sleep_for(chrono::milliseconds(10)); // Sleep for 10 milliseconds to simulate work
/*
Add function performed by the thread
*/
end_time = clock();
runningtime = end_time - start_time;
printf("%d thread_5 is worked..... Running Time : %d..... \n", thread_5_count, runningtime);
thread_5_count += 1;
if (thread_5_count > CNT)
{
thread_5_count = 0;
}
else {}
}
}
int main() {
// Initialize the semaphores
sem1 = CreateSemaphore(NULL, 0, 16, NULL); //(기본보안 속성, 세마포어 초기카운터, 최대 카운터, 세마포터의 이름 지정하는 문자열 포인터)
sem2 = CreateSemaphore(NULL, 0, 16, NULL);
sem3 = CreateSemaphore(NULL, 0, 16, NULL);
sem4 = CreateSemaphore(NULL, 0, 16, NULL);
// declare structure
MAIN_FORMAT main_data;
memset(&main_data, 0, sizeof(main_data));
// Create the threads
thread t1(thread_1, &main_data);
thread t2(thread_2, &main_data);
thread t3(thread_3, &main_data);
thread t4(thread_4, &main_data);
thread t5(thread_5, &main_data);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
CloseHandle(sem1);
CloseHandle(sem2);
CloseHandle(sem3);
CloseHandle(sem4);
CloseHandle(sem5);
return 0;
}
[Header Full Code - header_v1.h]
/*
This code defines 5 different structures, each of which represent a different thread.
Each structure contains an array of int32_t values with a length of OUT.
The MAIN_FORMAT structure contains an instance of each of these 5 structures.
*/
#pragma once
#include <iostream>
// Define
#define OUT 16
typedef struct _THREAD1_FORMAT {
int32_t g_thrad1_Ispare01[OUT];
}THREAD1_FORMAT;
typedef struct _THREAD2_FORMAT {
int32_t g_thrad2_Ispare01[OUT];
}THREAD2_FORMAT;
typedef struct _THREAD3_FORMAT {
int32_t g_thrad3_Ispare01[OUT];
}THREAD3_FORMAT;
typedef struct _THREAD4_FORMAT {
int32_t g_thrad4_Ispare01[OUT];
}THREAD4_FORMAT;
typedef struct _THREAD5_FORMAT {
int32_t g_thrad5_Ispare01[OUT];
}THREAD5_FORMAT;
typedef struct _MAIN_FORMAT {
THREAD1_FORMAT thread1_format;
THREAD2_FORMAT thread2_format;
THREAD3_FORMAT thread3_format;
THREAD4_FORMAT thread4_format;
THREAD5_FORMAT thread5_format;
}MAIN_FORMAT;
#pragma once
댓글