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)
13497정성태12/22/20232290오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232299Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232295Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232488Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232523닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232257개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232218Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232323개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232073개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20231992오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232292개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232092개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20231986오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232058개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232193닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232725닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232173개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232481개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232206개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232409닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232141닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232200닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232068개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232261닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232080C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232129Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...