Tuesday, July 7, 2015

kor,c++> multi thread programming (windows)

다음은 멀티스레드로 두 함수의 무한루프를 동시에 실행하는 예제 코드이다.

/*
multi thread example
*/
#include "stdafx.h"
#include <vector>
#include <windows.h>
#include <process .h> // _beginthread, _endthread
#include <conio .h> // _kbihit
class C {
public:
 void update(){
  v_.push_back(0);
  printf("v.size() = %d\n", v_.size());
 }
private:
 std::vector<int> v_;
};

unsigned int __stdcall t1(void*arg){
 C* c = (C*)arg;
 while(true){
  c->update();
 }
 return 0;
}
unsigned int __stdcall t2(void*arg){
 C* c = (C*)arg;
 while(true){
  c->update();
 }
 return 0;
}

int main() {
 C c;
 HANDLE h_t1 = (HANDLE)_beginthreadex(0, 0, &t1, &c, 0, 0);
 HANDLE h_t2 = (HANDLE)_beginthreadex(0, 0, &t2, &c, 0, 0);
 while(true){
  if(_kbhit())
   exit(0);
 }
 return 0;
}
예제 1-1

동시에 실행되는 함수 t1, t2에서 같은 인스턴스 c의 멤버 v_에 접근을 하는데도 고의로 스레드 보호를 하지 않았다. 그 결과 아래와 같이 에러가 발생한다. 


여러 개의 스레드가 공유 데이터를 사용할 때를 가르켜 race condition, 경쟁 상태, 경쟁 조건이라고 부른다. #
이와같은 런타임에러는 불규칙하게 발생하기 때문에 발견하기도 힘들고, 충돌 원인을 찾기도 힘들다. 그러므로 미리 잘 설계하는게 중요하다.
스레드 보호를 위해서 아래와 같이 mutex 변수를 선언하고 동기화를 수행하였다.

#include "stdafx.h"
#include <vector>
#include <windows.h>
#include <process.h> // _beginthread, _endthread
#include <conio.h> // _kbihit

class C {
public:
 C() : mutex_(CreateMutex(0, 0, 0)){
 }

~C(){
 CloseHandle(mutex_);
}

void update(){
 lock();
 v_.push_back(0);
 int d = v_.size();
 unlock();
 printf("v.size() = %d\n", d);
}

private:
 void lock(){
  WaitForSingleObject(mutex_, INFINITE);
 }

void unlock(){
  ReleaseMutex(mutex_);
 }
 HANDLE mutex_;
 std::vector<int> v_;
};

unsigned int __stdcall t1(void*arg){
 C* c = (C*)arg;
 while(true){
  c->update();
 }
 return 0;
}

unsigned int __stdcall t2(void*arg){
 C* c = (C*)arg;
 while(true){
  c->update();
 }
 return 0;
}

int main() {
 C c;
 HANDLE h_t1 = (HANDLE)_beginthreadex(0, 0, &t1, &c, 0, 0);
 HANDLE h_t2 = (HANDLE)_beginthreadex(0, 0, &t2, &c, 0, 0);
 while(true){
  if(_kbhit())
   exit(0);
 }
 return 0;
}
예제 1-2

WaitForSingleObject(mutex_, INFINITE) 함수가 호출되면, ReleaseMutex(mutex_)가 호출되기 전까지 다른 쓰레드에서는 WaitForSingleObject(mutex_, INFINITE) 함수가 완료되지 않는다. 만약 ReleaseMutex(mutex_)가 적절히 호출되지 못하는 코드를 구현하면, 시스템은 교착상태에 빠질수 있으므로 주의해야한다.

다른 예제로 넘어가, 접근자를 구현해보자.

#include "stdafx.h"
#include <vector>
#include <windows.h>
#include <process.h> // _beginthread, _endthread
#include <conio.h> // _kbihit

class C {
public:
 C() : v_(0), mutex_(CreateMutex(0, 0, 0)){

 }
 ~C(){
  CloseHandle(mutex_);
 }
 void set(int v){
  v_ = v;
 }
 int get()const{
  return v_;
 }
private:
 void lock(){
  WaitForSingleObject(mutex_, INFINITE);
 }
 void unlock(){
  ReleaseMutex(mutex_);
 }
 HANDLE mutex_;
 int v_;
};

unsigned int __stdcall t1(void*arg){
 C* c = (C*)arg;
 int t = 0;
 while(true){
  c->set(t++);
 }
 return 0;
}
unsigned int __stdcall t2(void*arg){
 C* c = (C*)arg;
 while(true){
  printf("v = %d\n", c->get());
 }
 return 0;
}
int main() {
 C c;
 HANDLE h_t1 = (HANDLE)_beginthreadex(0, 0, &t1, &c, 0, 0);
 HANDLE h_t2 = (HANDLE)_beginthreadex(0, 0, &t2, &c, 0, 0);
 while(true){
  if(_kbhit())
   exit(0);
 }
 return 0;
}
예제 2-1

예제 2-1은 get,set 접근자에 lock, unlock을 적용하기 전이다. 무서운점은 분명 충돌이 발생할 코드임에도, 3,40분 실행으로는 충돌이 발생하지 않았다는거다. 예제1-1에 비해 연산시간이 짧은 관계로 확률이 낮기 때문이다. 이런종류의 런타임 버그가 프로그램 전체의 신뢰성을 좀먹는 가장 무서운 버그다. ("내가 할땐 잘됬어! 다른데 니가 건드린게 문제있는거 아니야?")

본론으로 돌아와, get, set함수에 lock unlock을 선언해보자. 아래 주석과 같은 에러가 발생할 것이다.

#include "stdafx.h"
#include <vector>
#include <windows.h>
#include <process.h> // _beginthread, _endthread
#include <conio.h> // _kbihit

class C {
public:
        C() : v_(0), mutex_(CreateMutex(0, 0, 0)){

        }
        ~C(){
                CloseHandle(mutex_);
        }
        void set(int v){
                lock();
                v_ = v;
                unlock();
        }
        int get()const{
                // error C2662: 'void C::lock(void)' : cannot convert 'this' pointer from 'const C' to 'C &'
                lock();
                int v = v_;
                // error C2662: 'void C::unlock(void)' : cannot convert 'this' pointer from 'const C' to 'C &'
                unlock();
                return v;
        }
private:
        void lock(){
                WaitForSingleObject(mutex_, INFINITE);
        }
        void unlock(){
                ReleaseMutex(mutex_);
        }
        HANDLE mutex_;
        int v_;
};

unsigned int __stdcall t1(void*arg){
        C* c = (C*)arg;
        int t = 0;
        while(true){
                c->set(t++);
        }
        return 0;
}
unsigned int __stdcall t2(void*arg){
        C* c = (C*)arg;
        while(true){
                printf("v = %d\n", c->get());
        }
        return 0;
}
int main() {
        C c;
        HANDLE h_t1 = (HANDLE)_beginthreadex(0, 0, &t1, &c, 0, 0);
        HANDLE h_t2 = (HANDLE)_beginthreadex(0, 0, &t2, &c, 0, 0);
        while(true){
                if(_kbhit())
                        exit(0);
        }
        return 0;
}

일반적으로 접근자 get 함수는 객체의 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 선언하는게 맞다.# 그럼에도 스레드 보호를 위한 함수 lock, unlock은 상수형 함수가 아니므로 에러가 발생하는것이다.
lock, unlock을 상수 멤버 함수로 선언해도 같은 에러가 lock, unlock의 선언에서 발생한다. WaitForSingleObject, ReleaseMutex 함수가 멤버변수 mutex_를 변경하는 함수이기 때문에.

이러한 문제를 우회하는 방법은 두가지 있다.

1. get함수를 일반 멤버함수로 바꾸는 방법 - 가능하면 추천지 않는다.
2. mutex를 mutable 멤버변수로 변경하는 방법.

Q : Should mutexes be mutable? If I want to keep the public method interface as const as possible, but make the object thread safe, should I use mutable mutexes? In general, is this good style, or should a non-const method interface be preferred? Please justify your view.
A : Basically using const methods with mutable mutexes is a good idea (don't return references by the way, make sure to return by value), at least to indicate they do not modify the object. Mutexes should not be const, it would be a shameless lie to define lock/unlock methods as const... Actually this (and memoization) are the only fair uses I see of the mutable keyword.

 그렇다. mutable 문법을 쓰는게 정당해보이는 보기드문, 유일한 경우가 mutex 정도이니 변형없는 상수형 함수라 거짓말치는 mutable을 문법을 쓴다해서 부끄러워할 필요는 없다.

2번째 방법으로 문제를 해결하면 다음과 같다.
class C {
public:
 C() : v_(0), mutex_(CreateMutex(0, 0, 0)){

 }
 ~C(){
  CloseHandle(mutex_);
 }
 void set(int v){
  lock();
  v_ = v;
  unlock();
 }
 int get()const{
  lock();
  int v = v_;
  unlock();
  return v;
 }
private:
 void lock()const {
  // 상수형 멤버함수로 선언하는데, 더이상 mutex_가 문제되지 않는다.
  WaitForSingleObject(mutex_, INFINITE);
 }
 void unlock()const{
  ReleaseMutex(mutex_);
 }
 mutable HANDLE mutex_;
 int v_;
};

No comments:

Post a Comment