async-await가 추가 스레드를 만들지 않으면 응용 프로그램을 어떻게 반응하게합니까?


242

몇 번이고, async- await사용하면 추가 스레드가 생성되지 않는다고 말했습니다 . 컴퓨터가 한 번에 둘 이상의 작업을 수행하는 것처럼 보일 수있는 유일한 방법은

  • 실제로 한 번에 둘 이상의 작업을 수행 (병렬로 실행, 여러 프로세서 사용)
  • 작업을 예약하고 작업을 전환하여 시뮬레이션합니다 (약간 A, 약간의 B, 약간의 A 등).

그래서 경우 async- await그 중 어느 것도, 그 다음 어떻게 응용 프로그램이 응답 할 수 있습니까? 스레드가 하나만있는 경우 메서드를 호출한다는 것은 메서드를 완료하기 전에 다른 작업을 수행하기를 기다리는 것을 의미하며 해당 메서드 내의 메서드는 계속 진행하기 전에 결과를 기다려야합니다.


17
IO 작업은 CPU에 바인딩되지 않으므로 스레드가 필요하지 않습니다. 비동기의 주요 요점은 IO 바운드 작업 중에 스레드를 차단하지 않는 것입니다.
juharr 2016 년

24
@jdweng : 아뇨, 전혀 아닙니다. 새 스레드를 만들었더라도 새 프로세스를 만드는 것과는 다릅니다.
Jon Skeet

8
콜백 기반 비동기 프로그래밍을 이해 하면 스레드를 만들지 않고 await/ async작동 방식을 이해할 수 있습니다 .
user253751 2016 년

6
정확하게 응용 프로그램의 응답 속도를 높일 는 없지만 응답하지 않는 응용 프로그램의 일반적인 원인 인 스레드를 차단하지 못하게합니다.
Owen

6
@RubberDuck : 예, 계속하기 위해 스레드 풀의 스레드를 사용할 수 있습니다. 그러나 OP가 여기에서 상상하는 방식으로 스레드를 시작하지는 않습니다. "이 일반적인 방법을 사용하여 이제 별도의 스레드에서 실행하십시오. 비동기식입니다." 그것보다 훨씬 미묘합니다.
Jon Skeet

답변:


299

사실, async / await는 그렇게 마 법적이지 않습니다. 전체 주제는 상당히 광범위하지만 귀하의 질문에 대한 신속하면서도 충분한 답변을 위해 관리 할 수 ​​있다고 생각합니다.

Windows Forms 응용 프로그램에서 간단한 버튼 클릭 이벤트를 해결해 보겠습니다.

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

나는 그것이 무엇인지에 대해 명시 적으로 이야기 하지 않을 것입니다GetSomethingAsync 지금 돌아 오는 . 이것이 2 초 후에 완료 될 것이라고 가정 해 봅시다.

비동기식의 전통적인 세계에서 버튼 클릭 이벤트 핸들러는 다음과 같습니다.

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

양식에서 버튼을 클릭하면이 방법이 완료 될 때까지 기다리는 동안 응용 프로그램이 약 2 초 동안 정지 된 것으로 나타납니다. 기본적으로 루프 인 "메시지 펌프"가 차단됩니다.

이 루프는 계속해서 창에 묻습니다. "마우스를 움직이거나 무언가를 클릭 한 것과 같은 일을 한 사람이 있습니까? 무언가를 다시 칠해야합니까? 그렇다면 말 해주세요!" 그런 다음 "무언가"를 처리합니다. 이 루프는 사용자가 "button1"(또는 Windows의 동등한 유형의 메시지)을 클릭했다는 메시지를 받고 button1_Click위 의 메소드를 호출했습니다 . 이 메소드가 리턴 될 때까지이 루프는 대기 상태에 있습니다. 이 과정에는 2 초가 걸리며이 동안에는 메시지가 처리되지 않습니다.

창을 다루는 대부분의 작업은 메시지를 사용하여 수행됩니다. 즉, 메시지 루프가 메시지 펌핑을 중지하더라도 1 초 동안이라도 사용자가 빠르게 알아볼 수 있습니다. 예를 들어, 메모장이나 다른 프로그램을 자신의 프로그램 위로 옮긴 다음 다시 멀리 떨어 뜨리면 페인트 표시 메시지가 프로그램에 전송되어 창의 어느 영역이 갑자기 다시 표시되는지를 나타냅니다. 이러한 메시지를 처리하는 메시지 루프가 무언가를 기다리는 중이거나 차단 된 경우 페인팅이 수행되지 않습니다.

따라서 첫 번째 예에서 async/await새 스레드를 만들지 않으면 어떻게됩니까?

글쎄, 당신의 방법은 두 가지로 나뉩니다. 이것은 광범위한 주제 유형 중 하나이므로 너무 자세하게 설명하지는 않지만 방법이 다음 두 가지로 나뉘어 있다고 말할 수 있습니다.

  1. await대한 호출을 포함하여로 이어지는 모든 코드GetSomethingAsync
  2. 다음의 모든 코드 await

삽화:

code... code... code... await X(); ... code... code... code...

재정렬 :

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

기본적으로 메소드는 다음과 같이 실행됩니다.

  1. 그것은 모든 것을 실행합니다 await
  2. 그것은 GetSomethingAsync그 일을 하는 메소드를 호출 하고 미래에 2 초가 완료되는 것을 반환합니다.

    지금까지 우리는 여전히 메시지 루프에서 호출되는 메인 스레드에서 발생하는 button1_Click에 대한 원래 호출 내부에 있습니다. 코드 await가 많은 시간이 걸리더라도 UI는 여전히 멈 춥니 다. 이 예에서는 그리 많지 않습니다

  3. 어떤 await키워드가 함께 일부 영리한 컴파일러 마법, 일은 그것이 기본적으로 같은 "좋아, 당신은 단순히 여기에 버튼 클릭 이벤트 핸들러에서 돌아 갈거야 알고. 때 건에서와 같이 (우리 '입니다 다시 기다리고) 완료하기 위해, 아직 실행 코드가 남아 있기 때문에 알려주세요. "

    실제로 SynchronizationContext 클래스 에 완료되었음을 알립니다. 현재 실행중인 실제 동기화 컨텍스트에 따라 실행 대기 상태가됩니다. Windows Forms 프로그램에서 사용되는 컨텍스트 클래스는 메시지 루프가 펌핑하는 큐를 사용하여 큐에 넣습니다.

  4. 따라서 메시지 루프로 돌아갑니다. 이제 창 이동, 크기 조정 또는 다른 버튼 클릭과 같은 메시지를 계속 펌핑 할 수 있습니다.

    사용자에게는 이제 UI가 다시 반응하여 다른 버튼 클릭을 처리하고 크기를 조정하고 가장 중요하게 다시 그리기 때문에 멈추는 것처럼 보이지 않습니다.

  5. 2 초 후, 우리가 기다리고있는 것은 이제 완료됩니다. 이제 동기화 컨텍스트는 메시지 루프가보고있는 큐에 메시지를 배치하여 "이봐, 코드가 더 있습니다." "실행해야합니다."이 코드는 대기 후의 모든 코드입니다 .
  6. 메시지 루프가 해당 메시지에 도달하면 기본적으로 해당 메소드를 중단 한 직후에 "다시 입력" await하고 나머지 메소드 실행을 계속합니다. 이 코드는 메시지 루프에서 다시 호출되므로이 코드를 async/await올바르게 사용 하지 않고 긴 작업을 수행 하면 메시지 루프가 다시 차단됩니다.

여기 후드 아래에 움직이는 부분이 많으므로 여기에 자세한 정보에 대한 링크가 있습니다. "필요한 것"이라고 말하려고했지만이 주제 매우 광범위하며 이러한 움직이는 부분 중 일부 를 아는 것이 매우 중요합니다 . async / await는 여전히 누설 개념이라는 것을 이해하게 될 것입니다. 근본적인 제한 사항과 문제 중 일부는 여전히 주변 코드로 유출되며 그렇지 않은 경우 일반적으로 아무 이유없이 무작위로 중단되는 응용 프로그램을 디버깅해야합니다.


그렇다면 GetSomethingAsync2 초 안에 완료 될 실을 회전 시키면 어떻게 될까요? 예, 분명히 새로운 스레드가 있습니다. 그러나이 스레드 는이 메소드의 비동기 성 때문 이 아니라이 메소드의 프로그래머가 비동기 코드를 구현할 스레드를 선택했기 때문입니다. 거의 모든 비동기 I / O 스레드를 사용 하지 않고 다른 것을 사용합니다. async/await 자체적으로 새로운 스레드를 만들지는 않지만 분명히 "우리가 기다리는 것"은 스레드를 사용하여 구현 될 수 있습니다.

.NET에는 스레드를 자체적으로 가동시킬 필요는 없지만 여전히 비동기적인 많은 것들이 있습니다.

  • 웹 요청 (및 시간이 걸리는 다른 많은 네트워크 관련 사항)
  • 비동기 파일 읽기 및 쓰기
  • 문제의 클래스 / 인터페이스라는 이름의 한 방법 경우 많은 더 좋은 기호는 SomethingSomethingAsyncBeginSomething하고 EndSomething하고 거기에 IAsyncResult참여가.

일반적으로 이러한 것들은 후드 아래에서 실을 사용하지 않습니다.


자, "광범위한 주제"를 원하십니까?

버튼 클릭에 대해 Roslyn을 물어 보십시오 .

Roslyn을보십시오

나는 여기에 완전히 생성 된 클래스를 연결하지는 않지만 꽤 까다 롭습니다.


11
기본적으로 OP는 " 작업을 예약하고 이들 사이를 전환하여 병렬 실행 시뮬레이션 "이라고 설명 하지 않습니까?
Bergi

4
@ 버기 실행은 실제로 병렬입니다-비동기 I / O 작업이 진행 중이며 진행할 스레드가 필요하지 않습니다 (Windows가 시작되기 오래 전에 사용 된 것임)-MS DOS는 비동기 I / O를 사용했습니다. 멀티 스레딩이 있습니다!). 물론 설명하는 방식으로도 사용할 await 있지만 일반적으로 그렇지 않습니다. 콜백과 요청 사이에 스레드 풀에서 콜백 만 예약됩니다 (스레드 필요 없음).
Luaan

3
그렇기 때문에 자체 스레드를 생성하지 않는 async / await에 대한 질문이 있었기 때문에 해당 메소드가 수행 한 작업에 대해 너무 많은 이야기를 피하고 싶었습니다. 물론, 그들은 기다릴 수 있습니다 에 대한 완전한에 스레드.
Lasse V. Karlsen

6
@ LasseV.Karlsen-나는 당신의 큰 대답을 얻었지만 여전히 세부 사항에 매달 렸습니다. 나는 이벤트 핸들러가 메시지 펌프가 펌핑을 계속 할 수 있도록 4 단계에서와 같이, 존재 이해하지만 은 "2 초 걸리는 것은"하지 않을 경우 별도의 스레드에서 실행을 계속합니까? UI 스레드에서 실행 되는 경우 동일한 스레드에서 시간 을 실행해야하므로 메시지 펌프가 실행되는 동안 어쨌든 메시지 펌프를 차단합니다 . [계속] ...
rory.ap

3
메시지 펌프에 대한 설명이 마음에 듭니다. 콘솔 응용 프로그램이나 웹 서버와 같은 메시지 펌프가 없을 때 설명이 어떻게 다릅니 까? 분석법의 재 추적은 어떻게 이루어 집니까?
Puchacz

95

내 블로그 게시물 There Is No No Thread 에서 자세히 설명합니다 .

요약하면, 최신 I / O 시스템은 DMA (Direct Memory Access)를 많이 사용합니다. 네트워크 카드, 비디오 카드, HDD 컨트롤러, 직렬 / 병렬 포트 등에 특수한 전용 프로세서가 있습니다.이 프로세서는 메모리 버스에 직접 액세스 할 수 있으며 CPU와는 완전히 독립적으로 읽기 / 쓰기를 처리합니다. CPU는 데이터를 포함하는 메모리의 위치를 ​​장치에 알리기 만하면 장치가 읽기 / 쓰기가 완료되었음을 CPU에 알리는 인터럽트가 발생할 때까지 자체적으로 할 수 있습니다.

작업이 시작되면 CPU가 수행 할 작업이 없으므로 스레드가 없습니다.


async-await를 사용할 때 발생하는 높은 수준을 이해합니다. 스레드 생성 없음과 관련하여-요청 자체를 처리하는 자체 프로세서가 있다고 말한 장치에 대한 I / O 요청에만 스레드가 있습니까? 모든 I / O 요청이 이러한 독립적 인 프로세서에서 처리된다고 가정 할 수 있습니까?
Yonatan Nir

@YonatanNir : 별개의 프로세서가 아닙니다. 모든 종류의 이벤트 중심 응답은 당연히 비동기 적입니다. Task.RunCPU 바운드 작업에 가장 적합 하지만 다른 용도도 있습니다.
Stephen Cleary

1
나는 당신의 기사를 다 읽었지만 여전히 OS의 하위 레벨 구현에 익숙하지 않기 때문에 이해할 수없는 기본적인 것이 있습니다. "쓰기 작업이"비행 중 "입니다. 처리중인 스레드 수는 없습니까?" . 따라서 스레드가 없으면 스레드가 아닌 경우 작업 자체는 어떻게 수행됩니까?
Yonatan Nir

6
이것은 수천 가지 설명에서 빠진 부분입니다 !!! 실제로 백그라운드에서 I / O 작업으로 작업을 수행하는 사람이 있습니다. 스레드가 아니라 다른 전용 하드웨어 구성 요소로 작동합니다!
the_dark_destructor

2
@PrabuWeerasinghe : 컴파일러는 상태 및 로컬 변수를 보유하는 구조체를 만듭니다. 대기가 양보해야하는 경우 (즉, 호출자에게 반환) 해당 구조체는 박스로 채워져 힙에 남아 있습니다.
Stephen Cleary

87

컴퓨터가 한 번에 둘 이상의 작업을 수행하는 것처럼 보이는 유일한 방법은 (1) 실제로 한 번에 두 개 이상의 작업을 수행하는 것, (2) 작업을 예약하고 컴퓨터간에 전환하여 시뮬레이션하는 것입니다. 따라서 async-await가 이들 중 어느 것도 수행하지 않으면

기다릴 수있는 것은 아닙니다 . 목적은 동기 코드를 마술처럼 비동기 적await 으로 만드는 것이 아닙니다 . 그것은 활성화의 비동기 코드로 호출 할 때 우리는 동기 코드를 작성하기 위해 사용하는 것과 동일한 기술을 사용하여 . Await는 대기 시간이 긴 작업을 사용하는 코드를 대기 시간이 짧은 작업을 사용하는 코드처럼 보이게 만드는 것 입니다. 대기 시간이 긴 작업은 스레드에서, 특수 목적의 하드웨어에서, 작업을 작은 조각으로 찢어 나중에 UI 스레드에서 처리하기 위해 메시지 큐에 넣을 수 있습니다. 그들은 비동기 성을 달성하기 위해 무언가 를 하고 있지만 그들은하고있는 사람들입니다. 기다리다가 그 비동기 성을 활용할 수 있습니다.

또한 세 번째 옵션이 누락되었다고 생각합니다. 랩 음악을 가진 오늘날의 어린이들은 잔디밭에서 나와야합니다. 1990 년대 초 Windows의 세계를 기억하십시오. 다중 CPU 머신과 스레드 스케줄러가 없었습니다. 당신은 당신이했다, 같은 시간에 두 개의 Windows 응용 프로그램을 실행하고 싶었다 얻을 . 멀티 태스킹은 협조적 이었습니다 . OS는 프로세스에 실행을 알리고, 동작이 잘못되면 다른 모든 프로세스가 처리되지 않게합니다. 수율이 나올 때까지 실행되며 , 다음에 OS 손이 다시 제어 할 때 중단 된 부분을 가져 오는 방법을 알아야합니다.. 단일 스레드 비동기 코드는 "yield"대신 "await"를 사용하는 것과 비슷합니다. 기다리는 것은 "여기서 내가 어디에서 멈췄는지 기억하고 다른 누군가가 잠시 도망 가게 해주는 것입니다. 대기중인 작업이 완료되면 다시 전화하면 중단 한 곳에서 전화를받을 것입니다." Windows 3 일과 마찬가지로 앱의 응답 성을 향상시키는 방법을 알 수 있다고 생각합니다.

메소드를 호출한다는 것은 메소드가 완료되기를 기다리는 것을 의미합니다.

빠진 열쇠가 있습니다. 작업이 완료되기 전에 메서드가 반환 될 수 있습니다 . 그것이 바로 비동기의 본질입니다. 메소드는 "이 작업이 진행 중입니다. 완료되면 어떻게해야하는지 알려주십시오"라는 태스크를 리턴합니다. 이 메서드는 반환 되었음에도 불구하고 수행되지 않습니다 .

기다림 연산자 전에 스위스 치즈를 통해 스파게티처럼 생긴 코드를 작성하여 완료 후 해야 할 작업을 처리 했지만 반환 및 완료가 비 동기화되는 코드를 작성해야했습니다 . Await를 사용하면 리턴처럼 보이고 완료가 실제로 동기화 되지 않고 동기화 된 코드를 작성할 수 있습니다 .


다른 현대의 고급 언어도 이와 유사한 명시 적 협동 동작을 지원합니다 (예 : 함수가 일부 작업을 수행하고 [발신자에게 일부 값 / 객체를 전송 함], 제어가 다시 전달 될 때 중단 된 지점에서 계속 진행됨 [추가 입력이 제공 될 수 있음] ). 파이썬에서 제너레이터는 매우 크다.
JAB

2
@JAB : 물론입니다. C #에서는 생성기를 "반복자 블록"이라고하며 yield키워드를 사용합니다 . asyncC #의 메소드와 반복자는 모두 coroutine 의 형식으로 , 나중에 재개를 위해 현재 작업을 일시 중단하는 방법을 알고있는 함수의 일반적인 용어입니다. 요즘 많은 언어에 코 루틴이나 코 루틴 유사 제어 흐름이 있습니다.
Eric Lippert

1
수율에 대한 유추는 좋은 것 입니다. 한 프로세스 내 에서 협력적인 멀티 태스킹 입니다. (따라서 시스템 차원의 협업 멀티 태스킹의 시스템 안정성 문제 방지)
user253751

3
IO에 사용되는 "cpu 인터럽트"의 개념은 많은 모뎀 "프로그래머"에 대해 알지 못하므로 스레드가 IO의 각 비트를 기다려야한다고 생각합니다.
Ian Ringrose

@EricLippert WebClient의 비동기 메소드는 실제로 추가 스레드를 생성합니다. 여기를 참조하십시오 stackoverflow.com/questions/48366871/…
KevinBui

28

가장 긴 시간 동안 스레드가 동시성에 필요하다고 믿었 기 때문에 누군가이 질문을 한 것이 정말 기쁩니다. 내가 처음으로 이벤트 루프를 보았을 때 나는 그것이 거짓말이라고 생각했다. 나는 "이 코드가 단일 스레드에서 실행될 경우이 코드를 동시에 사용할 수있는 방법은 없다"고 생각했다. 이것은 동시성과 병렬 처리의 차이점을 이해하기 위해 이미 어려움을 겪은 후에 입니다.

내 자신의 연구 끝에 마침내 누락 된 부분을 발견했습니다 select(). 특히, 다른 이름으로 다양한 커널에 의해 구현 IO 멀티플렉싱, : select(), poll(), epoll(), kqueue(). 이들은 시스템 호출 구현 세부 사항이 다를 때, 당신은 세트에 전달할 수 있도록 파일 기술자 볼. 그런 다음 감시 된 파일 설명자 중 하나가 변경 될 때까지 차단되는 다른 호출을 수행 할 수 있습니다.

따라서 일련의 IO 이벤트 (주 이벤트 루프)를 대기하고 완료된 첫 번째 이벤트를 처리 한 다음 제어를 이벤트 루프로 되돌릴 수 있습니다. 헹구고 반복하십시오.

이것은 어떻게 작동합니까? 짧은 대답은 커널과 하드웨어 수준의 마법이라는 것입니다. 컴퓨터 외에 CPU 외에 많은 구성 요소가 있으며이 구성 요소는 병렬로 작동 할 수 있습니다. 커널은 이러한 장치를 제어하고 직접 통신하여 특정 신호를 수신 할 수 있습니다.

이러한 IO 멀티플렉싱 시스템 호출은 node.js 또는 Tornado와 같은 단일 스레드 이벤트 루프의 기본 빌딩 블록입니다. await함수일 때 특정 이벤트 (해당 함수의 완료)를 관찰 한 다음 다시 기본 이벤트 루프로 제어합니다. 보고있는 이벤트가 완료되면 기능은 (최종적으로) 중단 된 위치에서 선택됩니다. 이와 같이 계산을 일시 중단 및 재개 할 수있는 기능을 코 루틴 이라고 합니다.


25

awaitasync사용 작업 하지 스레드.

이 프레임 워크에는 Task 객체 형식으로 일부 작업을 실행할 수있는 스레드 풀이 있습니다. 풀에 태스크 를 제출 한다는 것은 태스크 조치 메소드를 호출하기 위해 이미 존재 하는 비어있는 1 스레드를 선택하는 것을 의미 합니다.
만들기 작업은 훨씬 빠른 방법은 새 스레드를 만드는 것보다, 새로운 객체를 생성하는 문제입니다.

주어진 작업은 첨부 할 수 있습니다 계속을 은 새로운, 그것으로 작업 스레드 종료 후 실행해야 할 객체입니다.

Task를async/await 사용 하기 때문에 스레드를 만들지 않습니다 .


인터럽트 프로그래밍 기술은 모든 최신 OS에서 널리 사용되지만 여기서는 관련이 없다고 생각합니다.
를 사용하여 단일 CPU에서 두 개의 CPU 본딩 작업 을 병렬로 (실제로 인터리브) 실행할 수 있습니다 aysnc/await.
OS가 큐잉 IORP를 지원한다는 사실만으로는 간단하게 설명 할 수 없습니다 .


마지막으로 컴파일러가 변환 한 async메소드를 DFA 로 확인하면 작업은 단계로 나뉘며 각 단계는 await명령으로 종료됩니다 .
await그 시작 작업을 하고 그것을 다음 단계를 실행하기 위해 계속 연결합니다.

개념의 예로서 여기에 의사 코드의 예가 있습니다.
명확성을 위해 상황이 단순화되고 있으며 모든 세부 사항을 정확하게 기억하지 못하기 때문입니다.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

이런 식으로 변형됩니다

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 실제로 풀에는 작업 생성 정책이있을 수 있습니다.


16

나는 Eric Lippert 또는 Lasse V. Karlsen과 경쟁하지 않을 것입니다. 다른 사람들은이 질문의 또 다른 측면에 주목하고 싶습니다. 명백하게 언급되지 않았다고 생각합니다.

await자체적으로 사용 한다고해서 앱이 마술처럼 반응하지는 않습니다. UI 스레드 블록에서 기다리고있는 메소드에서 무엇을 하든지 여전히 UI를 기다릴 수없는 버전과 동일한 방식으로 차단합니다 .

대기중인 메소드를 작성하여 새 스레드를 생성하거나 완료 포트와 같은 것을 사용하십시오 (현재 스레드에서 실행을 리턴하고 완료 포트가 신호 될 때마다 계속하기 위해 다른 것을 호출합니다). 그러나이 부분은 다른 답변에서 잘 설명되어 있습니다.


3
처음에는 경쟁이 아닙니다. 공동 작업입니다!
Eric Lippert

16

여기에 내가이 모든 것을 보는 방법이 있습니다. 기술적으로 정확하지는 않지만 적어도 :).

기본적으로 머신에서 발생하는 두 가지 유형의 처리 (계산)가 있습니다.

  • CPU에서 발생하는 처리
  • 다른 프로세서 (GPU, 네트워크 카드 등)에서 발생하는 처리를 IO라고합니다.

따라서 소스 코드를 작성할 때 컴파일 후 사용하는 객체에 따라 (매우 중요 함) 처리는 CPU 바운드 또는 IO 바운드 이며 실제로는 양자 모두.

몇 가지 예 :

  • FileStream객체 (Stream) 의 Write 메소드를 사용하면 처리는 1 % CPU 바운드 및 99 % IO 바운드입니다.
  • NetworkStream객체 (Stream) 의 Write 메소드를 사용하면 처리는 1 % CPU 바운드 및 99 % IO 바운드입니다.
  • Memorystream객체 (Stream) 의 Write 메소드를 사용하면 100 % CPU 바운드 처리됩니다.

보시다시피, 객체 지향 프로그래머 관점에서 볼 때, 항상 Stream객체에 액세스하고 있지만 아래에서 발생하는 일은 객체의 궁극적 인 유형에 따라 크게 달라질 수 있습니다.

이제 상황을 최적화하기 위해 가능하고 /하거나 필요한 경우 코드 를 병렬 로 실행할 수있는 것이 유용합니다 (비동기 성 단어는 사용하지 않음).

몇 가지 예 :

  • 데스크톱 앱에서 문서를 인쇄하고 싶지만 기다릴 필요가 없습니다.
  • 내 웹 서버는 동시에 많은 클라이언트를 서버에 연결합니다. 각 서버는 병렬로 페이지를 가져옵니다 (직렬화되지 않음).

async / await 전에 본질적으로 두 가지 해결책이있었습니다.

  • 스레드 . Thread 및 ThreadPool 클래스와 함께 사용하기가 비교적 쉽습니다. 스레드는 CPU에 한정됩니다 .
  • "이전" Begin / End / AsyncCallback 비동기 프로그래밍 모델. 그것은 단지 모델 일 뿐이며 CPU 또는 IO에 바인딩되어 있는지 여부를 알려주지 않습니다. Socket 또는 FileStream 클래스를 살펴보면 IO 바운드이며 멋지지만 거의 사용하지 않습니다.

async / await는 작업 개념을 기반으로 하는 일반적인 프로그래밍 모델 일뿐 입니다 . CPU 바운드 작업에는 스레드 또는 스레드 풀보다 사용하기가 쉽고 이전 시작 / 종료 모델보다 사용하기가 훨씬 쉽습니다. 그러나 비밀은 둘 다에있어 매우 정교하고 완전한 기능을 갖춘 래퍼입니다.

따라서 실제 승리는 주로 CPU를 사용하지 않는 IO Bound 작업 이지만 async / await는 여전히 프로그래밍 모델이므로 처리가 어떻게 / 어디에서 수행되는지를 결정하는 데 도움이되지 않습니다.

이것은 클래스에 "DoSomethingAsync"메소드가 있기 때문에 CPU에 바인딩 된 것으로 추정 할 수있는 Task 객체를 리턴하는 것이 아니기 때문에 ( 특히 취소 토큰 매개 변수가없는 경우 매우 쓸모 가 없을 수 있음) IO 바운드가 아닙니다. (이것은 아마도 필수 ) 또는 두 가지의 조합입니다 (모델이 바이러스 성이므로 결합 및 잠재적 이익은 결국 혼합되어 명확하지 않을 수 있습니다).

따라서 예제로 돌아가서 MemoryStream에서 async / await를 사용하여 쓰기 작업을 수행하면 CPU 바운드 상태로 유지됩니다 (아마도 이점이 없을 것입니다).


1
이것은 CPU 바운드 작업에 theadpool을 사용하는 것이 좋은 대답입니다 .TP 작업을 사용하여 IO 작업을 오프로드해야한다는 점에서 좋지 않습니다. CPU 바운드 작업 imo는 물론주의로 차단해야하며 다중 스레드 사용을 방해하는 것은 없습니다.
davidcarr

3

다른 답변을 요약 :

비동기 / 대기 (Async / await)는 주로 IO 바운드 작업을 위해 만들어 지므로 호출 스레드 차단을 피할 수 있습니다. 이들의 주요 용도는 IO 스레드 작업에서 스레드가 차단되는 것을 원하지 않는 UI 스레드입니다.

비동기는 자체 스레드를 만들지 않습니다. 호출 메소드의 스레드는 awaitable을 찾을 때까지 비동기 메소드를 실행하는 데 사용됩니다. 그런 다음 동일한 스레드가 비동기 메소드 호출 이외의 나머지 호출 메소드를 계속 실행합니다. 호출 된 async 메서드 내에서 awaitable에서 돌아온 후 스레드 풀의 스레드에서 연속을 실행할 수 있습니다. 별도의 스레드가 그림에 오는 유일한 장소입니다.


좋은 요약이지만 전체 그림을 제공하기 위해 두 가지 질문에 더 답변해야한다고 생각합니다. 1. 대기 코드가 실행되는 스레드는 무엇입니까? 2. 언급 된 스레드 풀 (개발자 또는 런타임 환경)을 누가 제어 / 구성합니까?
stojke

1.이 경우 대부분 대기 된 코드는 CPU 스레드를 사용하지 않는 IO 바운드 작업입니다. CPU 바운드 작업에 대기를 사용하려는 경우 별도의 작업이 생성 될 수 있습니다. 2. 스레드 풀의 스레드는 TPL 프레임 워크의 일부인 작업 스케줄러에서 관리합니다.
vaibhav kumar

2

나는 그것을 아래에서 설명하려고 노력한다. 누군가가 도움이 될 수 있습니다. 파스칼의 DOS에서 간단한 게임을 만들 때 나는 그것을 재창조했습니다.

그래서 ... 모든 이벤트 중심 응용 프로그램에는 다음과 같은 이벤트 루프가 있습니다.

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

프레임 워크는 일반적으로이 세부 정보를 숨기지 만 거기에 있습니다. getMessage 함수는 이벤트 큐에서 다음 이벤트를 읽거나 이벤트가 발생할 때까지 기다립니다 (마우스 이동, 키 다운, 키업, 클릭 등). 그러면 dispatchMessage는 해당 이벤트 핸들러로 이벤트를 디스패치합니다. 그런 다음 루프를 종료하고 응용 프로그램을 종료하는 종료 이벤트가 올 때까지 다음 이벤트를 기다립니다.

이벤트 처리기가 빠르게 실행되어 이벤트 루프가 더 많은 이벤트를 폴링하고 UI가 응답 상태를 유지하도록해야합니다. 버튼 클릭으로 이와 같이 비싼 작업이 발생하면 어떻게됩니까?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

컨트롤이 기능 내에서 유지되면서 10 초 작업이 완료 될 때까지 UI가 응답하지 않습니다. 이 문제를 해결하려면 작업을 빠르게 실행할 수있는 작은 부분으로 나눠야합니다. 즉, 단일 이벤트에서 전체를 처리 할 수 ​​없습니다. 작업의 작은 부분을 수행 한 다음 계속을 요청하려면 다른 이벤트 를 이벤트 큐에 게시 해야합니다.

따라서 이것을 다음과 같이 변경하십시오.

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

이 경우 첫 번째 반복 만 실행 한 후 다음 반복을 실행하기 위해 이벤트 큐에 메시지를 게시하고 리턴합니다. 예제 postFunctionCallMessage의사 함수는 "call this function"이벤트를 큐에 넣으므로 이벤트 디스패처가 도달하면이를 호출합니다. 이를 통해 다른 모든 GUI 이벤트를 처리하면서 장시간 실행되는 작업을 지속적으로 실행할 수 있습니다.

이 장기 실행 작업이 실행되는 한 연속 이벤트는 항상 이벤트 큐에 있습니다. 기본적으로 자체 작업 스케줄러를 발명했습니다. 큐의 연속 이벤트가 실행중인 "프로세스"인 경우 실제로 OS에서 컨텍스트 전환 코드를 등록한 CPU의 타이머 인터럽트를 통해 연속 이벤트 전송 및 스케줄러 루프로의 복귀를 제외하고는 운영 체제가 수행하는 작업이므로 신경 쓰지 않아도됩니다. 그러나 여기서는 자신의 스케줄러를 작성하므로 지금까지 걱정해야합니다.

따라서 GUI와 병렬로 단일 스레드에서 장기 실행 작업을 작은 청크로 나누고 연속 이벤트를 전송하여 실행할 수 있습니다. 이것이 Task수업 의 일반적인 아이디어입니다 . 그것은 하나의 작업을 나타내며 호출 .ContinueWith할 때 현재 조각이 끝날 때 다음 조각으로 호출 할 함수를 정의합니다 (반환 값이 연속으로 전달됨). 이 Task클래스는 스레드 풀을 사용하며, 각 스레드에는 처음에 보여주고 싶은 것과 비슷한 작업을 수행하기 위해 이벤트 루프가 있습니다. 이 방법으로 수백만 개의 작업을 병렬로 실행할 수 있지만 몇 개의 스레드 만 실행할 수 있습니다. 그러나 작업이 작은 조각으로 올바르게 분할되어 있으면 각 스레드가 병렬로 실행되는 것처럼 단일 스레드에서도 잘 작동합니다.

그러나 모든 체인 작업을 수동으로 작은 조각으로 나누는 것은 번거로운 작업이며 전체 백그라운드 작업 코드가 기본적으로 .ContinueWith엉망 이기 때문에 논리의 레이아웃을 완전히 망칩니다 . 이것이 컴파일러가 도와주는 곳입니다. 그것은 백그라운드에서 당신을 위해이 모든 연쇄 및 연속을 수행합니다. 당신이 말할 때 await당신이하는 컴파일러에게 말해 "여기서 정지, 연속 작업으로 함수의 나머지 부분을 추가합니다". 컴파일러는 나머지를 처리하므로 필요하지 않습니다.


0

실제로 async await체인은 CLR 컴파일러에 의해 생성 된 상태 머신입니다.

async await 그러나 TPL이 스레드 풀을 사용하여 태스크를 실행하는 스레드를 사용합니다.

응용 프로그램이 차단되지 않은 이유는 상태 시스템이 어떤 공동 루틴을 실행, 반복, 확인 및 다시 결정할 것인지 결정할 수 있기 때문입니다.

더 읽을 거리 :

비동기 및 대기는 무엇을 생성합니까?

비동기 대기 및 생성 된 StateMachine

비동기 C # 및 F # (III.) : 작동 방식 -토마스 페트리 크

편집 :

괜찮아. 내 정교가 잘못된 것 같습니다. 그러나 상태 머신은 async awaits의 중요한 자산임을 지적해야합니다 . 비동기 I / O를 수행하더라도 작업이 완료되었는지 확인하는 도우미가 여전히 필요하므로 상태 머신이 여전히 필요하며 비동기식으로 함께 실행할 수있는 루틴을 결정합니다.


0

이것은 질문에 직접 대답하지는 않지만 그것이 추가 정보라고 생각합니다.

비동기 및 대기는 자체적으로 새 스레드를 작성하지 않습니다. 그러나 비동기 대기 위치를 사용하는 위치에 따라 대기 대기 후 동기 파트는 대기 후 동기 파트와 다른 스레드에서 실행될 수 있습니다 (예 : ASP.NET 및 ASP.NET 코어는 다르게 동작 함).

UI-Thread 기반 응용 프로그램 (WinForms, WPF)에서는 이전과 이후에 동일한 스레드에있게됩니다. 그러나 스레드 풀 스레드에서 비동기를 사용하면 대기 전후의 스레드가 동일하지 않을 수 있습니다.

이 주제에 대한 좋은 비디오

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.