성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - SemaphoreSlim 사용 시 주의점</h1> <p> 이전 글에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13553'>https://www.sysnet.pe.kr/2/0/13553</a> </pre> <br /> 마지막에 "Wait/Pulse(All)를 lock(obj) 형태처럼 동작해야 할 코드에 응용하는 것은 자칫 디버깅을 힘들게 할 수 있으므로 사용 시 주의를 기울이는 것이 좋습니다."라는 글로 맺었는데요, 하필 그에 해당하는 시나리오로 사용하는 타입이 바로 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim'>SemaphoreSlim</a>입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - Mutex와 Semaphore/SemaphoreSlim 차이점 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13156'>https://www.sysnet.pe.kr/2/0/13156</a> </pre> <br /> SemaphoreSlim은 (AvailableWaitHandle 속성을 접근하지 않는 한) 커널 동기화 개체를 사용하지 않고 Wait/Pulse 방식을 사용하기 때문에 특정 스레드에서 SemaphoreSlim.Wait을 호출하고 지나간 경우, Count 값을 하나 소진만 할 뿐이어서 도대체 어떤 스레드가 Wait을 호출했는지 찾아내는 것이 여간 곤혹스러운 일이 아닐 수 없습니다.<br /> <br /> 간단한 예를 들어 볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class Program { static SemaphoreSlim _lock = <span style='color: blue; font-weight: bold'>new SemaphoreSlim(1, 1)</span>; static void Main(string[] args) { Console.WriteLine("Press any key to continue..."); Console.ReadLine(); Thread t = new Thread(() => { <span style='color: blue; font-weight: bold'>_lock.Wait();</span> try { Console.WriteLine("Hello, Lock!"); } finally { _lock.Release(); } }); <span style='color: blue; font-weight: bold'>_lock.Wait();</span> Console.WriteLine("Hello, World!"); try { t.Start(); <span style='color: blue; font-weight: bold'>t.Join();</span> } finally { _lock.Release(); } } } </pre> <br /> 위의 예제를 실행 후 "Press any key to continue..." 메시지가 출력된 시점에 windbg로 attach한 다음, thinlock 상황을 보면 1개가 열려 있는 것을 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:006> <span style='color: blue; font-weight: bold'>!dumpheap -thinlock</span> Address MT Size 00000263657b6040 00007fffeb46ce00 32 ThinLock owner 1 (0000026363d16490) Recursive 0 Found 1 objects. </pre> <br /> 일단 저건 SemaphoreSlim과는 무관한데요, Console.ReadLine으로 인해 내부에서 사용한 System.IO.TextReader+SyncTextReader에 대한 lock을 사용한 것이기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.IO\TextReader.cs <span style='color: blue; font-weight: bold'>[MethodImpl(MethodImplOptions.Synchronized)]</span> public override string ReadLine() { return _in.ReadLine(); } </pre> <br /> 그다음 Enter를 눌러 "Hello, World" 출력까지 진행한 시점에 thinlock을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:005> <span style='color: blue; font-weight: bold'>!dumpheap -thinlock</span> Address MT Size Found 0 objects. 0:005> <span style='color: blue; font-weight: bold'>!syncblk</span> Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner ----------------------------- Total 2 CCW 0 RCW 0 ComClassFactory 0 Free 0 </pre> <br /> 지난 글에 설명한 Wait/Pulse의 동작에 따라 위와 같이 "Total 2"라는 것만 알 수 있을 뿐 도대체 어떤 스레드에서 Wait을 풀고 있지 않아 그런 것인지 추적하는 것이 쉽지 않습니다. 물론, 위의 경우에는 2개의 스레드뿐이어서 호출 스택을 따라 _semaphore.Wait을 먼저 호출한 코드를 찾아 분석하면 되지만, 만약 수많은 스레드가 동작 중인 Web Application 등에서 저런 문제가 발생하면 분석 시간을 운에 맡기게 됩니다.<br /> <br /> 또한, Mutex와는 달리 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13156#sem_reent'>Semaphore는 스레드 재진입을 허용하지 않습니다</a>. 이런 특성상, 내부적으로 (lock이라고 부를 수 없는) lock을 소유한 스레드에 대한 정보를 유지하지도 않습니다. Slim해서 성능적으로 유리한 것은 사실이지만, 디버깅을 염두에 둔다면 (시나리오가 맞는 경우) 차라리 Mutex가 나을 정도입니다.<br /> <br /> <hr style='width: 50%' /><br /> <a name='dispose'></a> <br /> SemaphoreSlim의 또 다른 단점이 있다면, Dispose 처리가 미흡하다는 점입니다. 예를 들어, 아래의 코드는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class Program { static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); static void Main(string[] args) { Thread t = new Thread(() => { _semaphore.Wait(); // 2초 후에 SemaphoreSlim 자원이 해제되지만 여전히 wait 상태로 무한 대기 try { Console.WriteLine("Hello, Lock!"); } finally { _semaphore.Release(); } }); _semaphore.Wait(); Console.WriteLine("Hello, World!"); t.Start(); <span style='color: blue; font-weight: bold'>Thread.Sleep(2000); _semaphore.Dispose();</span> // 2초 후에 Main 스레드의 SemaphoreSlim을 자원 해제 t.Join(); } } </pre> <br /> 2초 후에 SemaphoreSlim.Dispose가 호출되지만 이전에 대기했던 스레드, 즉, (위의 경우에는 1개지만) Wait 중인 스레드들이 영원히 무한 대기 상태에 빠지는 문제가 발생합니다.<br /> <br /> 따라서, Dispose 전에는 Release를 반드시 해야 하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > _semaphore.Release(); Thread.Sleep(16); // 임의 시간 대기, 그렇지 않으면 Dispose 호출로 인해 Wait 대기 중인 스레드가 깨어나 Release를 호출할 때 예외 발생 _semaphore.Dispose(); </pre> <br /> Release와 Dispose 사이의 임의 시간을 결정할 수 없다면 차라리 Wait 중인 스레드의 Release에 try/catch를 하는 것이 좋습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Thread t = new Thread(() => { _semaphore.Wait(); try { Console.WriteLine("Hello, Lock!"); } finally { <span style='color: blue; font-weight: bold'>try { _semaphore.Release(); } catch { }</span> } }); _semaphore.Wait(); Console.WriteLine("Hello, World!"); t.Start(); Thread.Sleep(2000); <span style='color: blue; font-weight: bold'>_semaphore.Release(); _semaphore.Dispose();</span> </pre> <br /> 하지만, 저것도 그다지 좋은 방법이 아닙니다. 만약 여러 개의 Wait을 수신 대기하는 스레드들이 있는 시나리오라면 처음 한 번 깨어난 스레드만 대기가 풀리고 나머지 스레드는 여전히 무한 대기에 빠집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > for (int i = 0; i < 10; i++) { new Thread(() => { Thread.Sleep(500); _semaphore.Wait(); // Release + Dispose로 인해 1개만 풀리고 9개는 무한 대기 try { Console.WriteLine("Hello, Lock!"); } finally { try { _semaphore.Release(); } catch { } } }).Start(); } _semaphore.Wait(); Console.WriteLine("Hello, World!"); Thread.Sleep(2000); <span style='color: blue; font-weight: bold'>_semaphore.Release(); _semaphore.Dispose();</span> </pre> <br /> 이 상황을 해결하려면 Reflection까지 도입해 Release와 Dispose 사이에 대기하는 코드를 만들어야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > _semaphore.Release(); // 대기 스레드가 없어질 때까지 Dispose 보류 <span style='color: blue; font-weight: bold'>while (true) { Thread.Sleep(16); if (IsWaitCountZero(_semaphore) == true) { break; } }</span> _semaphore.Dispose(); private static bool IsWaitCountZero(SemaphoreSlim semaphore) { object value = typeof(SemaphoreSlim).GetField("m_waitCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(semaphore); return (int)value == 0; } </pre> <br /> 정말 까다롭죠? ^^;<br /> <br /> <hr style='width: 50%' /><br /> <br /> 비록 Semaphore라는 자원의 성격상 initialCount == 1, maxCount == 1로 설정해 Critical Section을 지정하는 용도로 쓰는 것이 가능하지만 위에서 보다시피 단지 그 목적으로 활용할 거라면 차라리 lock(obj) 구문, 또는 Mutex를 사용하는 것이 더 좋다는 것이, 저의 개인적인 의견입니다. ^^<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9534
(왼쪽의 숫자를 입력해야 합니다.)