티스토리 뷰

/**
  얼마전 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).

TAG
, ,
댓글
  • 프로필사진 Favicon of http://blog.ggamsso.wo.tc BlogIcon 깜쏘 오...
    지금 프로젝트가 멀티스레드 환경에서 돌아가야 한다는 조건이 붙었는데... (거기다 C++)
    선배^^ 좋은 글 보고 갑니다. (아쉽게로 서브로 돌고 있는 검색엔진[pure c]에 적용하는건 무리겠군요. ㅠ.ㅠ)

    PS::크리스마스 뭐 하실 겁니까?
    2007.12.03 15:35
  • 프로필사진 Favicon of https://kukuta.tistory.com BlogIcon kukuta 흠..어디서 긁어 온 글이 도움이 되었다니 나로써도 상당히 기쁘다만..
    솔로에게 크리스마스에 뭐 할건지 묻는 것은 예의가 아니야..-_-;;
    2007.12.03 22:59 신고
  • 프로필사진 Favicon of http://pudae.tistory.com BlogIcon pudae volatile은 내친구..저거 어디서 본건ㄷ...
    저거 언제 니 친구되었냐?
    2007.12.03 17:02
  • 프로필사진 Favicon of https://kukuta.tistory.com BlogIcon kukuta 바보야 멀티 쓰레드 프로그래머의 친구라고 적혀 있잖아.
    감자 친구가 아니라..ㅋㅋ
    나는 멀티 쓰레드 프로그래머 아닌가봐..volatile이 친구 먹자고 안하네..ㅠㅠ
    2007.12.03 23:00 신고
  • 프로필사진 김경순 정말 좋은 내용입니다. 유용하게 쓰도록 담아가겠습니다. 감사합니다. 2008.07.24 11:12
  • 프로필사진 Favicon of https://kukuta.tistory.com BlogIcon kukuta 허허허허..민망합니다..ㅋ 2008.08.05 11:44 신고
댓글쓰기 폼