Microsoft MVP성태의 닷넷 이야기
.NET Framework: 631. async/await에 대한 "There Is No Thread" 글의 부가 설명 [링크 복사], [링크+제목 복사]
조회: 20238
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 7개 있습니다.)
.NET Framework: 394. async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

.NET Framework: 512. async/await 사용 시 hang 문제가 발생하는 경우 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/10801

.NET Framework: 631. async/await에 대한 "There Is No Thread" 글의 부가 설명
; https://www.sysnet.pe.kr/2/0/11129

.NET Framework: 720. 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미
; https://www.sysnet.pe.kr/2/0/11418

.NET Framework: 721. WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요?
; https://www.sysnet.pe.kr/2/0/11419

디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
; https://www.sysnet.pe.kr/2/0/13563

닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
; https://www.sysnet.pe.kr/2/0/13573




async/await에 대한 "There Is No Thread" 글의 부가 설명

커뮤니티에 다음과 같은 글이 있군요.

스레드풀이 없는상태로 Task, Async, Await를 사용하면 
; http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_QnA_etc&page=1&page_num=35&select_arrange=last_comment&desc=desc&sn=off&ss=on&sc=on&keyword=&no=4676&category=

답글에 보면, 다음의 원문 글을 인용하면서 "스레드 따로 사용하지 않습니다."라고 합니다.

There Is No Thread
; http://blog.stephencleary.com/2013/11/there-is-no-thread.html

그리곤 다시 한번 저 글을 인용하면서 아래의 글을 썼습니다.

C# Async/Await 의 진실 
; http://www.gamecodi.com/board/zboard-id-GAMECODI_Talkdev-no-4470-z-20.htm

물론, await 자체는 스레드를 따로 사용하지 않는 것이 맞습니다. 하지만, await 자체보다는 이후의 분리된 코드들에 대한 처리를 고려했을 때 무조건 "스레드 따로 사용하지 않습니다."라고 하는 것은 오해의 소지가 있습니다.

그런 의미에서 ^^ "There Is No Thread" 글의 의미를 파헤쳐 보겠습니다.




시작하기에 앞서, C#의 async/await 키워드에 대한 정리부터 하겠습니다. 제 책(시작하세요! C# 6.0 프로그래밍)의 667페이지에도 나오지만 async 키워드는 await 키워드 때문에 나온 부가적인 예약어입니다. 예를 들어, C# 4.0까지의 문법으로 작성한 다음과 같은 코드가 있을 때,

void func()
{
    int await = 5;
}

이를 await 예약어가 추가된 C# 5.0으로 빌드하면 예약어를 식별자에 사용했으므로 오류가 발생하게 됩니다. 그래서 이러한 문제를 해결하기 위해 async 예약어를 (원래는 필요 없을 텐데도) 추가적으로 도입을 한 것입니다. 덕분에 위의 func 코드는 C# 5.0에서도 정상적으로 빌드가 가능합니다. 즉, await 예약어가 C# 5.0으로 하여금 식별자가 아닌 예약어로써 취급하라는 정보가 바로 async 예약어인 것입니다. 따라서 다음과 같이 빌드하면 이제는 오류가 발생합니다.

async void func()
{
    int await = 5; // 컴파일 오류 Error	CS4003	'await' cannot be used as an identifier within an async method or lambda expression
}

이에 기반을 둬서, "C# Async/Await 의 진실" 글에 글쓴이가 인용한 Q&A 글을 봐야 합니다.

질문: Does the “async” keyword cause the invocation of a method to queue to the ThreadPool? To create a new thread? To launch a rocket ship to Mars?

답변: No. No. And no. See the previous questions. The “async” keyword indicates to the compiler that “await” may be used inside of the method, such that the method may suspend at an await point and have its execution resumed asynchronously when the awaited instance completes. This is why the compiler issues a warning if there are no “awaits” inside of a method marked as “async”.


위의 질문 답변을 명확하게 하고 싶다면 "async"라는 예약어를 C# 언어 개발자들이 "enable_await"라고 이름 지었다고 가정하면 편합니다. (사실, 원래부터 그렇게 지었으면 오해가 덜 했을 것입니다. 또는 C# 1.0부터 await을 제공했다면 async 예약어는 없었을 것입니다.) 이런 가정 아래 다시 질문/답변을 해석해 보면 더 쉽게 이해가 됩니다.

질문: Does the “enable_await” keyword cause the invocation of a method to queue to the ThreadPool? To create a new thread? To launch a rocket ship to Mars?

답변: No. No. And no. See the previous questions. The “enable_await” keyword indicates to the compiler that “await” may be used inside of the method, such that the method may suspend at an await point and have its execution resumed asynchronously when the awaited instance completes. This is why the compiler issues a warning if there are no “awaits” inside of a method marked as “enable_await”.


그러니까, async 예약어가 부여되었다고 해서 해당 메서드("async void func")가 "create a new thread" 하거나 ThreadPool에 들어가는 것이 아닙니다. 하지만 그렇다고 해서 await 예약어까지 스레드와 상관없다는 의미는 아닙니다.

자, 그럼 논란의 중심이 되었던 글로 가볼까요?

There Is No Thread
; http://blog.stephencleary.com/2013/11/there-is-no-thread.html

위의 이야기는 장치에 비동기 쓰기 작업을 하는 코드로 시작합니다.

private async void Button_Click(object sender, RoutedEventArgs e)
{
  byte[] data = ...
  await myDevice.WriteAsync(data, 0, data.Length);
}

그러면서 질문을 던집니다.

We already know that the UI thread is not blocked during the await. Question: Is there another thread that must sacrifice itself on the Altar of Blocking so that the UI thread may live?


그리곤, 장치에 대한 비동기 작업이 이뤄졌을 때의 호출 순서를 Device Driver까지 내려가면서 설명합니다. 위의 내용을 다음의 실제 사례 코드로 설명을 해보겠습니다.

async void ReadFile()
{
    string filePath = typeof(Program).Assembly.Location;
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024];
    await fs.ReadAsync(buf, 0, buf.Length);
}

ReadAsync 코드 호출은 비동기로 이뤄지지만 그 ReadAsync 자체의 호출은 "async void ReadFile" 메서드를 호출하는 스레드에서 이뤄집니다. 이후 Win32 비동기(Overlapped I/O) 절차를 따르는 ReadFile 메서드가 호출되고 이후부터는 "async void ReadFile" 메서드를 호출했던 스레드는 더 이상 I/O에 관여하지 않고 벗어나게 됩니다. 나머지 작업은 이제 커널 단에서 이뤄지는 데 I/O Request Packet(IRP)가 구성된 후 Device Driver 단을 따라 흐르게 되고 최종적으로 Device Driver는 Read 신호를 "하드 디스크"에 전송하고 끝을 냅니다.

여기까지 봤을 때, async/await로 인해 별도의 스레드가 관여하고 있는 것은 없습니다. 이에 대한 설명을 "There Is No Thread" 글에서는 다음과 같이 하고 있는 것입니다.

The write operation is now “in flight”. How many threads are processing it?

None.

There is no device driver thread, OS thread, BCL thread, or thread pool thread that is processing that write operation. There is no thread.


그런 다음, 하드 디스크의 읽기 작업이 완료되면 인터럽트를 발생하게 되고, CPU는 다시 Device Driver로 하여금 해당 인터럽트를 처리하도록 합니다. 그렇게 물리 장치에서 읽은 데이터를 처리한 다음 Device Driver는 Win32 Overlapped I/O에서 정의한 핸들을 Signaled 상태로 만듭니다. 만약 await 이후 코드가 없다면 Signaled 되었을 때 별다르게 할 일이 없으므로 결국 async/await은 별도의 스레드 관여 없이 끝나게 됩니다.

"There Is No Thread" 글의 저자가 말하고 싶었던 것은 명확합니다. async/await 자체로는 스레드 관여가 없다는 것입니다.




하지만, 이것을 오해해서 await의 사용 이후의 코드까지 스레드 관여가 없다고 생각하면 안 됩니다. 가령, 다음과 같이 await 이후의 코드를 작성해 보면,

async void ReadFile()
{
    string filePath = typeof(Program).Assembly.Location;
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024];
    await fs.ReadAsync(buf, 0, buf.Length);

    fs.Dispose();
    Console.WriteLine("I/O Done");
}

Read 작업의 완료로 인해 "Signaled" 상태가 되었을 때 ThreadPool은 유휴 스레드를 하나 할당받아 fs.Dispose, Console.WriteLine 메서드의 호출 코드를 수행합니다. 이것은 테스트 코드로도 간단하게 확인할 수 있습니다.

static void Main(string[] args)
{
    ThreadPool.SetMinThreads(4, 4);
    ThreadPool.SetMaxThreads(4, 4); // 4개로 제한

    for (int i = 0; i < 4; i++)  // 미리 4개의 스레드 풀의 유휴 스레드를 소진하고
    {
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            Console.WriteLine("WorkerThread: " + arg);
            Thread.Sleep(1000 * 5);  // 5초 동안 대기하므로 그동안에는 스레드 풀의 유휴 스레드가 없음!

        }, i);
    }

    Thread.Sleep(1000);

    ReadFile();

    Console.ReadLine();
}

static async void ReadFile()
{
    string filePath = typeof(Program).Assembly.Location;
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024];
    await fs.ReadAsync(buf, 0, buf.Length);

    fs.Dispose(); // 이후의 코드는 스레드 풀의 유휴 스레드에서 실행되는 데 5초 동안은 여유 스레드가 없으므로 
                  // 실행되지 않고 블록킹이 됨.
    Console.WriteLine("Done");
}

위의 코드를 보면, 스레드 풀의 여유 스레드를 모두 사용하고 있는 바람에 await 이후의 분리된 코드가 비동기 I/O 수행이 완료된 후에도 수행하지 않고 대기하게 됩니다. 그리곤 약 5초 후에 ThreadPool.QueueUserWorkItem의 작업들이 풀리면서 fs.Dispose, Console.WriteLine 메서드가 비로소 호출이 됩니다.

이 역시 "There Is No Thread" 글의 저자는 다음과 같이 언급하고 있습니다.

So, we see that there was no thread while the request was in flight. When the request completed, various threads were “borrowed” or had work briefly queued to them.





당연한 이야기지만, 이 때문에 I/O 작업이 아닌 비동기 처리는 반드시 스레드를 수반하게 되어 있습니다. 가령, 사용자 코드를 다음과 같이 비동기 처리할 수 있습니다.

async void func()
{
    await Task.Run((Func<int>)userWork);
}

int userWork()
{
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    return 5;
}

위와 같이 하게 되면, 운영체제의 I/O 작업이 아닌 순수 사용자 코드를 실행하는 것이므로 CPU가 일을 해야 하고 당연히 그 대상은 스레드가 됩니다. 물론, 저 코드를 보면 await이 스레드를 생성한 것은 아니고 Task.Run의 호출로 된 것입니다. 하지만, 여기서도 await의 진정한 가치는 await 이후의 분리된 코드라는 점을 감안해야 합니다. 즉, 위와 같이 await을 사용하는 것은 아무 의미가 없고 차라리 다음과 같이 하는 것과 다를 바가 없습니다.

void func()
{
    Task.Run((Func<int>)userWork);
}

int userWork()
{
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    return 5;
}

위와 같은 상황이라면 async/await 예약어가 나올 필요가 없는 것입니다. 애당초 await의 매력은 다음과 같이 코드를 작성할 수 있다는 데에 있습니다.

async void func()
{
    int result = await Task.Run((Func<int>)userWork);
    Console.WriteLine(result);
}

int userWork()
{
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    return 5;
}

그리고, 개발자들의 관심은 int result = ..., Console.WriteLine(result)의 작업이 어디서 이뤄지느냐에 있는 것입니다. 대개의 경우, 그 작업에는 별도의 스레드가 반드시 관여하게 됩니다. (그런데, 왜 "대개의 경우"일까요? 그 이유가 잠시 후에 밝혀집니다.)




그런데, "There Is No Thread" 글에 보면 다음과 같은 설명이 나옵니다.

The task has captured the UI context, so it does not resume the async method directly on the thread pool thread. Instead, it queues the continuation of that method onto the UI context, and the UI thread will resume executing that method when it gets around to it.


엄밀히 말하자면, await 이후의 작업을 기본적으로 ThreadPool의 유휴 스레드에 무조건 맡기는 것은 아닙니다. 그전에, 해당 작업을 위한 호출 스레드에 SynchronizationContext 환경이 있는지를 보고, 제공되고 있다면 SynchronizationContext와 연관된 스레드에 await 이후의 작업을 맡기는 것입니다. SynchronizationContext가 제공되는 대표적인 환경이 바로 User Interface를 갖는 WinForm이나 WPF입니다. 각각 WindowsFormsSynchronizationContext, DispatcherSynchronizationContext라는 SynchronizationContext 환경을 제공합니다.

이것이 왜 필요하냐면?

예를 들면, UI를 갖는 프로그램에서 버튼 클릭에 대해 다음과 같이 반응하는 코드를 작성했다고 가정하겠습니다.

private async void Button_Click(object sender, RoutedEventArgs e)
{
    string text = await ReadTextFile();
    txtContents.Text = text;
}

보는 바와 같이, await 이후의 코드에 User Interface(위의 경우 txtContents 텍스트 컨트롤)와 상호 연동하는 코드가 있습니다. 잘 알려진 데로, UI 요소의 접근은 반드시 그 UI를 생성한 스레드에서만 해야 합니다. 하지만, 위와 같이 await으로 하게 되면 "txtContents.Text = text"라는 코드가 ThreadPool의 유휴 스레드에서 담당하게 됩니다. 따라서 오류로 연결되는데요.

마이크로소프트는 클라이언트 개발자들의 편의를 위해 이런 경우에도 자연스럽게 await 사용이 될 수 있도록 WindowsFormsSynchronizationContext를 Windows Forms의 응용 프로그램에 기본 제공을 합니다. 덕분에 await 사용 시 WindowsFormsSynchronizationContext와 연관된 스레드(즉, UI 스레드)를 기억하게 되고 이후 비동기 작업이 완료되었을 때 "txtContents.Text = text"에 대한 코드 실행을 UI 스레드에서 하도록 넘겨주는 것입니다.

이렇게 되면, await은 결국 아무런 스레드 생성 없이 작업을 모두 완료한 것이나 다름없습니다. 반면, 이렇게 편리한 기능임에도 불편함이 아주 없는 것은 아닙니다. UI 스레드와의 상호 연동을 숨겼기 때문에 이런 구조를 모르는 개발자들은 자칫 응용 프로그램이 멈추는 문제를 유발하곤 합니다.

async/await 사용시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

async/await 사용시 hang 문제가 발생하는 경우 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/10801

어쨌든, 이렇게 (결과적으로 봤을 때) await에는 스레드가 관여하기도 하고 안 하기도 합니다. 역시 이에 대해서도 "There Is No Thread" 글에 보면 다음과 같이 언급하고 있습니다.

The idea that “there must be a thread somewhere processing the asynchronous operation” is not the truth.

must라는 표현을 쓴 이유가 있습니다. must라고 썼을 때 "is not the truth"는 맞습니다. 하지만 "may"라고 했으면 위의 문장은 "is the truth"로 끝났을 것입니다.




"There Is No Thread" 글은 정말 좋은 글입니다. 자칫 간과하기 쉬운 await의 비동기 I/O 코드 실행이 어떤 식으로 흘러간다는 것을 아주 잘 설명해 주고 있습니다.

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/26/2021]

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

비밀번호

댓글 작성자
 



2017-01-20 05시15분
[guest] 명쾌한 설명이군요. 감사합니다.
[guest]
2017-01-20 05시20분
[spowner] @손님 이렇게 글을 상세하게 분석하여 기록을 남기는 사이트가 얼마나 될까요? 그래서 전 매일 정성태님 사이트를 찾습니다. 한마디로 팬이죠 ^^
[guest]
2017-08-24 01시40분
async void가 권장되지 않는 이유

C# Async Tips and Tricks Part 2 : Async Void
; http://www.jaylee.org/post/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.aspx
정성태
2017-08-24 01시42분
C# Async Tips and Tricks, Part 3: Tasks and the Synchronization Context
; http://jaylee.org/post/2012/09/29/C-Async-Tips-and-Tricks-Part-3-Tasks-and-the-Synchronization-Context.aspx
정성태
2021-07-29 09시22분
[guest] async void ReadFile()
{
    string filePath = typeof(Program).Assembly.Location;
    FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    byte[] buf = new byte[1024];
    await fs.ReadAsync(buf, 0, buf.Length);

    fs.Dispose();
    Console.WriteLine("I/O Done");
}
await fs.ReadAsync(buf, 0, buf.Length); 이 부분의 실행이 끝나면
ReadFile을 호출한 스레드에게 제어권이 넘어가서 fs.Dispose(); Console.WriteLine("I/O Done"); 두 코드는 주 스레드에서 담당해야하는거 아닌가요?
좀 헷갈리네요 ㅠㅠ
[guest]
2021-07-29 09시33분
async 메서드는 await 호출 이후의 코드를 분리해 냅니다. 즉, await fs.ReadAsync 호출의 다음 코드는 분리를 한 다음에, await fs.ReadAsync이 완료된 이후 호출하게 됩니다.

(혹시 제 책을 구매하셨다면, "10.2 비동기 호출" 절을 참고하시면 도움이 될 것입니다.)
정성태
2022-05-10 04시19분
[한예지] 예전에는 이 글이 이해가 되지 않았는데

지금 보니까 정말 쉽게 적으려고 노력하신 것이 느껴집니다.

쉬운 설명 감사드립니다^^
[guest]
2023-09-07 11시48분
...[생략]...
Thread.Sleep(1000); // ★
ReadFile();
Console.ReadLine();

선생님, 위의 코드에서 Thread.Sleep(1000);는 어떤 목적으로 두셨는지 궁금합니다.

제 생각에는 밑에 Console.ReadLine()이 존재하지 않는다면
★가 있는 부분에 Thread.Sleep(6000); 같은 코드로 5초보다 크게 Sleep을 줌으로써
Worker Thread가 작업 완료할 때까지 Main Thread가 묶어둘 수 있는데
현재 코드에는 Console.ReadLine()이 존재하는데 왜 Sleep(1초)가 있는지 의문이 듭니다.
★ 부분을 주석 처리해도 문제없이 동작합니다.
한예지
2023-09-07 01시35분
QueueUserWorkItem으로 던져서, 스레드 풀의 할당을 받아 실행하기까지 약간의 시간이 걸릴 수 있습니다. 그런 것을 감안해 4개의 스레드가 현재 확실하게 점유돼 있다는 것을 명확하게 하려고 준 것입니다. Thread.Sleep이 없더라도, 결국 FIFO 식으로 처리될 테니까 QueueUserWorkItem으로 던져진 것들을 먼저 처리할 것이므로 이후의 ReadFile에 있던 비동기 완료 처리는 마찬가지로 blocking이 되긴 할 것입니다.

아마... 제가 저거 실습하면서, 별도의 스레드를 만들어 현재 ThreadPool의 가용한 스레드 수를 체크하는,

ThreadPool.GetAvailableThreads(Int32, Int32) Method
; https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.getavailablethreads

코드를 넣어서 확인하느라 일부러 Thread.Sleep을 추가하기도 한 것 같습니다.
정성태

... 31  32  33  34  35  36  37  38  39  40  41  42  43  44  [45]  ...
NoWriterDateCnt.TitleFile(s)
12493정성태1/18/20218582.NET Framework: 1009. .NET 5에서의 네트워크 라이브러리 개선 (1) - HTTP 관련 [1]파일 다운로드1
12492정성태1/17/20217973오류 유형: 695. ASP.NET 0x80131620 Failed to bind to address
12491정성태1/16/20219557.NET Framework: 1008. 배열을 반환하는 C# COM 개체의 메서드를 C++에서 사용 시 메모리 누수 현상 [1]파일 다운로드1
12490정성태1/15/20219136.NET Framework: 1007. C# - foreach에서 열거 변수의 타입을 var로 쓰면 object로 추론하는 문제 [1]파일 다운로드1
12489정성태1/13/202110127.NET Framework: 1006. C# - DB에 저장한 텍스트의 (이모티콘을 비롯해) 유니코드 문자가 '?'로 보인다면? [1]
12488정성태1/13/202110344.NET Framework: 1005. C# - string 타입은 shallow copy일까요? deep copy일까요? [2]파일 다운로드1
12487정성태1/13/20218879.NET Framework: 1004. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법파일 다운로드1
12486정성태1/12/20219787.NET Framework: 1003. x64 환경에서 참조형의 기본 메모리 소비는 얼마나 될까요? [1]
12485정성태1/11/202110479Graphics: 38. C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제파일 다운로드1
12484정성태1/9/202111152.NET Framework: 1002. C# - ReadOnlySequence<T> 소개파일 다운로드1
12483정성태1/8/20218357개발 환경 구성: 521. dotPeek - 훌륭한 역어셈블 소스 코드 생성 도구
12482정성태1/8/20219762.NET Framework: 1001. C# - 제네릭 타입/메서드에서 사용 시 경우에 따라 CS8377 컴파일 에러
12481정성태1/7/20219511.NET Framework: 1000. C# - CS8344 컴파일 에러: ref struct 타입의 사용 제한 메서드파일 다운로드1
12480정성태1/6/202112078.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개파일 다운로드1
12479정성태1/6/20219448.NET Framework: 998. C# - OWIN 예제 프로젝트 만들기
12478정성태1/5/202111083.NET Framework: 997. C# - ArrayPool<T> 소개파일 다운로드1
12477정성태1/5/202113488기타: 79. github 코드 검색 방법 [1]
12476정성태1/5/202110148.NET Framework: 996. C# - 닷넷 코어에서 다른 스레드의 callstack을 구하는 방법파일 다운로드1
12475정성태1/5/202112728.NET Framework: 995. C# - Span<T>와 Memory<T> [1]파일 다운로드1
12474정성태1/4/202110284.NET Framework: 994. C# - (.NET Core 2.2부터 가능한) 프로세스 내부에서 CLR ETW 이벤트 수신 [1]파일 다운로드1
12473정성태1/4/20219073.NET Framework: 993. .NET 런타임에 따라 달라지는 정적 필드의 초기화 유무 [1]파일 다운로드1
12472정성태1/3/20219366디버깅 기술: 178. windbg - 디버그 시작 시 스크립트 실행
12471정성태1/1/20219834.NET Framework: 992. C# - .NET Core 3.0 이상부터 제공하는 runtimeOptions의 rollForward 옵션 [1]
12470정성태12/30/202010027.NET Framework: 991. .NET 5 응용 프로그램에서 WinRT API 호출 [1]파일 다운로드1
12469정성태12/30/202013618.NET Framework: 990. C# - SendInput Win32 API를 이용한 가상 키보드/마우스 [1]파일 다운로드1
12468정성태12/30/202010225Windows: 186. CMD Shell의 "Defaults"와 "Properties"에서 폰트 정보가 다른 문제 [1]
... 31  32  33  34  35  36  37  38  39  40  41  42  43  44  [45]  ...