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]

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12845정성태10/6/20218096.NET Framework: 1120. C# - BufferBlock<T> 사용 예제 [5]파일 다운로드1
12844정성태10/3/20216129오류 유형: 764. MSI 설치 시 "... is accessible and not read-only." 오류 메시지
12843정성태10/3/20216587스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/202124792오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/20218375스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/20218424.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/20219492.NET Framework: 1118. C# 11 - 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/20219146.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/20218086VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/20217667Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/20218946.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가 [2]파일 다운로드1
12834정성태9/7/20217318오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/20216771VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/20217608VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/20216149VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/20218365오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/202110016.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/20218145.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/20218127스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/202110371.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [1]파일 다운로드1
12825정성태9/3/20217925개발 환경 구성: 603. GoLand - WSL 환경과 연동
12824정성태9/2/202117000오류 유형: 760. 파이썬 tensorflow - Dst tensor is not initialized. 오류 메시지
12823정성태9/2/20216737스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법
12822정성태9/1/202111945오류 유형: 759. 파이썬 tensorflow - ValueError: Shapes (...) and (...) are incompatible [2]
12821정성태9/1/20217494.NET Framework: 1112. C# - .NET 6부터 공개된 ISpanFormattable 사용법
12820정성태9/1/20217799VC++: 147. Golang - try/catch에 대응하는 panic/recover [1]파일 다운로드1
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...