성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
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# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법</h1> <p> 답 먼저 이야기하면, ThreadPool에서 제공하는 RegisterWaitForSingleObject 정적 메서드를 이용하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > RegisterWaitForSingleObject function (winbase.h) ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerwaitforsingleobject'>https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerwaitforsingleobject</a> ThreadPool.RegisterWaitForSingleObject Method ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.registerwaitforsingleobject'>https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.registerwaitforsingleobject</a> </pre> <br /> ThreadPool.RegisterWaitForSingleObject의 경우 인자로 WaitHandle을 받아들이는데요, 따라서 그것을 상속한 System.Threading.EventWaitHandle, System.Threading.Mutex, System.Threading.Semaphore 개체와 연동할 수 있습니다.<br /> <br /> 예를 들어 볼까요?<br /> <br /> EventWaitHandle을 이용해 작업을 추가하고, 별도로 생성한 스레드에서 그 이벤트를 대기해 처리하는 코드를 다음과 같이 만들 수 있습니다. (전형적인 Producer/Consumer 모델입니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Collections.Concurrent; using System.Runtime.Versioning; [assembly: SupportedOSPlatform("windows")] internal class Program { static EventWaitHandle _signal = new EventWaitHandle(false, EventResetMode.AutoReset); static ConcurrentQueue<string> _works = new ConcurrentQueue<string>(); static void Main(string[] args) { Thread t = new Thread(consumerFunc); t.Start(); while (true) { string? text = Console.ReadLine(); if (string.IsNullOrEmpty(text)) { break; } <span style='color: blue; font-weight: bold'>_works.Enqueue(text); _signal.Set();</span> } } private static void doWork(string text) { Console.WriteLine($"echo: {text}"); } private static void consumerFunc(object? obj) { while (true) { <span style='color: blue; font-weight: bold'>_signal.WaitOne(); if (_works.TryDequeue(out var text) == true) { doWork(text); }</span> } } } </pre> <br /> 위의 코드를 보면, "Consumer" 역할을 위해 별도의 스레드를 생성했는데요, 사실 스레드를 하나 별도로 유지하는 게 은근히 귀찮은 작업입니다. (혹은 다중 스레드로 consumer 역할을 하는 경우까지 생각하면 더더욱 귀찮습니다. ^^;) 기왕이면, 기본적으로 활성화되어 있는 CLR ThreadPool에 작업을 맡기는 것도 좋을 텐데요, 바로 이런 처리를 ThreadPool.RegisterWaitForSingleObject로 할 수 있습니다.<br /> <br /> 실제로, 아래의 소스 코드는 ThreadPool.RegisterWaitForSingleObject를 이용해 동일한 기능을 구현한 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Collections.Concurrent; using System.Runtime.Versioning; [assembly: SupportedOSPlatform("windows")] internal class Program { static EventWaitHandle _signal = new EventWaitHandle(false, EventResetMode.AutoReset); static ConcurrentQueue<string> _works = new ConcurrentQueue<string>(); static void Main(string[] args) { <span style='color: blue; font-weight: bold'>RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSingleObject(_signal, doWork, null, -1, false);</span> while (true) { string? text = Console.ReadLine(); if (string.IsNullOrEmpty(text)) { break; } <span style='color: blue; font-weight: bold'>_works.Enqueue(text); _signal.Set();</span> } <span style='color: blue; font-weight: bold'>rwh.Unregister(_signal);</span> } private static void doWork(object? state, bool timedOut) { if (_works.TryDequeue(out var text) == true) { Console.WriteLine($"echo: {text}"); } } } </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;' > AutoReset, ManualReset, Monitor.Wait의 차이 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/1015#4'>https://www.sysnet.pe.kr/2/0/1015#4</a> </pre> <br /> Consumer 역할의 스레드 제거를 우아하게 하기 위해 ManualReset 이벤트까지 활용했어야 하는데 ThreadPool.RegisterWaitForSingleObject를 이용하면 그것마저도 필요가 없습니다. (어찌 보면, <a target='tab' href='https://www.sysnet.pe.kr/2/0/12995'>BlockingCollection<T>을 이용하는 것</a>보다 더 쉽습니다.)<br /> <br /> <hr style='width: 50%' /><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;' > PInvoke 호출을 이용한 비동기 파일 작업 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/1750'>https://www.sysnet.pe.kr/2/0/1750</a> </pre> <br /> 위의 코드는 C#의 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.writeasync?view=net-6.0'>FileStream.WriteAsync</a>를 Win32 API를 이용해 직접 구현한 것입니다. 하지만 아쉽게도 비동기 동작으로 WriteFile API는 호출했지만, WaitHandle.WaitAny를 이용해 대기함으로써 사실상 처리는 동기 방식과 다를 바가 없었습니다.<br /> <br /> 이것을 ThreadPool.RegisterWaitForSingleObject을 이용해 처리하면 BCL의 WriteAsync 메서드와 유사하게 동작을 흉내 낼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.Win32.SafeHandles; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; [assembly: SupportedOSPlatform("windows")] class Program { [DllImport("kernel32.dll", SetLastError = true)] public static extern SafeFileHandle CreateFile( String pipeName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplate); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteFile(SafeFileHandle hFile, byte[] lpBuffer, int nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, [In] ref System.Threading.NativeOverlapped lpOverlapped); public const int FILE_FLAG_OVERLAPPED = 0x40000000; static void Main(string[] args) { string txt = new string('t', 1024 * 1024 * 600); byte[] buf = Encoding.ASCII.GetBytes(txt); File.Delete(@"C:\temp\test.txt"); using (SafeFileHandle pHandle = CreateFile(@"c:\temp\test.txt", (uint)FileAccess.ReadWrite, 0, IntPtr.Zero, (uint)2, <span style='color: blue; font-weight: bold'>FILE_FLAG_OVERLAPPED</span>, IntPtr.Zero)) { uint written; NativeOverlapped o = new NativeOverlapped(); EventWaitHandle writeEvent = new EventWaitHandle(false, EventResetMode.AutoReset); o.EventHandle = writeEvent.SafeWaitHandle.DangerousGetHandle(); if (<span style='color: blue; font-weight: bold'>WriteFile(pHandle, buf, buf.Length, out written, ref o)</span> == false) { int lastError = Marshal.GetLastWin32Error(); if (lastError == 997) // ERROR_IO_PENDING == 997 { OverlappedParameter op = new OverlappedParameter() { Event = writeEvent, }; op.WaitHandle = <span style='color: blue; font-weight: bold'>ThreadPool.RegisterWaitForSingleObject(writeEvent, WriteCompleted, op, -1, false);</span> op.TryUnregister(); } else { // Write File Error Console.WriteLine("Write File Error"); writeEvent.Close(); } } } Console.WriteLine("Press Enter to exit ..."); Console.ReadLine(); } <span style='color: blue; font-weight: bold'>public static void WriteCompleted(object? objState, bool timedOut) { OverlappedParameter? op = objState as OverlappedParameter; if (op == null) { return; } Console.WriteLine("async WriteCompleted"); op.Done(); }</span> } public class OverlappedParameter { [AllowNull] public EventWaitHandle Event { get; set; } = null; [AllowNull] public RegisteredWaitHandle WaitHandle { get; set; } = null; bool _done; bool _disposed; public void Done() { _done = true; TryUnregister(); } public void TryUnregister() { if (_done == false) { return; } lock (Event) { if (_disposed == true) { return; } _disposed = true; WaitHandle.Unregister(Event); Event.Dispose(); } } } </pre> <br /> 오~~~ 좀 그럴 듯하게 바뀌었습니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나, ThreadPool.RegisterWaitForSingleObject로 인해 콜백을 호출하는 스레드는 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12250'>Worker일까요? I/O일까요?</a> ThreadPool.SetMaxThreads를 이용해 간단하게 테스트해 보면 됩니다. ^^ <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ThreadPool.SetMaxThreads 사용법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13058'>https://www.sysnet.pe.kr/2/0/13058</a> </pre> <br /> 각각 다음과 같은 2가지 경우로 실행해 보면 될 텐데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Worker 스레드 수를 최대 1개로 변경 ThreadPool.SetMinThreads(1, 4); ThreadPool.SetMaxThreads(1, 4); </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // I/O 스레드 수를 최대 1개로 변경 ThreadPool.SetMinThreads(4, 1); ThreadPool.SetMaxThreads(4, 1); </pre> <br /> 위와 같은 설정 상태에서 WriteAsync를 2번 연이어 호출하고 콜백 메서드에서는 의도적으로 5초 정도 지연하면 결과를 금방 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static void Main(string[] args) { <span style='color: blue; font-weight: bold'>ThreadPool.SetMinThreads(...[스레드 제약]...); ThreadPool.SetMaxThreads(...[스레드 제약]...);</span> // ...[생략]... <span style='color: blue; font-weight: bold'>WriteAsync(pHandle, buf); // RegisterWaitForSingleObject 호출을 이용한 비동기 파일 Write를 담고 있는 메서드 WriteAsync(pHandle, buf);</span> // ...[생략]... Console.WriteLine("Press Enter to exit ..."); Console.ReadLine(); } public static void WriteCompleted(object? objState, bool timedOut) { Console.WriteLine($"{DateTime.Now} async WriteCompleted"); <span style='color: blue; font-weight: bold'>Thread.Sleep(1000 * 5);</span> // ...[생략]... } </pre> <br /> 우선, Worker 스레드를 1개로 제한했을 때는 이런 식으로 출력이 되었고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 2022-05-14 오전 12:25:<span style='color: blue; font-weight: bold'>42</span> async WriteCompleted Press Enter to exit ... 2022-05-14 오전 12:25:<span style='color: blue; font-weight: bold'>42</span> async WriteCompleted </pre> <br /> I/O 스레드를 1개로 제한했을 때는 이런 식으로 출력이 되었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 2022-05-14 오전 12:26:<span style='color: blue; font-weight: bold'>01</span> async WriteCompleted Press Enter to exit ... 2022-05-14 오전 12:27:<span style='color: blue; font-weight: bold'>06</span> async WriteCompleted </pre> <br /> 즉, ThreadPool.RegisterWaitForSingleObject로 인한 콜백은 I/O 스레드에서 호출되는 것입니다. 그렇다면, 다음과 같이 ThreadPool의 사용법이 정리가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [Worker 스레드에 작업을 할당] ThreadPool.QueueUserWorkItem [I/O 스레드에 작업을 할당] ThreadPool.RegisterWaitForSingleObject </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1932&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2020
(왼쪽의 숫자를 입력해야 합니다.)