Swift - Sync와 Async와 Serial과 Concurrent 진짜 이해하기

3 분 소요


슥 봤을 땐 그런갑다 했었는데 생각해보니까 뭔가 헷갈려서 직접 돌려 보고 정리


개념

Sync/Async

  • Sync
    • 작업이 끝날 때까지 기다리고 수행
  • Async
    • 작업이 끝나지 않더라도 기다리지 않고 수행

일단 이 둘이는 워낙 자주 본 애들이니까… 대충 뭔 말인지 알겠고

Serial/Concurrent

  • Serial
    • 직렬로 큐의 작업 수행
  • Concurrent
    • 병렬로 큐의 작업 동시 수행

얘네는?? 잘 몰랐는데 보니까 대충 뭔 말인 지는 알겟네용

그럼 둘이 비슷한 거 아님?? 싶지만 스코프가 다르다고 해야 하나… Sync/Async는 스레드에서의 처리 방식, Serial/Concurrent는 큐에서의 처리 방식이라 생각하면 될까 싶네여

비교

메인 스레드에서 큐들을 사용한다고 보면

  Serial Concurrent
Sync <ul><li>메인 스레드는 큐에 넘기고, 작업이 완료될 때 까지 기다림</li><li>UI 업데이트 X</li><li>작업: 큐에 있던 다른 작업들이 다 끝나야 작업 수행 가능</li></ul> <ul><li>메인 스레드는 큐에 넘기고, 작업이 완료될 때 까지 기다림</li><li>UI 업데이트 X</li><li>작업: 큐에 담긴 순서지만, 다른 스레드가 있다면 거기서 실행됨</li></ul>
Async <ul><li>메인 스레드는 큐에 넘기고 바로 다시 돌아옴</li><li>UI 업데이트 O</li><li>작업: 큐에 있던 다른 작업들이 다 끝나야 작업 수행 가능</li></ul> <ul><li>메인 스레드는 큐에 넘기고 바로 다시 돌아옴</li><li>UI 업데이트 O</li><li>작업: 큐에 담긴 순서지만, 다른 스레드가 있다면 거기서 실행됨</li></ul>

음~~ 알겠…음…??
갑자기 저만… 모르겟나영 게슈탈트 붕괴 오는 것 같네요
뭔가 아하! 했는데 또 헷갈림


간단하게 예제 만들어서 확인해 봅시다


Case 1

let serialQ = DispatchQueue(label: "serialQ")
serialQ.async {
    sleep(3)
    print("a")
}
serialQ.sync {
    sleep(1)
    print("b")
}
serialQ.async {
    sleep(2)
    print("c")
}

let concurrentQ = DispatchQueue(label: "concurrentQ", attributes: .concurrent)
concurrentQ.async {
    sleep(3)
    print("aa")
}
concurrentQ.sync {
    sleep(1)
    print("bb")
}
concurrentQ.async {
    sleep(2)
    print("cc")
}

위 코드의 실행 결과는 어떻게 될까요??

3.001871109008789: a
4.006289005279541: b
5.007498025894165: bb
6.011527061462402: c
7.007543087005615: aa
7.012703061103821: cc

CFAbsoluteTimeGetCurrent()로 실행시간을 함께 찍은 결과 입니다

1

그림으로 그리면 이런 결과네요
뭐 이렇게 되는 거 맞는 거 같기도 하고…

Case 2

let serialQ = DispatchQueue(label: "serialQ")
serialQ.async {
    sleep(3)
    print("a")
}
serialQ.sync {
    sleep(1)
    print("b")
}
serialQ.async {
    sleep(2)
    print("c")
}

// Add!!
serialQ.sync {
    sleep(1)
    print("\(durationTime): d")
}
//

let concurrentQ = DispatchQueue(label: "concurrentQ", attributes: .concurrent)
concurrentQ.async {
    sleep(3)
    print("aa")
}
concurrentQ.sync {
    sleep(1)
    print("bb")
}
concurrentQ.async {
    sleep(2)
    print("cc")
}

근데 뭔가 좀 애매한 거 같아서 Serial에 sync로 하나 더 넣어 봤습니다

3.003862977027893: a
4.0102620124816895: b
6.011373043060303: c
7.012570023536682: d
8.013816952705383: bb
10.014379978179932: aa
10.014384031295776: cc

그랬더니 이런 결과가 나오네요?!
순서랑 시간이 꽤 달라져서 당황함

2

근데 또 그림 그려 보니까 그럴 듯 한데
시리얼 큐가 끝날 때까지 기다리고 컨커런트가 실행되는 게 맞지



1 2

근데 이게 또 참 같이 두고 보니까 좀 묘한 거 같기도 하고
시리얼 큐가 끝나는 거를 기다리기도 하고 안 기다리기도 하는데 거 참

그러고 보니 Concurrent 큐에서는 왜 동시에 일 안 하나요? 큐의 작업들은 스레드 있으면 동시에 한다매? cc가 bb 끝나는 거 왜 기다리고 함?? Sync든 Async든 일단 Concurrent 큐에 담긴 작업들은 동시에 되야 하는 거 아님??


Main Queue 고려하기

1_2

메인 큐 입장에서 그려보니까 조금 이해가 되더라구여

  1. 일단 a를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
  2. 그리고 바로 b를 Serial 큐에 Sync로 넘기고, b가 완료될 때까지 기다립니다!!
    • b는 실제로는 1초 걸리는 작업이지만, Main Queue 시점에서 보면 넘기고 나서 끝날 때까지 쭉 대기해서 늘어나 보임
  3. b가 끝났을 때 드디어 c를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
  4. 이제 aa를 Concurrent 큐에 Async로 던지고 바로 돌아오기
  5. 그리고 바로 bb도 Concurrent 큐에 Sync로 넘겼는데!! bb가 완료될 때까지 기다려야 함
  6. bb가 끝나면 이제 cc를 Concurrent 큐에 Async로 던지고 바로 돌아오기

2_2

비슷하게!! case 2도 보면
1, 2, 3은 위와 같고, c를 주고 바로 돌아온 직후에 d를 Serial 큐에 Sync로 넘기고, d가 완료될 때까지 기다리기!!
그리고 d가 완료된 후에 4, 5, 6 수행하기네요.

Concurrent 큐에서 왜 aa, bb, cc를 동시에 안 하는 거야!! 했는데 bb가 sync이기 때문에 메인에서 bb가 끝나기를 기다린 후에야 Concurrent 큐에 cc를 보내기 때문이엇슴다

Case 3

let serialQ = DispatchQueue(label: "serialQ")
let concurrentQ = DispatchQueue(label: "concurrentQ", attributes: .concurrent)

serialQ.async {
    sleep(3)
    print("\(durationTime): a")
}
serialQ.sync {
    sleep(1)
    print("\(durationTime): b")
}
concurrentQ.async {
    sleep(3)
    print("\(durationTime): aa")
}
serialQ.async {
    sleep(2)
    print("\(durationTime): c")
}
concurrentQ.sync {
    sleep(1)
    print("\(durationTime): bb")
}
concurrentQ.async {
    sleep(2)
    print("\(durationTime): cc")
}
serialQ.sync {
    sleep(1)
    print("\(durationTime): d")
}

이제 막 섞어 볼까요??
각 태스크들 길이랑, 큐에서의 순서(알파벳)는 같게 하고 큐끼리 섞어 봤습니다

3.0011839866638184: a
4.002965927124023: b
5.003633975982666: bb
6.008189916610718: c
7.004278898239136: aa
7.008728981018066: cc
7.0093629360198975: d

결과는 이렇게 나오네요

3

그려 보면 이렇습니다

  1. 일단 a를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
  2. 그리고 바로 b를 Serial 큐에 Sync로 넘기고, b가 완료될 때까지 기다립니다!!
  3. b가 끝났을 때 aa를 Concurrent 큐에 Async로 던지고 바로 돌아 오기
  4. 바로 돌아오자마자 c를 Serial 큐에 Async로 던지고 또 바로 돌아 오기
  5. 그리고 바로 bb도 Concurrent 큐에 Sync로 넘겼는데!! bb가 완료될 때까지 기다리기
  6. bb가 끝나면 이제 cc를 Concurrent 큐에 Async로 던지고 바로 돌아오기
  7. 마지막으로 d를 Serial 큐에 Sync로 넘기기.
    • d는 실제로는 bb가 끝나고 cc를 넘기고 바로 돌아온 시점인 5초에 넘겨 줬지만, Serial 큐 때문에 대기해서 늘어나 보임



재밌네요…
Case 1, Case 2를 보면 작업 시간 1초짜리 태스크 d를 추가한 것 뿐인데, Serial 큐에서 기다려야 해서 끝나는 시간은 3초나 늘어나기도 하고ㅋㅋ 이런 거 보면 큐에 넘겨줄 땐 잘 생각하고 해줘야 할 것 같슴니다


태그: ,

카테고리:

업데이트:

댓글남기기