멀티스레드 프로그래밍이란?
멀티스레드 프로그래밍은 하나의 프로세스 내에서
여러 개의 스레드를 사용하여 동시에 작업을 수행하는 프로그래밍 방식이다.
이를 통해 프로그램의 성능을 향상시키고 병렬 처리를 가능하게 하며,
여러 작업을 동시에 처리할 수 있도록 도와준다.
스레드는 경량 프로세스로, 하나의 프로세스 내에서 동작하는 여러 실행 흐름이다.
프로세스는 운영체제로부터 독립된 메모리 영역을 할당받아 실행되는 프로그램의 단위이며,
각 프로세스는 최소한 하나의 메인 스레드를 가지고 있다.
이 메인 스레드 외에도 프로세스 내에서 여러 개의 보조 스레드(멀티스레드)를 생성하여
병렬로 작업을 수행할 수 있다.
멀티스레드 프로그래밍의 이점
1) 성능 향상
여러 스레드가 동시에 작업을 처리하므로 병렬 처리로 인한 성능 향상이 가능하다.
특히, 다중 코어 프로세서에서 멀티스레드 프로그램은
여러 코어를 활용하여 작업을 분산시키며 성능을 높일 수 있다.
2) 반응성 향상
멀티스레드 프로그래밍을 통해 작업을 나누어 처리하므로,
사용자 인터페이스와 같은 반응성이 중요한 부분에서 더 빠른 응답을 제공할 수 있다.
3) 작업 분리
여러 스레드를 사용하여 여러 작업을 동시에 수행할 수 있으므로,
복잡한 작업을 더 작은 단위로 분리하고 관리하기 쉽다.
4) 리소스 공유
멀티스레드 프로그래밍은 스레드 간 데이터 및 자원을 공유할 수 있으며,
이를 통해 정보를 더 효율적으로 전달하고 활용할 수 있다.
5) 비용 감소
프로세스 간 통신보다 스레드 간 통신이 더 쉽고 비용이 적게 들어가므로,
프로세스 간 통신보다 경제적인 솔루션을 제공할 수 있다.
멀티스레드 프로그래밍 사용시 주의점
그러나 멀티스레드 프로그래밍은 동시에 여러 스레드가 실행되기 때문에 주의가 필요한 부분도 있다.
1) 동기화
여러 스레드가 공유 자원에 동시에 접근할 때 동기화 문제가 발생할 수 있다.
이를 위해 적절한 동기화 메커니즘을 사용하여 스레드 간의 순서와 일관성을 유지해야 한다.
2) 경쟁상태
여러 스레드가 동일한 자원에 동시에 접근할 때 발생하는 예측할 수 없는 결과를 경쟁 상태라고 한다.
이를 방지하기 위해 뮤텍스, 세마포어 등의 동기화 기법을 사용한다.
3) 데드락 (Dead Lock)
두 개 이상의 스레드가 서로가 가지고 있는 자원을 기다리며 무한정 대기하는 상황을 데드락이라고 한다.
이를 예방하기 위해 정확한 동기화와 교착 상태를 피하는 설계가 필요하다.
결국 멀티스레드 프로그래밍은 성능 향상과 응답성 향상을 가져오지만,
위험 요소도 존재하므로 신중한 설계와 구현이 필요하다.
멀티스레드 프로그래밍 예제
특정 프로그램을 실행하였을때,
멀티스레드 프로그래밍을 적용하면 어느정도의 성능향상이 이루어지는지
직접 예제를 통해서 확인해보자.
약 100,000개의 더미데이터를 Mysql 인스턴스 특정 테이블에 넣어주는 예제를
싱글스레드로 구현한 소스코드는 아래와 같다.
#include <iostream>
#include <mysqlx/xdevapi.h>
#include <thread>
#include <vector>
using namespace std;
using namespace std::chrono;
/**
* @brief
* MySqlSession Object
*/
class MySqlSession {
private:
mysqlx::Session session;
mysqlx::Schema db;
mysqlx::Table table;
public:
MySqlSession(const string &ipAddr, const int &port, const string &userName, const string &userPw, const string &schema, const string &tableName)
: session(ipAddr, port, userName, userPw), db(session.getSchema(schema)), table(db.getTable(tableName))
{
}
void insertDataIntger(const string &colName, const int &value)
{
this->table.insert(colName).values(value).execute();
}
~MySqlSession()
{
session.close();
}
};
/**
* @brief
* Method to insert dummy data
* @param ms
* @param colName
* @param cnt
*/
void insertBulkData(MySqlSession &ms, const string &colName, const int cnt)
{
for (int i = 0; i < cnt; i++)
{
ms.insertDataIntger(colName, i+1);
}
}
/**
* @brief
* Main function
* @return int
*/
int main() {
try {
auto start = high_resolution_clock::now();
MySqlSession mysqlConn("localhost", 33060, "root", "123", "admin", "C_TEST");
insertBulkData(ref(mysqlConn), "rand_key", 100000);
auto stop = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(stop - start);
cout << "Program execution time: " << duration.count() << " ms" << endl;
} catch (const std::exception &e) {
cerr << "Error: " << e.what() << endl;
}
return 0;
}
해당 프로그램을 컴파일 한뒤 실행시키면,
소요시간을 알 수 있다.
소요시간은 아래와 같이 33147ms 즉, 33.147 초 정도 소요되는 것을 볼 수 있다.
그럼 위의 로직을 멀티스레드를 통해서 처리하면, 어느정도의 시간개선이 있을까?
아래의 소스코드를 살펴보자.
#include <iostream>
#include <mysqlx/xdevapi.h>
#include <thread>
#include <vector>
using namespace std;
using namespace std::chrono;
class MySqlSession {
private:
mysqlx::Session session;
mysqlx::Schema db;
mysqlx::Table table;
public:
MySqlSession(const string &ipAddr, const int &port, const string &userName, const string &userPw, const string &schema, const string &tableName)
: session(ipAddr, port, userName, userPw), db(session.getSchema(schema)), table(db.getTable(tableName))
{
}
void insertDataIntger(const string &colName, const int &value)
{
this->table.insert(colName).values(value).execute();
}
~MySqlSession()
{
session.close();
}
};
void insertBulkData(const string &colName, const int start, const int end)
{
MySqlSession ms("localhost", 33060, "root", "123", "admin", "C_TEST");
for (int i = start; i < end; i++)
{
ms.insertDataIntger(colName, i+1);
}
}
int main() {
try {
auto start = high_resolution_clock::now();
const int totalData = 100000;
const int numOfThreads = 2;
const int chunkSize = totalData / numOfThreads;
vector<thread> threads;
for (int i = 0; i < numOfThreads; i++) {
int startIdx = i * chunkSize;
int endIdx = (i == numOfThreads - 1) ? totalData : startIdx + chunkSize;
threads.push_back(thread(insertBulkData, "rand_key", startIdx, endIdx));
}
for (thread &th : threads) {
if (th.joinable()) th.join();
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(stop - start);
cout << "Program execution time: " << duration.count() << " ms" << endl;
} catch (const std::exception &e) {
cerr << "Error: " << e.what() << endl;
}
return 0;
}
1) MySqlSession 클래스
데이터베이스 세션, 스키마, 테이블을 관리하는 클래스이다.
생성자에서는 MySQL 서버와의 세션을 초기화하고 특정 스키마와 테이블에 접근한다.
insertDataIntger 메서드를 통해 주어진 컬럼에 정수 값을 추가한다.
소멸자에서는 데이터베이스 세션을 종료합니다.
2) insertBulkData 함수
주어진 시작과 끝 사이의 값들을 데이터베이스에 삽입한다.
각 스레드에서 호출되며 동시에 다수의 레코드를 삽입할 수 있다.
3) main 함수
프로그램의 실행 시간을 측정하기 위한 타임 스탬프를 찍는다.
numOfThreads 변수를 사용하여 생성할 스레드의 수를 정의한다.
chunkSize는 각 스레드가 처리할 레코드의 수를 정의한다.
스레드들을 생성하여 insertBulkData 함수를 병렬로 실행한다.
각 스레드는 데이터베이스에 데이터를 추가한다.
모든 스레드가 작업을 완료할 때까지 기다린 후 (join) 실행 시간을 출력한다.
위의 소스코드에서 눈여겨 볼점은
기존에 스레드 1개를 사용하는 코드를 numOfThread 값을 2로 설정하여
스레드를 2개로 지정했다는 것이다.
해당 코드의 소요시간은 12046ms 즉, 12.046 초가 걸렸음을 알 수 있다.
기존 싱글 스레드는 33.147 초의 시간이 걸린것에 비해 약 두배정도의 성능향상을 보이고 있다.
그럼, 여기서 의문이 들 수 있다.
만약 스레드의 수를 더 늘린다면, 작업을 더 빨리 끝낼 수 있지 않을까?
아래와 같이 스레드의 수를 10개로 늘려보자.
#include <iostream>
#include <mysqlx/xdevapi.h>
#include <thread>
#include <vector>
using namespace std;
using namespace std::chrono;
class MySqlSession {
private:
mysqlx::Session session;
mysqlx::Schema db;
mysqlx::Table table;
public:
MySqlSession(const string &ipAddr, const int &port, const string &userName, const string &userPw, const string &schema, const string &tableName)
: session(ipAddr, port, userName, userPw), db(session.getSchema(schema)), table(db.getTable(tableName))
{
}
void insertDataIntger(const string &colName, const int &value)
{
this->table.insert(colName).values(value).execute();
}
~MySqlSession()
{
session.close();
}
};
void insertBulkData(const string &colName, const int start, const int end)
{
MySqlSession ms("localhost", 33060, "root", "123", "admin", "C_TEST");
for (int i = start; i < end; i++)
{
ms.insertDataIntger(colName, i+1);
}
}
int main() {
try {
auto start = high_resolution_clock::now();
const int totalData = 100000;
const int numOfThreads = 10;
const int chunkSize = totalData / numOfThreads;
vector<thread> threads;
for (int i = 0; i < numOfThreads; i++) {
int startIdx = i * chunkSize;
int endIdx = (i == numOfThreads - 1) ? totalData : startIdx + chunkSize;
threads.push_back(thread(insertBulkData, "rand_key", startIdx, endIdx));
}
for (thread &th : threads) {
if (th.joinable()) th.join();
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(stop - start);
cout << "Program execution time: " << duration.count() << " ms" << endl;
} catch (const std::exception &e) {
cerr << "Error: " << e.what() << endl;
}
return 0;
}
그럼 아래와 같이 4776ms 즉 약 4.7 초가 걸린것을 볼 수 있다.
위의 결과를 보고 멀티스레드 프로그래밍을 통한 성능 향상을 기대하기 쉽지만,
그것이 항상 효과적이라고 단정할 수는 없다.
제시된 소스코드는 데이터의 순서나 정합성에 크게 민감하지 않아
동시성 문제나 기타 멀티스레딩 고려 사항을 크게 고려하지 않아도 되는 상황이다.
실무에서는 멀티스레드 프로그래밍이 강력한 도구로 작용할 수 있지만,
철저한 설계와 명확한 논리 없이 적용될 경우 예상치 못한 복잡성과 문제를 야기할 수 있다.
잘못 구현된 멀티스레드 애플리케이션은 싱글스레드 애플리케이션보다 성능이 떨어지는 경우도 있다.
따라서 멀티스레딩을 도입할 때는 신중한 접근이 필요하다.
'C,C++' 카테고리의 다른 글
[C++] Mysql select, insert, update, delete (0) | 2023.08.16 |
---|---|
[C++] Mac os 에서 Mysql 연결하기 (0) | 2023.08.14 |
[C,C++] 포인터 배열, 배열 포인터 (2) | 2023.02.05 |
[C/C++] 포인터 (0) | 2022.06.02 |