[서버-2] C++에서 스레드와 뮤텍스 실습
#서버 프로그래밍소수 구하기 C++코드에서 스레드와 뮤텍스
싱글스레드 소수 구하기 코드
아래와 같이 소수를 구하는 C++ 코드가 있다.
#include <vector>
#include <iostream>
#include <chrono>
using namespace std;
const int MaxCount = 300000;
bool IsPrimeNumber(int number)
{
if (number == 1)
return false;
if (number == 2 || number == 3)
return true;
for (int i = 2; i < number - 1; i++)
{
if ((number % i) == 0)
return false;
}
return true;
}
void PrintNumbers(const vector<int>& primes)
{
for (int v : primes)
{
cout << v << endl;
}
}
int main()
{
vector<int> primes;
auto t0 = chrono::system_clock::now();
for (int i = 1; i <= MaxCount; i++)
{
if (IsPrimeNumber(i))
{
primes.push_back(i);
}
}
auto t1 = chrono::system_clock::now();
auto duration = chrono::duration_cast<chrono::milliseconds>(t1 - t0).count();
PrintNumbers(primes);
cout << "Took " << duration << " milliseconds." << endl;
return 0;
}
실행 결과는 아래처럼 소수와 소수를 구하는 시간이 출력이 된다.
···
299941
299951
299969
299977
299983
299993
Took 4603 milliseconds.
위 코드는 30만 이하의 소수를 구하는데(출력은 시간에 포함x) 약 4.6초가 걸렸다. 숫자가 커지면 시간이 더욱 오래 걸릴 것이다. 속도 향상을 위해서 스레드를 이용할 수 있다.
멀티스레드 소수 구하기 코드
···
int main() {
// 각 스레드는 여기서 값을 꺼내 온다.
int num = 1;
recursive_mutex num_mutex;
vector<int> primes;
recursive_mutex primes_mutex;
auto t0 = chrono::system_clock::now();
// 작동할 워커 스레드
vector<shared_ptr<thread>> threads;
for (int i = 0; i < ThreadCount; i++) {
shared_ptr<thread> thread1(new thread([&]() {
// 각 스레드의 메인 함수
// 값을 가져올 수 있으면 루프를 돈다.
while (true) {
int n;
{
lock_guard<recursive_mutex> num_lock(num_mutex);
n = num;
num++;
}
if (n >= MaxCount) break;
if (IsPrimeNumber(n)) {
lock_guard<recursive_mutex> primes_lock(primes_mutex);
primes.push_back(n);
}
}
}));
// 스레드 객체를 일단 갖고 있는다.
threads.push_back(thread1);
}
// 모든 스레드가 일을 마칠 때까지 기다린다.
for (auto thread : threads) {
thread->join();
}
// 끝
auto t1 = chrono::system_clock::now();
auto duration = chrono::duration_cast<chrono::milliseconds>(t1 - t0).count();
PrintNumbers(primes);
cout << "Took " << duration << " milliseconds." << endl;
return 0;
}
···
299941
299951
299969
299977
299983
299993
Took 1154 milliseconds.
4개의 스레드를 이용하여 대략 속도가 4배 빨라졌다.
num
과 primes
변수 두 개는 여러 스레드에서 접근할 수 있으므로 동시성 제어
가 필요하다. 그래서 두 변수에 대한 뮤텍스용 객체 mutex
를 아래와 같이 선언해준다.
int num = 1;
recursive_mutex num_mutex;
vector<int> primes;
recursive_mutex primes_mutex;
C++에서 뮤텍스 사용 방식은 아래와 같다.
std::recursive_mutex
나std::mutex
를 생성한다.- 보호가 필요한 변수 사용 전에
lock()
함수로 잠근다. - 다 쓰고 나면
unlock()
함수로 잠금을 푼다.
lock_guard
를 사용하면 unlock
을 자동으로 처리해준다. unlock을 하는 시점은 lock_guard 변수가 소멸되는 시점이다.
그래서 아래와 같이 중괄호({})
로 보호가 필요한 영역을 감싸준다. 중괄호가 없다면 recursive_mutex는 메인 함수가 끝나는 시점에 소멸되므로 싱글스레드와 다를 것이 없다 (정확히는 싱글스레드보다 못하다.).
int n;
{
lock_guard<recursive_mutex> num_lock(num_mutex);
n = num;
num++;
}
여러 스레드가 동시에 num
을 읽고 num을 증가시킨다. num을 증가시키는 연산은 기계어로 아래와 같이 나온다.
Register1 = num
Register1 = Register1 + 1
num = Register1
num을 보호해야되는 이유는 쉽게 납득이 가능한데 왜 primes 변수도 보호가 필요할까
num이라는 변수를 관리하는데 필요한 것은 정수형 4바이트 데이터 공간 하나뿐이다. 하지만 vector<int> primes
정보를 관리하는 데 필요한 데이터는 하나 이상이다.
우선 vector<int>
를 가르키는 포인터 변수
와 배열의 크기를 멤버 변수로 가질 것이다.
각 스레드는 소수를 찾으면 push_back
함수를 통해서 vector 뒤에 새로운 소수를 붙인다. 그러나 더 이상 공간이 없으면, 메모리를 재할당한다.
메모리를 재할당한 후 메모리를 가르키는 주소가 달라질 수 있다. 이런 경우 스레드 함수에서 이미 반환이 끝난 메모리 주소에 접근할 수 있다.
primes 변수를 보호 안 해주면 위와 같은 이유로 컴파일 조차 안되고 충돌이 날 것이다.
따라서 primes 변수도 뮤텍스로 보호가 필요하다.