2008-09-07

멀티쓰레딩 어플리케이션에 관해 모든 개발자가 알아야만 하는 것들 - (2)

[주의]
    초벌 번역한 것으로 세세하게 단어와 문맥을 다듬은 글이 아닙니다.;;;;
    대략적인 내용 파악을 위한 자료로만 활용해 주세요. ^^;;;

[원문]
    What Every Dev Must Know About Multithreaded Apps

지난 글...
    멀티쓰레딩 어플리케이션에 관해 모든 개발자가 알아야만 하는 것들 - (1)

----------------------------------------------------------------------------

락(Lock)

경합을 없애는 가장 일반적인 방법은 락을 사용하여 다른 쓰레드가 메모리에 접근하여 불변성을 깨는 행동을 못하게 하는 것이다. 이렇게 하면 위에 언급했던 네 가지 조건을 없애고 경합이 발생하지 않게 된다.
가장 많이 사용하는 락은 여러가지 이름으로 불린다. 모니터, 크리티컬 섹션(역주: 임계 영역이라고도 함), 뮤텍스 또는 이진 세마포어 등으로 불리지만, 이름을 불문하고 기본 기능은 동일하다. 락은 Enter와 Exit 메소드를 제공하며, 어떤 쓰레드가 Enter를 호출하게 되면, 다른 쓰레드들의 Enter 호출 시도는 Exit가 호출되기전까지 블럭(대기)이 된다. Enter를 호출했던 쓰레드가 락의 소유자이며, 소유자가 아닌 쓰레드에 의해 Exit가 호출되면 프로그래밍 에러로 간주된다. 락은 오직 하나의 쓰레드만이 주어진 시간에 코드의 특정 영역을 실행할 수 있음을 보증하는 메카니즘을 제공한다.

Microsoft® .NET Framework에서의 락은 System.Threading.Monitor 클래스에 의해 구현된다. Monitor 클래스는 인스턴스를 정의하지 않기 때문에 다소 특이하다. 이것은 락의 기능이 System.Object에 의해 효과적으로 구현되어 있기 때문인데, 그렇기 때문에 어떤 object이건 락을 사용할 수 있다. 여기에 totalRequests에 관련된 경합을 막기위해 락을 사용하는 방법을 나타내었다.

static object totalRequestsLock = new Object();  // executed at program
                                                                    // init
...
System.Threading.Monitor.Enter(totalRequestsLock);
totalRequests = totalRequests + 1;
System.Threading.Monitor.Exit(totalRequestsLock);

이 코드로 경합 문제는 수정했지만, 또 다른 문제가 발생될 수 있다. 락이 잡혀 있는 상황에서 예외(exception)이 발생하게 되면, Exit가 호출되지 않는다. 이렇게 되면 이 코드를 실행하려는 다른 쓰레드들은 영원히 블럭 상태가 된다. 많은 프로그램에서 예외는 프로그램에 치명적인 것으로 간주되기 때문에, 이런 경우가 발생하는 것은 재미없는 일이다. 그러나, 예외로부터 복구가능 하도록 하기 위해 finally 절에 Exit를 두면, 좀 더 견고해진다.

System.Threading.Monitor.Enter(totalRequestsLock);
try{
    totalRequests = totalRequests + 1;
} finally {
    System.Threading.Monitor.Exit(totalRequestsLock);
}


이 패턴은 C#이나 Visual Basic®.NET에서는 일반적인 것이다. 다음의 C#코드는 앞서 보인 try/finally와 동일한 표현이다.

lock(totalRequestsLock){
    totalRequests = totalRequests + 1;
}


개인적으로는 lock 구문을 사용하는 것을 반대한다. 어떤 면에서 이런 방식은 편리하고 간결하다. 그러나, 이는 프로그래머들이 견고한 코드를 작성하고 있다는 잘못된 편안함을 줄 수 있다. 락이 사용되는 영역은 프로그램상의 중요한 불변량이 지켜지지 않기 때문에 사용된다는 점을 기억하자. 만일 그 영역에서 예외가 발생한다면, 그 시점에 불변량도 깨지게 될 확률이 크다. 이 깨진 부분을 고치지 않고 프로그램이 계속 진행되도록 하는 것은 좋지 않은 생각이다.

totalRequests 예에서는 딱히 할만한 정리작업이 없기 때문에 사용된 lock 구문은 적당하다. 또한 lock 구문은 모든 데이터가 읽기 전용일 때에도 쓸만하다. 그러나 보통 예외가 발생하게되면 추가적인 정리작업이 수행될 필요가 있다. 이런 경우에는 어차피 try/finally 구문이 필요하기 때문에 굳이 lock 구문을 추가할 필요는 없는 것이다.

락 속성의 사용

대부분의 프로그래머는 경합과 씨름한적이 있고, 이를 방지하기 위해 락을 사용하는 방법의 간단한 예들을 보아 왔다. 그러나 추가적인 설명이 없이 이런 예제만으로는 실제의 프로그램에서 락을 효과적으로 사용하는데 필요한 중요한 이론까지 아우를수는 없다.

첫 번째 중요한 통찰은 락은 코드 영역에 대한 상호 배제(mutual exclusion: 역주-mutex도 이말을 줄인 것임)를 제공하지만, 보통 프로그래머들이 원하는 것은 메모리 영역에 대한 보호를 원한다는 점이다. totalRequests 예제에서, 원래 목적은 totalRequests(메모리 위치)를 올바른 값으로 유지하는 것이다. 그러나, 위와 같이 하게 되면 실제로는 코드의 영역(totalRequests를 증가시키는 코드 영역)에 락을 걸게된다. totalRequests를 참조하는 코드가 이 곳 뿐이기 때문에 totalRequests에 대한 상호 배제를 제공하는 셈이긴하다. 그러나 만일 다른 코드 영역에서 락을 걸지 않고 totalRequests를 갱신하게 된다면, 메모리에 대한 상호 배제를 가지지 않게 되는 셈이고, 결과적으로 코드는 경합 조건을 일으키게 된다.

이는 다음의 이론으로 연결된다. 메모리 영역에 대해 상호 배제를 제공하는 락을 위해 락을 걸지 않고서는 메모리에 기록을 할 수 없도록 해야 한다. 적절히 디자인된 프로그램이라면 상호 배제를 제공하기 위한 메모리 영역은 모두 락과 연결이 되어야 한다. 불행히도 이런 연결을 깔끔하게 만들어주는 코드상의 명확한 해법은 없으며, 이런 정보는  프로그램내의 멀티쓰레드의 동작에 관해 고민하는 모든 이에게 절대적으로 중요한 것이다.

결론적으로 모든 락은 상호 배제를 제공해야 하는 메모리의 특수 영역(예를 들어 데이터 구조체의 집합)을 기술한 문서와 연결해서 명확히 정리해 두어야만 한다. totalRequests의 예제에서 totalRequestsLock은 totalRequests 변수 하나만을 보호하는 역할을 한다. 실제의 프로그램에서 락은, 데이터 구조체나 이와 관련된 다른 구조체 또는 아예 연결될수 있는 모든 메모리와 같이 더 큰 영역을 보호할 수도 있다. 때때로 데이터 구조체의 특정 부분(해시 테이블의 버켓 체인과 같은)만을 보호할 수도 있지만, 그 영역이 뭐가 되었던 간에 프로그래머가 명시적으로 기술해야 함은 여전히 중요하다. 이런 명세를 가지고 있으면, 관련된 메모리가 갱신되기 전 락의 사용이 체계적으로 진행될 수 있다. 대부분의 경합은 관련 메모리에 접근하기전에 올바로 락을 일관적으로 걸어주지 않아 발생하기 때문에, 이 정도의 검수를 위해 시간을 들일만 하다.

각 락이 보호해야할 메모리에 대한 정확한 명세를 가지고 있다면, 어떤 영역이 서로 다른 락들로 인해 중첩으로 보호되지는 않는지 확인해야 한다. 중첩 자체가 잘못된 것은 아니지만, 이런식으로 연결된 메모리가 유용하지는 않기 때문에 피해야 한다. 두 락에서 공통으로 보호하는 메모리가 갱신되는 경우에는 어떤 일이 발생할지 생각해 보자. 어떤 락이 사용되어야 할까? 다음과 같은 가능성을 생각해 볼 수 있겠다:

아무 락이나 임의로 건다 이 방식은 더이상 상호 배제를 제공할 수 없기 때문에 안된다. 각기 다른 락을 사용하는 두 개의 쓰레드가 갱신을 하는 것이 가능해지고, 결국 동시에 같은 메모리를 갱신하게 된다.

항상 두 개의 락을 건다 이렇게 하면 상호 배제는 지원하지만, 비용이 두배나 들고 하나의 락을 사용하는것에 비해 이점이 없다.

항상 특정 하나만 골라서 락을 건다 이것은 특정 영역을 보호하는 하나의 락을 사용한다는 말과 다를게 없다.




댓글 없음:

댓글 쓰기