프로덕션 코드에서 스레드를 중단시키지 말자

 

프로덕트 코드에서는, 결과를 예측할 수 없고 프로그램을 불안정하게 만드는 스레드 강제 중단의 사용을 피하라.

 

 Thread 개체에서 제공하는 Abort() 메소드는 스레드를 제거하는 기능을 제공한다. 그러려고 Abort()는 런타임을 이용해서 대상 스레드 내에서 ThreadAbortException 예외가 발생하게 한다. 이 예외를 catch할 수 있지만, catch해서 무시하도록 처리한다고 해도 스레드가 실제로 제거되었는지 확인할 때 다시 발생된다. 이렇게 스레드를 강제로 중단하는 것이 바람직하지 않은 수많은 이유 가운데 몇 가지를 들어보면 다음과 같다.

l  이 메소드는 스레드를 중단시키는 시도를 할 뿐이지 그 시도의 성공을 보장하지 않는다. 예를 들어 스레드 내의 제어점이 현재 finally 블록(방해해서는 안 되는 리소스 해제를 위한 중요한 코드를 실행중일 수 있기 때문) 혹은 비관리 코드(비관리 코드 내부에서 중단 시킬 경우  CLR 자체에 문제가 생길 수 있다) 내부에 있는 경우 런타임이 ThreadAbortException을 발생시키지 않는다. 대신 런타임은 제어가 finally 블록을 벗어나거나 관리 코드로 반환되기를 기다렸다가 예외를 발생시킨다. 하지만 반드시 이렇게 처리된다는 보장이 없다. 중단하려는 스레드의 finally 블록에 무한 루프가 있을 수 있고, 역설적으로 애초에 finally 문의 무한 루프 때문에 스레드를 중단하려고 했을 수도 있다.

l  중단된 스레드가 lock 문으로 보호되는 임계 코드 내에 있을 수 있다. finally와 달리 lock 문은 예외를 막지 않는다. 따라서 진행 중인 임계 코드는 예외를 맞아 실행 중에 중단되고 lock 개체는 자동으로 해체되면서 임계 영역에 들어가려던 다른 코드의 진입이 허용된다. 이때 실행되다가 중단된 코드의 상태가 임계 영역에 그대로 남아 있게 되는데, 근본적으로 잠금이라는 개념을 이용하는 이유가 이런 상태(원자성을 제공하지 못하는 상태)를 막기 위한 것이라는 점을 떠올린다면 이것은 분명 잘못된 것이며, 스레드를 중단함으로 인해 스레드에 안전한 코드를 한 순간에 위험한 코드로 만들어 버릴 수 있다.

l  CLR은 스레드의 중단으로부터 내부 데이터 구조가 안전하게 보홈되을 보장하지만 BCL(Base Class Liabrary)의 경우는 그렇지 못하다. 따라서 운 나쁘게 좋지 않은 시점에 예외로 인해 스레드가 중단된다면 개발자가 애써 구축한 데이터 구조나 BCL에서 제공하는 데이터 구조는 망가질 수 있다. 다른 스레드의 코드나 중단되는 스레드의 finally 블록에 있는 코드에서 망가진 데이터를 참조할 수 있기 때문에 오류가 확산될 수 있다.

 

결론적으로 최후 수단이 아니라면 스레드를 강제 중단시켜서는 안 된다. , 전체 어플리케이션 도메인 혹은 프로세스를 비상 종료하는 경우의 일환으로 중단하는 경우에만 사용하는 것이 이상적이다. 다행히 태스크 기반의 비동기 방식은 스레드 강제 종료를 위한 더 단단하고 안전한 취소 패턴을 이용하고 있으며 뒤에서 소개할 비동기 작업 절에서 살펴본다.

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

프로덕션 코드에서 스레드를 유휴 상태로 만들어서는 안 된다

 

프로덕션 코드에서 Thread.Sleep()을 사용하지 마라.

 

Thread.Sleep() 정적 메소드는 현재 스레드를 유휴 상태로 만드는데, 주어진 시간 동안 운영체제로 하여금 현재 스레드에 시간을 할당하지 않게 된다. 밀리초 단위의 숫자 혹은 TimeSpan 형식의 매개변수를 통해서 실행을 재개하기 전에 운영체제가 대기해야 할 시간을 지정한다. 대기하는 동안에 운영체제는 대기 중인 다른 스레드에 시간을 할당한다. 꽤 그럴듯하게 들리지만 이런 상황에 대처하기 위한 더 나은 설계가 있을 것 같은 나쁜 코드 냄새가 난다.

 스레드는 종종 여러 이벤트와 보조를 맞추려고 유휴 상태에 놓이곤 한다. 하지만 운영체제는 이와 관련된 시점 조절과 관련해 정확성을 일체 보장하지 않는다. , ‘123 밀리초 동안 유휴 상태로 유지해 주세요라고 요청했을 때, 운영체제는 해당 스레드를 적어도 123 밀리초 동안 유휴 상태로 만드는 데 훨씬 더 오래 걸릴 가능성도 있다. 스레드가 유휴 상태로 진입하고 또 벗어나는 데 걸리는 시간은 절대적인 것이 아니며 임의의 시간이 소요될 수 있다. Thread.Sleep()을 고정밀 타이머처럼 사용해서는 안 되는 이유다.

 더 심한 경우는 Thread.Sleep()바보들의 동기화 시스템으로 사용하는 것이다. 예를들어 일정량의 비동기 작업을 수행해야 하고 이 비동기 작업이 끝나기 전에는 현재의 스레드를 더 이상 진행해서는 안 되는 상황을 가정해 보자. 이때 비동기 작업이 끝날 것으로 예상되는 대략적인 시간보다 충분히 더 여유를 잡아서 스레드를 유휴 상태로 만들고 스레드가 유휴 상태에서 깨어났을 때 비동기 작업이 끝났을 것이라는 위험한 가정을 기준으로 개발을 시도할 수 있다. 이것은 정말 위험한 생각이다. 비동기 작업은 여러분이 생각하는 것 보다 더 긴 시간을 요할 수 있다. 이와 같은 상황에 적합한 스레드 동기화 메커니즘을 다음 장에서 살펴본다.

 스레드를 유휴 상태로 만드는 것 자체도 나쁜 프로그래밍 습관이라고 볼 수 있다. 일단, 스레드가 유휴 상태에 들어가면 스레드에서 포함하고 있는 코드는 원해도 실행할 수 없는 무응답 상태가 되기 때문이다. 윈도우 응용 프로그램의 주 스레드를 유휴 상태로 만들면 사용자 인터페이스에서 발생하는 메시지도 처리하지 않게 되기 때문에 결국 프로그램은 응답이 없는 상태에 빠진다.

 더 일반적인 시각에서 보면 스레드와 같은 값비싼 리소스를 할당해 놓고 작업을 하지 않게 하는 것은 역시 바람직한 프로그래밍이 아니다. 아무도 잠만 자는 직원에게 월급을 주지 않으려 할 것 이다. 따라서 수백만이나 수십억에 이르는 프로세서 주기를 허송세월로 흘려보내려고 힘들게 스레드를 할당하지 않는 것이 바람직하다.

 한편, Thread.Sleep()을 적절하게 사용하는 경우도 물론 있다. 유휴 대기 시간을 0으로 설정해 호출하는 방식으로, 운영체제에게 남아있는 시간 할당을 포기할 의사를 전달해서 대기 중인 다른 스레드로 순서를 재빨리 넘기도록 할 수 있다. 이렇게 양보한 스레드는 추가적인 대기 시간 없이 다시 일반적인 스케줄에 따라 작업을 진행한다. 다음으로 테스트 코드에서는 종종 실제로 프로세서에 의미 없는 연산을 시켜 부하를 주는 방법 대신 Thread.Sleep()을 이용해서 일부 장기간의 대기 시간을 요하는 작업을 모의하기도 한다. 다른 이유로 스레드를 유휴 상태로 만들고 있다면 원하는 기능을 구현할 수 있는 더 좋은 방안이 없는지 조심스럽게 검토해 보는 것이 좋다.

 C# 5.0의 태스크 기반 비동기 프로그래밍에서는 Task.Delay() 메소드의 결과에 await 연산을 이용해서 현재 스레드의 진행에 영향을 주지 않고 비동기 지연 처리가 가능하다. 자세한 내용은 다음 장의 타이머 절을 참고하라.

 

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

 

TabControl에 추가 되어 있는 Tab을 드래그하여 빼내면 윈도우가 나오게 하는 기능을 구현하였습니다.

데모 프로그램은 아래와 같습니다.

 

 

 

 

1. DragDrop 탭추가 : Drag&Drop하여 넣고 뺄 수 있는 탭을 추가합니다.

 

 

 

2. DragDrop MDI탭추가 : Drag&Drop하여 넣고 뺄 수 있는 MDI 윈도우를 추가합니다.

 

 

 

3. 일반 탭추가 : 일반탭을 추가하여 탭밖으로 빠지질않는것을 확인합니다.

 

 

4. 일반 폼을 추가 : 일반폼을 추가하여 탭안에 삽입이 안되는 것을 확인합니다.

 

 

프로젝트 파일 : KDYFramework.Winform.Controls.DragDropTabControl.zip

 

 (VS2010 작성)

+ Recent posts