crossorigin="anonymous"> $(function(){ $('.article_view').find('table').each(function (idx, el) { $(el).wrap('
') }); $('img[alt="N"]').each(function(){ $(this).replaceWith('

N

') }); });

새소식

고수만/필드에서 써먹은 것들

비동기 동기 Blocking Non-Blocking 개념 (하) StateMachine -어려움

  • -

 

 

지난 이야기

 

비동기에 대하여

비동기란 무엇인가? 비동기의 반대는 동기이다. C#에서의 비동기 메서드의 작동 방식은 아래에 설명이 잘 되어있다. https://learn.microsoft.com/ko-kr/dotnet/csharp/asynchronous-programming/task-asynchronous-programming

dog-foot-sleep.tistory.com

 

 

 

 


우연히 아래의 글을 보게 되었고 비동기와 Blocking에 대한 내용에 대해 생각을 하게 되었다.

그런데 비동기와/Blocking에서 혼란이 오기 시작하였다.

async 비동기를 의미하고 await는 어떤 작업이 끝나기를 기다린다는 뜻인데 

그럼 이것이 Blocking이 아닌가??

 

 

👩‍💻 완벽히 이해하는 동기/비동기 & 블로킹/논블로킹

동기/비동기 & 블로킹/논블록킹 프로그래밍에서 웹 서버 혹은 입출력(I/O)을 다루다 보면 동기/비동기 & 블로킹/논블로킹 이러한 용어들을 접해본 경험이 한번 쯤은 있을 것이다. 대부분 사람들은

inpa.tistory.com

 

 

다른 문서들을 읽어 보아도 async / await는 비동기/블로킹 이 아닌 비동기 / Non-Blocking을 유지하게 해준다라고 한다.

 

전 글에서도 설명하였지만 동기와 비동기는 "동시에 일어나는가?" 이다.

그럼 Blocking 과 Non-Blocking은 무엇이 차이인가?

 

블로킹에 대하여

 

우선 Blocking이란

"블로킹(blocking)"이라는 용어는 코드나 작업의 실행이 특정 조건이 충족될 때까지 멈춰있게 되는 상태를 가리킵니다. 블로킹은 리소스가 사용 가능해질 때까지 현재의 실행 흐름을 일시 정지시키므로 다른 작업을 처리할 수 없게 합니다.

I/O 작업: 파일 읽기/쓰기나 네트워크 요청과 같은 I/O 작업을 동기적으로 수행할 때, 해당 작업이 완료될 때까지 현재 스레드는 블로킹됩니다.

Locks와 동기화 프리미티브: Monitor, Mutex, Semaphore와 같은 동기화 도구를 사용할 때, 특정 리소스에 대한 접근을 기다리는 동안 블로킹 상태가 발생할 수 있습니다.

Thread.Sleep: Thread.Sleep 메서드를 호출하면 현재 스레드는 지정된 시간 동안 블로킹됩니다.

동기 메서드 호출: 동기 메서드가 긴 시간 동안 실행되는 경우, 그 메서드를 호출하는 스레드는 메서드의 실행이 완료될 때까지 블로킹됩니다.

 

 

Blocking에 대해 진행되는 전체적인 흐름을 막느냐 안막는냐로 설명하는 곳도 있고, 제어권이 호출자에 있는가 호출대상으로 넘어가는가 로 설명하는 곳이 있는데 멈춤 혹은 일시정지에 너무 집중하면 await에서 이해가 안될 수 있다.

 

 

 

위 그림은 Blocking에 대하여 설명하는 예시다.

Main 작업 흐름이  A라면 B 작업을 호출 하였을 떄 A작업이 일시 중단되는 Blocking, A 작업이 중단 없이

흘러가는 Non-Blocking으로 설명한다.

 

우리는 async / await는 비동기 Non-Blocking 방식이라고 공부했다.

 

그러면 생각해보자. 메인 메서드 A가 다른 메서드 B를 호출 할 때 await를 걸면 어떻게 될까?

 static async Task Main()
    {
        Console.WriteLine($"Before Delay: {DateTime.Now}");

        await Task.Delay(2000); // 비동기적으로 2초 동안 대기

        Console.WriteLine($"After Delay: {DateTime.Now}");
    }

 

분명 위 코드를 돌려보면 아래와 같이 나올 것이다.

 

또한 Before가 시작 된 후 2초의 딜레이 이후에 after가 찍힐 것이다.

await에서 Delay의 실행의 결과를 대기 후 다음 코드가 진행된다.

이건 Blocking 당한거 아닌가? 

 

그렇기에 Blocking에 대한 설명에 추가적으로 언급하고 싶은 것이 있다.

 

await가 있는 호출 스레드는 일시적으로 멈추지만 멈춘 스레드는 대기만 하고 있는 것이 아니라

다른 작업을 진행할 수 있다.

 

이 부분이 상당히 중요하다. Blocking과 Non-Blocking은 결국 스레드의 다른 작업 진행 여부에서 갈린다.

 

Thread.Sleep vs Task.Delay

 

Blocking과 Non-Blocking의 대표적인 예시로는 Thread의 Sleep과 Task의 Delay를 들 수 있겠다.

    static void Main()
    {
        Console.WriteLine($"Before Sleep: {DateTime.Now}");

        Thread.Sleep(2000); // 현재 스레드를 2초 동안 블록함

        Console.WriteLine($"After Sleep: {DateTime.Now}");
    }

위는 Thread.Sleep을 사용하여 2초 정지를 시키는 코드이다.  코드만 본다면 작동도 같아 보이며 차이점을

알기 어렵다.

 

  • Thead의 Sleep은 Thread Class를 사용하며 해당 호출자 스레드를 일시정지 시킨다. -> Blocking
  • Task의 Delay는 Task Class를 사용하며 Task 형식을 반환하는 비동기 메서드이고 해당 호출자 스레드를 정지 시킨 후 상태저장을 하고 다른 작업을 진행한다.(스레드풀에서 가져온 스레드의 경우 스레드 풀에 반환) -> Non-Blocking

 

즉 Blocking은 실행 스레드의 정지,  Non-Blocking은 정지 후 상태 저장, 다른 작업에 사용 이다.

 

이를 더 상세하게 구분 짓는 예시를 들어 보겠다.

//start main
Console.WriteLine("MainThread ID: " + Thread.CurrentThread.ManagedThreadId);
 Task.Run(async () =>
{
    Console.WriteLine($"Do TaskA Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
    await Task.Delay(20000);//10초동안 일시정지 다른 작업 실행 가능
    Console.WriteLine($"Do TaskA End (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
});

await Task.Delay(5000); //2초 동안 Blocking
Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);
Task.Run(() =>
{
    Console.WriteLine($"Do TaskB Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
    Thread.Sleep(3000); //3초 동안 Blocking
    Console.WriteLine($"Do TaskB End (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
    
});
Console.WriteLine("finish : " + Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();

 

 

위 코드를 보면서 흐름을 생각보자. 본인의 생각과 일치하는가?

해당 코드는 Delay와 Sleep 의 차이를 보이기 위해 의도한 코드이다.

 

MainThread ID가 시작 되고 기본적으로 1로 시작한다.

여기서 Task.Run이란 메서드를 알아야 하는데 

이 메서드로 사용하면 무조건 스레드풀의 백그라운드로 진행된다.

 

여기서 보면 모두 Task.Run으로 돌아가도록 했는데

그 이유는 Thread를 스레드풀에서 사용하여 ID가 바뀌도록 의도한 것이다.

 

Console.WriteLine("MainThread ID: " + Thread.CurrentThread.ManagedThreadId); // ID 1로 시작
 Task.Run(async () =>             // 스레드 풀에서 가져온 새로은 스레드 ID 8로 작업
{

     ///작업들

}

 

그래서 Task.Run 안의 Do TaskA Start를 보면 ID가 8로 바뀌는 걸 볼 수 있다. 

Task.Run이 스레드풀에서 새로 스레드를 가져와 Run을 실행한 것이다.

 

여기서 가장 중요한 부분이 나오는데

첫번째 Task.Run의 작업엔 Delay를 사용하였지만 

두번째 작업에는 Sleep을 사용하였다.

 

Delay는 비동기 함수이며 Non-Blocking 함수이며

Sleep은 Blocking 함수이다.

 

이 두 메서드를 가장 크게 차이 내는 것은 비동기가 아니라 스레드의 반환 유무이다.

 

Delay는 현재 스레드를 일시 정지 시키고 상태를 저장 후 다른 작업을 진행하던지

현재 스레드가 스레드 풀에서 가져온 스레드라면 스레드풀에 반환하여 다른 곳에서

사용할 수 있도록 한다.

 

반면에 Sleep은 현재 진행 중인 스레드를 중지시켜 버린다. 그리고 더 진행이 나아가지 

않는다.

 

이 부분이 가장 큰 부분이다. 

스레드가 반환되는지는 아래에서 설명하겠다.

 


 

 Task.Run(async () =>
{
    Console.WriteLine($"Do TaskA Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})"); //ID 8로 시작
    await Task.Delay(20000);//10초동안 일시정지 다른 작업 실행 가능 //작업 중이던 스레드 8 반환
    Console.WriteLine($"Do TaskA End (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
});

await Task.Delay(5000); //너무 빠르게 그 다음 작업이 진행되면 반환된 스레드를 재사용 안하기에 딜레이
Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId); //메안 스레드가 진행
Task.Run(() =>     // 새로운 스레드를 스레드 풀에서 가져와 진행 -> 가장 최근에 사용되었던 반환된 ID 8 재사용
{
    Console.WriteLine($"Do TaskB Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})"); //ID 8 
    Thread.Sleep(3000); //3초 동안 Blocking  하기 때문에 스레드 반환 안함 
    Console.WriteLine($"Do TaskB End (ThreadID:{Thread.CurrentThread.ManagedThreadId})"); // 시작과 무조건 같은 ID 8
});

 

이러한 이해를 가지고 코드로 다시 돌아가 보면

첫번째 Task Run은  ID 8로 진행되지만 20초 정지를 만난다.

 

Delay(5000)을 넣은 이유는 너무 빠르게 진행되면 Task.Run이 진행될 때

스레드 풀에서 마냥 새로운 스레드를 가져와 반환된 스레드를 사용 안 할 수 있기 때문에 

Delay에서 스레드를 반환하는 시간 여유를 주기 위함이었다.

 

두번째 Task.Run에는 Sleep을 넣었는데 Sleep은 Blocking이기 때문에

Task.Run의 실행 스레드를 멈추기만 하고 스레드 풀에 반환하지 않는다.

 

그래서 Sleep에서는 시작 Thread ID는 종료 Thread ID가 항상 같다.

그러나 Delay를 사용한 부분은 시작과 종료 Thread ID가 다를 수 있다.

 

왜냐하면 Delay에서 기존 실행 스레드를 반환하여 재사용 가능하게 했다가

Delay가 끝나면 다시 스레드풀에서 가져오기 때문이다.

 

여기서 Task  B를 보면 ID가 8 인것을 볼 수 있다. 이는 Delay가 Non-Blocking이라는 것을

증명한다. 반환되어 다른곳에서 사용 되었다는 것을 보여준다.

왜냐하면 아직까지 멈춘  Task A는 Task  B가 끝날 때 까지 종료되지 않았기 때문이다.

 

만약  Task A가 Sleep이 되었다면 ID 8은 계속 사용될 수 없었을 것이다.

그리고 Task A의 Delay 종료 후 Thread ID는 3으로 바뀌는 것을 볼수 있다.

 

물론 Task A의 시작 ID와 종료 ID가 같을 수는 있다. 또한 Task B의 종료 ID와 Task A의

종료 ID가 같은 경우가 더 많을 것이다.

왜냐하면 스레드풀의 최적화 전략 때문에 가장 최근에 사용한 스레드를 재사용 할 수 있기 

때문이다.

 

 

 

비동기 와 async / await

 

아래는 또 다른 예제 코드이다.

천천히 살펴보자

//Start Main
await AsyncAwaitA();   //await 사용 비동기 메서드
AsyncB();				//동기 메서드


async Task AsyncAwaitA()
{
    Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);	//메인 스레드 ID와 같을 수 있음
   await Task.Run(async () =>	//백그라운드 실행 하면서 await 사용
   {
       Console.WriteLine($"Do TaskA Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})");	//백 그라운드 스레드 ID
       await Task.Delay(1000);
       Console.WriteLine($"Do TaskA End (ThreadID:{Thread.CurrentThread.ManagedThreadId})");   /백 그라운드 스레드 ID
   });

    Console.WriteLine("finished AsyncAwaitA " + Thread.CurrentThread.ManagedThreadId); //비동기 메서드 시작 ID와 같아야 되지만 응용 프로그램이라 다름
}
  void SyncB()
{
    Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    Task.Run( () =>   //백그라운드 실행 하면서 미사용 -> 기다리지 않음
   {
       Console.WriteLine($"Do TaskE Start (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
        Thread.Sleep(1000);
       Console.WriteLine($"Do TaskE End (ThreadID:{Thread.CurrentThread.ManagedThreadId})");
   });
    Console.WriteLine("finished AsyncB :" + Thread.CurrentThread.ManagedThreadId);  // 바로 진행됨.
}

 

 

 

 

위 코드에서 메인 호출문인

 

await AsyncAwaitA();
SyncB();

 

을 집중하여 보자.

AsyncAwaitA는 비동기 메서드로 만들어져 있으며

SyncB는 동기 메서드로 만들어져 있다.

 

비동기만 확인하고 싶다면 해당 코드를  아래처럼 바꾸면 된다. 

 

//Start Main
AsyncAwaitA();
AsyncB();


async Task AsyncAwaitA()
{
	//A 메서드
}
  async Task AsyncB()
{
	//B 메서드
}

 

그러면 AsyncAwaitA() 와 AsyncB()가 동시에 시작되는 걸 볼 수 있다. 

그러나 원본 코드에는 await를 붙였기 때문에 비동기로 실행되지만 작업의 결과 값이 올 때 까지 기다린다.

 

단. 기다리는 동안 실행 스레드는 Non-Blocking이기 때문에 다른 작업을 진행 가능하다.

 

 

스레드의 흐름

 

이번엔 스레드의 흐름을 살펴보겠다.

 

 

await를 써 동기처럼 진행되도록 하였다. 당연히 순서대로 진행되며 (1)~(5) 까지는 위에서 설명을 다했기에 넘어가겠다.

 

(5)  가 갈리는데 Task.Delay는 비동기 함수이다. 기본적으로 새로운 스레드를 생성하여 작동하는

것이 아닌 기존 .NET에 지원하는 System.Threading.Timer 사용하기 때문에 ID가 바뀌지 않아야 하지만

응용프로그램에서 await로 받고 있기 때문에 스레드풀을 사용하여 ID가 바뀌어 나온다.

환경과 상황에 따라 변경이 없이 같은 ID가 나올 순 있다.

(SynchronizationContext 는 await를 만났을 때  실행 스레드의 상태가 캡쳐해  호출한  Thread로 회귀할 수 있도록 하는데 콘솔 응용프로그램에는SynchronizationContext가 없기 때문에 그냥  await를 만나면  스레드풀에서 실행 가능한 스레드를 가져와 실행해서 그렇다. 어려우니 넘어가자

 

그리고 (2)   (6)   같은 ID가 나온다. 메인 스레드의 흐름이기 때문이다.

그래서 원래는 (1),(2),(6),(7)은 메인 스레드 ID 1과 같아야 된다.

async로 비동기 메소드를 돌려도 메인스레드로 실행이 가능하면 스레드 풀에서 가져와

비동기를 실행하지 않는다.

 

실제로는 (7)  (1) 과 다르다.  이 역시 해당 코드가 콘솔 응용프로그램에서 돌렸기 때문이다.

(5번의 이유와 같음)

 

그래서 실제 실행 순서를 보면

(1)->await (2)-> await 백그라운드(3)->(4)->(5)->(6)->(7)->백 그라운드(8)->(11)->(9)->(10)

로 진행된다.

 

async / await를 사용하였기 때문에 Task.Run으로 혹은 async로 동시에 시작할 수 있는 작업들이 await 덕에 흐름을 제어되고 어디서 결과를 기다리는지 코드만 보아도 확인 할 수 있게 되었다.

 


Async / Await의 내부 로직

 

그럼 우린 처음 질문으로 다시 돌아가 본다.

async /await 가 실행 스레드를 멈추고 다른 작업을 실행할 수 있기에 블로킹이 아니라 논 블로킹인건 알겠다.

 

그럼 어떻게 내부로직이 돌아가는지 궁금하지 않은가?

 

만약 await로 비동기 메서드를 모두 호출하고 정지 된 메인 로직에서

while문으로 해당 await가 끝났는지를 확인하는 로직일수도 있지 않은가?

 

물론 답은 No다. 

위의 예시는 동기 / Non blocking 이다.

메인문이 while문으로 돌면서 await 메서드들을 기다리는 동기 / Non-Blocking의 예시는

게임 로딩창을 예로 들수 있겠다.

 

예를 들어

 

while(!게임준비완료)

{

   await 맵 로딩();

   await 배경음악준비();

   await 캐릭터세팅();

...

}

 

위 예시 처럼 비동기 메서드들이 Non-Blocking으로 실행되지만 모든 메서드들이 끝나기 전까진

메인 화면은 아무것도 진행되지 않는다.

 

비동기를 동기처럼 진행하게 해주는 로직이기 때문에 메인문이 동기여도 될꺼 같지만

보이기만 비동기로 보이고 실제는 콜백으로 구현이 되어있다.

 

 

StateMachine

async await의 내부로직을 확인하려면 await를 사용하고 컴파일 된 IL 코드를 다시 디컴파일하면

알 수 있다. 실제 코드는 복잡하기 때문에 축약하여 서술하겠다.

 

async Task  MethodAsync()
{

        await MethodAsyncA(); // 비동기 메서드 A
        await MethodAsyncB(); // 비동기 메서드 B
}

먼저 컴파일 한 후 디컴파일 할 비동기 함수이다. 

이 코드를 컴파일 후 디컴파일 하면 아래와 같다. 

 

[AsyncStateMachine(typeof(MethodAsyncStateMachine))]
public Task MethodAsync()
{
    MethodAsyncStateMachine stateMachine = new MethodAsyncStateMachine();
    stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.__state = -1;
    stateMachine.t__builder.Start(ref stateMachine);
    return stateMachine.t__builder.Task;
}

private sealed class MethodAsyncStateMachine : IAsyncStateMachine
{
    public int __state;
    public AsyncTaskMethodBuilder t__builder;
    private TaskAwaiter u__1;

    private void MoveNext()
    {
        switch (__state)
        {
            case -1:
                u__1 = MethodAsyncA().GetAwaiter();
                if (!u__1.IsCompleted)
                {
                    __state = 0;
                    t__builder.AwaitUnsafeOnCompleted(ref u__1, ref this);
                    return;
                }
                break;

            case 0:
                u__1.GetResult();
                u__1 = default(TaskAwaiter);
                u__1 = MethodAsyncB().GetAwaiter();
                if (!u__1.IsCompleted)
                {
                    __state = 1;
                    t__builder.AwaitUnsafeOnCompleted(ref u__1, ref this);
                    return;
                }
                break;

            case 1:
                u__1.GetResult();
                __state = -1;
                break;
        }
    }

    void IAsyncStateMachine.MoveNext()
    {
        this.MoveNext();
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        this.t__builder.SetStateMachine(stateMachine);
    }
}

 

우선 stateMachine 이라는 것이 보인다.

위에서 async / await는 현재 스레드의 상태를 저장하고 다른 일을 한다고 설명하였는데

 

stateMachine이 바로 그 역할을 한다.  비동기 메서드의 상태를 저장하고 로직의 흐름을 추적하는

역할을 한다.

 

async 키워드가 컴파일을 만나면 await를 가진 수만큼 아래 코드에 case가 늘어난다.

 

switch (__state)
        {
            case -1:
                u__1 = MethodAsyncA().GetAwaiter();  // A 메서드 실행 후 awaiter 객체를 가져온다
                if (!u__1.IsCompleted) // 완료되지 않았다면
                {
                    __state = 0; //상태 값 저장
                    t__builder.AwaitUnsafeOnCompleted(ref u__1, ref this); //완료되면 호출되는 이벤트를 발생시키도록 하는 부분
                    return;
                }
                break;

            case 0:
                u__1.GetResult();
                u__1 = default(TaskAwaiter);
                u__1 = MethodAsyncB().GetAwaiter(); // B 메서드 실행 후 awaiter 객체를 가져온다
                if (!u__1.IsCompleted) 		// B가 완료 되지 않았다면
                {
                    __state = 1;  //상태 값 변경
                    t__builder.AwaitUnsafeOnCompleted(ref u__1, ref this); //완료되면 MoveNext 호출 하도록
                    return;
                }
                break;

            case 1:
                u__1.GetResult();
                __state = -1;
                break;
        }
    }

 

 

콜백 / AwaitUnsafeOnCompleted / ContinueWith

 

기본적으로 stateMachine의 state는 -1로 시작하며 순서대로 await에 걸린 메서드가 실행 된 후 await 객체를 가져온다. await가 사용된 메서드가 완료되지 않은 Not IsCompleted면 상태 값을 저장 한 뒤 AwaitUnsafeOnCompleted 를 호출한다.

 

AwaitUnsafeOnCompleted 는 콜백형태로 Task 실행이 완료되면 대기 중인 Task에 CountinueWith로 MoveNext를 호출하는 역할을 한다.

 

AwaitUnsafeOnCompleted가  콜백 형태로 Task의 종료 시 CountinueWith 로 MoveNext를 호출하기 때문에 await 로 결과를 기다린 뒤 비동기로 실행하다가 결과 값이 나오면 이어서 진행할 수 있는 것이다.

 

CountinueWith는 앞선 비동기에서 설명하였는데 스레드풀의 특정 Task의 작업이 완료되면 비동기적으로 같은 스레드로 작업을 이어나가는 것을 의미한다.

 

만약 해당 await 객체가 완료가 되어있으면 MoveNext 결과 값을 리턴한다.

 

이처럼 async await는 컴파일 시 switch 구문에 콜백으로 재호출 되도록 구현되어있으며 콜백 스택이 비워질 때 까지 반복되며 진행된다.

 

이렇게 우리는 async와 await를 사용하여 비동기적으로 작업을 진행하면서 동기적으로 흘러가는 것 처럼 코드를 볼 수 있는 것이다.


 

지금까지 비동기와 Blocking의 개념, async await의 흐름과 내부 구현까지 알아보았다.

 

비동기에 대한 글들이 많고 표현하는 방법들이 다양하다.

또한 비동기/ 동기 blocking과 non-blocking을 경우의 수에 따라 설명한 곳도 있다.

 

그 글들을 읽으며 다시 한번 정리하게 된 이유는

async await 가 왜 써야 되고 어떻게 쓰는게 효율적인지 모르고 사용할 수 있기 때문이다.

 

async await를 사용하다 보면 비동기 결과에 대응하기 위해 연쇄적으로 비동기를 사용하게 되는

async contagion , async 전염이 일어날 수있다.

그래서 무턱대고 모든 코드에 async await 사용하게 되는 문제가 생길 수있다.

 

또한 async await 사용하면 가독성과 비동기와 흐름제어에 장점을 가지지만 성능을 효율화 시키기에 사용하는 것은 아니다.

많은 작업이 반복되면 스레드의 컨택스트 스위칭이나 스케쥴링에서의 오버헤드들이 많아질 수 있다.

 

async await를 제대로 알고 적절하게 계획하에 써야 된다고 생각하여 반성문을 마친다.

 


참고할만한 글들

 

 

.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드

글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 부모글 보이기/감추기 (연관된 글이 8개 있습니다.) (시리즈 글이 4개 있습니다.) .NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동

www.sysnet.pe.kr

 

 

C# Async Await 원리

개요 전에는 그냥 대충 [Async 함수를 호출하면 ThreadPool에서 Task를 하나 꺼내서 함수를 던져주겠지~]라고 생각했었다. 하지만 현실은 물론 그렇지 않았으며, 내부에서 신기하고 재미있는 일이 일

developstudy.tistory.com

 

 

👩‍💻 완벽히 이해하는 동기/비동기 & 블로킹/논블로킹

동기/비동기 & 블로킹/논블록킹 프로그래밍에서 웹 서버 혹은 입출력(I/O)을 다루다 보면 동기/비동기 & 블로킹/논블로킹 이러한 용어들을 접해본 경험이 한번 쯤은 있을 것이다. 대부분 사람들은

inpa.tistory.com

 

Contents