스택 프로세스가 비동기 프로세스에 사용됩니까?


10

이 질문 에는 Eric Lippert가 스택의 용도를 설명하는 훌륭한 답변이 있습니다. 지난 몇 년 동안 나는 일반적으로 말해서 스택이 무엇이고 어떻게 사용되는지 알고 있었지만, 그의 대답 중 일부는 오늘날 비동기 프로그래밍이 표준 인 경우이 스택 구조가 덜 사용되는지 궁금해합니다.

그의 대답에서 :

스택은 코 루틴이없는 언어로 연속을 통합하는 것의 일부입니다.

특히, 코 루틴없는 부분은 궁금합니다.

그는 여기에 조금 더 설명합니다 :

코 루틴은 그들이 있던 위치를 기억하고 잠시 동안 다른 코 루틴에 대한 제어를 양보 한 다음 나중에 중단 된 곳에서 재개 할 수 있지만, 이른바 코 루틴 수율 직후에 반드시 그런 것은 아닙니다. 다음 항목이 요청되거나 비동기 작업이 완료 될 때의 위치를 ​​기억해야하는 C #의 "수율 반환"또는 "대기"를 생각하십시오. 코 루틴 또는 유사한 언어 기능이있는 언어는 연속성을 구현하기 위해 스택보다 더 고급 데이터 구조가 필요합니다.

이것은 스택과 관련하여 훌륭하지만 스택이 너무 단순하여 고급 데이터 구조가 필요한 언어 기능을 처리 할 수 ​​없을 때 어떤 구조가 사용되는지에 대한 답을 얻지 못합니까?

기술이 발전함에 따라 스택이 사라지고 있습니까? 그것을 대체하는 것은 무엇입니까? 하이브리드 유형입니까? (예를 들어, 내 .NET 프로그램은 비동기 호출에 도달 할 때까지 스택을 사용하고 완료 될 때까지 다른 구조로 전환합니까?이 시점에서 스택은 다음 항목을 확인할 수있는 상태로 풀립니다. )

이러한 시나리오가 스택에 비해 너무 진보되어 있다는 것은 완벽한 의미가 있지만 스택을 대체하는 것은 무엇입니까? 내가 몇 년 전에 알게되었을 때 스택은 매우 빠르고 가벼 웠기 때문에 힙에서 멀리 떨어진 응용 프로그램에 할당 된 메모리 조각으로 인해 현재 작업에 대한 효율적인 관리를 지원했기 때문에 스택이있었습니다. 무엇이 바뀌 었습니까?

답변:


14

하이브리드 유형입니까? (예를 들어, 내 .NET 프로그램은 비동기 호출에 도달 할 때까지 스택을 사용하고 완료 될 때까지 다른 구조로 전환합니까?이 시점에서 스택은 다음 항목을 확인할 수있는 상태로 풀립니다. )

기본적으로 그렇습니다.

우리가 가지고 있다고 가정

async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }

다음 은 연속이 어떻게 구체화되는지에 대한 매우 간단한 설명입니다. 실제 코드는 훨씬 더 복잡하지만 아이디어를 얻습니다.

버튼을 클릭하십시오. 메시지가 대기 중입니다. 메시지 루프는 메시지를 처리하고 클릭 핸들러를 호출하여 메시지 큐의 리턴 주소를 스택에 둡니다. 즉, 핸들러가 완료된 후에 발생하는 것은 메시지 루프가 계속 실행되어야한다는 것입니다. 따라서 처리기의 연속은 루프입니다.

클릭 핸들러는 Foo ()를 호출하여 스택의 리턴 주소를 스택에 넣습니다. 즉, Foo의 연속은 나머지 클릭 처리기입니다.

Foo는 Task.Delay를 호출하여 자신의 반환 주소를 스택에 넣습니다.

Task.Delay는 즉시 작업을 반환하기 위해 필요한 모든 마법을 수행합니다. 스택이 튀어 나와 Foo로 돌아 왔습니다.

Foo는 반환 된 작업이 완료되었는지 확인합니다. 그렇지 않습니다. 계속AWAIT은 푸 작업의 연속으로 최대 위임 할 것을 어쩌구를 (호출하는 대리자를) 생성 및 징후, 그래서) 어쩌구를 (호출하는 것입니다. (나는 방금 약간의 잘못된 진술을했다; 당신은 그것을 잡았습니까? 그렇지 않으면, 우리는 잠시 후에 그것을 공개 할 것입니다.)

그런 다음 Foo는 고유 한 Task 개체를 만들어 불완전한 것으로 표시 한 다음 스택을 클릭 처리기로 반환합니다.

클릭 핸들러는 Foo의 작업을 검사하고 불완전한 것을 발견합니다. 핸들러에서 대기의 연속은 Bar ()를 호출하는 것이므로 클릭 핸들러는 Bar ()를 호출하는 델리게이트를 작성하고 Foo ()에 의해 리턴 된 태스크의 연속으로 설정합니다. 그런 다음 스택을 메시지 루프로 되돌립니다.

메시지 루프는 메시지 처리를 유지합니다. 결국 지연 작업에 의해 생성 된 타이머 매직은 그 작업을 수행하고 지연 작업의 연속을 이제 실행할 수 있다는 메시지를 큐에 게시합니다. 따라서 메시지 루프는 작업 연속을 호출하여 평소처럼 스택에 배치됩니다. 이 델리게이트는 Blah ()를 호출합니다. Blah ()는 기능을 수행하고 스택을 반환합니다.

이제 어떻게됩니까? 여기 까다로운 부분이 있습니다. 지연 작업의 계속은 Blah () 호출 하지 않습니다 . 또한 Bar () 호출을 트리거해야 하지만 해당 작업은 Bar에 대해 알지 못합니다!

Foo는 실제로 (1) Blah ()를 호출하고 (2) Foo가 생성하고 이벤트 처리기로 전달한 작업의 연속을 호출하는 델리게이트를 만들었습니다. 이것이 Bar ()를 호출하는 델리게이트를 호출하는 방법입니다.

그리고 이제 우리는 필요한 모든 것을 올바른 순서로 수행했습니다. 그러나 우리는 메시지 루프에서 메시지 처리를 오랫동안 멈추지 않았으므로 응용 프로그램은 응답 상태를 유지했습니다.

이러한 시나리오가 스택에 비해 너무 진보되어 있다는 것은 완벽한 의미가 있지만 스택을 대체하는 것은 무엇입니까?

대리자의 클로저 클래스를 통해 서로에 대한 참조를 포함하는 작업 개체의 그래프. 이러한 폐쇄 클래스는 가장 최근에 실행 된 대기 위치와 로컬 값을 추적하는 상태 머신 입니다. 또한, 주어진 예에서, 운영 체제에 의해 구현 된 전역 상태 동작 큐 및 그 동작을 실행하는 메시지 루프.

연습 : 메시지 루프가없는 세상에서이 모든 것이 어떻게 작동한다고 생각하십니까? 예를 들어 콘솔 응용 프로그램입니다. 콘솔 앱에서 기다리는 것은 상당히 다릅니다. 지금까지 알고있는 것과 어떻게 작동하는지 추론 할 수 있습니까?

내가 몇 년 전에 알았을 때 스택은 번개가 빠르고 가벼워서 힙에서 멀리 떨어진 응용 프로그램에 할당 된 메모리 조각이기 때문에 현재 작업에 대한 효율적인 관리를 지원했기 때문에 스택이있었습니다. 무엇이 바뀌 었습니까?

스택은 메소드 활성화의 수명이 스택을 형성 할 때 유용한 데이터 구조이지만, 예제에서는 클릭 핸들러 Foo, Bar 및 Blah의 활성화가 스택을 형성하지 않습니다. 따라서 워크 플로를 나타내는 데이터 구조는 스택이 될 수 없습니다. 오히려 워크 플로를 나타내는 힙 할당 작업 및 대리자의 그래프입니다. 대기는 워크 플로에서 이전에 시작된 작업이 완료 될 때까지 워크 플로에서 더 이상 진행할 수없는 지점입니다. 기다리는 동안 완료된 특정 시작 작업에 의존 하지 않는 다른 작업을 실행할 수 있습니다 .

스택은 프레임의 배열 일뿐입니다. 여기서 프레임에는 (1) 함수 중간에 대한 포인터 (호출이 발생한 위치) 및 (2) 로컬 변수 및 온도 값이 포함됩니다. 작업의 계속은 동일합니다 : 델리게이트는 함수에 대한 포인터이며 함수 중간에 특정 지점을 참조하는 상태 (대기 중이 발생한 위치)를 가지며 클로저에는 각 로컬 변수 또는 임시 필드가 있습니다 . 프레임은 더 이상 멋진 깔끔한 배열을 형성하지 않지만 모든 정보는 동일합니다.


1
매우 도움이되었습니다. 감사합니다. 두 답변을 모두 허용 된 답변으로 표시 할 수는 있지만 답변을 비워 둘 수 없기 때문에 답장 할 시간을주지 않았다고 생각하지 않기를 원했습니다.
jleach

3
@ jdl134679 : 질문에 대한 답변이 있다고 생각되면 답변으로 표시하는 것이 좋습니다. 그것은 그들이 원한다면 사람들이 여기 와서해야한다는 신호 전송 읽기 좋은 답변보다는 쓰기 를. (물론 좋은 답변을 쓰는 ​​것이 항상 권장됩니다.) 누가 체크 표시를 받는지는 상관하지 않습니다.
Eric Lippert

8

Appel은 오래된 종이 가비지 수집이 스택 할당보다 빠를 수 있다고 썼습니다 . 그의 연속 편집 안내서가비지 수집 핸드북 도 읽으십시오 . 일부 GC 기술은 (직관적으로) 매우 효율적입니다. 계속 전달 스타일은 정식 정의 전체 프로그램 변환합니다 (CPS의 변환 스택 (개념적으로 힙 할당과 전화 프레임을 교체 제거하는) 폐쇄 각 "값"개별 호출 프레임 또는 "객체" "reifying"즉를, ).

그러나 콜 스택 은 여전히 ​​매우 널리 사용되며 현재 프로세서에는 콜 스택 전용의 전용 하드웨어 (스택 레지스터, 캐시 기계 등)가 있습니다 (따라서 대부분의 저수준 프로그래밍 언어, 특히 C)가 더 쉽습니다. 호출 스택으로 구현). 또한 스택은 캐시 친화적 이며 성능에 많은 영향 을줍니다.

실제로, 통화 스택은 여전히 ​​여기에 있습니다. 그러나 우리는 이제 그것들 중 다수를 가지고 있으며, 때때로 호출 스택은 많은 작은 세그먼트 (예 : 각각 4KB의 몇 페이지)로 나뉘며, 때로는 가비지 수집되거나 힙 할당됩니다. 이러한 스택 세그먼트는 일부 연결된 목록 (또는 필요한 경우 좀 더 복잡한 데이터 구조)으로 구성 될 수 있습니다. 예를 들어, GCC의 컴파일러는 -fsplit-stack옵션 (이동, 그 "goroutines"과 "비동기 프로세스"에 대한 특히 유용). 분할 스택을 사용하면 수백만 개의 작은 스택 세그먼트로 구성된 수천 개의 스택 (및 공동 루틴이 구현하기 쉬워 짐)을 가질 수 있으며 스택을 "풀기"는 속도가 더 빠를 수 있습니다 스택).

즉, 스택과 힙의 구별이 흐려 지지만 전체 프로그램 변환이 필요하거나 호출 규칙과 컴파일러를 비호 환적으로 변경해야 할 수도 있습니다.

CPS 변환에 대한 this & that 및 많은 논문 (예 : this ) 도 참조하십시오 . ASLR & call / cc에 대해서도 읽어보십시오 . 연속 에 대해 자세히 알아보십시오 (& STFW) .

.CLR 및 .NET 구현에는 여러 가지 실용적인 이유로 최신 GC 및 CPS 변환이 없을 수 있습니다. 전체 프로그램 변환과 관련된 트레이드 오프입니다 (낮은 수준의 C 루틴을 사용하기 쉽고 런타임이 C 또는 C ++로 코딩되어 있음).

Chicken Scheme 은 CPS 변환과 함께 기계 (또는 C) 스택을 틀에 얽매이지 않고 사용합니다. 모든 할당은 스택에서 발생하며 너무 커지면 세대 별 GC 복사 및 전달 단계에서 최근 스택 할당 값 (및 아마도 현재 연속)을 힙으로 가져간 다음 스택이 크게 줄어 듭니다 setjmp.


SICP , Programming Language Pragmatics , Dragon Book , Lisp in Small Pieces를 읽어보십시오 .


1
매우 도움이되었습니다. 감사합니다. 두 답변을 모두 허용 된 답변으로 표시 할 수는 있지만 답변을 비워 둘 수 없기 때문에 답장 할 시간을주지 않았다고 생각하지 않기를 원했습니다.
jleach
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.