[Swift 5.5] GCD의 문제?


안녕하세요. 이 게시물 Swift 5.5 WWDC 비동기/대기최근에 등장한 async/await를 살펴보고 배운 내용을 정리하도록 하겠습니다. Async/await는 WWDC를 통해 다시 학습되었지만 원래는 비동기/대기 패턴여러 프로그램에서 초안을 작성했다는 것도 알게 되었습니다. Swift 5.5 동시성은 구조화된 동시성의 원칙을 기반으로 합니다. 기존의 동시성 API에도 불구하고 새로운 동시성 모델을 도입한 이유가 궁금했는데 그 이유 중 하나가 async/await가 코드 길이 감소, 단순성 등의 장점이 있기 때문이라는 것을 알게 되었습니다.

Swift의 비동기 API에 대한 기존 문제가 있습니까?

Swift 5.5 이전에는 GCD와 Combine을 많이 사용했습니다. NotificationCenter, Combine 및 Rx와 같은 더 많은 비동기 프로세스가 있습니다. 예를 들어 동시성을 포함하는 GCD는 스레드 제어 없이 sync() 및 async() 함수를 통해 DispatchQueue를 통해 작업을 대기열에 직접 디스패치합니다. 스레드를 관리하고 작업을 자동으로 실행합니다. GCD의 disaptchWorkItem으로 작업을 관리하면 취소도 가능합니다. Swift 5.5에서 GCD를 대체할 수 있는 새로운 동시 API를 개발한 이유는 무엇인가요?

GCD의 대기열 기반 모델에는 일반적으로 세 가지 문제가 있습니다.

  • 쓰레드 폭발

한 번에 너무 많은 스레드를 생성하려면 활성 스레드 간에 지속적으로 전환해야 합니다. (앱 속도 저하) 큐는 이에 따라 스레드를 생성 -> 디스패치합니다.

  • 우선순위 반전

GCD의 서비스 품질(QoS)은 주어진 대기열에서 우선 순위가 높은 작업을 완료하기 위해 우선 순위가 낮은 시스템 리소스를 뺍니다.

  • 누락된 실행 계층

비동기 코드 블록에는 계층적 실행이 없습니다. 즉, 각 작업은 독립적으로 관리됩니다. 이로 인해 실행 중인 작업 간에 취소 및 액세스가 어렵습니다. 실행 중인 작업이 더 이상 필요하지 않다는 조건을 처리하기 위해 dispatchWorkItem을 중단하지 않는 한 작업은 계속 실행됩니다(컴파일러에서 감지할 수 없음). 사용자가 이미지 로딩 화면을 종료하면 진행 중인 작업을 취소할 수 있습니다(SwiftUI에서).

이는 기존 멀티스레딩의 단점으로, 비동기 코드가 실행 중일 때 코드가 제어를 포기할 때까지 CPU 코어가 정상적으로 복구할 수 없습니다. (작업이 더 이상 필요하지 않더라도 리소스를 소모하고 작업을 실행합니다.) 반면 Swift 5.5 동시성 모델은 await를 사용하여 서스펜션 포인트, 작업 계층이 있습니다. 일시 중단 지점에서 주어진 코드 조각이 await 덕분에 일시 중단 및 재개를 반복하면 런타임에 확인할 수 있으며 취소할 수도 있습니다.

GCD의 기능 외에도 Swift 5.5 이전에 존재했던 비동기 API의 completeHandlers 대부분은 클로저를 통한 콜백을 사용했습니다.

GCD와 동시에 작업을 할 때 작업이 언제 끝날지 모르기 때문에 콜백 완료를 통해 작업을 완료하거나 오류 핸들러를 포함할 수 있습니다. 결과사용. 비동기를 지원하는 대부분의 API는 예를 들어 dataTask(with:completionHandler:)마찬가지로 의 경우 서버에서 데이터 검색이 완료되었을 때 사용할 수 있는 completeHandler 클로저도 콜백 메서드로 구현됩니다.

대부분의 기존 API의 공통 기능은 콜백 구조입니다. 블록이 겹치는 코드는 콜백 구조 때문에 한눈에 이해하기 어렵습니다. 중간 오류 발생을 작성하지 않는 실수를 저지르면 컴파일러가 이를 인식하지 못하기 때문에 변경 사항이 없는 상황이 될 수 있습니다. 자세한 내용은 이 링크에서 공부하는 것이 좋을 것입니다. 또한 WWDC Swift에서 async/await 만나기그것도 정말 자세하게.

Swift 5.5 동시성 모델

기존 비동기 API의 불편함과 GCD의 단점을 균형있게 새로운 동시성 모델이번 WWDC에서 발표되었습니다. 네 가지 주요 새 기능이 있습니다.

1. 협력 스레드 풀

Swift 5.5의 새로운 동시성 모델은 사용 가능한 CPU 코어 수를 초과하지 않으면서 스레드 풀을 관리합니다. 런타임 시 스레드 전환 비용이 많이 들지 않고 스레드가 지속적으로 생성 및 소멸될 필요가 없습니다. 기본적으로 정의된 스레드를 최대한 활용하는 대신 풀의 특정 스레드에서 코드를 빠르게 일시 중지하고 다시 시작할 수 있습니다.

2. 협력 스레드 풀

컴파일 또는 런타임 환경에서 코드가 일시 중지 또는 계속되는지 여부를 확인할 수 있습니다. 런타임에서 스레드와 코어를 균일하게 처리하므로 스레드와 코어에 대해 걱정할 필요가 없습니다.

기존의 비동기식 API는 콜백을 많이 사용한다고 하는데, 클로저는 캡쳐할 수 있는 능력이 있고, 약한 참조에서 강한 참조를 만들지 않도록 주의해야 합니다.

async 및 await는 Swift 5.5에 도입되었으므로 이에 대해 걱정할 필요가 없습니다. @escaping 클로저를 선언하지 않고 동기식 함수처럼 처리되는 결과 값만 반환하면 되기 때문입니다.

3. 구조화된 동시성

이전 포스트에서 조금 공부했는데, 계층 구조가 강화되고 부모와 자식 작업 간의 자연스러운 흐름이 가능해 취소도 가능하고 우선 순위도 부모 작업에서 상속받을 수 있습니다. DispatchGroup.wait()와 마찬가지로 부모 작업이 완료되기 전에 모든 자식 작업이 완료될 때까지 기다릴 수 있습니다.

4. 컨텍스트 인식 코드 컴파일

컴파일러는 코드 실행을 비동기식으로 지속적으로 추적합니다. 따라서 공유 상태를 변경하기 위해 잠재적으로 스레드에 안전하지 않은 코드를 미리 방지합니다. 그게 다야 배우(개념 릴리스 포스트). 상태 격리는 상태 변경을 액터 내에서만 처리할 수 있도록 하여 동시 문제를 방지하는 이점이 있습니다.

Swift 5.5 비동기/대기

Swift 5.5 동시성 API에는 Actor, globalActor 및 AsyncSequence와 같은 개념이 있지만 가장 일반적으로 사용되는 개념은 아마도 async/await일 것입니다. 기존의 대부분의 비동기 API는 콜백 클로저로 완료 핸들러를 처리하지만 새로 도입된 async/await 버전은 라이브러리에 별도의 기능을 추가했습니다.

기존 비동기 함수와 비동기 함수의 차이점


https://developer.apple.com/documentation/foundation/urlsession/1407613-datatask

(completionHandler.. @escaping (비동기이므로 완성은 나중에 한다) @Sendable (액터에서 사용 가능)) 기존 비동기 API 중 하나.


https://developer.apple.com/documentation/foundation/urlsession/1407613-datatask

비동기가 적용된 새로운 비동기 API.

일반적으로 Data 및 URLResponse를 수신하고 후속 작업을 수행하여 Data 및 URLResponse를 반환하거나 오류를 발생시킵니다. 차이점이 있다면 많습니다.

기존 비동기 기능

첫 번째 경우, 클로저에 의해 dataTask()가 완료되면 이스케이프 처리된 완료 핸들러 클로저가 호출됩니다. 스레드가 dataTask()를 호출하면 completeHandler가 즉시 호출되지 않고 dataTask()가 먼저 반환됩니다. 결국 completeHandler가 실행됩니다. 결국 completeHandler가 호출됩니다. 이때 데이터 요청 작업이 완료됩니다. 이 기능 덕분에 반대로 현재 실행 중인 코드의 실행 포인트가 올라갈 수 있으며 디버깅이 어렵습니다.


https://developer.apple.com/videos/play/wwdc2021/10095/

이미지를 가져오는 대략적인 코드 흐름입니다.

대부분의 기존 비동기 API 함수에는 completionHandler의 @escaping 완료 유형이 있습니다. 스레드가 async 함수 아래에서 코드를 실행하는 상황에서 async 함수가 실행되고 함수가 메모리에서 해제된 후 함수의 인수 값으로 @escaping 완료 유형 “completionHandler”가 지정되기 때문입니다. 이는 클로저의 특성과 @escapen으로 메모리에 남아 있는 기능 때문입니다.

dataTask URL에서 데이터를 가져오는 데 일정 시간 이상이 걸리므로 다음 코드는 실행을 위해 dataTask(with:completionHandler:)의 두 번째 매개변수로 작성된 completeHandler 완료를 기다리지 않고 실행됩니다. will do dataTask()를 호출하면 바로 Data를 받지 못하므로 dataTask() 다음의 코드인 task.resume()을 먼저 실행!! 꽤 효율적입니다.

무엇보다 많이클로저에서 데이터를 처리한 후 다시 호출하여 성공적으로 데이터를 반환한 값을 completionHandler fetchPhoto 매개 변수로 UIImage에 전송해야 합니다. fetchPhoto가 UIImage를 직접 반환할 수 없는 이유는 두 번째 매개변수 종결자 ‘~ 아니다’ 그런 얘기니까.

이 클로저가 언제 발생할지 모르기 때문에 fetchPhoto에서 UIImage 객체를 생성하고 dataTask()가 완료될 때 값을 할당하더라도 fetchPhoto가 이미 빈 값을 반환하고 종료되었을 가능성이 있습니다. completeHandler로 변환된 값을 인수 값으로 호출해야 합니다.

@escaping을 사용하면 해당 클로저가 비동기적으로 호출되기 때문에 @escaping이 포함된 값을 가져오기 위해 계속해서 @escaping을 사용하며 코드는 피라미드 구조를 갖는다. 클로저와 @escaping의 이점이 있지만 중첩 시 계속 사용한다면 아래 코드에서 많은 것을 찾을 수 있습니다.

func processImageData1(completionBlock: (_ result: Image) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource in
        loadWebResource("imagedata.dat") { imageResource in
            decodeImage(dataResource, imageResource) { imageTmp in
                dewarpAndCleanupImage(imageTmp) { imageResult in
                    completionBlock(imageResult)
                }
            }
        }
    }
}
processImageData1 { image in
    display(image)
}

각 경우에 오류 처리도 필요하므로 코드 길이가 늘어납니다. 실수로 오류를 처리하지 않으면 컴파일러가 오류를 감지하지 못하므로 오류가 발생한 위치를 모를 수 있습니다.

Swift 5.5 비동기 함수


https://developer.apple.com/videos/play/wwdc2021/10095/

이 경우 async/await를 통해 이미지를 가져오는 코드입니다. 엄청난. 코드는 명확하게 하향식으로 실행됩니다. 동기 함수의 논리적 흐름과 마찬가지로 비동기라고 명확하게 지칭됩니다.

위에서 언급한 기존 비동기 함수에서는 오류가 발생할 때마다 fetchPhoto “completionHandler” 매개 변수의 인수 값으로도 전달되었습니다. 저는 fetchPhoto에 CompletionHandler를 많이 썼습니다(코드 길이를 늘림).

반면 위 이미지의 경우 에러가 발생하면 바로 던질 수 있다. 티hrow가 실행되지 않으면 이미지가 성공적으로 반환됩니다. 그러나 분명히 URL에서 데이터를 얻는 중 일정 이상의 시간이 필요합니다그리고 위에서 언급한 것처럼 기존의 비동기식 API는 콜백 방식이나 결과를 사용했습니다.

이 경우 await 키워드를 사용하여 대체합니다. 여기 당신이 알아야 할 것이 있습니다. async, await 및 task의 개념을 이해해야 합니다.

비동기

async는 throws 키워드와 같은 함수에서 사용됩니다.

func defineAsyncFunc() async -> Void { ... }

Async는 함수가 비동기 상태임을 나타냅니다. 함수 내에서 일반 함수(동기 함수)와 비동기 함수를 모두 사용할 수 있습니다. 그러나 비동기 함수를 사용하려면 await 키워드를 함께 사용해야 합니다.

일반 함수(viewDidLoad() 등) 또는 메인 스레드와 같은 동기 범위에서 await 키워드와 함께 비동기 함수를 사용할 수 없습니다. 비동기적으로 작동할 수 있는 환경(Task()) 또는 비동기 함수 내에서만 사용해야 합니다. 그리고 비동기 유형 함수 내부의 코드는 위에서 아래로 동기식으로 작동합니다. 여러면에서 많은 이점이 있습니다. B. 디버깅, 코드 브라우징, 프로파일링 등

동기 함수와 마찬가지로 비동기 함수는 함수를 하향식으로 반환합니다. 차이점은 동기식 함수는 함수의 내부 논리를 실행하기 위해 해당 함수를 호출한 스레드와 스택을 최대한 활용할 수 있다는 것입니다. 반면에 비동기 함수는 스택을 포기하고 별도의 메모리를 사용합니다. 비동기 함수만 사용 중인 스레드를 해제(일시 중단)할 수 있습니다.

예상하다

func asyncTask() async { ... }
func defineAsyncFunc() async -> Void { 
	print("async func start")
    await asyncTask() // suspend, resume 반복 이 코드가 수행되야 다음 코드 수행됩니다.
    print("async func end")
}

await는 비동기 범위(비동기 유형 func 내부, 작업 내부)에서 비동기 함수를 사용할 때 사용해야 하는 키워드입니다. 기대라고 표시된 지점은 서스펜션 포인트보지마. 위에서 설명한 것처럼 일반 동기 함수와 달리 await 키워드는 점유된 스레드를 포기합니다(함수가 실행되지 않음을 의미). 첫 번째 정지 상태(물론 상황이 좋으면 바로 실행할 수도 있다.) (적절한 코드 로직 x 실행) 그리고 시스템 상황에 따라 Resume과 Suspend를 이용하여 추후에 실행할 시간을 시스템이 제어하게 하여 실행을 반복한다. 권한은 시스템의 마음입니다)는 비동기적으로 실행됩니다. 이런 이유로 대기!! 표시한 것입니다.

“시스템, 바쁘신 거 알아요. 시간 되시면 데이터(for:request) 비동기 기능도 해주세요!!” 요요 느낌입니다.


https://developer.apple.com/videos/play/wwdc2021/10132/

await 키워드를 사용하는 코드는 런타임에 일시 중지 및 재개 작업을 반복하며 시스템 리소스를 잘 소비하여 처리됩니다. 그러나 await를 사용하면 반드시 일시 중단 상태에 있는 것이 아니라 상황에 따라 즉시 실행될 수 있습니다(제 기준에서는 일반적으로 일시 중단됨). Suspend 상태에 있을 때 스레드는 해당 대기 코드 아래에 코드가 존재하더라도 다음 코드를 실행하지 않습니다.

그 이후의 코드는 await의 비동기 함수 작업이 완료될 때만 실행됩니다. await 덕분에 코드는 동기 작업처럼 실행되며 마지막 경우에는 결과 또는 오류 발생을 중간에 가져올 수 있습니다. 이는 기존 콜백을 통해 결과를 반환하는 것과 다릅니다.

await 키워드와 일시 중지 지점 덕분에 시스템은 계속해서 우선 순위를 변경할 수 있습니다. 그리고 취소하기 쉽습니다.


https://developer.apple.com/videos/play/wwdc2021/10132/

요약하면 func는 해당 함수가 실행되는 동안 async로 반복적으로 중단될 수 있습니다. sync 및 async func는 async func 내에서 사용할 수 있지만 await는 async func를 사용할 때 사용해야 합니다. 예상은 중단 지점을 의미합니다.

기존의 비동기 처리 코드를 비동기로 대체하는 대표적인 예는 WWDC매우 상세한 것 같습니다.

비동기 기능이 동작할 수 있는 환경을 구성하는 개체입니다. 작업을 취소하고 완료될 때까지 기다릴 수 있습니다. 액터의 관점에서 고립된 상태와 비고립된 상태를 구분하듯이 컨텍스트는 비동기적 관점에서 비동기적 공간과 비비동기적 공간으로 나눌 수 있다. 비동기 함수를 동기 컨텍스트에서 실행하려면 비동기 공간을 만들어야 하며 태스크가 이를 담당합니다.

...
override func viewDidLoad() {
    //Main thread
    
    defineAsyncFunc() // error. non-async context
    Task() {
    	// asynchronous context.
        await defineAsyncFunc() // async는 당연히 시스템에 의해 관리되니까 await키워드를 붙여야 합니다.
    }
}

작업은 호출한 액터가 수행합니다. 원래 작업(우선순위:작업:)를 통해 우선순위를 지정할 수도 있습니다. 우선순위를 지정하지 않으면 기본적으로 현재 Task() {…}가 실행 중인 컨텍스트의 우선순위를 상속합니다. 기본적으로 Task()를 호출한 컨텍스트의 우선 순위를 상속하지 않습니다. Task.detached(우선순위:작업:)당신이 사용할 수있는.

요게 있긴 한데 콤바인과 잘 어울립니다. 그 외에도 다음과 같은 다양한 기능을 제공합니다. B. 취소가 가능합니다.

이미 언급했듯이 비동기 실행에는 여러 가지 방법이 있습니다. B. Swift 5.5, GCD, NotificationCenter, rx 및 Combine의 async/await. await로 표시된 함수가 재개 및 일시 중단을 반복한 후 실행되면 대기 후 코드가 계속됩니다.

Swift 5.5의 Async/await 또는 AsyncSequence는 비동기 실행을 수행할 수 있음을 의미합니다. 그러나 동시성을 사용하려면 async let 또는 TaskGroup을 사용해야 합니다. 동시성을 사용할 때 서로 다른 스레드가 공통 변경 가능한 상태에 동시에 액세스하는 경우가 발생할 수 있습니다. 이것은 배우가 해결할 수 있습니다.

스위프트 5.5 다른 게시물

액터 #1. 액터, 스레드 세이프, 액터 시리즈 실행자 개념 요약

배우 #2. 배우, 배우 격리, 교차 배우 참조 개념 시놉시스

프로토콜 개념에 대한 전송 가능한 요약 기여

구조화된 동시성 개념에 기여

참조

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md

https://developer.apple.com/videos/play/wwdc2021/10095/

https://developer.apple.com/videos/play/wwdc2021/10132

https://www.kodeco.com/books/modern-concurrency-in-swift

Swift의 최신 동시성

Master Swift의 최신 동시성 모델! 수년 동안 Swift로 강력하고 안전한 동시 앱을 작성하는 것은 실행 조건과 대규모 콜백 클로저에 숨겨진 설명할 수 없는 충돌로 가득 찬 벅찬 작업으로 쉽게 바뀔 수 있었습니다. 스위프트 5에서.

www.codeco.com