Post List

레이블이 async인 게시물을 표시합니다. 모든 게시물 표시
레이블이 async인 게시물을 표시합니다. 모든 게시물 표시

2016년 5월 1일 일요일

Using async method in static constructor ( C# )

Using async method in static constructor ( C# )

static constructor 내부에서 async 함수를 호출할 경우 제대로 동작을 하지 않습니다.
(왠만해서는 이런식으로 code가 이루어지지 않도록 해야하지만, 어쩔수 없이 이렇게 사용해야 할 경우가 발생 할 수 있습니다.)
  • 참고로 static constructor는 해당 class가 가장 먼저 사용 될 때 실행됩니다.

CLR-internal lock

static constructor는 정확히 1번만 실행되어야 합니다.
그러므로 static constructor가 실행될 때는 내부적으로 CLR-internal lock으로 해당 code를 1번만 실행되도록 수행합니다.
이렇게 lock이 걸린 상태에서 async를 이용하여 다른 thread로 작업을 수행할 경우 thread-lock과 CLR-internal lock간의deadlock이 발생해서 안된다고 생각 할 수 있습니다.
http://blogs.msdn.com/b/pfxteam/archive/2011/05/03/10159682.aspx 링크의 posting을 보면 설명이 되어 있습니다.
http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html 링크의 posting에도 절대로 static constructor에서async 작업을 하는 건 BAD CODE!!!라고 경고하고 있습니다.
참고로 위 Link에 있는 posting에서 안된다고 하는 예제들을 만들어서 해보면 잘 됩니다.
진짜로 async 한 작업 (DB, Network, disk I/O) 을 이용하는 경우에는 안 될 수 있지만, 단순히 async 키워드만 붙였다고 해서deadlock이 재현되지는 않습니다.

내가 상상하는 안되는 이유...

위 내용들은 어느정도 검증(?)된 posting을 바탕으로 한 내용이구요.
제가 생각했을 때 안되는 이유를 말씀드리겠습니다.
어디까지나 혼자만의 상상의 나래로 기록한 썰(?)이지, 검증된 내용은 아닙니다.
constructor라 함은 object가 생성될때 가장 먼저 해줘야 하는 작업입니다.
static constructor는 해당 class의 static properties, method들이 사용되기 전에 먼저 실행되어야 할 code들을 모아둔 곳이어야 겠죠.
C++98까지의 modern하지 않은 C++에서는 member variable (C# 의 properties) 들을 초기화 하는 작업을 constructor에서 했습니다.
C++11 에서는 C# 과 같이 선언과 동시에 초기값를 바로 써 줄 수 있지만, 아마 동작은 constuctor실행시 할 거라 생각됩니다.
C# 도 편의상 선언과 동시에 초기값을 써 줄 수는 있지만, 아마 동작은 constuctor 수행시 할 거라 생각됩니다.
static constuctor는 해당 class의 static method보다 먼저 수행되어야 할 code라는 썰을 전제로 생각해 본다면,
그럼 static constructor에서 다른 static method를 호출하면 어떻게 될까요 ???
아마 그 시점에 해당 static method의 code를 실행가능 하도록 해주지 않을까 예상됩니다.
(memory에 load한다던지, 아니면 다른 방법으로 실행가능하게 뭔가 조치를 해주겠죠.)
이게 동일 thread 내에서는 당연히 판단이 가능하여서 아무런 문제가 없이 동작하지만,
static constructor가 수행중이고 아직 완료되지 않은 시점에,
갑자기 다른 thread가 해당 class의 다른 static method에 접근을 할 경우에는 어떻게 해야 할까요 ?
아마 static constructor가 수행중이니 CLR-internal lock으로 보호되고 있겠지요 ?
이 lock은 static constructor가 수행을 종료해야 풀리겠구요.
그런데 그 다른 thread가 static method를 수행완료해야만 static constructor가 종료될 수 있다면 ??? 여기서 dead lock이 발생할 것입니다.
동일 thread 내에서는 CLR-internal lock 내부에서 수행이 되도록 잘 설계 했습니다. 당연히 다른 thread에서 접근은 lock으로 보호해야하는 건 맞구요.
하지만, 해당 thread가 종료되길 기다리는게 static constructor인 경우에는 ???
그래서 아래에 제가 적어놓은 해결 방법 중에,
static constructor에서 자신의 class가 아닌 다른 class의 async 작업을 기다리는 경우는 잘 동작합니다. 이것을 이용해서async한 작업을 별도 class로 나누면 역시나 잘 동작합니다.

Deadlock in async method in static constuctor

강제로 deadlock을 발생시키는 code를 만들어 보았습니다.
using System.Collections.Generic;
using System.Threading.Tasks;

class StaticClass
{
    public static IEnumerable<string> Names { set; get; }

    static StaticClass()
    {
        Names = Task.Run(async () => { return await GetNamesAsync(); }).Result;
    }

    public static async Task<IEnumerable<string>> GetNamesAsync()
    {
        List<string> nameList = new List<string>
        {
            "Luna", "Star", "Philip"
        };

        return nameList;
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (string name in StaticClass.Names)
        {
            System.Console.WriteLine(name);
        }
    }
}
이런 code를 어떻게 고쳐야 하는지 3가지 방법을 살펴보겠습니다.

1. async한 구현의 method를 추가

위 예제의 경우 GetNamesAsync() method와 같은 기능을 하는 sync한 metho인 GetNames()를 추가하는 방법이 있습니다.
동일한 구현이 2개가 되므로 별로 추천드리는 방법은 아닙니다.
참고로 아래 예제도 썩 그렇게 좋은 예제코드는 아닙니다.
using System.Collections.Generic;
using System.Threading.Tasks;

class StaticClass
{
    public static IEnumerable<string> Names { set; get; }

    static StaticClass()
    {
        Names = GetNames();
    }

    public static async Task<IEnumerable<string>> GetNamesAsync()
    {
        List<string> nameList = new List<string>
        {
            "Luna", "Star", "Philip"
        };

        return nameList;
    }

    public static IEnumerable<string> GetNames()
    {
        List<string> nameList = new List<string>
        {
            "Luna", "Star", "Philip"
        };

        return nameList;
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (string name in StaticClass.Names)
        {
            System.Console.WriteLine(name);
        }
    }
}
하지만 sync한 작업으로 구현 자체가 될 것을 굳이 async로 선언할 일은 잘 없습니다.
그러므로 이렇게 해결될 수 있는 일이라면 애초에 async로 구현한거 자체가 제대로 된 설계가 아닐 수 있습니다.

2. async 작업을 별도 class로 분리 (또는 async 작업 호출을 별도 class로 제한)

async 작업을 별도 class로 분리하거나, static constructor에서 호출하는 async 작업을 다른 class의 method로 제한하는 방법이 있습니다.
이렇게 구현할 경우 원래 class에서 sync한 구현과 async한 구현이 모두 필요할 경우 1번과 같은 code 모양이 될 경우가 많습니다.
static constructor에서 호출하는 async method가 다른 class의 method일 경우에는 deadlock이 걸리지 않았습니다.
using System.Collections.Generic;
using System.Threading.Tasks;

class StaticClass
{
    public static IEnumerable<string> Names { set; get; }

    static StaticClass()
    {
        Names = GetNames();
    }

    public static IEnumerable<string> GetNames()
    {
        return Task.Run(async () => { return await AsyncClass.GetNamesAsync(); }).Result; ;
    }
}

class AsyncClass
{
    public static async Task<IEnumerable<string>> GetNamesAsync()
    {
        List<string> nameList = new List<string>
        {
            "Luna", "Star", "Philip"
        };

        return nameList;
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (string name in StaticClass.Names)
        {
            System.Console.WriteLine(name);
        }
    }
}

3. 초기화 작업을 별도 Init method로 분리

개인적으로 이 방법이 가장 깔끔해 보입니다.
해당 class가 사용되기 전에 Init()을 호출한 뒤에 사용하면 됩니다.
Init()함수가 호출되기 전에 이미 해당 class의 static constructor가 실행된 상태이기 때문에 CLR-internal lock은 이미unlcok된 상태에서 async작업을 수행하게 됩니다.
하지만 여러 thread에서 Init() 함수가 호출될 가능성이 있을 경우에는 사용자가 별도로 lock을 걸어서 호출을 해야 합니다.
해당 기능은 clsss가 최초로 사용되기 이전 시점의 아무 곳에서나 호출이 가능하므로, lock이 필요없는 적당한 시점에 호출시켜 주는 것이 좋습니다.
using System.Collections.Generic;
using System.Threading.Tasks;

class StaticClass
{
    public static IEnumerable<string> Names { set; get; }

    static StaticClass()
    {
        ...
    }

    public static void Init()
    {
        Names = Task.Run(async () => { return await GetNamesAsync(); }).Result;
    }

    public static async Task<IEnumerable<string>> GetNamesAsync()
    {
        List<string> nameList = new List<string>
        {
            "Luna", "Star", "Philip"
        };

        return nameList;
    }
}

class Program
{
    static void Main(string[] args)
    {
        StaticClass.Init();
        foreach (string name in StaticClass.Names)
        {
            System.Console.WriteLine(name);
        }
    }
}

2015년 9월 15일 화요일

C# async / await 를 이용한 비동기 프로그래밍

* 비동기 프로그램의 필요성

  - 예전에는 PC에 CPU 및 내부 core가 1개라서 동기식으로 프로그램을 작성하여도 아무런 문제가 없었습니다.
  - Single core에 multi-thread 프로그램을 작성한들 최종 결과가 나오는 시간은 더 빨라지지 않습니다.

  - 하지만 CPU도 이제는 multi-core 가 되었기 때문에 계속해서 동기식 프로그램으로 작성을 한다면,
  - 해당 CPU에서 하나의 core만을 사용해서 application이 동작하게 되므로 비동기식 프로그램으로 작성을 하면 성능을 향상 시킬 수 있습니다.

* 기존의 비동기 프로그램 방법

  - 비동기 프로그램 ? 그거 그까이꺼 대충 그냥 이렇게 하면 되자나요.

  1. 함수 호출시 별도의 thread를 만들어서 callback 함수를 전달해야 합니다.
  2. 해당 함수는 작업이 끝난 후 callback 함수를 호출합니다.
  3. 원래 thread 에서는 비동기 작업과는 별개인 작업들을 진행하고 있습니다.
  4. 비동기 작업의 결과가 필요한 시점에서는 해당 작업이 끝났는지 기다려야 합니다.
  5. 보통 기다리기 위해서는 while 문등을 이용하여 무한-loop를 돌고 있으면서 기다립니다.
  6. 기다리는 동안의 CPU도 아깝자나요. 그러니깐 기다리는 것도 특정 간격으로 기다리면서 해당 threadsleep 시키면서 다른 thread 에게 작업을 양보해주는 센스 정도야 발휘해 줘야 겠죠 ?

  - 어때요 ? 참 쉽죠 ? ㅎ
  - 위 방법 말고도 다른 방법들이 많습니다.
  - callbackcallback 을 넣고 하는 방법도 있구요.
  - 결과를 특정 위치에 기록하고, 그 결과를 pulling 하는 곳에서는 해당 값을 보고 진행을 하는 방법도 있구요.
  - pulling 조차 하지않고 저기서도 callback을 이용해서 다음 진행을 할 수도 있습니다.

  - 어쨌거나 위 사항들을 여러 개의 메서드 들로 쪼개서 직접 구현해야 합니다.

  - 구현이야 개발자는 그게 일이니깐요. 그냥 하면 될껀데,
  - 개발자들의 업무 특성상 코딩을 하는 시간 보다는 코드를 읽는 시간이 훨씬 더 많습니다.
  - 위 방법으로 구현된 코드를 따라 가면서 읽는건 결코 쉬운 일이 아닙니다.

* async / await 의 등장배경

  - 동기식 프로그램 같은 코드의 흐름으로 작성이 가능하면서도,
  - 동작은 비동기식으로 하는 기능을 제공해 줍니다.
  - 작성이 간편해 진것은 물론이고,
  - 해당 함수의 사용 및 코드를 읽기도 훨씬 편해졌습니다.
  - .NET Framework 4.5부터 지원합니다.
  - 기존 class library 들 대부분의 메서드에서 ~Async를 접미사로 붙인 메서드들이 제공됩니다.
    해당 메서드들은 Task, Task<TResult>return 하는 async 메서드들 입니다.

* async

  - 메서드 선언시 붙여 줄 수 있는 키워드 입니다.
  - 비동기로 해당 메서드를 실행하라는 말입니다.
  - 비동기 라는 말이 어려우면 별도 thread로 해당 함수를 실행하라는 것으로 생각하셔도 됩니다.
  - 하지만 실제로 추가로 thread를 생성해서 실행시키지는 않습니다.
  - 내부적으로 windows 가 해당 code를 어떻게 실행하느냐 까지 설명하려면, 상당히 복잡해 집니다.
  - 한마디로 만지면 커집니다. ;;;
  - 내부에 await 가 사용되고 있다는 뜻을 내포합니다.
    (내부에 await가 없는데 async로 선언을 한 경우에는 async가 없는거랑 똑같이 취급됩니다.)
  - async 가 선언된 메서드의 return 타입으로는 Task, Task<TResult> 만을 허용합니다.
  - 매개변수로 ref, out 의 사용이 불가능 합니다.

* await

  - async 로 선언됨 함수의 결과를 기다리라는 뜻입니다.
    (return 타입이 Task, Task<TResult> 인 함수들만 됩니다.)
  - 결과를 받아 올 때까지는 해당 메서드의 나머지 작업들은 더 이상 실행되지 않습니다.
  - try 절의 catch, finally 내부에서는 사용이 불가능 합니다. try 내부에서는 가능합니다.
  - lock, unsafe 내부에서도 사용이 불가능 합니다.

* 예제

  - 예제를 console 로 작성해도 똑같겠지만, console 로 하려고 C#을 하진 않자나요. 간단한 Form 을 하나 작성해보겠습니다.

  - 먼저 그림과 같이 버튼 하나와 textbox 만을 가진 form을 생성합니다.
    (필자는 RichTextBox로 했습니다.)

 

  - 버튼 click event 를 추가하여 아래 code와 같이 비동기 메서드를 구현합니다.



  - 결과는 다음과 같습니다. (첫 두줄의 순서는 CPU scheduling에 따라 달라 질 수 있습니다.)




* 결과 분석

  -  btn_start_Click() 에서 GetWebSync() 메서드를 호출했습니다.
  - 해당 메서드는 async로 선언되었으므로 비동기로 수행을 하며 btn_start_Click() 는 아래에 작업들을 실행시킵니다.
  - GetWebAsync() 내부에서 client.GetStringAsync() 라는 비동기 메서드를 호출합니다.
  - 해당 메서드는 string 이라는 결과 값이 있는 메서드 이므로 별도의 Task에 할당합니다.
  - 그 결과값이 필요한 시점 이전까지의 작업을 수행합니다.
  - 결과값이 필요한 시점에서는 await를 이용하여 Task의 값을 기다립니다.
  - 기다리는 시점 이후의 코드들은 결과가 올 때까지 실행되지 않습니다.