Swift Concurrency: 탄생

틀틀보·2025년 4월 26일

Swift Concurency

목록 보기
1/11

WWDC2021에 등장한 새로운 동시성 프로그래밍 모델
기존 GCD의 코드 가독성, 에러 처리 등에 의한 문제점을 해소하기 위해 등장한 새로운 동시성 프로그래밍 모델

GCD에서는 무엇이 문제였는가?

성능적인 문제

Thread Explosion

Thread가 과도하게 만들어져 비효율적이게 되는 현상
저렴한 context Switching 비용을 위해 생성되었으나, 그 수가 너무 많아 생기는 문제

GCD에서의 sync, async

결론: Queue는 Thread를 무조건 생성 하지 않음
GCDsyncThread를 재사용
async는 놀고 있는 Thread가 없을 때, Thread를 생성 아니면 재사용

let q = DispatchQueue(label: "TestQueue")

q.sync {
    sleep(1)
    print("1", Thread.current)
}

DispatchQueue.main.async {
    print("2", Thread.current)
}


q.async {
    sleep(1)
    print("3", Thread.current)
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

1 <_NSMainThread: 0x6000022900c0>{number = 1, name = main}
2 <_NSMainThread: 0x6000022900c0>{number = 1, name = main}
4 <_NSMainThread: 0x6000022900c0>{number = 1, name = main}
3 <_NSThread: 0x600002292840>{number = 5, name = (null)}

생성된 TestQueuesync일 경우에는 자신이 호출된 MainThread에서 동작하는 모습을 볼 수 있고,
async일 경우 Thread를 생성해서 그 위에서 동작하는 모습을 볼 수 있다.

async를 여러 군데에서 많이 사용할수록 과도하게 Thread가 생성되는 문제 발생

과도한 Thread로 인한 Scheduling overhead

  • 제한된 core수로 Thread가 많아지면 업무 처리 속도 저하 및 CPU 동작이 비효율적이 됨.

Swift Concurrency 에서는?

협력적 스레드 풀(Cooperative Thread Pool)

  • CPU core 수와 동일한 수의 Thread로 제한하여 제한된 Thread로 동작
  • 비동기 함수내에서 await을 만나면 해당 비동기 함수의 작업은 일시 중단되고, Thread는 즉시 시스템에 반환
  • 시스템은 필요한 다른 작업에 Thread 제어권을 부여
  • 다시 해당 비동기 함수로 돌아올 때 Thread가 바뀔 수 있음.

async frame

  • async한 함수가 Thread에서 벗어나면 함수에 필요한 정보가 동작했던 ThreadStack에서 사라짐. (다른 작업에 필요한 정보를 Stack에 새로 담아야 하므로)
  • 해당 함수의 동작이 다시 어느 Thread에서 동작할 지 모르므로, heap 영역에 저장
  • 필요한 정보를 async frame에 담아 heap 영역에 저장
  • 다시 동작 시, heap 영역에서 async frame을 꺼내와 재동작

Swift Concurrency: Async Frame

코드 구조적 문제

1. 피라미드형 중첩 클로저들의 가독성

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)
}
  • 필요로 인해 깊이 중첩된 클로저들은 가독성을 방해
  • 코드를 추적하기 어려워짐

2. 장황한 오류처리

// (2a) Using a `guard` statement for each callback:
func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

processImageData2a { image, error in
    guard let image = image else {
        display("No image today", error)
        return
    }
    display(image)
}
  • 각각의 클로저마다의 오류 처리로 가독성 저하 및 장황함.

3.비동기 함수의 조건부 실행 까다로움

func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) {
    let swizzle: (_ contents: Image) -> Void = {
      // ... continuation closure that calls completionBlock eventually
    }
    if recipient.hasProfilePicture {
        swizzle(recipient.profilePicture)
    } else {
        decodeImage { image in
            swizzle(image)
        }
    }
}
  • 코드는 위에서 아래로 읽히는 것이 좋음.
  • but 위의 코드와 같이 swizzle이라는 메서드를 먼저 정의해야 아래의 조건에 따라 조건부 실행 가능
  • 코드 구조가 뒤죽박죽되는 문제
  • +클로저의 사용으로 메모리 누수 문제도 생각해야함.

4. 동기화 처리 관련 에러

GCD의 경우?
asyncObject.asyncFunc { data, error in
	self.dataCount += 1 // 해당 부분은 dataRace 문제 발생가능성 있음.
    ...
}

GCD에서 동기화를 처리해주기 위해선 DispatchQueue.sync, 뮤텍스, 세마포어와 같은 방법으로 처리해주어야한다.
하지만 개발자의 실수로 처리를 해주지 않아도 컴파일러가 오류 발생을 알려주지 않음.

Swift concurrency의 경우?
try await asyncObject.asyncFunc { data in
	self.dataCount += 1 // 컴파일 에러 발생
    ...
}

비 독립적(non-isolated) 구문이 변할 수 있는 프로퍼티에 접근하는 것을 금지한다는 메시지와 함께 컴파일 에러 발생

조금 더 개발자의 실수를 미연에 방지할 수 있음.

Swift concurrency를 쓰면?

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

위에서 설명한 문제점이 어느정도 해결됨!

참고: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0296-async-await.md#problem-1-pyramid-of-doom

https://wwdcnotes.com/documentation/wwdcnotes/wwdc21-10254-swift-concurrency-behind-the-scenes/

https://swiftsenpai.com/swift/swift-concurrency-prevent-thread-explosion

https://velog.io/@enebin777/Swift-GCD-queue%EB%93%A4-%EA%B9%8A%EC%9D%B4-%EB%93%A4%EC%97%AC%EB%8B%A4%EB%B3%B4%EA%B8%B0

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글