• Threading Model

    • GCD와의 비교
      • GCD에서는 Queue라는 것을 이용해서 작업을 스케쥴링 한다.

      • 상호 배제를 하기 위해 serialQueue를 사용한다.

      • 병렬적으로 작업을 수행하기위해 concurrent Queue를 사용한다.

        let urlSession = URLSession(configuration: .default, delegate: self, 
        														delegateQueue: concurrentQueue)
        
        for feed in feedsToUpdate {
        	let dataTask = urlSession.dataTask(with: feed.url) { data, response, error in
        			// 이 부분은 delegateQueue에서 실행된다.
        			guard let data = data else { return }
        
        			do {
        				let articles = try deserializeArticles(from: data)
        				databaseQueue.sync {
        					updateDatabase(with: articles)
        				}
        			} catch { ... }
        	}
        }
        
      • GCD는 최초에는 코어수만큼 스레드를 사용하다가, 스레드가 블록상태가 되면 이후 작업을 위해 새로운 스레드를 만들어낸다. → 블록된 스레드가 많아질 수록 스레드 폭발 위험이 늘어난다.

        • 메모리 오버헤드와 스케쥴링 오버헤드로도 이어진다. → 동시성 상황에서는 어쩔 수 없이 존재하나, 스레드 폭발이 일어나면 이게 너무 과해진다.
      • Swift는 스레드를 만드는 대신, Continuation이라는 객체를 만들어서, 작업의 실행 과정을 추적한다.

        • 즉 응용 레벨에서 스위칭을 한다고 보면 된다.
        • 이를 위해서는 런타임과 언어가 둘 다 지원을 해줘야 한다.
    • 언어 차원에서의 지원
      • await과 스레드를 블록하지 않는 모델
      • Swift task 모델에서의 의존성 추적
    func deserializeArticles(from data: Data) -> [Article]
    func updateDatabase(with articles: [Article], for feed: Feed) async
    
    await withThrowingTaskGroup(of: [Article].self) { group in
    	for feed in feedsToUpdate {
    		group.async {
    			let (data, response) = try await URLSession.shared.data(from: feed.url)
    			
    			let articles = try deserializeArticles(from: data)
    			await updateDatabase(with: articles, for: feed)
    			// ...
    
    			return aritcles
    		}
    	}
    }
    
    • 어떻게 await가 스레드를 블록하지 않을 수 있는가?
      • 기본적으로 스레드는 개별적으로 스택을 가진다.

      • 여기에는 함수 호출정보(지역 변수, 복귀 주소, 기타 등등의 정보)가 쌓인다.

      • async로 선언된 함수는 별도로 heap 영역에 쌓인다.

        • 중단 지점(await)에 걸쳐서 사용되지 않는 변수는 스택 프레임에 저장된다.
        • 중단 지점에 걸쳐서 사용되는 변수는 힙 프레임에 저장된다.
      • async 메소드가 메소드 중간에 호출되면, 원래의 스택 프레임은 새로운 함수 호출 프레임으로 대체된다.(이미 힙 영역에 추적해야할 정보는 저장되어 있으니까)

        • 그리고 새로운 frame이 힙 영역에 쌓인다.
      • await하고 있는 동안, 해당 스레드는 다른 작업을 할 수 있도록 블록되지 않는다. 관련 정보는 Continuation에 의해 저장되고, 다른 작업이 진행된다.

      • 작업이 끝나면 결과 값을 반환하고, 이후 코드 실행을 이어나간다.

        • non-async 함수 호출은 시스템 스택을 그대로 쓰기 때문에, C,Obj-C 코드를 호출할 수 있고, 반대로도 가능하다.

      • 태스크 간 의존성 추척

        • 함수는 중단점(await가 쓰인 곳)을 기준으로 continuation이 생긴다. await 이후는 continuation이다.
          • await 이후에 continuation이 실행된다. swift 런타임은 이걸 추적한다.
        • 부모 task는 자식 task의 작업을 추적해서, 자식이 모두 종료되어야 본인이 끝난다.
      • Runtime Contract

        • 스레드는 언제나 작업을 진행할 수 있어야 한다.
        • 이를 활용하기 위해 OS 차원에서의 지원을 추가했다. → Cooperative thread pool
      • Cooperative thread pool

        • Swift의 기본 실행기
        • CPU 코어 만큼만 width(스레드 최대 수)가 제한됨
        • 그래서 워커 스레드는 절대로 블록되지 않는다.(코어 수에 딱 맞게 스레드가 생기니)
        • 또한 스레드 폭발과 지나친 컨텍스트 스위칭을 막아준다.
        • 기존 GCD는 이를 제어하기 위해 subsystem당 한개의 serial Queue를 두도록 권장했다. → 이 역시 스레드 수를 제한하기 위함
        • 하지만 Swift는 언어 자체로 과도하게 동시성을 가져가지 못하도록 제한해준다.
      • 적용 시 고려 사항

        • 동시성은 비용이 발생한다. 동시성을 도입하는게 그걸 관리히는 비용보다 더 저렴해야 한다.
          • 단순히 값을 가져오는 코드는 이러한 이점을 누리지 못할 가능성이 높다.
          • 하지만 프로파일링을 해봐야 한다.
        • 원자성: await 이전의 코드를 실행했던 스레드가 await 이후의 코드를 실행할거란 보장은 없다. → 즉 원자성이 깨지는 부분이라는 것이다.
          • lock을 await 전후로 걸지 마라.
          • 특정 thread에 묶인 데이터는 await 전후로 보존되지 않는다.
        • runtime contract는 항상 준수되어야 한다.
          • 컴파일러 차원에서 지원해주는 await, actor, task group은 상관 없다.

          • os_unfair_lock, NSLock 등은 제한된 범위에서 짧게 쓰면 괜찮지만, 잘못썼을 때 막아줄 컴파일러 지원이 없기 때문에 주의해야 한다.

          • DispatchSemaphore나 pthread_cont, NSCondition, pthread_rw_lock 등은 unsafe하다.

            • task 경계에서 저걸 쓰면 안된다.
            // 잘못된 코드
            func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
            	let semaphore = DispatchSemaphore(value: 0)
            
            	async {
            		await asyncUpdateDatasource()
            		semaphore.signal()
            	}
            
            	semaphore.wait()
            }
            
          • runtime Contract가 준수되는지 확인하기 위해서는, 디버그할 때 환경변수에서LIBDISPATCH_COOPERATIVE_POOL_STRICT 값을 1로 주면 된다.

  • Synchronization

    • 상호 배제

      • 시리얼 큐

        • sync: 경쟁이 없으면, 스레드를 재사용한다. 경쟁이 생기면 스레드가 블록된다. → 스레드 폭발의 잠재적 가능성 → lock도 동일하다.
        • async: 스레드를 블록하지는 않지만, 경쟁이 없어도 매번 새로운 스레드를 요청한다.
      • 액터: 경쟁이 없으면 스레드를 재사용하고, 경쟁이 있어도 스레드를 블록하지 않는다. → 좋은 점만 합침

      • 기존에 서브시스템 단위로 큐를 썼던 것은 1개 이상의 액터로 대체된다.

        • 이때 액터가 suspend될 때 스레드가 다른 액터를 실행하는 것을 actor hopping이라고 합니다.
        • hopping이 일어날 때, 스레드는 블록되지 않는다.
        • hopping이 일어날 때, 또 다른 스레드를 필요로 하지 않는다.
        • 실행중인 actor에 또 다른 접근이 시도되면, 해당 접근은 pending된다.
        • 이후 actor작업이 완료되면, 해당 스레드는 pending된 작업을 실행하거나 suspend된 actor를 불러온다.

    • 재진입과 우선순위 지정

      • suspended된 actor중 어떤 것을 실행할지는 우선순위에 따라 결정된다.
      • GCD는 언제나 workItem이 들어온 순서대로 한다.
        • 이때 우선순위가 낮은 workItem이 우선순위가 높은 workItem보다 앞서 있을 수 있다.(priority inversion)
        • 이 때 serial queue는 이 우선순위가 낮은 아이템을의 우선순위를 높여서 빠르게 처리되도록 한다. → 물론 근본적인 해결책은 되지 않는다.
        • 이를 위해서는 serial queue의 기본 모델인 FIFO를 버려야 한다.
      • actor가 새로운 작업을 받았을 때, 해당 actor에 모든 작업이 suspended되었다면 새로운 작업을 시작할 수 있다.(reentrancy) → 즉
        • 한 actor에서 한번에 돌아가는 workItem은 하나다.(상호 배제)
        • 먼저 생긴 아이템도 더 늦게 끝날 수도 있다.
          • 기본적으로는 fifo지만 더 유연하다
          • 우선순위가 높은 작업이 더 먼저 실행된다.
    • 메인 액터: 메인스레드를 나타내는 액터

      • 메인 스레드는 thread pool과는 독립적으로 존재하기 때문에, 다른 액터와는 특성이 다르다. → DispatchQueue.main이 그랬듯이
      • 메인 액터로 선언된 메소드는 메인에서 호출되어야 하고, 메인이 아닐경우 스레드 스위칭이 일어난다.
      func loadArticle(with id: ID) async throws -> Article
      
      @MainActor func updateUI(for article: Article) async
      
      @MainActor func updateArticles(For ids: [ID]) async throws {
      	for id in ids { 
      		let article = try await database.loadArticle(with: id) // context switch!
      		await updateUI(for: article) // context switch!
      	}
      }
      
      • 그래서 메인 액터 호출은 최대한 몰아서 해주는 게 좋다. 비록 이 hopping하는 작업이 빠르더라도 많아지면 오버헤드다.
      func loadArticle(with ids: [ID]) async throws -> Article
      
      @MainActor func updateUI(for articles: [Article]) async
      
      @MainActor func updateArticles(For ids: [ID]) async throws {
      
      	let articles = try await database.loadArticles(with: ids)
      	await updateUI(for: articles)
      }