Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 8개 있습니다.)

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.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/28/2023]

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

비밀번호

댓글 작성자
 



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

... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13023정성태4/6/20226658.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅 [3]
13022정성태3/31/20226549Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/20226710Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/20226442.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/20226432.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 구하고 싶다면?파일 다운로드1
13018정성태3/29/20226952.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/20226794.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/20227646.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/20228990.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/20227595VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/20225923개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
13012정성태3/21/20225277오류 유형: 803. C# - Local '...' or its members cannot have their address taken and be used inside an anonymous method or lambda expression
13011정성태3/21/20226754오류 유형: 802. 윈도우 운영체제에서 웹캠 카메라 인식이 안 되는 경우
13010정성태3/21/20225703오류 유형: 801. Oracle.ManagedDataAccess.Core - GetTypes 호출 시 "Could not load file or assembly 'System.DirectoryServices.Protocols...'" 오류
13009정성태3/20/20227255개발 환경 구성: 640. docker - ibmcom/db2 컨테이너 실행
13008정성태3/19/20226544VS.NET IDE: 176. 비주얼 스튜디오 - 솔루션 탐색기에서 프로젝트를 선택할 때 csproj 파일이 열리지 않도록 만드는 방법
13007정성태3/18/20226172.NET Framework: 1181. C# - Oracle.ManagedDataAccess의 Pool 및 그것의 연결 개체 수를 알아내는 방법파일 다운로드1
13006정성태3/17/20227233.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
13005정성태3/17/20226088오류 유형: 800. C# - System.InvalidOperationException: Late bound operations cannot be performed on fields with types for which Type.ContainsGenericParameters is true.
13004정성태3/16/20226112디버깅 기술: 182. windbg - 닷넷 메모리 덤프에서 AppDomain에 걸친 정적(static) 필드 값을 조사하는 방법
13003정성태3/15/20226260.NET Framework: 1179. C# - (.NET Framework를 위한) Oracle.ManagedDataAccess 패키지의 성능 카운터 설정 방법
13002정성태3/14/20227020.NET Framework: 1178. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 http_multiclient.c 예제 포팅
13001정성태3/13/20227405.NET Framework: 1177. C# - 닷넷에서 허용하는 메서드의 매개변수와 호출 인자의 최대 수
13000정성태3/12/20226966.NET Framework: 1176. C# - Oracle.ManagedDataAccess.Core의 성능 카운터 설정 방법
12999정성태3/10/20226475.NET Framework: 1175. Visual Studio - 프로젝트 또는 솔루션의 Clean 작업 시 응용 프로그램에서 생성한 파일을 함께 삭제파일 다운로드1
12998정성태3/10/20226082.NET Framework: 1174. C# - ELEMENT_TYPE_FNPTR 유형의 사용 예
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...