C++11 <random>
기존의 난수 생성
#include "pch.h"
#include <time.h>
int main()
{
::setlocale(LC_ALL, "kor");
time_t t = ::time(NULL);
::srand(t);
::wcout << L"난수 : ";
for (int i = 0; i < 10; ++i) {
::cout << ::rand() % 100 << " ";
}
::cout << ::endl << "--------------------------------------" << ::endl;
::srand(t);
::wcout << L"난수 : ";
for (int i = 0; i < 10; ++i)
{
::cout << ::rand() % 100 << " ";
}
return 0;
}
기존 C언어에서 0부터 99 사이의 난수를 구하는 방법이다. srand()로 시드값을 설정해서 시드값을 기반으로 난수를 생성한다. 위 실행 결과를 보면 알다시피 같은 시드값으로 구한 난수는 모두 동일하다. 즉, 진짜 난수가 아니고 난수처럼 보이는 것이다. MSDN에서도 rand()는 의사 난수를 생성하며 <random> 사용을 권장하고 있다.
또한 rand 함수의 결괏값을 나머지 연산을 통해 난수를 구하기 때문에 확률은 달라지게 된다. MSDN에 따르면 rand 함수의 범위는 0~32767이다. 여기서 23이 나오기 위한 경우의 수는 23, 123, 223, ... , 32723이다. 그러나 70이 나오기 위한 경우의 수는 70, 170, 270, ... , 32670로 하나가 적다. 따라서 %100을 이용한 연산에서는 70부터는 다른 수보다 확률이 낮다는 뜻이다.
rand 함수는 선형 합동 생성기(Linear congruential generator, LCG)라는 의사 난수 생성기(PRNG) 알고리즘을 사용하고 있다. 쉽게 말하면 단순하게 특정한 수에 나누기 더하기 곱하기 등을 이용하여 결괏값을 도출하는 것이다. MSDN에서 말하는 암호화적으로 안전하지 못하다는 뜻이 이 말이다.
선형 합동 생성기 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 선형 합동 생성기(Linear congruential generator, LCG)는 널리 알려진 유사난수 생성기이다. 선형 합동 생성기는 다음 재귀 관계로 정의된 순열 X i {\displaystyle X_{i}} 을 반
ko.wikipedia.org
실제 작동
실제로 rand 함수를 호출을 하면 참 단순하게 작동을 한다. MSDN에서 srand 함수는 현재 스레드에 의사 난수 정수 생성을 위한 시작점을 생성한다고 나와 있다. 해당 시드값은 스레드 별로 존재하는 ptd구조체에 저장이 된다. (_beginthreadex 시, 스레드 별로 C런타임 라이브러리가 초기화되며 ptd 구조체가 할당되고, CRL함수에 필요한 리소스를 관리한다.) rand 함수 호출 시 해당 시드값을 가져와서 214013(0x343FD)을 곱하고, 2531011(0x269EC3) 만큼 더하고, 2바이트(0x10)만큼 right shift 연산을 해주고, 0111'1111'1111'1111(0x7 FFF)와 and 연산을 하여 도출한 의사 난수를 반환한다.
C++11 <random>
#include <pch.h>
#include <random>
int main()
{
::setlocale(LC_ALL, "kor");
// random_device 생성 (시드값)
::random_device rd;
// mt19937_64 (난수 생성 엔진) 생성 및 초기화
::mt19937_64 gen(rd());
// 0 ~ 9 균등한 정수 분포 생성
::uniform_int_distribution<int> dis(0, 99);
::wcout << L"난수 : ";
for (int i = 0; i < 10; ++i)
{
::cout << dis(gen) << " ";
}
}
컴퓨터는 모든 것을 계산으로 움직이게 된다. 따라서 컴퓨터에 있어서 랜덤이라는 것은 존재하지 않는다. (C의 rand 함수도 의사 난수를 계산하여 도출하였다) 다만 한 가지 컴퓨터가 예상하지 못하는 값이 있다. 바로 내부 디바이스 간의 상호작용 중에 일어나는 무작위 값들이다. OS는 이러한 값들을 난수로 제공한다.
다만 random_device 통한 진짜 난수를 얻기는 매우 느리게 동작한다. 따라서 이 진짜 난수를 시드값으로 난수 생성 알고리즘을 사용하곤 한다. 대부분의 애플리케이션에서 사용되는 빠른 메르센 트위스터 알고리즘이 그중 하나이다. ::mt19937은 random 라이브러리에서 제공하는 해당 알고리즘으로 실행되는 난수 생성 엔진이다. (::mt19937_64는 64bit 버전이다)
마지막으로 분포를 정의해주어야 한다. 예제에서는 시작범위와 끝범위가 포함되는 출력 범위를 가진 균등 분포를 정의해서 사용하였다. (다른 분포도 존재하는데 자세한 것은 MSDN 참고)
DummyClients의 랜덤 행동 주기
int main()
{
/* 더미클라이언트 생성 */
.
.
/* Recv 스레드 생성 */
.
.
/* 난수 생성 세팅 */
::random_device rd;
::mt19937_64 gen(rd());
::uniform_int_distribution<int32> dist(0, 9);
/* 루프 세팅 */
::chrono::time_point<::chrono::high_resolution_clock> start;
::chrono::time_point<::chrono::high_resolution_clock> end;
while (true)
{
start = ::chrono::high_resolution_clock::now();
/** 행동 확률 (1s)
* isLogin !_isLogin
* Login : 0 | 0.3
* Logout : 0.1 | 0
* Chat : 0.4 | 0
* NONE : 0.5 | 0.7
*/
for (DummyClient* client : vDummyClients)
{
int32 randNum = dist(gen);
if (client->_isLogin)
{
switch (randNum)
{
case 0:
CRASH_IF(!client->Logout());
break;
case 1:
case 2:
case 3:
case 4:
client->Chat();
break;
default:
break;
}
}
else
{
switch (randNum)
{
case 0:
case 1:
case 2:
CRASH_IF(!client->Login());
break;
default:
break;
}
}
}
end = ::chrono::high_resolution_clock::now();
auto diff = end - start;
int64 diffTime = ::chrono::duration_cast<::chrono::milliseconds>(diff).count();
int64 sleepTime = 100 - diffTime < 0 ? 0 : 1000 - diffTime;
::Sleep(sleepTime);
}
return 0;
}
마지막으로 채팅서버의 스트레스 테스트를 위해 만들 수 있는 간단한 더미클라이언트 예제를 간단하게 끄적여봤다. 조만간 이를 접목시켜 테스트한 결과를 DevLog에 기록할 예정이다. 추가로 chrono 라이브러리 또한 다룰 예정이다.