'Thread'에 해당되는 글 6건

  1. 2007.12.05 volatile - Multithreaded Programmer's Best Friend (6)
  2. 2007.04.19 Threads Scheduling (1)
  3. 2007.04.01 Leader/follower pattern
  4. 2007.03.25 Condition Variables (1)
  5. 2007.03.18 Read/Write lock (8)
  6. 2007.03.06 libevent and multithread (6)

/**
  얼마전 Head first Design pattern의 싱글톤 패턴에서 volatail을 사용하
  는 것을 보고, 보다 정확한 용도에 대해 알기 위해 인터넷을 떠돌던중
  Dr.Dobb's Portal에서 volatil에 관련된 흥미로운 기사를 읽고 여기에 옮
  겨 봅니다.

  포스트를 보시다 보면 어떻게 이렇게 긴 영어가 저렇게 짧은 한글이 될
  수 있지...라고 의문을 가지 실 수 있으나..

  역자의 영어 실력이 상당히 달리고, 후천적 부지럼 결핍증으로 인해 내
  용이 상당히 축약 된 것이니 그 점 유의하셔서 영어 실력이 되시는 분들
  은 원문에 보다 충실함이 더 좋을 듯 합니다.

  간단하게 내용을 요약하자면 volatile이라는 키워드의 특성을 이용하여
  쓰레드간에 공유 되는 변수에 강제적으로 lock을 잡고 사용하도록 하는
  방법을 제시하고 있습니다. lock을 잡지 않고 사용하고자 한다면 컴파
  일 타임에 에러를 발생 시켜 프로그램이 동작하다 코어 덤프를 남기며
  죽는 불상사를 막아 주도록 합니다.

  오타나 잘 못 해석된 내용에 대해서는 댓글을 달아 주시면 즉시 수정 하
  도록 하겠습니다.
*/


Andrei Alexandrescu

The volatile keyword was devised to prevent compiler optimizations that might render code incorrect in the presence of certain asynchronous events. For example, if you declare a primitive variable as volatile, the compiler is not permitted to cache it in a register -- a common optimization that would be disastrous if that variable were shared among multiple threads. So the general rule is, if you have variables of primitive type that must be shared among multiple threads, declare those variables volatile. But you can actually do a lot more with this keyword: you can use it to catch code that is not thread safe, and you can do so at compile time. This article shows how it is done; the solution involves a simple smart pointer that also makes it easy to serialize critical sections of code.

volatile 키워드는 비동기 이벤트 환경에서 컴파일러 최적화를 통해 코드가 꼬이는 것을 방지 하기 위해 만들어 졌습니다. 어떻게 해서 코드가 꼬여 버리는지에 알고 싶으시다면 Debug lapvolatile에 관련된 페이지를 참고 할 것을 추천 드립니다. 암튼 결론은 일반적으로 여러 쓰레드 간에 공유되는 primitive의 변수를 사용한다면 그 변수를 volatile 키워드로 하세요. 그리고 volatile 키워드로는 앞에서 말한 것 뿐만 아니라, 다른 용도로도 더 풍요롭게 사용될 수 있습니다. 여러분은 thread safe하지 않은 코드를 찾아 내기 위해 사용 할 수도 있습니다. 그리고 그걸 컴파일 타임에 이루어 낼 수 있지요. (놀랍지 않습니까? 프로그램을 실행 시켜서 터지는지 않터지는지 확인하기 위해 기다리지 않아도 컴파일 타임에 thread safe 여부를 검증 할 수 있습니다!!) 이 문서는 크리티컬 섹션 안에서 보다 쉽게 동기화를 이루어 낼 수 있는 간단한 스마트 포인터를 구현 하므로써 어떻게 그것을 이루어 가는지 보여 드릴겁니다.


I don't want to spoil your mood, but this column addresses the dreaded topic of multithreaded programming. If — as the previous installment of Generic<Programming> says — exception-safe programming is hard, it's child's play compared to multithreaded programming.

이 컬럼에서는 멀티 쓰레드 프로그래밍의 까칠함에 대해서 다루 도록 하겠습니다.

Programs using multiple threads are notoriously hard to write, prove correct, debug, maintain, and tame in general. Incorrect multithreaded programs might run for years without a glitch, only to unexpectedly run amok because some critical timing condition has been met.

멀티 쓰레드 환경에서의 프로그래밍은 어렵기로(?) 유명합니다. 제대로 만들어지지 않은 멀티 쓰레드 프로그램이 몇 년동안 아무런 말성 없이 돌아가다, 결정적인 순간에 미쳐 날뛴다는 것은 자명한 사실 입니다.

Needless to say, a programmer writing multithreaded code needs all the help she can get. This column focuses on race conditions — a common source of trouble in multithreaded programs — and provides you with insights and tools on how to avoid them and, amazingly enough, have the compiler work hard at helping you with that.

이 컬럼은 주로 '레이스 컨디션'에 관련하여 포커스를 맞추었습니다.

Just a Little Keyword

Although both C and C++ Standards are conspicuously silent when it comes to threads, they do make a little concession to multithreading, in the form of the volatile keyword.

Just like its better-known counterpart const, volatile is a type modifier. It's intended to be used in conjunction with variables that are accessed and modified in different threads. Basically, without volatile, either writing multithreaded programs becomes impossible, or the compiler wastes vast optimization opportunities. An explanation is in order.

const, volatile은 타입 변환자로써 잘 알려져 있습니다. 목적은 다른 여러 쓰레드에서 접근되고 수정될 수 있는 변수 들에게 사용 되는 것입니다. 기본적으로 volatile 없이 멀티 쓰레드 프로그래밍을 하는 것은 불가능하거나 컴파일러는 대략난감할 정도의 최적화 기회를 낭비하게 됩니다.

Consider the following code:

class Gadget { public: void Wait() { while (!flag_) { Sleep(1000); // sleeps for 1000 milliseconds } } void Wakeup() { flag_ = true; } ... private: bool flag_; };


The purpose of Gadget::Wait above is to check the flag_ member variable every second and return when that variable has been set to true by another thread. At least that's what its programmer intended, but, alas, Wait is incorrect. Suppose the compiler figures out that Sleep(1000) is a call into an external library that cannot possibly modify the member variable flag_. Then the compiler concludes that it can cache flag_ in a register and use that register instead of accessing the slower on-board memory. This is an excellent optimization for single-threaded code, but in this case, it harms correctness: after you call Wait for some Gadget object, although another thread calls Wakeup, Wait will loop forever. This is because the change of flag_ will not be reflected in the register that caches flag_. The optimization is too ... optimistic. Caching variables in registers is a very valuable optimization that applies most of the time, so it would be a pity to waste it. C and C++ give you the chance to explicitly disable such caching. If you use the volatile modifier on a variable, the compiler won't cache that variable in registers — each access will hit the actual memory location of that variable. So all you have to do to make Gadget's Wait/Wakeup combo work is to qualify flag_ appropriately:

Gadget::Wait 함수의 목적은 flag_ 멤버 변수를 매초마다 체크하여 변수가 다른 쓰레드에 의해 true로 셋팅되면 리턴 하는 것입니다. 뭐..최소한 그게 프로그래머의 의도였겠지요. 하지만 슬프게도 Wait 함수는 정상적으로 동작하지 않습니다(뭐..디버그 모드에서는 최적화를 하지 않기에 정상적으로 동작 할 수 있습니다만...디버그 모드로 제품을 출시하는 것은 아니잖아요?).

한 가지 상황을 가정해 봅시다. 컴파일는 Sleep(1000)가 외부에서 호출되며 이 함수가 절대 flag_ 멤버 변수를 바꿀 가능성이 없다는 사실을 알아 냅니다(싱글 쓰레드의 관점에서 보자면 함수가 하나 실행 되고 있을 때 다른 함수가 실행 되는 경우는 없지요?). 그럼 컴파일러는 이 flag_라는 변수가 레지스터 메모리에 캐싱되어 매번 느려터진 메인 보드의 메모리를 참조하는 것 보다 레지스터를 참조 하는 것이 더 좋다고 판단하게 됩니다. 싱글 쓰레드의 관점에서 본다면 최고의 최적화 일겁니다. 하지만 이 경우에는 Wait함수를 어떤 Gadget 객체에서 호출하고, 그 객체의 Wakeup 함수를 다른 쓰레드가 호출 한다고 하더라도, 그 객체는 계속 Wait의 루프에서 머물러 있을 겁니다. flag_ 멤버 변수의 변경 사항이 레지스터에 캐싱되어 있는 flag_에 반영 되지 않으니 당연한 말이겠지요.

C와 C++에서는 명시적으로 이런 캐싱 기능을 사용하지 못하게 할 수 있는 방법을 제공 합니다. 변수 앞에 volatile이라는 키워드 하나만 더 붙여 주면 됩니다. 그럼 컴파일러는 절대 그 변수를 레지스터 않에 두지 않을 것이고, 모든 참조는 직접 메모리를 참조 할 것이며, 그렇게 되면 Gadget의 Wait/Wakeup 함수는 정상적으로 동작하게 되겠지요.

class Gadget { public: ... as above ... private: volatile bool flag_; };

Most explanations of the rationale and usage of volatile stop here and advise you to volatile-qualify the primitive types that you use in multiple threads. However, there is much more you can do with volatile, because it is part of C++'s wonderful type system.

대부분의 설명이 여기서 끝나지요. 하지만 volatile 키워드로 할 수 있는 재밌는 일들이 더 남았습니다(후훗~).

Using volatile with User-Defined Types

You can volatile-qualify not only primitive types, but also user-defined types. In that case, volatile modifies the type in a way similar to const. (You can also apply const and volatile to the same type simultaneously.) Unlike const, volatile discriminates between primitive types and user-defined types. Namely, unlike classes, primitive types still support all of their operations (addition, multiplication, assignment, etc.) when volatile-qualified. For example, you can assign a non-volatile int to a volatile int, but you cannot assign a non-volatile object to a volatile object. Let's illustrate how volatile works on user-defined types on an example.

사용자 정의 타입(class, struct..etc)에도 volatile을 사용 할 수 있습니다. 예를 들면, volatile은 const와도 비슷하게 사용 됩니다(더욱 놀라운 것은 같은 타입데 const와 volatile을 동시에 지정 할 수 있습니다). 하지만 const와는 다르게 volatile은 primitive 타입과 user-defined 타입을 차별 합니다. 다시 말하자면, 클래스와 다르게 primitive 타입은 여전히 모든 오퍼레이션(더하기, 빼기, 나누기, 할당 등..)을 지원합니다. 예를 들어 volatile이 아닌 int를 volatile인 int에 할당 할 수 있습니다. 하지만 volatile이 아닌 user-defined 객체를 volatile객체에 집에 넣을 수는 없습니다. Volatile이 어떻게 user-defined 타입에 대해 동작하는 지를 살펴 보도록 합시다.

class Gadget { public: void Foo() volatile; void Bar(); ... private: String name_; int state_; }; ... Gadget regularGadget; volatile Gadget volatileGadget;

If you think volatile is not that useful with objects, prepare for some surprise.

만일 volatile이 객체에는 별 유용하지 않다라고 느끼시는 분들은..놀랄 준비 하세요.

volatileGadget.Foo(); // ok, volatile fun called for // volatile object regularGadget.Foo(); // ok, volatile fun called for // non-volatile object volatileGadget.Bar(); // error! Non-volatile function called for // volatile object!

The conversion from a non-qualified type to its volatile counterpart is trivial. However, just as with const, you cannot make the trip back from volatile to non-qualified. You must use a cast:

volatile이 적용되지 않은 객체가 volatile 멤버 함수를 호출하는 것은 괜찮습니다. 하지만 const에서와는 다르게 volatile 적용된 객체가 volatile로 꾸며지지 않은 함수를 호출 하는 것은 안됩니다. 이것은 cast를 통해서만 이루어 질 수 있습니다.

Gadget& ref = const_cast<Gadget&>(volatileGadget); ref.Bar(); // ok

























A volatile-qualified class gives access only to a subset of its interface, a subset that is under the control of the class implementer. Users can gain full access to that type's interface only by using a const_cast. In addition, just like constness, volatileness propagates from the class to its members (for example, volatileGadget.name_ and volatileGadget.state_ are volatile variables).

Volatile이 달린 클래스는 오직 volatile이 적용된 인터페이스에만 억세스 할 수 있습니다. 사용자는 객체에 const_cast를 이용해야만 모든 인터페이스에 억세스 가능하지요. 추가적으로 const와 같이 volatile도 클래스에서 멤버 변수로 전파됩니다. volatileGadget.name_과 volatileGadget.state_도 volatile 변수지요.

volatile, Critical Sections, and Race Conditions

The simplest and the most often-used synchronization device in multithreaded programs is the mutex. A mutex exposes the Acquire and Release primitives. Once you call Acquire in some thread, any other thread calling Acquire will block. Later, when that thread calls Release, precisely one thread blocked in an Acquire call will be released. In other words, for a given mutex, only one thread can get processor time in between a call to Acquire and a call to Release. The executing code between a call to Acquire and a call to Release is called a critical section. (Windows terminology is a bit confusing because it calls the mutex itself a critical section, while "mutex" is actually an inter-process mutex. It would have been nice if they were called thread mutex and process mutex.) Mutexes are used to protect data against race conditions. By definition, a race condition occurs when the effect of more threads on data depends on how threads are scheduled. Race conditions appear when two or more threads compete for using the same data. Because threads can interrupt each other at arbitrary moments in time, data can be corrupted or misinterpreted. Consequently, changes and sometimes accesses to data must be carefully protected with critical sections. In object-oriented programming, this usually means that you store a mutex in a class as a member variable and use it whenever you access that class' state. Experienced multithreaded programmers might have yawned reading the two paragraphs above, but their purpose is to provide an intellectual workout, because now we will link with the volatile connection. We do this by drawing a parallel between the C++ types' world and the threading semantics world.

멀티 쓰레드 프로그래밍에서 동기화를 위해 가장 많이 쓰이는 것이 바로 mutex 일겁니다. mutex는 Acquire와 Release 이렇게 두가지 인터페이스를 제공하고 있지요. 만일 어떤 쓰레드(T1)에서 Acquire를 호출하고, 다른 쓰레드(T2)에서 다시 동일한 mutex에 대해 Acquire를 호출 한다면 두 번째 Acquire를 호출한 쓰레드(T2)는 블록킹 상태에 놓이게 될 겁니다. 그리고 처음에 Acquire를 호출했던 쓰레드(T1)가 Release를 호출 하고 난 후에야 그 쓰레드(T2)는 Acquire 다음으로 넘어 갈 수 있게 되지요. 다시 말해서, mutex의 Acquire와 Release의 사이에서는 딱 하나의 쓰레드만이 프로세서 타임을 얻을 수 있고, 이것을 '크리티컬 섹션(critical section)'이라고 합니다.
 

  • Outside a critical section, any thread might interrupt any other at any time; there is no control, so consequently variables accessible from multiple threads are volatile. This is in keeping with the original intent of volatile — that of preventing the compiler from unwittingly caching values used by multiple threads at once.
  • 크리티컬 섹션의 외부에서는 쓰레드 끼리 레이스 컨디션을 벌일 수 있습니다. 그래서 여러 쓰레드에서 접근 가능한 변수에는 volatile 키워드를 붙여 줍니다(컴파일러가 마음대로 레지스터에 집어 넣는 것을 방지 하도록 말이죠).
  • Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.
  • mutex로 보호되는 크리티컬 섹션 내부에서는 오직 한개의 쓰레드만이 변수에 접근 가능 합니다. 고로 크리티컬 섹션 안에서는 싱글 쓰레드 형식의 구문을 따라가면 되겠지요. 위에서 volatile로 수식 되던 변수는 더이상 volatile일 필요가 없습니다. const_cast를 이용해서 volatile을 떼어 버릴 수 있습니다.

 short, data shared between threads is conceptually volatile outside a critical section, and non-volatile inside a critical section. You enter a critical section by locking a mutex. You remove the volatile qualifier from a type by applying a const_cast. If we manage to put these two operations together, we create a connection between C++'s type system and an application's threading semantics. We can make the compiler check race conditions for us.

간단하게 요약 하자면..(사실 이 부분이 가장 중요하다고 생각 됩니다만..), 쓰레드 간에 공유되는 데이터는 크리티컬 섹션 바깥 부분에서는 volatile 변수이어야 합니다. 반면에 크리티컬 섹션 안에서는 non-volatile변수가 되어야 합니다. 꼭 이렇게 해야만 컴파일이 되는 것은 아니지만 크리티컬 섹션 외부에서는 변수에 volatile을 적용하고, 크리티컬 섹션 내부에서는 const_cast를 이용하여 volatile 속성을 제거함으로써 컴파일 타임에 '레이스 컨디션(race condition)을 체크 할 수 있습니다.

잊지마세요. 약속입니다. '크리티컬 섹션 밖에서는 volatile, 크리티컬 섹션 안에서는 const_cast '입니다.

LockingPtr

We need a tool that collects a mutex acquisition and a const_cast. Let's develop a LockingPtr class template that you initialize with a volatile object obj and a mutex mtx. During its lifetime, a LockingPtr keeps mtx acquired. Also, LockingPtr offers access to the volatile-stripped obj. The access is offered in a smart pointer fashion, through operator-> and operator*. The const_cast is performed inside LockingPtr. The cast is semantically valid because LockingPtr keeps the mutex acquired for its lifetime. First, let's define the skeleton of a class Mutex with which LockingPtr will work:

LockingPtr 템플릿 클래스를 만들어 봅시다. LockingPtr 클래스는 마치 스마트 포인터와 비슷한 구조를 가지고 있습니다. 템플릿으로 지정된 객체의 포인터를 리턴하지요. 하지만 단순히 객체의 포인터만을 리턴하지는 않습니다. LockingPtr이라는 이름 답게 lock을 걸고 난후 객체의 포인터를 리턴합니다.

구현의 첫단계로 각 시스템에 걸맞게 제작된 mutex 클래스가 필요 합니다. mutex는 Acquire와 Release를 구현 해야만 합니다(뭐 이름이야 LockingPtr에서 호출 할 이름을 정해야 하지만, 교과서적인 관점에서 mutex는 Acqurie와 Release 두 가지 오퍼레이션을 제공하지요..).그리고 LockingPtr이 감싸고 있을 객체는 volatile로 초기화 되어야 합니다. LockingPtr의 생명주기동안 mutex의 Acquire가 계속 걸려 있다고 생각하시면 됩니다. 또한 LockingPtr이 감싸고 있는 객체 obj에 접근이 필요한 경우 volatile 속성을 제거 시켜 줍니다. 위에서도 언급했다 싶이 LockingPtr은 마치 스마트 포인터의 구조와 비슷하게 작동 하지요. 그래서 ->와 * 오퍼레이터도 제공 합니다.

class Mutex { public: void Acquire(); void Release(); ... };

To use LockingPtr, you implement Mutex using your operating system's native data structures and primitive functions. LockingPtr is templated with the type of the controlled variable. For example, if you want to control a Widget, you use a LockingPtr <Widget> that you initialize with a variable of type volatile Widget. LockingPtr's definition is very simple. LockingPtr implements an unsophisticated smart pointer. It focuses solely on collecting a const_cast and a critical section.

LockingPtr은 템플릿 클래스 입니다. 예를 들어 Widget이라는 클래스가 각 쓰레드간에 공유되는 변수의 타입이라고 가정하면, LockingPtr<Widget> 이라고 쓸수 잇습니다.

template <typename T> class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); };

In spite of its simplicity, LockingPtr is a very useful aid in writing correct multithreaded code. You should define objects that are shared between threads as volatile and never use const_cast with them — always use LockingPtr automatic objects. Let's illustrate this with an example. Say you have two threads that share a vector<char> object:

이런 간단한 구조임에도, LockingPtr은 정확한 멀티 쓰레드 코드를 작성하는데 있어 상당한 도움을 줍니다. 여러분은 쓰레드 간에 공유되는 객체를 반드시 'volatile'을 이용해서 정의해야 하고, 절대 'const_cast'를 사용해서는 안됩니다. 그것은 어디까지나 LockingPtr이 자동적으로 처리해야만 하지 여러분이 임의로 건드려서는 안됩니다. 이제는 여러분이 vector<char> 타입의 객체를 공유하는 두 개의 쓰레드를 사용한다는 가정하에 예를 들어 보도록 하지요.

class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };

Inside a thread function, you simply use a LockingPtr<BufT> to get controlled access to the buffer_ member variable:

쓰레드 함수를 보면 buffer_ 멤버 변수에 접근하기 위해 LockingPtr<BufT>를 사용하는 간단한 코드를 보실 수 있습니다.

void SyncBuf::Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } }

The code is very easy to write and understand — whenever you need to use buffer_, you must create a LockingPtr<BufT> pointing to it. Once you do that, you have access to vector's entire interface. The nice part is that if you make a mistake, the compiler will point it out:

그렇게 이해하기 여려운 코드는 아니지요? 지금 부터는 buffer_ 멤버 변수에 접근 할 때는 무조건 LockingPtr<BufT>를 사용 해야만 합니다. 그렇게 해야만 vector의 인터페이스에 접근이 가능하지요. 그리고 여기서 정말 기분 좋은 것은, 만일 여러분이 실수 할 때(쓰레드 엔트리 함수에서 공유되는 변수에 접근 할 때, lock을 걸지 않고 접근 하는 경우) 컴파일러가 그 사실을 지적해 준다는 것이지요.

void SyncBuf::Thread2() { // Error! Cannot access 'begin' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access 'end' for a volatile object for (; i != lpBuf->end(); ++i) { ... use *i ... } }

You cannot access any function of buffer_ until you either apply a const_cast or use LockingPtr. The difference is that LockingPtr offers an ordered way of applying const_cast to volatile variables. LockingPtr is remarkably expressive. If you only need to call one function, you can create an unnamed temporary LockingPtr object and use it directly:

위의 코드에서 const_cast를 사용하거나 LockingPtr을 사용하기 전에는 어떠한 buffer_의 함수에도 접근이 불가능 합니다. 만일 함수 하나만 호출하고 종료하기를 원한다면 unnamed LockingPtr 객체를 생성하므로써 간단히 해결 할 수 있습니다.

unsigned int SyncBuf::Size() { return LockingPtr<BufT>(buffer_, mtx_)->size(); }

Back to Primitive Types

We saw how nicely volatile protects objects against uncontrolled access and how LockingPtr provides a simple and effective way of writing thread-safe code. Let's now return to primitive types, which are treated differently by volatile. Let's consider an example where multiple threads share a variable of type int.

지금 까지 volatile이 어떻게 통제되지 않는 멀티 쓰레드의 접근에서 객체를 보호해 줄수 있는지 보았고, 또 어떻게 LockingPtr이 간단하고 효과적으로 thread-safe 코드를 작성하는데 도움을 주는지 보앗습니다.

지금 부터는 다시 primitive 타입으로 돌아가 보도록 하겠습니다(위에서 volatile은 primitive 타입 변수와 user defined 타입 변수를 다르게 취급한다고 이미 언급한 바있습니다. 잊지 말아주세요^^).

멀티 쓰레드가 int 타입의 변수 하나를 공유한다고 가정해 보도록 하겠습니다.

class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —ctr_; } private: int ctr_; };

If Increment and Decrement are to be called from different threads, the fragment above is buggy. First, ctr_ must be volatile. Second, even a seemingly atomic operation such as ++ctr_ is actually a three-stage operation. Memory itself has no arithmetic capabilities. When incrementing a variable, the processor:

만일 Increment와 Decrement가 다를 쓰레드에서 제각각 불린다면, 위의 코드는 버그를 일으킬 것입니다. 첫째, ctr_은 volatile이 되어야 합니다. 둘째, 겉으로 보기에는 ++ctr_ 이 atomic 오퍼레이션 처럼 보이지만 사실은 세개의 오퍼레이션을 가집니다. 메모리 자체는 아무런 산술적 기능이 없기 때문에 변수를 증가 시킬 때는 아래와 같은 과정을 거칩니다.

  • Reads that variable in a register (레지스터에서 변수를 읽어 옵니다)
  • Increments the value in the register (레지스터의 변수를 하나 증가 시킵니다)
  • Writes the result back to memory (결과를 메모리에 적습니다)

This three-step operation is called RMW (Read-Modify-Write). During the Modify part of an RMW operation, most processors free the memory bus in order to give other processors access to the memory. If at that time another processor performs a RMW operation on the same variable, we have a race condition: the second write overwrites the effect of the first. To avoid that, you can rely, again, on LockingPtr:

이 세 단계의 과정을 RMW(Read-Modify-Write)라고 하지요. RMW 오퍼레이션에서 2단계, 수정을 하는 단계서 대부분의 프로세서는 다른 프로세서가 메모리에 접근 할 수 있도록 해줍니다. 만일 그 때, 다른 프로세서가 같은 변수에 RMW 연산을 해버린다면 레이스 컨디션이 발생 해 버리지요(두 번째 발생한 RMW가 첫 번째 메모리 공간을 덮어 써버리는 경우). 이런 것을 방지 하기위해 다시 한번 LockingPtr을 사용 할 수 있습니다.

class Counter { public: ... void Increment() { ++*LockingPtr<int>(ctr_, mtx_); } void Decrement() { —*LockingPtr<int>(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; };

Now the code is correct, but its quality is inferior when compared to SyncBuf's code. Why? Because with Counter, the compiler will not warn you if you mistakenly access ctr_ directly (without locking it). The compiler compiles ++ctr_ if ctr_ is volatile, although the generated code is simply incorrect. The compiler is not your ally anymore, and only your attention can help you avoid race conditions. What should you do then? Simply encapsulate the primitive data that you use in higher-level structures and use volatile with those structures. Paradoxically, it's worse to use volatile directly with built-ins, in spite of the fact that initially this was the usage intent of volatile!

이제 위의 코드는 정상적으로 작동 할 것입니다. 하지만 SyncBuf의 코드와 비교 했을 때 조잡합니다. 왜냐구요?  ctr_변수가 volatile로 선언되어 있다고 하더라고 primitive 타입은 volatile과 non-volatile에 대해 오퍼레이션에 차별을 두지 않기 때문에, 실수로 Counter 클래스에서 ctr_ 변수에 lock을 걸지 않고 접근해도 컴파일러는 아무런 경고를 해주지 않습니다.

volatile Member Functions

So far, we've had classes that aggregate volatile data members; now let's think of designing classes that in turn will be part of larger objects and shared between threads. Here is where volatile member functions can be of great help. When designing your class, you volatile-qualify only those member functions that are thread safe. You must assume that code from the outside will call the volatile functions from any code at any time. Don't forget: volatile equals free multithreaded code and no critical section; non-volatile equals single-threaded scenario or inside a critical section. For example, you define a class Widget that implements an operation in two variants — a thread-safe one and a fast, unprotected one.

지금 부터는 volatile 멤버 함수에 대해서 살펴 보도록 하겠습니다. 먼저 모든 코드에서는 volatile 함수만을 호출 한다고 가정합니다(잊지 마세요. volatile은 크리티걸 섹션 외부에서만 사용 됩니다. non-volatile은 싱글쓰레드, 즉 크리티컬 섹션 안에서만 사용됩니다.)

class Widget { public: void Operation() volatile; void Operation(); ... private: Mutex mtx_; };







위에서는 두 개의 멤버 함수를 구현 했습니다. 하나는 volatile이 적용 되어진 느리지만 thread-safe한 함수, 다른 하나는 쓰레드에서 보호 되지 못하지만 빠른 함수 입니다.

Notice the use of overloading. Now Widget's user can invoke Operation using a uniform syntax either for volatile objects and get thread safety, or for regular objects and get speed. The user must be careful about defining the shared Widget objects as volatile. When implementing a volatile member function, the first operation is usually to lock this with a LockingPtr. Then the work is done by using the non- volatile sibling:

오버로딩에 주의 해주세요. 지금 부터는 Widget의 Operation함수를 동일한 방법으로 호출이 가능합니다. 사용자는 공유되는 Widget 객체를 volatile로 선언해주는 것에 유의 해야 합니다. volatile 멤버 함수를 구현 할 때, LockingPtr을 이용해 this 포인터에 lock을 잡아 줍니다. 그리고 그 포인터를 이용하여 non-volatile Operation 멤버 함수를 호출 하는 것이지요.

void Widget::Operation() volatile { LockingPtr<Widget> lpThis(*this, mtx_); lpThis->Operation(); // invokes the non-volatile function }

Summary

When writing multithreaded programs, you can use volatile to your advantage. You must stick to the following rules:

멀티 쓰레드 프로그래밍을 할 때 volatile을 유용한 도구로써 사용 할 수 있습니다. 하지만 아래의 사항들을 꼭 지키셔야 합니다

  • Define all shared objects as volatile.
    (모든 공유 객체는 volatile을 이용해 선언 되어야 합니다.)
  • Don't use volatile directly with primitive types.
    (Primitive 타입에는 volatile을 직접적으로 사용해서는 안됩니다.)
  • When defining shared classes, use volatile member functions to express thread safety.
    (공유 되는 클래스를 선언 할 때는, thread-safe를 위해 volatile 멤버 함수를 사용합니다.)

If you do this, and if you use the simple generic component LockingPtr, you can write thread-safe code and worry much less about race conditions, because the compiler will worry for you and will diligently point out the spots where you are wrong.

이 사항들만 잘 지켜 주시고, LockingPtr 클래스를 적절히 사용하신다면 보다 thread-safe한 코드를 작성하시는데 많은 동움이 될 것입니다. 만일 여러분이 크리티컬 섹션 외부에서 공유 변수에 lock을 걸지 않고 접근하신다면(LockingPtr을 사용하지 않고 접근 하신다면) 컴파일러가 그 부분을 지적해 주니까요.

A couple of projects I've been involved with use volatile and LockingPtr to great effect. The code is clean and understandable. I recall a couple of deadlocks, but I prefer deadlocks to race conditions because they are so much easier to debug. There were virtually no problems related to race conditions. But then you never know.

Acknowledgements

Many thanks to James Kanze and Sorin Jianu who helped with insightful ideas.

Andrei Alexandrescu is a Development Manager at RealNetworks Inc. (www.realnetworks.com), based in Seattle, WA, and author of the acclaimed book Modern C++ Design. He may be contacted at www.moderncppdesign.com. Andrei is also one of the featured instructors of The C++ Seminar (www.gotw.ca/cpp_seminar).

Posted by kukuta
TAG C/C++, lock, Thread

댓글을 달아 주세요

  1. Favicon of http://blog.ggamsso.wo.tc BlogIcon 깜쏘 2007.12.03 15:35  댓글주소  수정/삭제  댓글쓰기

    오...
    지금 프로젝트가 멀티스레드 환경에서 돌아가야 한다는 조건이 붙었는데... (거기다 C++)
    선배^^ 좋은 글 보고 갑니다. (아쉽게로 서브로 돌고 있는 검색엔진[pure c]에 적용하는건 무리겠군요. ㅠ.ㅠ)

    PS::크리스마스 뭐 하실 겁니까?

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2007.12.03 22:59 신고  댓글주소  수정/삭제

      흠..어디서 긁어 온 글이 도움이 되었다니 나로써도 상당히 기쁘다만..
      솔로에게 크리스마스에 뭐 할건지 묻는 것은 예의가 아니야..-_-;;

  2. Favicon of http://pudae.tistory.com BlogIcon pudae 2007.12.03 17:02  댓글주소  수정/삭제  댓글쓰기

    volatile은 내친구..저거 어디서 본건ㄷ...
    저거 언제 니 친구되었냐?

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2007.12.03 23:00 신고  댓글주소  수정/삭제

      바보야 멀티 쓰레드 프로그래머의 친구라고 적혀 있잖아.
      감자 친구가 아니라..ㅋㅋ
      나는 멀티 쓰레드 프로그래머 아닌가봐..volatile이 친구 먹자고 안하네..ㅠㅠ

  3. 김경순 2008.07.24 11:12  댓글주소  수정/삭제  댓글쓰기

    정말 좋은 내용입니다. 유용하게 쓰도록 담아가겠습니다. 감사합니다.

/**
 이래저래 먹고 사는데 바쁘다 보니 글하나 올리기도 빡세구나. 아직도 해야 할 일이 많은데 내가 생각하는 뭔가를 정리하고 올린다면 시간이 더 많이 걸릴 것 같고, 오늘은 웹에서 떠돌아 다니는 원문을 간단하게 해석해 보는 것으로 블로깅을 마무리 해야겠다.
 
 주제는 쓰레드 스케줄링(thread scheduling)에 관련한 것으로, 내가 멀티 쓰레드를 이용해서 뭔가를 하는데 아무리해도 성능이 안나오길래 혹시 scheduling에 관련한 문제가 있지 않나 해서 조사하다 찾은 문서를 설명 할 것이다. 원론적인 스케줄링에 관련된 이야기는 아니고, 여러가지 스케줄링 기법이 있고, 내가 생성한 쓰레드가 어떤 스케줄링 알고리즘을 사용하게 설정 하느냐 하는 API 사용법 정도라고 생각하면 되겠다. 보다 자세한 쓰레드 스케줄링에 대해서 알고 싶으면 'Operating System Concepts'라는 책을 추천한다. 흔히들 공룡책이라고 하면 안다.
*/

Inheritsched Attribute 

inheritsched attribute(사전에 없는 단어지만 상속받은 스케줄 속성 이라고 생각하면 될 듯)가 어떻게 정해 질 것인지를 정한다. 정해질 것을 정한다고 하니 말이 이상하게 꼬이는 감이 있는데, 쓰레드는 기본적으로 쓰레드를 생성하는 객체의 속성을 물려 받겠끔 되어 있다. 하지만 이런 속성들을 변경하고 싶다면 기본 속성을 물려 받지 않고 따로 정의 하겠다고 표시를 해야 한다. 그것이 스케줄 속성이 어떻게 정해 질지(물려 받을지 새로 지정할지)를 정하는 것이라고 하겠다. 속성은 아래와 같이 두 가지가 정의 되어 있다.

PTHREAD_INHERIT_SCHED 새로 생성되는 쓰레드가 스케줄링 속성(정책, 파라메터 속성등)을 부모쓰레드(새로운 쓰레드를 생성하는 쓰레드)에게서 물려 받겠다는 의미. 이 플래그를 지정하면 다른 모든 쓰레드 스케줄링 속성들이 무시되고 부모의 속성만을 물려 받는다.
PTHREAD_EXPLICIT_SCHED 새로 모든 속성들을 재 지정 해주겠다는 의미.

 위에서도 이야기 했듯이 기본 값은 PTHREAD_INHERIT_SCHED다. pthread_attr_setinheritsched 함수에 의해서 셋팅이 되고, 현재 셋팅 되어져 있는 값은 pthread_attr_getinheritsched 함수를 통해 얻어 올 수 있다.
 
 새로운 속성들을 정의 해주기 위해서는 반드시 PTHREAD_EXPLICIT_SCHED로 셋팅 되어야 하고, 그렇지 않으면 모든 값들이 무시된다.

Scheduling Policy and Priority

쓰레드 라이브러리는 아래의 세 가지 스케줄링 방법을 제공하고 있다 :

SCHED_FIFO First-in first-out (FIFO) scheduling. 각 쓰레드들이 고정된 priority를 가지고 있고, 여러 쓰레드들이 같은 prioirity level을 가지고 있으면 FIFO 형태로 돌아 간다.
SCHED_RR Round-robin (RR) scheduling. 각 쓰레드들이 고정된 priority르 가지고 있고, 여러 쓰레드 들이 같은 priority level로, 고정된 time slice 내에서만 동작하면 RR형태로 돌아간다.
SCHED_OTHER 기본 AIX scheduling. 각 쓰레드들이 동적으로 (activity나 기타 요소에 따라) 변경이 가능한 priority를 할당 받고, 쓰레드의 실행 시간이 일정 time-slice 내에서 실행 되게 되는 형태. 그런데 이것은 시스템의 구현 마다 다 다를 수 있다.

기본적인 스케줄링 알고리즘은 SCHED_OTHER로 셋팅되어져 있다.
/**
  사족이지만, 별다른 특별한 이슈가 없지 않는한 기본으로 셋팅되어지는 SCHED_OTHER가 가장 성능이 좋다고 개인적으로 판단한다. 아니 개인적 뿐만 아니라, 이 문서에서도 별다른 특정한 상황이 아니면 SCHED_OTHER가 가장 좋은 성능을 보일 것이라고 이야기 한다.
*/

Priority 는 정수형태의 값으로써 1에서 127까지의 값을 가질 수 있다. 1이 가장 우선순위가 낮고, 127이 가장 우선 순위가 높다. Priority level 0은 시스템에 의해 예약 되어 있기 때문에 사용 할 수 없다.
※ AIX 커널에서는 앞에 이야기 했던 priority level의 순서가 거꾸로 라는 것에 주의 하도록 하자. ps 커맨드를 이용하면 priority를 볼 수 있다.

쓰레드 라이브러리는 sched_param structure로 priority를 조절한다. sched_param은 sys/sched.h 파일에 정의 되어 있고,, 지금은 아래와 같이 두 개의 필드를 가지고 있다 :

sched_priority priority를 지정.
sched_policy 쓰레드 라이브러리에 의해 무시되며, 사용 해서는 안된다.

앞으로 쓰레드의 다른 속성을 나타내기 위해 필드가 더 추가 될 수 있다.
/**
  나의 경우는 sys/sched.h가 아니라 bit/sched.h에 sched_param이 정의 되어 있었고, 필드도 __sched_priority 하나 뿐이 었다. 운영체제는 CentOS2.0 다.
*/

Setting the Scheduling Policy and Priority at Creation Time

 쓰레드가 생성 될 때 pthread_attr_setschedpolicy 함수를 이용해서 위에서 언급한 세 가지 스케줄링 알고리즘 중에 하나를 지정 할 수 있다. 그리고 현재 지정되어 있는 스케줄링 알고리즘 값을 얻어 오려면 pthread_attr_getschedpolicy 함수를 이용하면 된다.

 Scheduling priority는 쓰레드 생성 시점에 sched_param 속성을 통해 지정 될수 있다. pthread_attr_setschedparam 함수로 schedparam 을 셋팅하거나,  pthread_attr_getschedparam 함수로 schedparam 값을 얻어 올 수 있다.

아래 코드는 RR(round-robin) 스케줄링 알고리즘과 priority level 3을 가지고 쓰레드를 생성 시킨다 :

sched_param schedparam;
 
schedparam.sched_priority = 3;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr, SCHED_RR);
pthread_attr_setschedparam(&attr, &schedparam);
pthread_create(&thread, &attr, &start_routine, &args);
pthread_attr_destroy(&attr);

Setting the Scheduling Attributes at Execution Time

 현재 스케줄링 알고리즘과 priority는 pthread_getschedparam 함수로 알아 낼 수 있다. 역시 이 값들은  pthread_setschedparam 함수로 셋팅될 수 있다. 만일 대상 쓰레드가 현재 프로세서에 의해 처리중이라면 새로 셋팅되는 스케줄링 알고리즘과 priority는 다음 스케줄될 때(프로세서에 의해 처리 될 때) 반영 된다. 반대로 현재 쓰레드가 동작 중(running)이 아니라면, 함수 호출과 동시에 반영이 된다.

 예를 들어, 쓰레드 T는 현재 RR알고리즘으로 동작 중(running)이며, 이것을 FIOF로 변경 하고자 한다. 일단 쓰레드 T는 주어진 타임 슬라이스 동안 계속 RR로 동작 할 것이며, 주어진 타임슬라이스를 다 쓰고 나면 다시 running 상태로 가기 위해 대기 상태로 간다. 만일 쓰레드 T 보다 더 높은 우선 순위를 가진 쓰레드가 없다면 다시 running 상태로 넘어 갈것이고, 이제서야 T는 FIFO로 변경 된다. 한 가지 예를 더 들어 보도록 하자. 낮은 우선 순위를 가진 쓰레드 T가 대기 상태에 있고, 다른 쓰레드가 pthread_setschedparam 함수를 호출하여 T의 우선 순위를 높였다고 하자. 이 경우 T보다 높은 우선 순위를 가진 쓰레드가 동작 중이 아니라면 T는 바로 running 상태로 넘어가게 된다.

Note: Both subroutines use two parameters: a policy parameter and a sched_param structure. Although this structure contains a sched_policy field, programs should not use it. The subroutines use the policy parameter to pass the scheduling policy and ignore the sched_policy field.

Considerations about Scheduling Policies

 무슨 특별한 이유가 있지 않는 이상, 어플리케이션은 디폴트 스케줄링 알고리즘을 사용해야만 한다.

 RR 알고리즘은 같은 우선 순위를 가진 스케줄들이 같은 타임 슬라이스를 할 당 받는 것을 보장 한다. 단, 쓰레드의 activity와 전혀 관계 없이 시간을 할당 하기 때문에 쓸모 없는 자원의 낭비가 발생 할 수도 있다. 이런 RR 알고리즘은 센서에서 값을 읽어 오거나(센서는 보통 일정 주기로 한 번씩 메시지를 보낸다) 동작기에 뭔가를 쓸 때 사용 하면 좋다.

 FIFO는 크리티컬한 작업들을 다룰 때 사용하면 좋다. FIFO를 사용하게 되면 특별한 인터럽트가 들어오기 전까지는 작업이 완료 될때 까지 계속 진행 된다. 높은 우선 순위를 가지고 있는 FIFO 쓰레드는 다른 쓰레드들에 의해 running 상태를 안 빼앗길 확률이 높으며(non-preermptive) 시스템 전체의 성능에 영향을 끼칠 수 있다. 예를 들어 FIFO는 강도가 세고 연산시간을 오래 잡아 먹는 계산(커다란 행렬 연산 같은 것)을 하는데 사용되면 전체 성능을 다운 시길 수 있다.

Contention Scope

쓰레드 라이브러리는 아래의 두 가지 contention scopes(분쟁 범위?)를 정의 한다 :
/**
 영향을 미치는 범위가 한 프로세스 내에서만인지, 아니면 시스템 전역인지를 구분 하는 것을 contention scope라고 한다.
*/

PTHREAD_SCOPE_PROCESS Process (or local) contention scope. 프로세스 내에 있는 모든 다른 쓰레드들에 한정하여 contention scope를 설정한다.
PTHREAD_SCOPE_SYSTEM System (or global) contention scope. 시스템 전역적으로 contention scope를 설정한다.

Setting the Contention Scope

 Contention scope 는 오직 쓰레드가 생성 되는 시점에만 셋팅 될 수 있다. pthread_attr_setscope 함수와 pthread_attr_getscope 함수를 통해 관련된 값을 셋팅하거나 얻어 올 수 있다.

 Contention scope 라는 것은 오직 M:N 쓰레드 구조(커널 쓰레드와 유저 쓰레드의 관계가 다대다 라는 이야기. 자세한 사항은 'Operating System Concepts'라는 책을 추천)에서만 유효하다. single-scope(1:1) 구조(예를 들어 AIX version 4.3)에서는 PTHREAD_SCOPE_PROCESS를 셋팅하면 항상 에러를 리턴한다. 이유는 모든 쓰레드들이 system contention scope를 가지고 있기 때문이다. 아래의 TestImplementation 처럼 함으로써 지원 여부를 쉽게 알아 낼 수 있다 :

int TestImplementation()
{
        pthread_attr_t a;
        int result;
        pthread_attr_init(&a);
        switch (pthread_attr_setscope(&a, PTHREAD_SCOPE_PROCESS))
        {
                case 0:          result = LIB_MN; break;
                case ENOTSUP:    result = LIB_11; break;
                case ENOSYS:     result = NO_PRIO_OPTION; break;
                default:         result = ERROR; break;
        }
        pthread_attr_destroy(&a);
        return result;
}

Prior to AIX Version 4.3, this routine would return LIB_11.

In AIX Version 4.3, this routine returns LIB_MN.

Impacts of Contention Scope on Scheduling

 Contention scope는 쓰레드 스케줄링에 영향을 미치게 된다. 각 system contention scope는 하나의 커널 쓰레드에 묶이게 된다. 그래서 글로벌 유저 쓰레드의 스케줄링 알고리즘과 우선순위를 변경하게 되면 그 밑에 놓여 있는 커널 쓰레드의 스케줄링 알고리즘과 우선 순위를 변경 하게 되는 결과를 낳는다.

AIX에서는, 오직 root권한을 가진 커널 쓰레드만이 FIFO나 RR을 사용 할 수 있다. 아래 코드를 보도록 하자 :

	schedparam.sched_priority = 3;
	pthread_setschedparam(pthread_self(), SCHED_FIFO, schedparam);

 위의 코드를 system contention scope로 설정 되있고, 루트 권한 없이 실행 했다면 EPERM error code를 리턴 할 것이다. 하지만 process contention scope를 가지고 실행하게 된다면 에러를 리턴 하지 않는다. process contention scope에서는 쓰레드 스케줄링을 하기 위해 루트 권한이 필요하지 않다.

 로컬 유저 쓰레드(Local user thread)는 프로세스 내에서 어떠한 스케줄링 알고리즘이나 우선 순위를 설정 할 수 있다. 하지만 두 쓰레드가 같은 스케줄링 알고리즘과 우선순위를 가지고 있고, 다른 contention scope를 자기고 있다면 같은 방법으로 스케줄링 되지 않는다. Process contention scope를 가지고 있는 쓰레드는 커널 쓰레드에 의해 처리된다.

sched_yield Subroutine

 sched_yield 함수는 yield 함수와 같다. 이 함수를 호출한 쓰레드는 프로세서를 반환 하고 다른 쓰레드에게 스케줄링 될 기회를 주게 된다. 다음에 스케줄링 되는 쓰레드는 같은 프로세스 내애 있는 스레드이거나 다른 프로세스의 쓰레드일 수 있다. yield 함수는 멀티 쓰레드 프로그램에서 사용되면 안된다.
/**
 개인적으로 나는 쓰레드가 프로세서를 반납 할 때 usleep(1)을 사용해 왔는데, 이것은 낭비하지 않아도 될 1마이크로세컨드를 낭비한 것이다. 앞으로는 sched_yield()나 yield() 함수를 애용 해야 겠다.
 
 그리고 이 문서에서는 멀티쓰레드 환경에서 yield를 사용하면 안된다라고 나오지만 man page에서는 그런 것에 대한 것은 언급이 되어있지 않고 다만 '멀티 쓰레드인 프로세스에서 호출하게 되면, 호출한 쓰레드만 영향을 받는다' 라고 나와 있다. 보다 자세한 설명이 있었으면 하지만 물어 볼 메일 주소도 없고 물어 볼 사람도 없으니 그냥 내가 테스트 해보는 수 밖에..그런데 테스트도 만만치 않쿤하~아놔~
*/

The interface pthread_yield subroutine is not available in XOPEN VERSION 5.

원문 보기 : http://www.unet.univie.ac.at/aix/aixprggd/genprogc/threads_sched.htm
참고 : http://blog.naver.com/raon_pgm?Redirect=Log&logNo=140010348579

Posted by kukuta

댓글을 달아 주세요

  1. kts123 2007.04.19 10:55  댓글주소  수정/삭제  댓글쓰기

    스케쥴링 정책을 사용자가 고를 수 있다는 것인데,
    이 문서는 AIX에 구현된 내용에 관한 것이고,
    이 내용에서 언급하고 있는 스케쥴링 종류에 대한 것은 본문에서 언급한 대로 공룡책을 보면 되겠지만,
    Programming with Posix Threads
    (http://book.naver.com/bookdb/book_detail.php?bid=222506)
    에 더 정확하고 자세하게 나와 있는 듯 합니다. 그리고 특히 사용상의 주의점과 부작용에 대해 아주 자세히 설명해 주고 있으니 실제로 스케쥴링을 변경하려면 반드시 읽어 보아야 될 듯 싶네요.

    또 하나더 yield 는 C 런타임 라이브러리에 있고, 쓰레드가 아니라 프로세스를 명시적으로 run 상태에서 ready로 만드는 함수이므로 쓰레드에 최적화 되어 있지 않아서 사용하지 말라고 한 듯 합니다. yield 의 메뉴얼에 보면 쓰레드에 대해서도 동작한다고 했지만 pthread 와는 독립적으로 만들어진 것이니 pthread 에서 사용하는 thread와는 궁합이 맞지 않는 부분이 있을 것 같네요.

  얼마전에 libevent가 threadsafe 하지 않다는 글을 대충 번역 해서 올려 놓았더니, pudae가 leader/follower pattern이라는 것을 소개 해줬다. 인터넷 검색 결과 ACE framework에서 사용 되는 서버패턴의 일종이라는데 아직 구현을 해보지 않아서 성능이 얼마나 나올는지는 장담하지 못한다.

  기존의 내가 구현 하려고 했던 형식은 이벤트(accept/read ..etc)를 감시하는 thread가 하나 있고, 이벤트가 발생하며 event queue에 그 이벤트들을 집어 넣는다. 그럼 event queue 반대편의 worker thread들이 그 이벤트에 맞는 핸들링을 하는 구조 였는데 오버헤드가 많아서 실질적으로 사용하기는 다소 무리가 있는 형식 이었다.
 
  그에 반에 leader/follower pattern은 이벤트를 대기 하고 있는 leadther thread, 아무것도 하지 않고 자신의 차례가 오길 기다리는 follower thread로 구성된다. leader thread가 이벤트를 대기 하다 이벤트가 발생하면 leader thread는 processing thread가 되어 event를 처리하러 가고, 대기 하고 있던 follower thread중에 하나가 leader가 된다. 그리고 processing thread가 제 할일을 다 끝내고 나면 다시 follower thread가 되어 자신의 차례가 올 때 까지 대기하게 된다.

  지금 고민하고 있는 것은 follower thread들이 어디엔가 저장 되어 있어야 한다는 것이고, 내 생각에는 그런 자료구조로는 queue가 적당하지 않겠냐는 것이다. enqueue와 dequeue를 할 때 당연히 lock을 걸어줘야 할 것인데..그러면 이 것이 event queue를 운용할 때와 과연 얼마나 차이가 날까 하느냐 이다. 물론 event queue를 사용할 때는 하나의 이벤트가 발생할때 마다 이벤트 객체를 동적으로 생성 했기에 그에 대한 overhead가 있었을지 모르지만 그게 그렇게 크다고는 생각되지 않는다(잘 못 생각하는 것인가??).

  이런 저런 것을 비교해 놓고, 이런 방법을 사용하면 이런면에 있어서 오버헤드가 줄어들기 때문에 성능이 월등히 좋아 진다..라는 문서라도 있으면 좋으련만..결국은 처음 부터 끝까지 구현해 보고 서버들을 벤치 마킹 하는 방법 밖에는 없을 것같다.

  나중에 시간이 나면 one connection - one thread(가칭), event queue(가칭), leader/follower pattern을 서로 비교 해 보고 성능 평가 보고서를 한장 썼으면 한다.

참고 : http://deuce.doc.wustl.edu/doc/pspdfs/lf.pdf

'진리는어디에' 카테고리의 다른 글

간단한 에코 서버 제작  (2) 2007.04.06
도메인 이름을 이용해 IP 주소 얻기(gethostbyname)  (3) 2007.04.03
Leader/follower pattern  (0) 2007.04.01
Condition Variables  (1) 2007.03.25
sendto & recvfrom  (0) 2007.03.23
half-close  (0) 2007.03.20
Posted by kukuta

댓글을 달아 주세요

 Condition variables와 mutex를 같이 사용할 경우 condition variable은 임의의 상태(condition)이 발생하길 기다리는 race-free 한 방법을 제공 한다.
 
 Condition과 mutex의 사용에 대한 간략한 개념을 이야기 하자면, condition 자체는 mutex와 함께 사용되어야만 한다. mutex는 condition variable이 wait로 넘어가기 전의 초기화라던지 기타 등등에 대한 concurrency를 보장한다.

 Condition variable이 사용되기 전에 역시 다른 동기화 객체와 마찬가지로 초기화라는 것이 필요하다. C/C++에 서 Condition variable은 pthread_cond_t data type으로 표시되어 진다. 초기화 방법은 두가지가 있는데 한가지는 PTHREAD_COND_INITIALIZER (static하게 정해져 있는 변수)이고 다른 한가지는 pthread_cond_init (dynamic하게 할당하는 함수)가 있다.

 Condition variable을 해제 할때는 pthread_mutex_destroy 함수를 사용한다.

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);

int pthread_cond_destroy(pthread_cond_t *cond);

Both return: 0 if OK, error number on failure

만일 별다른 옵션을 줄 필요가 없다면 attr은 단순히 NULL로 설정 하도록 하자(대부분 NULL이면 충분하다. 그리고 Linux Thread에서는 attribute를 지원하지 않기 때문에 attr 값은 무시된다.)

 pthread_cond_wait 를 이용해 conditions이 발생(나중에 나올 pthread_cond_signal함수를 호출하는 것) 하기를 기다릴 수 있다.

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict timeout);

Both return: 0 if OK, error number on failure

 pthread_cond_wait 에게 넘겨지는 mutex는 condition variable을 보호하며 함수에 lock을 건다. 이러한 과정을 거치면 호출 thread를 waiting thread list에 대기 시키고, 자동적으로 mutex를 해제한다(이말인 즉슨 pthread_cond_wait 이후 코드들이 계속 진행 된다는 말이다). thread는 이 과정에서 가사상태(sleep)에 빠지게 되므로 CPU 점유율을 먹지 않고, 더 이상 thread를 진행 하지 않으므로 mutex가 풀린다고 해서 concurrency에 문제가 생기지는 않는다.

 pthread_cond_timedwait 는  pthread_cond_wait 와 동일 하게 동작하지만 timeout이라는 기능을 넣을 수 있다. timeout으로 넘어 가는 timespec 구조체는 아래와 같다.

    struct timespec {
            time_t tv_sec;   /* seconds */
            long   tv_nsec;  /* nanoseconds */
    };

 이 구조체를 이용함에있어서 우리는 상대적 시간이 아니라 절대적 시간을 적어 줘야 한다. 예를 들어서 우리가 3분을 기다릴 것이라고 가정 한다면, timespec 구조체에 3분이라고 표시하는 대신, 현재시간 + 3이라고 적어 줘야 한다는 것에 유의 해야 한다.

 만일 아무런 condition의 발생이 없이 timeout이 된다면, pthread_cond_timedwait 함수는 mutex를 재획득하고  ETIMEDOUT을 리턴 할 것이다.  pthread_cond_wait or pthread_cond_timedwait, 에서 정상적으로 리턴하게 된다면 다른 thread가 이미 run하고 condition을 이미 바꾸었는지에 대해서 condition을 다시 테스트해야 할 필요가 있다.

 Condition이 발생했다는 것을 알리는 것은 아래의 두 함수가 있다. pthread_cond_signal 함수는 하나의 thread를 waiting 상태에서 깨우는 것이고 pthread_cond_broadcast 는 모든 thread를 wait 상태에서 깨우는 것이다.

The POSIX specification allows for implementations of pthread_cond_signal to wake up more than one thread, to make the implementation simpler.

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

Both return: 0 if OK, error number on failure

주의 사항은 pthread_cond_signal or pthread_cond_broadcast을 호출하는 것은 condition이 변경 되고 난 뒤에 호출되어야만 한다는 것이다. 예를들어 queue에 값이 있는 것을 확인 하는 thread 같은 경우는 queue에 값을 넣고 난 뒤에 condition을 변경 해야만 한다.
Example

 Condition은 ThreadSafeQueue의 상태를 나타내는 변수가 된다. while 루프 안에서 condition의 상태를 검사 할 것이고 queue에 data를 넣게 되면 condition의 상태를 만족 시키게 된다. 여기서 signal 함수를 꼭 mutex 사이에 위치 시켜야 할 필요는 없다. pthread_cond_signal 함수를 호출 하기 전에 queue에 data가 들어 갔다는 사실만 확정 되면 된다. Condition을 체크하는 부분이 while 루프 안에 있기 때문에 thread가 깨어나고, queue가 비었는지 확인하고, 다시 waiting 상태로 들어가고 하는 비효율 적인 polling 방법은 사용되지 않을 것이다.

Using condition variables
#include <pthread.h>
#include <queue>
template<class T> class ThreadSafeQueue {
    public :
        ThreadSafeQueue() {
            pthread_mutex_init(&m_mutex, NULL);
            pthread_cond_init(&m_cond, NULL);
        }
        ~ThreadSafeQueue() {
            pthread_cond_destroy(&m_cond);
            pthread_mutex_destroy(&m_mutex);
        }
        void enqueue(T t) {
            pthread_mutex_lock(&m_mutex);
            m_queue.push(t);
            pthread_mutex_unlock(&m_mutex);
            pthread_cond_signal(&m_cond);
        }
        bool dequeue(T* t) {
            pthread_mutex_lock(&m_mutex);
            while(m_queue.empty()) {
                pthread_cond_wait(&m_cond, &m_mutex);
            }
            *t = m_queue.front();
            m_queue.pop();
            pthread_mutex_unlock(&m_mutex);
            return true;
        }
    private :
        pthread_mutex_t m_mutex;
        pthread_cond_t  m_cond;
        std::queue<T>   m_queue;
};









참고 :
http://www.joinc.co.kr/modules.php?name=News&file=article&sid=65&mode=nested

원문 :
Advanced Programming in Unix Enviroment : Stevens


'진리는어디에' 카테고리의 다른 글

도메인 이름을 이용해 IP 주소 얻기(gethostbyname)  (3) 2007.04.03
Leader/follower pattern  (0) 2007.04.01
Condition Variables  (1) 2007.03.25
sendto & recvfrom  (0) 2007.03.23
half-close  (0) 2007.03.20
소켓 종료와 TIME_WAIT(Socket termination and TIME_WAIT)  (10) 2007.03.19
Posted by kukuta

댓글을 달아 주세요

  1. Favicon of http://control.tistory.com BlogIcon 04규민 2011.07.05 16:28  댓글주소  수정/삭제  댓글쓰기

    인곤선배님 ㅎ

    OS 공부 한다고 google에 condition variable 검색 했는데 선배 블로그네요 ㅎ

Read/Write Lock(이하 rwlock)의 기본적인 생각은 아래와 같이 간단하다.

  * Read 작업은 값을 변경하지 않으니 몇개가 붙어도 동기화에 문제가 생기지 않는다.
  * Write 작업은 값을 변경 할 수도 있으니, 하나의 크리티컬 섹션에 하나만 붙어야 한다.
  * Read 작업은 끼리는 여러개가 하나의 크리티컬 섹션에 접근 가능하나, Read하는 것을 도중에 바꾸면 안된다.

 

 위의 세 가지 사항 정도만 제외 한다면 rwlock은 mutex랑 비슷하다. 개념도 비슷하고, 사용법도 비슷하고, 하는
일도 비슷하다.

 rwlock의 장점은 read 작업에 있어서 여러개의 쓰레드가 서로 블록킹 하는 일이 없어 괜한 오버헤드를 가지고 오
지 않고, 단점은
그 특성상 read작업이 많다면 거기에 눌려 write 오퍼레이션은 뒤로 밀리기 마련이다.
 
 write lock을 걸려고 하는 쓰레드가 자원을 못 받아 말라 죽는
시나리오를 만들어 보자.

 1) 1번 쓰레드가 A라는 공유 자원을 참조 하려고 한다.
   *
아직 아무런 lock이 걸려 있지 않으므로 커널은 lock을 허락한다.
 2) 2번 쓰레드가 A라는 공유 자원을 변경 하려고 한다.
   * 변경이라는 것은 wrtie lock을 걸어야 하는데 아직 read 작업이 있으므로 write를 잠시 멈춘다.
     
하지만 그 사이에 다른 read 작업이 또 들어 온다. read 작업은 공유가 되므로 커널은 뒤 늦게 온 read 작업을
     크리티컬섹션 안으로 넣어 버린다. 이런 현상의 반복으로 write를 요청하는 thread는 굶어 죽는 경우가 발생
     해 버릴수 있다.(현실적으로는 이런 경우를 대비하여 만들었 겠지만, 그래도 rwlockmodify작업보
    다는 read가 더 많은 곳에 사용하는 것이 가장  이상적이다
.)


#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict    rwlock, const pthread_rwlockattr_t    *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);


 API가 상당히 직관적이다. mutex같은 것들과 마찬가지로 pthread_rwlock_t 객체를 사용하기 이전에
pthread_rwlock_init으로 객체르르 초기화 해주고, 사용이 끝나고 커널에 반환 해야 할 경우에는 pthread_rwlock_destroy로 객체를 해제해 준다.

 read 작업을 하는 경우에는 pthread_rwlock_rdlock을 호출 하고, write작업을 할경우에는
pthread_rwlock_wrlock을 호출 하면 된다. 그리고 어떠한 read 작업이든, write작업이든 lock을 풀어 줄때는
pthread_rwlock_unlock을 호출 한다. read작업을 다른 말로 shared 작업, 즉 공유가 가능한 작업이라고 하는데 구현상 그 갯수에 제한을 두고 있다.
 
 
Single Unix
계열에서는 아래와 같은 함수를 지원한다..

#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

  try라는 이름과 같이 단시 시도를 할 뿐이다. 만일 lock을 잡을 수 없는 상황이라면 바로 리턴을 한다. 리턴 값은
lock를 잡을 경우에는 0을 리턴하고, 그렇지 못하다면 EBUSY를 리턴한다.

 아래의 예제 코드는 두개의 read lock과 하나의 write lock을 사용한다. 하나의 read lock을 걸고 다른 read lock
를 걸었을 경우 과연 예상대로 lock이 잡히는지 안잡히는지, 그리고 read lock가 걸려 있는 상태에서 write lock는 과연 대기 상태에 있는지를 알아 보고자 하는 코드다.


참고 : Advance Programming in Unix Enviroment(section 11.6)

'진리는어디에' 카테고리의 다른 글

half-close  (0) 2007.03.20
소켓 종료와 TIME_WAIT(Socket termination and TIME_WAIT)  (10) 2007.03.19
Read/Write lock  (8) 2007.03.18
libevent and multithread  (6) 2007.03.06
extern  (0) 2007.02.23
소켓 강제 종료시 파이프(pipe) 깨짐  (0) 2007.01.27
Posted by kukuta

댓글을 달아 주세요

  1. h,ts 2009.12.04 09:26  댓글주소  수정/삭제  댓글쓰기

    오 -ㅁ-~~!! 이렇게 활용하는거군요~
    감사합니다. ^^

  2. Favicon of https://skywish25.tistory.com BlogIcon 허은호 2010.02.09 15:26 신고  댓글주소  수정/삭제  댓글쓰기

    구글링하다보니 주인장님 자료만 벌써 두번째 보게 되네요.
    좋은 정보 감사합니다다

  3. Favicon of http://www.opensource.apple.com/source/Libc/Libc-498.1.7/pthreads/pthread_rwlo.. BlogIcon 음흠~ 2010.04.12 16:33  댓글주소  수정/삭제  댓글쓰기

    http://www.opensource.apple.com/source/Libc/Libc-498.1.7/pthreads/pthread_rwlock.c
    를 보면(음.. 다른소스는 어떤지 모르겠지만 ^^)
    blocked_writer 가 있으면 read lock 이 기다립니다.

    while (rwlock->blocked_writers || rwlock->state < 0)
    {
    /* give writers priority over readers */
    PLOCKSTAT_RW_BLOCK(rwlock, READ_LOCK_PLOCKSTAT);
    ret = pthread_cond_wait(&rwlock->read_signal, &rwlock->lock);

  4. 상해 2011.02.10 16:00  댓글주소  수정/삭제  댓글쓰기

    ㅎㅎ설명 너무 잘해주시네요

  5. dfdf 2012.05.04 19:48  댓글주소  수정/삭제  댓글쓰기

    감사합니다.
    책에서 설명이 부족했었는데 여기 글 읽고 이해했습니다.

/**
  libevet 멀티 쓰레드 환경에서 사용코자 했는데, 이것이 생각 처럼 동작
하지 않았다. connection이 하나만 들어 왔을 때는 정상동작 했지만, 둘 이
상이되면서 부터는 event_dispatch() 에서 1을 리턴(set 되어 있는 이벤트
가 없다라는 의미)하면서 계속 종료  되었다.
  혹시나 싶어 libevent의 멀티 쓰레드 환경에 대해 찾아 보니 아래와 같은
글이 있어 짧은 영어 실력이나마 번역을 해 보았다.
 
  혹시나 누군가가 잘 못된 내용을 진실로 받아 들이고 그걸 다른 사람에게
진실인양 전파 한다면 세상에 잘못된 지식들이 판치게 되고, 지식을 추구하
는 사람들에게 있어 그것만큼 나쁜 일이 없다.

  아래의 글 중 잘 못된 내용이 있다면 언제든지
kukuta@gmail.com으로 알
려주면 고맙겠다.
*/


As the guy who added thread support to memcached, I feel qualified to
answer this one:

 memcached(libevent 이용해서 만든 일종의 메모리 DB)에 멀티쓰레드를
사용하려는 사람들에게 추천합니다.

What libevent doesn't support is sharing a libevent instance across
threads. It works just fine to use libevent in a multithreaded process
where only one thread is making libevent calls.

 libevent는 쓰레드간에 공유를 지원히지 않습니다. 만일 당신이 쓰레드를 써
야 하겠다면 한 쓰레드만이 libevent 호출하도록 만들어야 합니다.

What also works, and this is what I did in memcached, is to use multiple
instances of libevent. Each call to event_init() will give you back an
"event base," which you can think of as a handle to a libevent instance.
(That's documented in the current manual page, but NOT in the older
manpage on the libevent home page.) You can have multiple threads each
with its own event base and everything works fine, though you probably
don't want to install signal handlers on more than one thread.

 이것이 제가 memcached에서 libevent 인스턴스를 여러개 만들어서 사용하
려는 이유입니다. 각각 event_init() 호출은 개개별로 "event base" 객체를 생
성 합니다(이것은 요즘에 나오는 libevent man page에도 기록되어 있습니다).
 이런 방법으로 각각의 event base 가진 쓰레드를 생성 할 수 있으며, 비록
signal handler 하나의 쓰레드 이상에 인스톨 하는 것을 원치 않았더라도,
모든 것들이 순조롭게 잘 돌아 갈겁니다.

 In the case of memcached, I allocate one event base per thread at
startup time. One thread handles the TCP listen socket; when a new
request comes in, it calls accept() then hands the file descriptor to
another thread to handle from that point on -- that is, each client is
bound to a particular thread. All you have to do is call
event_base_set() after you call event_set() and before you call event_add().
 
 memcached의 경우, 나는 쓰레드의 시작점에서 event base 각 쓰레드 마다
할당 했습니다. 하나의 쓰레드가 listen 소켓을 들고, 새로운 접속이 시도 될 때
accept() 호출 하고 파일디스크립터(클라이언트소켓)을 생성하여 다른 쓰레드
에 넘깁니다. 이런 식으로 해서 각 클라이언트는 각자의 쓰레드에 바인드 됩니다.
 여기서 해야 할 일은 event_set()함수 호출 후 event_add() 호출 하기 전에
event_base_set() 호출하는 것 뿐입니다
.

Unfortunately, you pretty much have to use pipe() to communicate between
libevent threads, That's a limitation, and honestly it's a pretty big
one: it makes a master/worker thread architecture, where one thread
handles all the I/O, much less efficient than you'd like. My initial
implementation in memcached used an architecture like that and it chewed
lots of CPU time. That's not really libevent's fault -- no UNIX-ish
system I'm aware of has an equivalent to the Windows
WaitForMultipleObjects API that allows you to wake up on semaphores /
condition variables and on input from the network. Without that, any
solution is going to end up using pipes (or maybe signals, which have
their own set of issues in a multithreaded context) to wake up the
libevent threads.

 하지만 불행하게도, libevent 쓰레드간의 통신에는 pipe() 사용해야만 합니다.
그게 한계점이고, 상당히 까칠합니다. 이런 이유로 하나의 쓰레드가 모든 IO 쓰레
드들을 관리 하는 master/worker 쓰레드 아키텍쳐가 만들어 졌는데, 이게 성능에
있어서 또한 상당히 까칠 합니다. CPU 많이 잡아 먹는다는 말입니다. 이것은
결코 libevent의 결함이 아닙니다. 어떤 UNIX-ish(?) 시스템에서도 윈도우의
WaitForMutipleObject API 같이 세마포어/condition variables wake 할수 있는
기능을 제공 하지 않습니다. WaitForMultipleObject 같은 지원이 없고서는 어떠한
시도도 결국 pipe 사용하는 것으로 끝났습니다(그렇지 않다면 아마도 각각의
쓰레드 마다 발행 셋(?set of issues) 가진 signal을 이용하던지 말입니다)

-Steve

원문 보기 : http://monkeymail.org/archives/libevent-users/2007-January/000450.html
추가 사항 : 2007. 3. 9 : libevent가 사용하고 있는 epoll 라이브러리 자체는 threadsafe하다고 한다.
                http://www-gatago.com/linux/kernel/6116742.html

'진리는어디에' 카테고리의 다른 글

소켓 종료와 TIME_WAIT(Socket termination and TIME_WAIT)  (10) 2007.03.19
Read/Write lock  (8) 2007.03.18
libevent and multithread  (6) 2007.03.06
extern  (0) 2007.02.23
소켓 강제 종료시 파이프(pipe) 깨짐  (0) 2007.01.27
Singleton vs Critical section  (0) 2007.01.10
Posted by kukuta

댓글을 달아 주세요

  1. Favicon of http://pudae.net BlogIcon pudae 2007.03.08 12:16  댓글주소  수정/삭제  댓글쓰기

    leader/follow pattern 사용 추천

  2. Favicon of http://blog.ggamsso.wo.tc BlogIcon 깜쏘 2007.03.16 01:56  댓글주소  수정/삭제  댓글쓰기

    트랙백 한 두번 하시는 것도 아니고...
    자신의 글을 보낼 해당 유저 글의 트랙백 주소를 찾는다.
    관리자로 자신의 블로그에 로그인 한다.
    관리자 페이지 접속->글 목록->보낼 글의 라인에 보면 IE의 새로고침 버튼 비스므리 한게 보인다.
    클릭한다.
    주소창이 아래에 뜨면 자신의 글을 보낼려고 하는 해당 유저 글의 트랙백 주소를 넣는다.
    전송한다.

    =3=;;