.NET 의 비동기 프로 그래 밍-continuation passing style 및 yield 를 사용 하여 비동기 화

전통 적 인 비동기 방식 은 원래 의 치밀 한 코드 를 두 부분 으로 나 누 었 다.코드 의 가 독성 을 낮 출 뿐만 아니 라 일부 기본 적 인 프로그램 구 조 를 사용 할 수 없 게 하기 때문에 대부분의 개발 자 들 은 이 보 를 사용 해 야 할 곳 을 만 나 고통 을 참 으 며 사랑 을 베 었 다.원래 저 는 이 글 에서.NET 세계 에 존재 하 는 몇 가지 비동기 개발 을 보조 하 는 라 이브 러 리 에 대해 토론 하고 싶 었 지만 생각 을 한 후에 그 전에 이론 지식 을 소개 하 는 것 이 뒤의 라 이브 러 리 와 업 데 이 트 된 내용 을 이해 하 는 데 도움 이 될 것 이 라 고 생각 했 습 니 다.오늘 우리 가 토론 할 것 은 Continuation Passing Style 이 며,약칭 CPS 이다.
  CPS
우선,다음 방법 을 살 펴 보 자.
   1: public int Add(int a, int b)
   2: {
   3:     return a + b;
   4: }

우 리 는 일반적으로 이렇게 그것 을 호출한다.
   1: Print(Add(5, 6))
   2:  
   3: public void Print(int result)
   4: {
   5:     Console.WriteLine(result);
   6: }

만약 우리 가 CPS 방식 으로 위의 코드 를 작성 한다 면:
   1: public void Add(int a, int b, Action<int> continueWith)
   2: {
   3:     continueWith(a+b);
   4: }
   5:  
   6: Add(5, 6, (ret) => Print(ret));

마치 우리 가 방법 을 거꾸로 하 는 것 처럼 우 리 는 더 이상 직접 방법의 결과 로 돌아 가 는 것 이 아니다.우리 가 지금 하고 있 는 것 은 하나의 의뢰 를 받 는 것 이다.이 의뢰 는 내 가 이 방법 을 연산 한 후에 무엇 을 해 야 하 는 지 를 나타 내 는 것 이다.바로 전설의 contine 이다.여기 서 Add 의 continue 는 Print 입 니 다.
위 와 같은 코드 예시 뿐만 아니 라한 방법 에서 이 문장 뒤에 실 행 된 문장 은 모두 이 문장의 contine 이 라 고 할 수 있다.
CPS 와 Async
그럼 누가 물 어 볼 수도 있 겠 네요.이렇게 많은 말 을 하 는 것 이 비동기 와 무슨 관계 가 있 습 니까?네,비동기 와 큰 관계 가 있 습 니 다.이전 글 을 돌 이 켜 보면 전형 적 인 비동기 모드 는 모두 Begin 으로 시작 하 는 방법 으로 비동기 요청 을 하고 이 방법 에 반전(callback)을 보 냅 니 다.비동기 실행 이 끝 난 후에 이 반전 이 실 행 됩 니 다.그러면 우 리 는 이 비동기 가 요구 하 는 contine 이 라 고 할 수 있 습 니 다.
   1: stream.BeginRead(buffer, 0, 1024, continueWith, null)

이게 무슨 소 용이 야?그럼 우리 가 원 하 는 비동기 코드 부터 살 펴 보 자.
   1: var request = HttpWebRequest.Create("http://www.google.com");
   2: var asyncResult1 = request.BeginGetResponse(...);
   3: var response = request.EndGetResponse(asyncResult1);
   4: using(stream = response.GetResponseStream())
   5: {
   6:     var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
   7:     var actualRead = stream.EndRead(asyncResult2);
   8: }

맞 아,우 리 는 동기 화 방식 처럼 비동기 코드 를 만 들 고 싶 어.나 는 그렇게 많은 반전 을 싫어한다.특히 한 링 에 한 링 을 끼 워 넣 은 반전 을 싫어한다.
앞에서 CPS 에 대한 토론 을 참조 하 십시오.request.Bigin GetResponse 이후 의 코드 는 모두 contine 입 니 다.만약 에 제 가 contine 을 얻 을 수 있 는 메커니즘 이 있다 면 제 가 실 행 된 후에 contine 을 호출 하 는 것 이 좋 겠 습 니 다.안 타 깝 게 도 C\#Scheme 과 같은 제어 연산 자 call/cc 가 contine 을 가 져 오지 않 았 습 니 다.
생각 이 여기까지 끊 긴 것 같다.그러나 우 리 는 각 도 를 바 꾸 어 생각해 볼 수 있 습 니까?만약 에 우리 가 위의 코드 에 표 지 를 추가 할 수 있다 면 모든 비동기 요청 이 시 작 된 곳 에 표 지 를 추가 하고 표지 의 다음 부분 은 contine 입 니 다.
var request = HttpWebRequest.Create("http://www.google.com");
  1 var asyncResult1 = request.BeginGetResponse(...);
var response = request.EndGetResponse(asyncResult1);
using(stream = response.GetResponseStream())
{
      2 var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
    var actualRead = stream.EndRead(asyncResult2);
}

표지 1 에 실 행 했 을 때 바로 돌아 가 고 이번 집행 은 표지 1 에 만 실 행 된 것 을 기억 합 니 다.비동기 요청 이 끝 난 후에 마지막 에 표지 1 에 실 행 된 것 을 알 게 되 었 습 니 다.그러면 이때 표지 1 의 다음 줄 에서 실 행 됩 니 다.표지 2 에 실 행 했 을 때 또 하나의 비동기 요청 을 만 났 습 니 다.바로 표지 2 에 실 행 된 것 을 기억 합 니 다.그리고 요청 이 완료 되면 표지 2 의 다음 줄 에서 실행 을 재 개 합 니 다.그러면 현재 의 임 무 는 표 지 를 하고 비동기 요청 이 끝 난 후에 표지 위치 에서 어떻게 다시 실행 하 는 지 하 는 것 이다.
yield 와 비동기
C\#2.0 에 추 가 된 교체 기 특성 을 잘 알 고 있다 면,yield 는 우리 가 표 지 를 할 수 있 는 물건 이라는 것 을 알 게 될 것 이다.다음 코드 보기:
   1: public IEnumerator<int> Demo()
   2: {
   3:     //code 1
   4:     yield return 1;
   5:     //code 2
   6:     yield return 2;
   7:     //code 3
   8:     yield return 3;
   9: }

컴 파일 을 통 해 다음 과 같은 코드 를 생 성 합 니 다.
   1: public IEnumerator<int> Demo()
   2: {
   3:    return new GeneratedEnumerator();
   4: }
   5:  
   6: public class GeneratedEnumerator
   7: {
   8:     private int state = 0;
   9:  
  10:     private int currentValue = 0;
  11:     
  12:     public bool MoveNext()
  13:     {
  14:         switch(state)
  15:         {
  16:             case 0:
  17:                 //code 1
  18:                 currentValue = 1;
  19:                 state = 1;
  20:                 return true;
  21:             case 1:
  22:                 //code 2
  23:                 currentValue = 2;
  24:                 state = 2;
  25:                 return true;
  26:             case 2:
  27:                 //code 3
  28:                 currentValue = 3;
  29:                 state = 3;
  30:                 return true;
  31:             default:return false;
  32:         }
  33:     }
  34:     
  35:     public int Current{get{return currentValue;}}
  36: }

네,C\#컴 파일 러 가 상태 기 로 번역 되 었 습 니 다.yield return 은 마치 많은 표 시 를 한 것 같 습 니 다.MoveNext 는 호출 할 때마다 다음 yield return 이전 코드 를 실행 하고 바로 돌아 갑 니 다.
자,이제 표 시 를 하 는 기능 이 생 겼 습 니 다.우 리 는 어떻게 비동기 요청 이 끝 난 후에 호출 을 재 개 합 니까?위의 코드 를 통 해 당신 은 이미 생각 했 을 것 입 니 다.우 리 는 여기에서 호출 을 재 개 하려 면 MoveNext 를 다시 호출 하면 됩 니 다.그 상태 기 회 는 우 리 를 도와 모든 것 을 처리 할 수 있 습 니 다.
그러면 우 리 는 우리 의 비동기 코드 를 개조 합 니 다.
   1: public IEnumerator<int> Download()
   2: {
   3:     var request = HttpWebRequest.Create("http://www.google.com");
   4:     var asyncResult1 = request.BeginGetResponse(...);
   5:     yield return 1;
   6:     var response = request.EndGetResponse(asyncResult1);
   7:     using(stream = response.GetResponseStream())
   8:     {
   9:         var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
  10:         yield return 1;
  11:         var actualRead = stream.EndRead(asyncResult2);
  12:     }
  13: }

표 시 를 다 했 습 니 다.비동기 호출 이 끝나 면 MoveNext 를 실행 하 는 방법 을 고려 해 보 세 요.
하하,비동기 호출 된 AsyncCallback 리 셋 기억 나 세 요?비동기 요청 이 끝나 면 호출 될 겁 니 다.만약 우리 가 비동기 요청 을 한 Begin XXX 방법 에 AsyncCallback 을 전달한다 면,이 리 셋 에서 MoveNext 를 호출 하 는 것 은 어 떻 습 니까?
   1: public IEnumerator<int> Download(Context context)
   2: {
   3:     var request = HttpWebRequest.Create("http://www.google.com");
   4:     var asyncResult1 = request.BeginGetResponse(context.Continue(),null);
   5:     yield return 1;
   6:     var response = request.EndGetResponse(asyncResult1);
   7:     using(stream = response.GetResponseStream())
   8:     {
   9:         var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);
  10:         yield return 1;
  11:         var actualRead = stream.EndRead(asyncResult2);
  12:     }
  13: }

Continue 방법의 정 의 는:
   1: public class Context
   2: {
   3:     //...
   4:     private IEnumerator enumerator;
   5:     
   6:     public AsyncCallback Continue()
   7:     {
   8:         return (ar) => enumerator.MoveNext();
   9:     }
  10: }

Continue 방법 을 호출 하기 전에 Context 클래스 는 다운로드 방법 이 있 는 IEnumerator 를 저장 해 야 합 니 다.따라서:
   1: public class Context
   2: {
   3:     //...
   4:     private IEnumerator enumerator;
   5:     
   6:     public AsyncCallback Continue()
   7:     {
   8:         return (ar) => enumerator.MoveNext();
   9:     }
  10:  
  11:     public void Run(IEnumerator enumerator)
  12:     {
  13:         this.enumerator = enumerator;
  14:         enumerator.MoveNext();
  15:     }
  16: }

다운로드 방법 을 호출 하면 다음 과 같이 쓸 수 있 습 니 다.
   1: public void Main()
   2: {
   3:     Program p = new Program();
   4:     
   5:     Context context = new Context();
   6:     context.Run(p.Download(context));
   7: }

실행 방식 이 다른 것 을 제외 하고 우 리 는 거의 동기 화 방식 처럼 비동기 코드 를 작성 할 수 있다.
전체 코드 는 다음 과 같 습 니 다.
   1: public class Context
   2: {
   3:     private IEnumerator enumerator;
   4:     
   5:     public AsyncCallback Continue()
   6:     {
   7:         return (ar) => enumerator.MoveNext();
   8:     }
   9:  
  10:     public void Run(IEnumerator enumerator)
  11:     {
  12:         this.enumerator = enumerator;
  13:         enumerator.MoveNext();
  14:     }
  15: }
  16:  
  17: private void btnDownload_click(object sender,EventArgs e)
  18: {
  19:     Context context = new Context();
  20:     context.Run(Download(context));
  21: }
  22:  
  23: private IEnumerator<int> Download(Context context)
  24: {
  25:     var request = HttpWebRequest.Create("http://www.google.com");
  26:     var asyncResult1 = request.BeginGetResponse(context.Continue(),null);
  27:     yield return 1;
  28:     var response = request.EndGetResponse(asyncResult1);
  29:     using(stream = response.GetResponseStream())
  30:     {
  31:         var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);
  32:         yield return 1;
  33:         var actualRead = stream.EndRead(asyncResult2);
  34:     }
  35: }

눈치 채 셨 는 지 모 르 겠 습 니 다.우 리 는 비동기 코드 를 순서대로 작성 할 수 있 을 뿐만 아니 라 using 과 같은 구조 도 사용 할 수 있 습 니 다.이 코드 를 더 깊이 이해 하고 싶다 면 Reflector 를 사용 하여 교체 기 가 마지막 으로 생 성 한 코드 를 보 는 것 을 추천 합 니 다.나 는 여기 서 간단 한 설명 을 한다.
1.Context 의 Run 호출 시 Dowload 방법 을 호출 하여 IEnumerator 대상 을 얻 습 니 다.저 희 는 이 대상 을 Context 의 인 스 턴 스 필드 에 저장 하여 나중에 사용 할 수 있 도록 합 니 다.
2.이 IEnumerator 대상 의 MoveNext 방법 을 호출 합 니 다.이 방법 은 첫 번 째 yield return 위치 로 실 행 된 다음 에 되 돌아 갑 니 다.이때 request.Begin GetResponse 가 호출 되 었 습 니 다.이 때 스 레 드 는 다른 일 을 할 수 있 습 니 다.
3.BeginGetResponse 호출 시 Context 의 Continue 방법 을 통 해 리 셋 을 전 달 했 습 니 다.이 리 셋 에 서 는 방금 저 장 된 IEnumerator 대상 의 MoveNext 방법 을 실행 합 니 다.즉,BeginGetResponse 라 는 비동기 요청 이 실 행 된 후에 MoveNext 방법 을 호출 하여 흐름 을 제어 하고 다시 다운로드 방법 으로 돌아 가 다음 yield return 으로 실 행 됩 니 다.
총결산
본 고 를 정리 하면 우 리 는 우리 가 원 하 는 것 이 순서 스타일 의 코드 를 어떻게 CPS 방식 으로 바 꾸 는 지,어떻게 비동기 적 으로 이 코드 를 요청 하 는 contine 을 찾 는 지 알 게 되 었 다.C\#는 yield 라 는 메커니즘 을 제 공 했 기 때문에 C\#컴 파 일 러 는 상태 기 를 생산 하여 제어 권 을 호출 코드 와 호출 된 코드 사이 에서 교환 할 수 있 습 니 다.
주의해 야 할 것 은 본 논문 이 마지막 으로 실현 한 비동기 집행 방식 이 매우 초라 하기 때문에 제품 코드 에 절대 응용 할 수 없다 는 것 이다.이곳 은 단지 목적 을 시범 하기 위 한 것 이다.이 방면 에서 마이크로소프트 커 뮤 니 티 의 큰 소 Jeffrey Ritcher 는 우리 가 Power Threading 이라는 라 이브 러 리 를 개발 했다 고 생각 했 는데 그 안에 AsyncEnumerator 류 를 제공 하여 더욱 믿 을 만 한 실현 이 었 다.
마이크로소프트 가 자체 적 으로 로봇 개발 에 제공 한 CCR 도 비슷 한 실현 을 제공 했다.우 리 는 다음 문장 에서 이 두 종류의 라 이브 러 리 를 배 울 것 이다.

좋은 웹페이지 즐겨찾기