성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
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# - Mutex의 비동기 버전</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;' > Async Mutex ; <a target='tab' href='https://dfederm.com/async-mutex/'>https://dfederm.com/async-mutex/</a> </pre> <br /> 사실 위의 내용을 다루려고 이전 글에서 뮤텍스와 세마포어를 미리 다뤄야만 했습니다. ^^<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 /> 간단하게 테스트를 해볼까요? ^^ 우선 Mutex를 쓰지 않는 버전으로 이렇게 작성한 후,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > namespace ConsoleApp1 { internal class Program { static void Main(string[] args) { Task t1 = Task.Run(async () => { await PrintOut(); }); Task t2 = Task.Run(async () => { await PrintOut(); }); t1.Wait(); t2.Wait(); } static async Task PrintOut() { Console.WriteLine($"{DateTime.Now:T} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-before"); Thread.Sleep(2000); Console.WriteLine($"{DateTime.Now:T} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-after"); } } } </pre> <br /> 실행하면 다음과 같은 결과를 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 오전 9:36:40 [7]: PrintOut-EX-before 오전 9:36:40 [6]: PrintOut-EX-before 오전 9:36:42 [6]: PrintOut-EX-after 오전 9:36:42 [7]: PrintOut-EX-after </pre> <br /> 예상 가능한 출력이죠? ^^ 자, 여기다 이제 비동기 mutex를 장착하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > namespace ConsoleApp1 { internal class Program { static AsyncMutex m = <span style='color: blue; font-weight: bold'>new AsyncMutex(@"Global\MyMutex");</span> // ...[생략]... static async Task PrintOut() { <span style='color: blue; font-weight: bold'>await m.AcquireAsync(CancellationToken.None);</span> // ...[생략]... <span style='color: blue; font-weight: bold'>await m.ReleaseAsync();</span> } } } </pre> <br /> Mutex의 영향으로 PrintOut 내부의 코드가 동기화돼 다음과 같은 출력 결과가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 오전 9:37:27 [11]: PrintOut-EX-before 오전 9:37:30 [11]: PrintOut-EX-after 오전 9:37:30 [12]: PrintOut-EX-before 오전 9:37:32 [12]: PrintOut-EX-after </pre> <br /> 잘 동작하는군요. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 뭔가 좀 아쉽습니다. AsyncMutex의 소스 코드를 보면 AcquireAsync에서 Task.Factory.StartNew를 사용해 동기 작업에 해당하는 부분을 스레드로 감싸 비동기 처리하고 있습니다.<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;' > C# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13059'>https://www.sysnet.pe.kr/2/0/13059</a> </pre> <br /> 명시적인 스레드 사용 없이 다음과 같은 식으로 커널 개체가 Signaled 상태로 바뀌는 알림을 받아, 이후의 실행을 I/O 스레드에 맡겨보는 것도... 생각해 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > namespace ConsoleApp1 { internal class AsyncMutex2 { Mutex _m; public AsyncMutex2() { _m = new Mutex(false); } public AsyncMutex2(string name) { _m = new Mutex(false, name); } public Task AcquireAsync() { TaskCompletionSource taskCompletionSource = new(); <span style='color: blue; font-weight: bold'>ThreadPool.RegisterWaitForSingleObject(_m, signalWork, taskCompletionSource, -1, true);</span> return taskCompletionSource.Task; } public void Release() { <span style='color: blue; font-weight: bold'>_m.ReleaseMutex();</span> } void signalWork(object? state, bool timedOut) { if (state is TaskCompletionSource taskSource) { <span style='color: blue; font-weight: bold'>taskSource.SetResult();</span> } } } } </pre> <br /> 하지만, 이런 구현은 유효하지 않습니다. "<a target='tab' href='https://dfederm.com/async-mutex/'>Async Mutex</a>" 글의 저자도 이 부분에 대해 언급했는데요, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Mutexes have thread affinity; that is, the mutex can be released only by the thread that owns it. </pre> <br /> <a target='tab' href='https://www.sysnet.pe.kr/2/0/13156'>지난번 글에 정리</a>한 것처럼, Mutex는 WaitOne을 호출한 스레드에서 반드시 ReleaseMutex를 호출해야 하므로 await로 인해 스레드가 달라지는 상황에서는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static async Task PrintOut() { <span style='color: blue; font-weight: bold'>await m.AcquireAsync(); // 내부에서 WaitOne을 호출하는 스레드와,</span> Console.WriteLine($"{DateTime.Now:T} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-before"); Thread.Sleep(2000); Console.WriteLine($"{DateTime.Now:T} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-after"); <span style='color: blue; font-weight: bold'>m.Release(); // 이곳에서 ReleaseMutex 호출하는 스레드는 I/O 스레드 풀로부터 가져온 것이므로.</span> } </pre> <br /> 결국 ReleaseMutex에서 "Object synchronization method was called from an unsynchronized block of code" 예외가 발생하는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이런 문제를 (Mutex처럼 사용할 수 있는) Semaphore를 이용하면 해결할 수 있습니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > namespace ConsoleApp1 { internal class AsyncMutex2 : IAsyncDisposable { <span style='color: blue; font-weight: bold'>Semaphore _smp;</span> public AsyncMutex2(string name) { _smp = new Semaphore(1, 1, name); } public Task AcquireAsync() { return AcquireAsync(-1); } public Task AcquireAsync(int millisecondsTimeOutInterval) { TaskCompletionSource taskCompletionSource = new(); <span style='color: blue; font-weight: bold'>ThreadPool.RegisterWaitForSingleObject(_smp, signalWork, taskCompletionSource, millisecondsTimeOutInterval, true);</span> return taskCompletionSource.Task; } public void Release() { <span style='color: blue; font-weight: bold'>_smp.Release();</span> } void signalWork(object? state, bool timedOut) { if (state is TaskCompletionSource taskSource) { if (timedOut) { <span style='color: blue; font-weight: bold'>taskSource.SetCanceled();</span> return; } <span style='color: blue; font-weight: bold'>taskSource.SetResult();</span> } } public ValueTask DisposeAsync() { _smp.Dispose(); return ValueTask.CompletedTask; } } } </pre> <br /> 실제로 위의 코드를 사용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static AsyncMutex2 m = new AsyncMutex2(@"Global\MyMutex"); static void Main(string[] args) { Task t1 = Task.Run(async () => { await PrintOut(1); }); Task t2 = Task.Run(async () => { await PrintOut(2); }); Task t3 = Task.Run(async () => { await PrintOut(3, 1000); }); try { Task.WaitAll(t1, t2, t3); } catch (Exception ex) { Console.WriteLine(ex.Message); } Task t4 = Task.Run(async () => { await PrintOut(4); }); Task t5 = Task.Run(async () => { await PrintOut(5); }); Task.WaitAll(t4, t5); } static async Task PrintOut(int workId) { await PrintOut(workId, -1); } static async Task PrintOut(int workId, int timeOut) { await m.AcquireAsync(timeOut); Console.WriteLine($"{DateTime.Now} {workId} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-before"); Thread.Sleep(2000); Console.WriteLine($"{DateTime.Now} {workId} [{Thread.CurrentThread.ManagedThreadId}]: PrintOut-EX-after"); m.Release(); } </pre> <br /> 잘 동작합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 2022-11-05 오후 1:28:38 1 [12]: PrintOut-EX-before 2022-11-05 오후 1:28:40 1 [12]: PrintOut-EX-after 2022-11-05 오후 1:28:40 2 [14]: PrintOut-EX-before 2022-11-05 오후 1:28:42 2 [14]: PrintOut-EX-after One or more errors occurred. (A task was canceled.) // 3번 work는 1000ms 대기 시간을 초과해 취소됨 2022-11-05 오후 1:28:42 4 [14]: PrintOut-EX-before 2022-11-05 오후 1:28:44 4 [14]: PrintOut-EX-after 2022-11-05 오후 1:28:44 5 [14]: PrintOut-EX-before 2022-11-05 오후 1:28:46 5 [14]: PrintOut-EX-after </pre> <br /> 위에서 구현한 Mutex는 Semaphore를 바탕으로 구현했으므로 기반 동작은 세마포어의 특성을 띕니다. 그렇긴 해도 어차피 "<a target='tab' href='https://dfederm.com/async-mutex/'>Async Mutex</a>" 글의 AsyncMutex도 결국 원래 Mutex의 고유 성격인 "재진입"을 허용하지 않으므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > namespace ConsoleApp1 { internal class Program { static AsyncMutex m = <span style='color: blue; font-weight: bold'>new AsyncMutex(@"Global\MyMutex");</span> // ...[생략]... static async Task PrintOut() { <span style='color: blue; font-weight: bold'>await m.AcquireAsync(CancellationToken.None);</span> <span style='color: blue; font-weight: bold'>await m.AcquireAsync(CancellationToken.None);</span> // 재진입 불가능 - hang!!!! // ...[생략]... <span style='color: blue; font-weight: bold'>await m.ReleaseAsync();</span> <span style='color: blue; font-weight: bold'>await m.ReleaseAsync();</span> } } } </pre> <br /> 오히려 스레드 낭비 없는 AsyncMutex2 버전이 더 나을 것입니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> (AsyncMutex는 named mutex를 사용했고) AsyncMutex2 버전은 named Semaphore를 사용하고 있습니다. 왜냐하면 unnamed는 어차피 SemaphoreSlim에서 이미 비동기 버전의 WaitAsync를 제공하기 때문에 그것을 사용하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static async Task Main(string[] args) { SemaphoreSlim ss = new SemaphoreSlim(1, 1); <span style='color: blue; font-weight: bold'>await ss.WaitAsync(); ss.Release();</span> } </pre> <br /> 따라서, 굳이 named일 필요가 없다면 SemaphoreSlim으로 간단하게 해결하시면 됩니다.<br /> <br /> 물론, 이것 역시 Mutex/Semaphore의 차이점이 허용된다는 가정이 성립할 때만 기존의 Mutex 코드를 안전하게 비동기로 바꿀 수 있을 것입니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1981&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, 찾아보니 RegisterWaitForSingleObject에 Mutex를 사용할 수 없다는 글을 누가 이미 써놨군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > RegisterWaitForSingleObject and mutexes don't mix ; <a target='tab' href='http://joeduffyblog.com/2007/05/13/registerwaitforsingleobject-and-mutexes-dont-mix/'>http://joeduffyblog.com/2007/05/13/registerwaitforsingleobject-and-mutexes-dont-mix/</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1370
(왼쪽의 숫자를 입력해야 합니다.)