[C++11] std::thread

기존의 C++에서의 Thread는 운영체제에 종속되는 API로 제공되거나 비표준이 아닌 라이브러리를 통해 제공되다가 C++11에서 완전한 표준으로써 자리잡아 단일 API로 동일한 코드로 개발이 가능하게 되었습니다. 아래의 코드는 C++11에서 제공하는 std::thread를 사용한 간단한 예입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

void thread_function(int tid) {
    cout << "abcdefghijklmnopqrstuvwxyz" << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    const int cntThreads = 100000;
    thread t[cntThreads];

    for(int i = 0; i < cntThreads; ++i) {
        t[i] = thread(thread_function, i);
    }

    for(int i = 0; i < cntThreads; ++i) {
        t[i].join();
    }

    return 0;
}

위의 코드는 10만개의 스레드를 생성하고 있고 각 스레드는 7번 코드에서 정의한 thread_function 함수를 실행합니다. 17번 코드가 스레드를 생성하고 실행하는 코드로 첫번째 인자는 실행할 함수이고 두번째는 스레드의 id입니다. 그리고 21번 코드는 스레드가 완전히 종료될때까지 주 스레드를 대기시키는 join 함수입니다. 위의 코드를 실행해 보면 10만개의 스레드가 abc..... 문자열이 서로 뒤섞여 표시되는 것을 볼 수 있는데, 이를 위해 동기화를 해줄 필요가 있습니다. 다음 코드는 이러한 동기화를 위해 C++11에서 제공되는 std::mutex를 사용한 예제입니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

mutex m;

void thread_function(int tid) {
    m.lock();

    cout << "abcdefghijklmnopqrstuvwxyz" << endl;

    m.unlock();
}

int _tmain(int argc, _TCHAR* argv[])
{
    const int cntThreads = 100000;
    thread t[cntThreads];

    for(int i = 0; i < cntThreads; ++i) {
        t[i] = thread(thread_function, i);
    }

    for(int i = 0; i < cntThreads; ++i) {
        t[i].join();
    }

    return 0;
}

변경된 부분은 8번 코드에서 동기화를 위한 mutex 객체를 하나를 정의했고 스레드 함수 내에서 abc.... 문자열을 표시하는 부분을 동기화 하기 위해 이 코드의 앞뒤에 mutex의 lock과 unlock 함수를 호출하고 있습니다. 이렇게 하면 이들 코드 사이에 있는 코드가 동기화가 되어 문자열이 뒤섞이지 않습니다.

[C++11] 정밀한 시간 측정을 위한 chrono

하드웨어의 발전으로 보다 정밀한 시간을 측정할 수 있게 되었고, 이를 위해 C++11에서는 chrono를 제공합니다. chrono를 이용하여 측정한 시간값의 정밀도는 나노(nano, 0.000000001)초라고 합니다. 아래는 코드는 어떠한 연산에 대한 소요 시간을 측정하는 경우로써 chrono를 사용한 코드입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;
using namespace chrono;

int _tmain(int argc, _TCHAR* argv[])
{
    system_clock::time_point tp1 = system_clock::now();
	
    double v = 0;
    for(long i = 0; i < 100000000; i++) {
        v += 0.1;
    }

    system_clock::time_point tp2 = system_clock::now();

    nanoseconds t = tp2 - tp1;
    cout << "nanoseconds: " << t.count() << endl;
    cout << "seconds: " << t.count() / 1000000000.0 << endl;

    return 0;
}

실행 시간을 측정한 연산에 대한 코드는 12번 ~ 15번입니다. 이 코드 부분의 앞뒤로 chrono를 이용하여 시간을 측정하고 있습니다. 측정된 시간의 차이(17번 코드)를 통해 연산에 소요되는 시간을 알 수 있습니다. 시간 차이에 대한 단위는 nano이며 이를 초(second)로 변환하기 위해 21번 코드처럼 1000000000값으로 나누어 출력하고 있습니다.

[C++11] std::tuple

tuple은 여러개를 하나의 것으로 묶은 것을 의미합니다. tuple이 C++11에 새롭게 도입되어 사용되는 가장 일반적인 형태는 함수의 반환값으로 사용입니다. 함수는 하나의 값 또는 객체만을 반환하는데 tuple을 사용하면 다수의 값 또는 객체들을 반환시킬 수 있습니다. 이에 대한 예제 코드는 아래와 같습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

tuple getSomething(double a, double b)
{
    return make_tuple(a+b, a/b, "Hello Tuple");
}

int _tmain(int argc, _TCHAR* argv[])
{
    auto r = getSomething(10.0, 20.0);

    cout << "Sum: " << get<0>(r) << endl;
    cout << "Div: " << get<1>(r) << endl;
    cout << "Msg: " << get<2>(r) << endl;

    return 0;
}

위의 코드 중 8번에 getSomething이라는 함수가 있습니다. 이 함수의 반환형이 tuple이고 double, double, string 템플릿 인자를 갖습니다. 즉, 이 tuple은 실수값 2개와 문자열값 1개로 구성되어 있다는 것이고 getSomething의 반환값은 실수값 2개와 문자열값 1개를 반환한다는 의미로 해석할 수 있습니다. 이 함수의 실제 호출은 15번 줄에서 이루어지는데요. 이 함수의 반환값인 tuple의 각 구성 요소에 접근하기 위해 std::get이라는 함수를 사용하며 이 std::get 함수의 사용 예가 바로 17번 ~ 19번 코드입니다.

[C++11] 스마트포인터(Smart Pointer)

C++11에서 새롭게 도입된 스마트 포인터는 unique_ptr, shared_ptr, weak_ptr입니다. unique_ptr은 실제 데이터에 대한 소유권을 오직 하나만 가지도록 하는 것이고 shared_ptr은 실제 데이터에 대한 소유권을 여러 개의 스마트포인터가 공유하면서 참조 카운트 방식으로 관리하여 참조 카운트가 0일때 실제 데이터를 해제하는 것입니다. weak_ptr은 share_ptr처럼 실제 데이터를 공유하지만 참조 카운트의 값을 증가하거나 감소시키지 않는다는 특징이 있습니다. 이 세개의 스마트포인터에 대해 하나씩 살펴 보도록 하겠습니다. 먼저 unique_ptr입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    return 0;
}

위의 코드중 9번은 정수값 10을 관리하는 unique_ptr로써 10번 코드에서처럼 스마트포인트가 관리하는 실제 데이터 값을 화면에 표시하고 있습니다. 중요한 것은 unique_ptr은 실제 데이터에 대한 소유를 오직 하나의 스마트포인트만이 가진다는 것입니다. 이에 대해 확인할 수 있도록 다음 코드를 살펴보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    unique_ptr p2 = p1;

    return 0;
}

새롭게 추가된 코드는 오직 12번으로써 기존의 unique_ptr에 대한 스마트 포인터를 또 다른 unique_ptr에 대입함으로써 하나의 실제 데이터를 2개의 unique_ptr이 소유하려고 하는 것입니다. 이에 대해 컴파일 타임에서 12번 코드는 오류가 납니다. 그러나 경우에 따라서는 실제 데이터에 소유를 다른 unique_ptr로 이동시켜야 할 때가 있습니다. 이에 대해 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    unique_ptr p3 = move(p1);
    //cout << *p1 << endl;

    cout << p1.get() << endl;
    cout << p3.get() << endl;

    return 0;
}

새로운 코드는 12번입니다. 표준 함수인 move를 이용해 unique_ptr이 가지고 있는 소유권을 다른 unique_ptr이 갖도록 합니다. 이렇게 되면 p1이 관리하는 실제 데이터의 주소는 null이 되어 소유권이 없다는 의미가 되고 원래 p1이 관리하는 실제 데이터의 주소값을 p3가 갖게 됩니다. 결국 주석 처리된 13번 코드가 실행된다면 오류가 발생할 것입니다. 15번과 16번 코드에서 보이는 unique_ptr의 get 함수는 스마트 포인트가 관리하는 실제 데이터의 주소값을 얻는 함수이므로 15번 코드는 null에 해당하는 0을, 16번 코드는 실제 데이터의 주소값을 표시하게 됩니다.

다음은 shared_ptr에 대해 정리해 보겠습니다. shared_ptr은 unique_ptr과 다르게 관리하는 실제 데이터의 소유를 여러개의 shared_ptr이 공유한다는 것입니다. 이렇게 공유를 하다가 실제 데이터에 대해 소유하는 shared_ptr 객체가 존재하지 않을 경우, 즉 참조 개수(Reference Count)가 0개일 경우 실제 데이터를 메모리에서 해제하게 됩니다. 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr p1(new int(10));
    shared_ptr p2 = p1;

    cout << *p1 << endl;
    cout << *p2 << endl;

    cout << p2.use_count() << endl;
    p1.reset();
    cout << p2.use_count() << endl;
    p2.reset();
    cout << p2.use_count() << endl;

    return 0;
}

위의 코드에서 9번 코드에서 생성한 shared_ptr 객체 p1을 같은 타입인 p2에 대대입(assign)함으로써 하나의 실제 데이터에 대해 2개의 shared_ptr 객체가 소유하고 있습니다. 12번과 13번 코드를 통해 이 2개의 shared_ptr 객체가 관리하는 동일한 실제 데이터의 값을 화면에 표시할 것이고요. shared_ptr 객체가 소유하는 실제 데이터에 대한 참조 개수는 use_count 함수를 통해 얻을 수 있고 15번, 17번, 19번 코드를 통해 확인하고 있습니다. 15번 코드는 참조 개수로 2를 출력할 것입니다. shared_ptr의 reset 함수를 통해 실제 데이터에 대한 소유를 더 이상 하지 않겠다고 명시적으로 지정되었으므로 17번 코드는 이제 1을 출력할 것입니다. 그리고 18번 코드에서 다시 한번 reset 함수를 호출했으므로 19번 코드에서 참조 개수로써 이제 0을 표시할 것입니다. 사실 실제 업무단에서는 reset 함수를 직접 해줄 필요가 없습니다. 객체의 scope를 벗어나면 자동으로 이루어지기 때문입니다.

이제 마지막으로 weak_ptr에 대해 정리해 보겠습니다. weak_ptr은 shared_ptr과 참조 카운트를 이용해 실제 데이터에 대한 소유를 함께 한다는 점은 동일하지만 참조 카운트를 증가시키지도 감소시키지도 않는다는 것입니다. 다음 코드를 살펴 보게습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr sp1(new int(10));
    weak_ptr wp1 = sp1;

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    return 0;
}

shared_ptr인 sp1을 weak_ptr인 wp1에 할당하고 있고 sp1과 wp1에 대한 참조 카운트를 표시하는 코드입니다. weak_ptr은 참조 카운트의 개수를 증가시키지 않는다고 했으므로 1과 1을 표시할 것입니다. weak_ptr은 lock이라는 함수를 제공하는데 이 함수는 shared_ptr 객체를 생성해 반환해 줍니다. 즉, weak_ptr의 lock 함수가 실행도면 shared_ptr 객체가 생성되므로 자동으로 참조 카운트는 1이 증가될 것입니다. 이에 대한 내용을 아래의 코드를 통해 살펴볼 수 있습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr sp1(new int(10));
    weak_ptr wp1 = sp1;

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    {
        shared_ptr sp2 = wp1.lock();

        cout << sp1.use_count() << endl;
        cout << wp1.use_count() << endl;
    }

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    return 0;
}

15번 ~ 20번 코드를 살펴보면 새로운 블럭 범위 안에서 weak_ptr 객체인 wp1의 lock 함수를 통해 shared_ptr 객체를 생성하여 sp2에 할당하고 있습니다. 18번과 19번 코드에서 참조 카운트를 출력해 보면 각각 2의 값이 표시됩니다. 즉, 실제 데이터에 대해 3개의 스마트 포인터가 소유하고 있는데 말입니다. 3개가 소유함에도 참조 카운트가 2인 이유는 2개는 shared_ptr이고 1개는 weak_ptr이기 때문입니다. 20번 코드까지 실행되면 자동으로 sp2 객체가 소멸되면서 실제 데이터에 대한 소유도 해제됩니다. 즉 참조 카운트가 1개 감소합니다. 감소했다는 것은 22번과 23번 코드에서 각각 모두 1이 표시되는 것을 통해 확인할 수 있습니다.