'lock'에 해당되는 글 3건

  1. 2007.12.05 volatile - Multithreaded Programmer's Best Friend (6)
  2. 2007.03.25 Condition Variables (1)
  3. 2007.03.18 Read/Write lock (8)

/**
  얼마전 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  댓글주소  수정/삭제  댓글쓰기

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

 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  댓글주소  수정/삭제  댓글쓰기

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