Thread

10 분 소요

Sync

Monitor.Enter()Monitor.Exit() 을 사용하면 그 구간에는 한 스레드만이 진입해서 실행할 수 있다. 이는 lock Context 로 축약해서 사용할 수 있다.

Interlocked.Exchange() 등을 통해 단순한 유형의 Atomic 연산을 할 수 있다.

Semaphore 도 존재하며 Cross-Process Synchronization 이 필요없다면 SemaphoreSlime 을 사용할 것이 권장된다1.

신호처리 매커니즘을 사용한다면 AutoResetEvent, ManualResetEvent 을 통해 동기화를 편하게 할 수 있다. 이때 후자의 경우 Reset() 한 Thread 만 WaitOne() 에서 깨어날 수 있다. 단 깨어나기 전에 Reset() 을 또 호출하면 아무 의미가 없어 카운팅같은 건 Semaphore 를 써야한다.

Async

Thread 로 직접 함수를 호출해도 비동기적 작업이 가능하다. 그럼에도 비동기적 함수가 필요한 이유는 편의성 이외에도 하나가 더 있다.

예를들어 I/O 처리를 생각해보자. 이는 크게 I/O 준비, I/O 처리, I/O 후처리 부분으로 나뉘어진다. 만약 Thread 를 새로 생성해 비동기적인 동작을 구현한다면 위의 모든 구간이 다른 Thread 에서 동작할 수도 있다. 하지만 보통 I/O 준비 구간은 다른 Thread 에서 동작할 필요가 없다. 그래서 BeginRead()ReadAsync() 같은 I/O 처리 함수가 비동기적으로 수행한 후에 I/O 후처리도 호출해주면 Thread 에 대한 부담이 줄어들게 된다.

과거에는 delegate 에서 지원하는 BeginInvoke() 를 사용하는 패턴을 썼지만 .Net Framework 에만 지원된다. 지금은 Task 를 이용하는 TAP 패턴 을 쓰도록 권장한다.

Task (C# 4.0)

TPL(Task Parallel Library) 에 속한 타입이다. 기존의 ThreadPool.QueueUserWorkItem() 에서는 어려웠던 Thread 동기화나 리턴값 반환이 가능해져 더 편리해졌다.

Task.Factory.StartNew<>() 를 통해 객체를 만들지 않고 바로 생성할 수도 있다.

Task 는 단순히 Thread 를 경량화 한 것이라고 생각할 수 있는데 그렇지 않다. Threads execute Tasks which as scheduled by a TaskScheduler2.

async / await (C# 5.0)

asnyc 키워드는 await 를 예약어로 컴파일러가 인식하게한다. 그리고 리턴 타입을 void. Task, Task<T> 로 제한시킨다.

  • Task<T> 의 경우 마치 원래 리턴타입이 T 인 것처럼 구현해야한다.
  • 리턴 타입이 Task 의 경우 await 구문이 있어야 한다.
  • 리턴 타입이 void 의 경우 함수 내에서 await 를 쓸 수 있다. 하지만 예외가 발생 시 try...catch 로 처리할 수 없어 EventHandler 같은 특수한 경우 외에는 자제된다3.

awaitasync 식별자가 있을 때에 한해서 Task 에 대해서 적용된다. 그 이하의 코드가 Task 가 끝나고 동작하도록 컴파일러에 의해 코드가 변경된다는 의미를 갖는다.

  • 이때 await 이후 코드가 같은 Thread 에서 동작하라는 보장이 없다. 예를들어 UI Thread 에서는 await 이후의 코드가 UI Thread 에서 동작하도록 되어있다. 그래서 await 가 포함된 async 함수에 대해서 Task.Wait() 를 수행하면 DeadLock 이 발생한다.
  • 그래서 현재 수행되는 Thread 가 어떻게 처리하는지가 중요한데 다음을 참고하자.

ValueTask (C# 7.0)

ValueType<T> 를 사용하면 Method 내에서 await 를 호출하지 않는 경우 Task 를 만들지 않아 성능을 높힌다. C# 5.0 에도 Task 를 상속받아 구현하면 가능한 기능이긴 했다.

AsyncEnumerable (C# 8.0)

기존의 yeild Context 에서는 비동기 처리를 할 수가 없다. 왜냐하면 IEnumerable<T>async 의 리턴타입 제약과 맞지 않기 때문이다.

이에 비동기 스트림을 이라는 별명을 가진 IAsyncEnumerable<T> 가 추가되어 yeild Context 도 async 가 가능해졌다. 그리고 이를 위해 await foreach 예약어가 추가되었다.

class Program
{
    static async IAsyncEnumerable<int> Gen()
    {
        for(int i = 0; i < 10; i++)
        {
            await Task.Delay(100);
            yield return Thread.CurrentThread.ManagedThreadId;
        }     
    }
    static async Task Main()
    {
        // 방법 1
        var aaa = Gen().GetAsyncEnumerator();
        while(await aaa.MoveNextAsync())
        {
            Console.WriteLine(aaa.Current);
        }

        // 방법 2
        await foreach(var a in Gen())
        { 
            Console.WriteLine(a);            
        }
        Console.WriteLine("Cur " + Thread.CurrentThread.ManagedThreadId);
    }
}

참고문헌

시작하세요 C# 10 프로그래밍

댓글남기기