[c#] 비동기 및 동기화 버전의 코드를 모두 사용해야 할 때 DRY 원칙을 위반하지 않는 방법은 무엇입니까?

동일한 논리 / 방법의 비동기 및 동기화 버전을 모두 지원 해야하는 프로젝트를 진행 중입니다. 예를 들어 다음이 필요합니다.

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

이러한 메소드의 비동기 및 동기화 논리는 하나가 비동기이고 다른 하나가 비동기라는 점을 제외하고는 모든면에서 동일합니다. 이런 종류의 시나리오에서 DRY 원칙을 위반하지 않는 합법적 인 방법이 있습니까? 사람들이 비동기 메소드에서 GetAwaiter (). GetResult ()를 사용하여 sync 메소드에서 호출 할 수 있다고 말했습니까? 모든 시나리오에서 스레드가 안전합니까? 이것을 수행하는 또 다른 더 좋은 방법이 있습니까? 아니면 논리를 복제해야합니까?



답변

질문에 몇 가지 질문을했습니다. 나는 당신과 약간 다르게 분류 할 것입니다. 그러나 먼저 질문에 직접 대답하겠습니다.

우리 모두는 가볍고, 고품질이며, 저렴한 카메라를 원하지만, 그 말처럼 세 개 중 최대 두 개만 얻을 수 있습니다. 당신도 같은 상황에 처해 있습니다. 효율적이고 안전하며 동기 경로와 비동기 경로간에 코드를 공유하는 솔루션을 원합니다. 당신은 그 중 두 가지만 얻을 것입니다.

그 이유를 설명하겠습니다. 이 질문부터 시작하겠습니다 :


사람들이 GetAwaiter().GetResult()비동기 메소드에서 사용할 수 있고 귀하의 동기화 메소드에서 호출 할 수 있다고 말한 것을 보았 습니까? 모든 시나리오에서 스레드가 안전합니까?

이 질문의 핵심은 “동기 경로가 단순히 비동기 버전에서 동기 대기를하게함으로써 동기 경로와 비동기 경로를 공유 할 수 있습니까?”입니다.

이 점이 중요하기 때문에이 점을 명확하게 설명하겠습니다.

이 사람들로부터 조언을 즉시 중단해야 합니다.

그것은 매우 나쁜 조언입니다. 작업이 정상적으로 또는 비정상적으로 완료되었다는 증거가 없으면 비동기 작업에서 결과를 동 기적으로 가져 오는 것이 매우 위험합니다 .

이것이 매우 나쁜 조언 인 이유는이 시나리오를 고려하는 것입니다. 잔디를 깎고 싶지만 잔디 깎는 기계 블레이드가 파손되었습니다. 이 워크 플로우를 따르기로 결정했습니다.

  • 웹 사이트에서 새 블레이드를 주문하십시오. 대기 시간이 길고 비동기 작업입니다.
  • 동기식으로 기다립니다. 즉 , 블레이드를 손에 넣을 때까지 잠을 자십시오 .
  • 블레이드가 도착했는지 정기적으로 사서함을 확인하십시오.
  • 상자에서 날을 제거하십시오. 이제 손에 넣었습니다.
  • 모어에 블레이드를 설치하십시오.
  • 잔디밭을 깎습니다.

무슨 일이야? 의 동작 때문에 영원히 잠 메일을 확인이 지금 메일이 도착 후 일어나는 일에 문 .

임의의 작업을 동 기적으로 기다릴 때이 상황에 빠지기매우 쉽습니다 . 해당 태스크는 현재 대기중인 스레드의 미래에 스케줄 된 작업을 수행했을 수 있으며 이제는 대기 중이므로 해당 미래는 도달하지 않습니다.

비동기식 대기 를 수행하면 모든 것이 정상입니다! 정기적으로 우편물을 확인하고 기다리는 동안 샌드위치를 ​​만들거나 세금을 내야합니다. 기다리는 동안 계속 일을 끝내고 있습니다.

동 기적으로 기다리지 마십시오. 작업이 완료되면 필요하지 않습니다 . 작업이 완료되지 않았지만 현재 스레드를 실행하도록 예약 된 경우 현재 스레드가 대기하는 대신 다른 작업을 처리 할 수 ​​있기 때문에 비효율적 입니다. 작업이 완료되지 않고 현재 스레드에서 일정이 실행 되면 동 기적으로 대기하기 위해 중단 됩니다. 작업이 완료된 것을 이미 알고 있지 않는 한 동 기적으로 다시 기다릴 이유가 없습니다 .

이 주제에 대한 자세한 내용은 다음을 참조하십시오.

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen은 실제 시나리오를 할 수있는 것보다 훨씬 잘 설명합니다.


이제 “다른 방향”을 고려해 봅시다. 비동기 버전이 단순히 작업자 스레드에서 동기 버전을 수행하도록하여 코드를 공유 할 수 있습니까?

아마도 실제로 아마 다음과 같은 이유로, 나쁜 생각.

  • 동기 작업이 대기 시간이 긴 IO 작업 인 경우 비효율적입니다. 이것은 본질적으로 작업자를 고용하고 작업이 완료 될 때까지 해당 작업자를 잠자 게 합니다. 스레드는 미친 듯이 비싼 . 기본적으로 최소 백만 바이트의 주소 공간을 소비하고 시간이 걸리며 운영 체제 자원이 필요합니다. 쓸모없는 일을하는 실을 태우고 싶지 않습니다.

  • 동기 조작이 스레드 안전으로 작성되지 않았을 수 있습니다.

  • 이것은 이다 높은 대기 시간 작업 프로세서 바운드 인 경우보다 합리적인 기술, 그러나 다음의 경우 당신은 아마 단순히 작업자 스레드에 넘겨 싶지 않아요. 작업 병렬 라이브러리를 사용하여 가능한 한 많은 CPU로 병렬 처리하고 취소 논리를 원할 수 있으며 동기 버전이 모든 것을 수행하도록 할 수는 없습니다 . 이미 비동기 버전 이기 때문 입니다.

더 읽을 거리; 다시, Stephen은 그것을 매우 명확하게 설명합니다.

Task.Run을 사용하지 않는 이유 :

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Task.Run에 대한 추가 “하지 말고”시나리오 :

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


그러면 우리에게 무엇을 남길까요? 코드를 공유하는 두 가지 기술 모두 교착 상태 또는 큰 비 효율성을 초래합니다. 우리가 도달한다는 결론은 당신이 선택을해야한다는 것입니다. 효율적이고 정확하며 호출자를 즐겁게하는 프로그램을 원하십니까? 아니면 동기 경로와 비동기 경로 사이에 소량의 코드를 복제하여 몇 가지 키 입력을 저장하고 싶습니까? 둘 다 얻지 못해 두렵습니다.


답변

이것에 대해 하나의 크기에 맞는 답변을 제공하는 것은 어렵습니다. 불행히도 비동기 코드와 동기 코드 사이에서 재사용 할 수있는 간단하고 완벽한 방법은 없습니다. 그러나 고려해야 할 몇 가지 원칙은 다음과 같습니다.

  1. 비동기 및 동기 코드는 기본적으로 다릅니다. 비동기 코드에는 일반적으로 취소 토큰이 포함되어야합니다. 그리고 종종 다른 방법을 호출하거나 (예를 들어 Query()하나의 호출 과 QueryAsync()다른 호출 ) 다른 설정으로 연결을 설정합니다. 따라서 구조적으로 비슷하더라도 요구 사항이 다른 별도의 코드로 처리 할만한 동작 차이가 종종 있습니다. 예를 들어 File 클래스에서 메소드의 비동기 구현 과 동기화 구현의 차이점에 유의하십시오 . 동일한 코드를 사용하도록 노력하지 않습니다.
  2. 인터페이스 구현을 위해 비동기 메소드 서명을 제공하고 있지만 동기 구현이 발생하는 경우 (즉, 메소드가 수행하는 작업에 대해 본질적으로 비동기가없는 경우) 간단히 리턴 할 수 있습니다 Task.FromResult(...).
  3. 로직 상관 동기 조각 이다 두 방법 같은 별도 헬퍼 메소드를 추출하고, 두 방법에 활용 될 수있다.

행운을 빕니다.


답변

쉬운; 동기식 하나를 비동기식으로 호출하십시오. Task<T>이를 수행 하는 편리한 방법도 있습니다 .

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously();

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{
          task.Wait();
          return task.Result;
      }
      catch(Exception ex)
      {
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}


답변