Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 7개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 369. ThreadPool.QueueUserWorkItem의 실행 지연
; https://www.sysnet.pe.kr/2/0/1455

.NET Framework: 919. C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법
; https://www.sysnet.pe.kr/2/0/12250

.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue
; https://www.sysnet.pe.kr/2/0/12253

.NET Framework: 2010. C# - ThreadPool.SetMaxThreads 사용법
; https://www.sysnet.pe.kr/2/0/13058

.NET Framework: 2011. C# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법
; https://www.sysnet.pe.kr/2/0/13059




C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법

오늘 우연히 다음의 글을 읽게 되었는데요.

I/O Threads Explained
; https://enterprisecraftsmanship.com/posts/io-threads-explained/

글쓴이는 아래의 코드를 예로 들면서 I/O 스레드 사례를 들고 있습니다.

public void ReadData(string filePath, int byteCount)
{
    byte[] buffer = new byte[byteCount];
    using (FileStream fs = new FileStream(filePath, FileMode.Open))
    {
        fs.Read(buffer, 0, byteCount); // 1
    }
}

I/O thread is started at the line marked as "1". The main thread falls to sleep and waits for the I/O thread to complete. After it’s done, it sends the data back to the main thread. Then the main thread wakes up and continues working.


어떠세요? 설명이 맞는 것 같나요?




그러고 보니, 저도 그동안 I/O 스레드가 언제 발동하는지 궁금했는데... 이참에 한번 정리해봐야겠습니다. ^^

일단, 위에서 설명한 내용은 완전히 틀렸습니다. 실제로 다음과 같이 코딩해서 확인해 보는 것도 가능합니다.

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace TestApp
{
    // x64 + .NET 4.8 + Debug 모드로 빌드
    class Program
    {
        static void Main(string[] args)
        {
            Debug.Assert(ThreadPool.SetMinThreads(2, 0));
            Debug.Assert(ThreadPool.SetMaxThreads(4, 1)); 
            // Worker Thread Max == 4
            // I/O Thread Max == 1

            for (int i = 0; i < 4; i++) 
            {
                ThreadPool.QueueUserWorkItem((arg) =>
                {
                    Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} WorkerThread: " + arg);
                    int bytesRead = ReadFile();
                    Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} WorkerThread: " + arg + $": End - {bytesRead}");

                }, i);
            }

            Console.ReadLine();
        }

        static int ReadFile()
        {
            string filePath = @"...[filepath]...";
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            byte[] buf = new byte[1024 * 1024 * 500]; // 500MB
            int bytesRead = fs.Read(buf, 0, buf.Length);
            fs.Dispose();

            return bytesRead;
        }

        static string GetThreadPoolInfo()
        {
            ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads);
            return $"({workerThreads}, {ioThreads})";
        }

        static int tid => AppDomain.GetCurrentThreadId();
        static int mid => Thread.CurrentThread.ManagedThreadId;
    }
}

"I/O Threads Explained" 글의 설명대로 만약 fs.Read 내부에서 I/O 스레드가 시작한다면, 위의 예제에서는 I/O 스레드의 Max를 1로 설정했으므로 ReadFile 호출에서 병목이 걸렸어야 합니다. 하지만 실행해 보면 다음과 같이,

[2020-06-30 오후 5:51:36] 3160, (0, 1) 3 WorkerThread: 0
[2020-06-30 오후 5:51:36] 12224, (0, 1) 4 WorkerThread: 1
[2020-06-30 오후 5:51:36] 12156, (0, 1) 5 WorkerThread: 2
[2020-06-30 오후 5:51:36] 11740, (0, 1) 6 WorkerThread: 3
[2020-06-30 오후 5:51:40] 12156, (0, 1) 5 WorkerThread: 2: End - 524288000
[2020-06-30 오후 5:51:40] 12224, (0, 1) 4 WorkerThread: 1: End - 524288000
[2020-06-30 오후 5:51:40] 11740, (0, 1) 6 WorkerThread: 3: End - 524288000
[2020-06-30 오후 5:51:40] 3160, (0, 1) 3 WorkerThread: 0: End - 524288000

(managed thread id가) 3, 4, 5, 6인 스레드가 동시에 fs.Read를 수행하는 걸로 봐서 I/O 스레드와는 무관하다는 것을 쉽게 알 수 있습니다.




그렇다면, 혹시 "...Async" 접미사가 붙는 API인 경우 I/O 스레드가 사용될까요? 이것 역시 다음의 코드로 간단하게 테스트할 수 있습니다.

static async Task Main(string[] args)
{
    Debug.Assert(ThreadPool.SetMinThreads(2, 0));
    Debug.Assert(ThreadPool.SetMaxThreads(4, 1));

    Console.WriteLine(Process.GetCurrentProcess().Id);

    for (int i = 0; i < 4; i++)
    {
        ThreadPool.QueueUserWorkItem(async (arg) =>
        {
            Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} WorkerThread: " + arg);
            await ReadFileAsync();
            Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} WorkerThread: ReadFileAsync, " + arg);
        }, i);
    }
    
    Console.ReadLine();
}

static async Task ReadFileAsync()
{
    string filePath = @"...[filepath]...";
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024 * 1024 * 500]; // 500MB

    Task<int> task = fs.ReadAsync(buf, 0, buf.Length);
    int result = task.Result;

    fs.Dispose();
    Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} Done ({result})");
}

실행해 보면, 이번에는 아예 task.Result 호출 상태에서 block이 걸려 이렇게 출력이 됩니다.

[2020-06-30 오후 6:58:23] 23144, (0, 1) 5 WorkerThread: 2
[2020-06-30 오후 6:58:23] 12236, (0, 1) 6 WorkerThread: 3
[2020-06-30 오후 6:58:23] 1932, (0, 1) 4 WorkerThread: 1
[2020-06-30 오후 6:58:23] 15564, (0, 1) 3 WorkerThread: 0

이유는 간단합니다. ReadAsync로 인해 시작한 비동기 Read 작업에 대해 Completed 상태를 알리는 콜백 메서드를 실행해 줄 여유 Worker Thread가 (이미 QueueUserWorkItem으로 Max = 4개인 스레드를 모두 소비하고 있어) 없기 때문입니다. 만약, 해당 콜백 메서드를 I/O 스레드에서 처리하도록 되어 있었다면 1개의 여유 스레드가 있으므로 위의 호출은 hang 상태에 빠지지 말았어야 합니다. (실제로, I/O 스레드의 사용 목적 중 하나가 바로 이런 hang 상태를 방지하는 것입니다.)

하지만, ReadFileAsyc의 호출을 다음과 같이 바꾸면 이번엔 hang 상태에는 빠지지 않습니다.

static async Task ReadFileAsync()
{
    string filePath = @"...[filepath]...";
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024 * 1024 * 500]; // 500MB

    int result = await fs.ReadAsync(buf, 0, buf.Length);

    fs.Dispose();
    Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} Done ({result})");
}

/*
[2020-06-30 오후 9:55:52] 17204, (0, 1) 3 WorkerThread: 0
[2020-06-30 오후 9:55:52] 9944, (0, 1) 6 WorkerThread: 2
[2020-06-30 오후 9:55:52] 21128, (0, 1) 4 WorkerThread: 1
[2020-06-30 오후 9:55:52] 18764, (0, 1) 5 WorkerThread: 3
[2020-06-30 오후 9:55:53] 18764, (1, 1) 5 Done (524288000)
[2020-06-30 오후 9:55:53] 18764, (1, 1) 5 WorkerThread: ReadFileAsync, 3
[2020-06-30 오후 9:55:53] 17204, (1, 1) 3 Done (524288000)
[2020-06-30 오후 9:55:53] 17204, (1, 1) 3 WorkerThread: ReadFileAsync, 0
[2020-06-30 오후 9:55:53] 9944, (1, 1) 6 Done (524288000)
[2020-06-30 오후 9:55:53] 9944, (2, 1) 6 WorkerThread: ReadFileAsync, 2
[2020-06-30 오후 9:55:53] 18764, (3, 1) 5 Done (524288000)
[2020-06-30 오후 9:55:53] 18764, (3, 1) 5 WorkerThread: ReadFileAsync, 1
*/

왜냐하면, async/await 동작의 특성으로 await ReadAsync를 호출한 스레드는 - 여기서는 Worker 스레드이므로 ThreadPool에 반환되므로, 이후 Completed 상태를 알리는 콜백 메서드가 ThreadPool의 여유 스레드를 이용해 호출을 이어나가기 때문입니다. 즉, await ReadAsync 상황에서도 I/O 스레드는 여전히 사용되지 않고 있습니다.




도대체, 그럼 언제 I/O 스레드가 사용되는 걸까요? 잠시 시간을 거슬러서 APM(Asynchronous Programming Model) 패턴으로 호출하던 때로 올라가 보겠습니다. APM 패턴을 이용해 이전 예제를 다시 작성해도,

static void Main(string[] args)
{
    Debug.Assert(ThreadPool.SetMinThreads(2, 0));
    Debug.Assert(ThreadPool.SetMaxThreads(4, 1));

    for (int i = 0; i < 4; i++)
    {
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} WorkerThread: " + arg);
            ReadFileBeginEnd();
        }, i);
    }

    Console.ReadLine();
}

static void ReadFileBeginEnd()
{
    string filePath = @"...[filepath]...";
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

    byte[] buf = new byte[1024 * 1024 * 500];

    IAsyncResult result = fs.BeginRead(buf, 0, buf.Length, (ar) =>
    {
        Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} callback-Start");
        Thread.Sleep(1000 * 5);
        Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} callback-End");
        int bytesRead = fs.EndRead(ar);
    }, null);

    result.AsyncWaitHandle.WaitOne();
}

마찬가지로 I/O 스레드를 사용하지 않아 (Max == 4개의 Worker 스레드를 모두 소모해) hang 현상을 겪는 출력이 나옵니다.

[2020-06-30 오후 10:00:53] 11516, (0, 1) 4 WorkerThread: 0
[2020-06-30 오후 10:00:53] 10712, (0, 1) 5 WorkerThread: 1
[2020-06-30 오후 10:00:53] 14752, (0, 1) 6 WorkerThread: 3
[2020-06-30 오후 10:00:53] 19304, (0, 1) 3 WorkerThread: 2

자... 이제 ^^ 정답이 나올 시간이군요. I/O 스레드를 사용하려면 명시적으로 해당 파일이 비동기를 사용하겠다고 명시를(Win32의 경우 FILE_FLAG_OVERLAPPED 지정) 해야만 합니다. 그래서, 위의 모든 예제들에서, FileStream의 6번째 인자에 대해 true를 줘야만 하고,

FileStream fs = new FileStream(filePath, FileMode.Open, 
            FileAccess.Read, FileShare.ReadWrite, 4096, true);

이제 실행해 보면 다음과 같은 출력 결과를 얻습니다.

[2020-06-30 오후 10:14:49] 15468, (0, 1) 3 WorkerThread: 1
[2020-06-30 오후 10:14:49] 13472, (0, 1) 4 WorkerThread: 0
[2020-06-30 오후 10:14:49] 20716, (0, 1) 5 WorkerThread: 2
[2020-06-30 오후 10:14:49] 24668, (0, 1) 6 WorkerThread: 3
[2020-06-30 오후 10:14:50] 17080, (1, 0) 7 callback-Start
[2020-06-30 오후 10:14:55] 17080, (1, 0) 7 callback-End
[2020-06-30 오후 10:14:55] 17080, (1, 0) 7 callback-Start
[2020-06-30 오후 10:15:00] 17080, (2, 0) 7 callback-End
[2020-06-30 오후 10:15:00] 17080, (1, 0) 7 callback-Start
[2020-06-30 오후 10:15:05] 17080, (3, 0) 7 callback-End
[2020-06-30 오후 10:15:05] 17080, (3, 0) 7 callback-Start
[2020-06-30 오후 10:15:10] 17080, (4, 0) 7 callback-End

BeginRead에 전달한 익명 콜백 메서드를 (I/O 스레드를 Max = 1로 설정했으므로) 단일하게 17080 스레드가 호출하는 것을 볼 수 있고, 처음으로 "(1, 0)"으로 (여유 I/O 스레드의 수를 나타내는) 두 번째 인자가 0으로 떨어졌습니다.

참고로, ReadFileBeginEnd 내부를 다음과 같이 Task.Factory.FromAsync로 바꾸면,

static async void ReadFileBeginEnd()
{
    string filePath = @"...[filepath]...";
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1024, true);
    byte[] buf = new byte[1024 * 1024 * 500]; // 500MB

    Task<int> task = Task<int>.Factory.FromAsync(fs.BeginRead, fs.EndRead, buf, 0, buf.Length, null);
    Console.WriteLine($"[{DateTime.Now}] {tid}, {GetThreadPoolInfo()} {mid} Done - {task.Result}");
    fs.Dispose();
}

이번엔 출력이 이렇게 바뀝니다.

[2020-06-30 오후 10:29:06] 18812, (0, 1) 6 WorkerThread: 1
[2020-06-30 오후 10:29:06] 22324, (0, 1) 4 WorkerThread: 0
[2020-06-30 오후 10:29:06] 22720, (0, 1) 5 WorkerThread: 2
[2020-06-30 오후 10:29:06] 25512, (0, 1) 3 WorkerThread: 3
[2020-06-30 오후 10:29:07] 25512, (0, 0) 3 Done - 524288000
[2020-06-30 오후 10:29:07] 18812, (0, 0) 6 Done - 524288000
[2020-06-30 오후 10:29:07] 22324, (0, 0) 4 Done - 524288000
[2020-06-30 오후 10:29:07] 22720, (1, 1) 5 Done - 524288000

분명히 I/O 스레드가 사용은 되었는데, 스레드 ID로 봐서는 "... Done ..." 메시지를 출력한 것은 I/O 스레드가 아니라 Worker 스레드입니다. 그 이유는, I/O 스레드가 수행한 것은 FromAsync(fs.BeginRead, fs.EndRead)에서 fs.EndRead 수행에만 관여를 했기 때문입니다. 즉, I/O 스레드에서는 EndRead 호출로 빠르게 완료 처리를 하고 이후의 실행에 대해서는 Worker 스레드로 맡긴 것입니다.

이런 식으로 I/O 스레드는 최소한의 일을 하고 다른 작업은 Worker 스레드에 맡기는 것은 아래와 같은 글에서도 언급하고 있는 내용입니다.

Simple description of worker and I/O threads in .NET
; https://stackoverflow.com/questions/2099947/simple-description-of-worker-and-i-o-threads-in-net

The developer does need to take some care when handling an I/O callback in order to ensure that the I/O thread is returned to the ThreadPool -- that is, I/O callback code should do the minimum work required to service the callback and then return control of the thread to the CLR threadpool. If more work is required, that work should be scheduled on a worker thread. Otherwise, the application risks 'hijacking' the CLR's pool of reserved I/O completion threads for use as normal worker threads, leading to the deadlock situation described above.


자... 그럼 정리해 보면, 관련 커널 자원(파일, Named Pipe, 소켓 등)을 어떻게 열었느냐에 따라 비동기 메서드를 호출해도 처리가 달라집니다.

동기 모드로 연 경우:
    - 비동기 코드(예: FileStream.BeginRead)를 수행해도, I/O 작업을 수행하는 동기 코드(예: FileStream.Read)를 ThreadPool의 Worker 스레드에서 호출
    - I/O 완료 후 콜백 코드를 ThreadPool의 Worker 스레드에서 호출

비동기 모드로 연 경우:
    - 비동기 코드(예: FileStream.BeginRead)를 수행하고 곧바로 호출자 스레드로 제어 반환
    - I/O 완료 후 콜백 코드를 ThreadPool의 I/O 스레드에서 호출

그렇습니다. 커널 자원을 명시적으로 비동기 모드로 열어야 우리가 원래 바랐던 진정한 비동기 I/O 처리가 이뤄지는 것입니다. (휴~~~ 정리하고 나니 속이 다 시원하군요. ^^)

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




첨언해 보면, Worker 스레드와 I/O 스레드는 운영체제 차원에서 구분되는 것이 아닌, .NET에서 구현한 2가지 타입의 ThreadPool 중 어디에 속해 있느냐에 따라 나뉩니다. 이에 대해서는 아래의 글에서,

ThreadPool.UnsafeQueueNativeOverlapped
; https://learn.microsoft.com/en-us/archive/blogs/junfeng/threadpool-unsafequeuenativeoverlapped

Junfeng Zhang's Musing - ThreadPool.BindHandle
; https://learn.microsoft.com/en-us/archive/blogs/junfeng/threadpool-bindhandle

CLR’s thread pool has two pools of threads. The first pool is used by ThreadPool.QueueUserWorkItem. The second pool is an IoCompletionPort thread pool used by ThreadPool.BindHandle and ThreadPool.UnsafeQueueNativeOverlapped.


설명하고 있는데, I/O 스레드는 Worker 스레드에 비해 내부적으로 IOCP(IO Completion Port)와 바인딩하는 코드가 더 추가되는 것입니다. 관련 글의 내용을 (혹시 없어질 사태를 대비해) 아래에 그대로 복붙했으니 참고하세요. ^^

CLR’s thread pool has two pools of threads. The first pool is used by ThreadPool.QueueUserWorkItem. The second pool is an IoCompletionPort thread pool used by ThreadPool.BindHandle and ThreadPool.UnsafeQueueNativeOverlapped.

ThreadPool.BindHandle is used by CLR to implement asynchronous IO. For example, FileStream uses it to implement BeginRead/BeginWrite. Developers can take advantage of it too. We will talk about that in a separate article.

ThreadPool.UnsafeQueueNativeOverlapped can be used to queue a non IO work item to the IoCompletionPort thread pool, just like ThreadPool.QueueUserWorkItem.

Why will you want to use ThreadPool.UnsafeQueueNativeOverlapped instead of ThreadPool.QueueUserWorkItem?

In our development, we discover an inefficiency of ThreadPool.QueueUserWorkItem. If we have some alternate high and low number of work items, some of the threads may do busy waiting, artificially increase the CPU usage of our application.

If you have the same pattern, and you have observed high CPU usage when it should not, you can try ThreadPool.UnsafeQueueNativeOverlapped.

The following is an example how to ThreadPool.UnsafeQueueNativeOverlapped.

using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace UQNO
{
    internal class AsyncHelper
    {
        WaitCallback callback;
        object state;
        internal AsyncHelper(WaitCallback callback, object state)
        {
            this.callback = callback;
            this.state = state;
        }
        unsafe internal void Callback(uint errorCode, uint numBytes, NativeOverlapped* _overlapped)
        {
            try
            {
                this.callback(this.state);
            }
            finally
            {
                Overlapped.Free(_overlapped);
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            ManualResetEvent wait = new ManualResetEvent(false);
            WaitCallback callback = delegate(object state)
            {
                Console.WriteLine("callback is executed in thread id {0} name {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Name);
                ManualResetEvent _wait = (ManualResetEvent)state;
                _wait.Set();
            };
            AsyncHelper ah = new AsyncHelper(callback, wait);
            unsafe
            {
                Overlapped overlapped = new Overlapped();
                NativeOverlapped* pOverlapped = overlapped.Pack(ah.Callback, null);
                ThreadPool.UnsafeQueueNativeOverlapped(pOverlapped);
                wait.WaitOne();
            }
        }
    }
}


I mentioned that we can use ThreadPool.BindHandle to implement asynchronous IO. Here are roughly the steps necessary to make it happen:

1. Create an overlapped file handle

        SafeFileHandle handle = CreateFile(
                            filename,
                            Win32.GENERIC_READ_ACCESS,
                            Win32.FILE_SHARE_READ | Win32.FILE_SHARE_WRITE | Win32.FILE_SHARE_DELETE,
                            (IntPtr)null,
                            Win32.OPEN_EXISTING,
                            Win32.FILE_FLAG_OVERLAPPED,
                            new SafeFileHandle(IntPtr.Zero, false));

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern SafeFileHandle CreateFile(
           string lpFileName,
           uint dwDesiredAccess,
           uint dwShareMode,
            //SECURITY_ATTRIBUTES lpSecurityAttributes,
           IntPtr lpSecurityAttributes,
           uint dwCreationDisposition,
           int dwFlagsAndAttributes,
           SafeFileHandle hTemplateFile);

2. Bind the handle to thread pool.
            if (!ThreadPool.BindHandle(handle))
            {
                Console.WriteLine("Fail to BindHandle to threadpool.");
                return;
            }

3. Prepare your asynchronous IO callback.

                byte[] bytes = new byte[0x8000];
                IOCompletionCallback iocomplete = delegate(uint errorCode, uint numBytes, NativeOverlapped* _overlapped)
                {
                    unsafe
                    {
                        try
                        {
                            if (errorCode == Win32.ERROR_HANDLE_EOF)
                                Console.WriteLine("End of file in callback.");

                            if (errorCode != 0 && numBytes != 0)
                            {
                                Console.WriteLine("Error {0} when reading file.", errorCode);
                            }
                            Console.WriteLine("Read {0} bytes.", numBytes);
                        }
                        finally
                        {
                            Overlapped.Free(pOverlapped);
                        }
                    }
                };

4. Create a NativeOverlapped* pointer.

        Overlapped overlapped = new Overlapped();
        NativeOverlapped* pOverlapped = overlapped.Pack(iocomplete, bytes);
        pOverlapped->OffsetLow = (int)offset;

5. Call the asynchronous IO API and pass the NativeOverlapped * to it.

        fixed (byte* p = bytes)
        {
            r = ReadFile(handle, p, bytes.Length, IntPtr.Zero, pOverlapped);
            if (r == 0)
            {
                r = Marshal.GetLastWin32Error();
                if (r == Win32.ERROR_HANDLE_EOF)
                {
                    Console.WriteLine("Done.");
                    break;
                }
                if (r != Win32.ERROR_IO_PENDING)
                {
                    Console.WriteLine("Failed to read file. LastError is {0}", Marshal.GetLastWin32Error());
                    Overlapped.Free(pOverlapped);
                    return;
                }
            }
        }

        [DllImport("KERNEL32.dll", SetLastError = true)]
        unsafe internal static extern int ReadFile(
            SafeFileHandle handle,
            byte* bytes,
            int numBytesToRead,
            IntPtr numBytesRead_mustBeZero,
            NativeOverlapped* overlapped);

Your IO callback will be invoked by CLR thread when the IO completed.

So when should you use ThreadPool.BindHandle? The answer is almost *Never*. .Net Framework's FileStream class internally uses ThreadPool.BindHandle to implement the async IO. You should always use FileStream if possible.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 5/19/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2020-07-01 07시30분
[쿠드] 좋은글감사합니다.
[guest]

... 151  152  153  154  155  156  157  158  159  [160]  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1048정성태5/27/201132230개발 환경 구성: 123. Apache 소스를 윈도우 환경에서 빌드하기
1047정성태5/27/201126083.NET Framework: 217. Firebird ALinq Provider - 날짜 필드에 대한 낙관적 동시성 쿼리 오류
1046정성태5/26/201130727.NET Framework: 216. 라이선스까지도 뛰어넘는 .NET Profiler [5]
1045정성태5/24/201131828.NET Framework: 215. 닷넷 System.ComponentModel.LicenseManager를 이용한 라이선스 적용 [1]파일 다운로드1
1044정성태5/24/201132386오류 유형: 122. zlib 빌드 오류 - inflate.obj : error LNK2001: unresolved external symbol _inflate_fast
1043정성태5/24/201131328.NET Framework: 214. 무료 Linq Provider - DbLinq를 이용한 Firebird 접근파일 다운로드1
1042정성태5/23/201137671개발 환경 구성: 122. PHP 소스를 윈도우 환경에서 빌드하기
1041정성태5/22/201128601.NET Framework: 213. Linq To SQL - ALinq Provider를 이용하여 Firebird 사용파일 다운로드1
1040정성태5/21/201138934개발 환경 구성: 121. .NET 개발자가 처음 설치해 본 Apache + PHP [2]
1039정성태5/17/201131614.NET Framework: 212. Firebird 데이터베이스와 ADO.NET [2]파일 다운로드1
1038정성태5/16/201133599개발 환경 구성: 120. .NET 프로그래머에게도 유용한 Firebird 무료 데이터베이스 [2]
1037정성태5/11/201128416개발 환경 구성: 119. Visual Studio Professional 이하 버전에서도 TFS의 정적 코드 분석 정책 연동이 가능할까? [3]
1036정성태5/7/201194244오류 유형: 121. Access DB에 대한 32bit/64bit OLE DB Provider 관련 오류 [11]
1035정성태5/7/201128973오류 유형: 120. File cannot be opened. Ensure it is a valid Data Link file.
1034정성태5/2/201126050.NET Framework: 211. 파일 잠금 없이 .NET 어셈블리의 버전을 구하는 방법 [2]파일 다운로드1
1033정성태5/1/201131742웹: 19. IIS Express - appcmd.exe를 이용한 applicationHost.config 변경 [2]
1032정성태5/1/201128380웹: 18. IIS Express를 NT 서비스로 변경
1031정성태4/30/201129540웹: 17. IIS Express - "IIS Installed Versions Manager Interface"의 IIISExpressProcessUtility 구하는 방법 [1]파일 다운로드1
1030정성태4/30/201151798개발 환경 구성: 118. IIS Express - localhost 이외의 호스트 이름으로 접근하는 방법 [4]파일 다운로드1
1029정성태4/28/201140922개발 환경 구성: 117. XCopy에서 파일/디렉터리 확인 질문 없애기 [2]
1028정성태4/27/201138310오류 유형: 119. Visual Studio 2010 SP1 설치 후 Windows Phone 개발자 도구로 인한 재설치 문제 [3]
1027정성태4/25/201127491디버깅 기술: 40. 상황별 GetFunctionPointer 반환값 정리 - x86파일 다운로드1
1026정성태4/25/201145780디버깅 기술: 39. DebugDiag 1.1을 사용한 덤프 분석 [7]
1025정성태4/24/201127851개발 환경 구성: 116. IIS 7 관리자 - Active Directory Certification Authority로부터 SSL 사이트 인증서 받는 방법 [2]
1024정성태4/22/201129188오류 유형: 118. Windows 2008 서버에서 Event Viewer / PowerShell 실행 시 비정상 종료되는 문제 [1]
1023정성태4/20/201130070.NET Framework: 210. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 [1]
... 151  152  153  154  155  156  157  158  159  [160]  161  162  163  164  165  ...