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]

1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13324정성태4/17/20234255.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234150개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20234947VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20233746개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233747개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234204개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234016개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234169오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233495오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233688Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20233766개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20233860개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20233830Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234325C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20233890C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234057.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20233949스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20233733.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20233571Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20233918Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234280VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20233592Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234224Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/20234335Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션파일 다운로드1
13300정성태3/28/20233961Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/20233737Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...