Swift - Sync와 Async와 Serial과 Concurrent 진짜 이해하기
슥 봤을 땐 그런갑다 했었는데 생각해보니까 뭔가 헷갈려서 직접 돌려 보고 정리
개념
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()
로 실행시간을 함께 찍은 결과 입니다
그림으로 그리면 이런 결과네요
뭐 이렇게 되는 거 맞는 거 같기도 하고…
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
그랬더니 이런 결과가 나오네요?!
순서랑 시간이 꽤 달라져서 당황함
근데 또 그림 그려 보니까 그럴 듯 한데
시리얼 큐가 끝날 때까지 기다리고 컨커런트가 실행되는 게 맞지
근데 이게 또 참 같이 두고 보니까 좀 묘한 거 같기도 하고
시리얼 큐가 끝나는 거를 기다리기도 하고 안 기다리기도 하는데 거 참
그러고 보니 Concurrent 큐에서는 왜 동시에 일 안 하나요? 큐의 작업들은 스레드 있으면 동시에 한다매? cc가 bb 끝나는 거 왜 기다리고 함?? Sync든 Async든 일단 Concurrent 큐에 담긴 작업들은 동시에 되야 하는 거 아님??
Main Queue 고려하기
메인 큐 입장에서 그려보니까 조금 이해가 되더라구여
- 일단 a를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
- 그리고 바로 b를 Serial 큐에 Sync로 넘기고, b가 완료될 때까지 기다립니다!!
- b는 실제로는 1초 걸리는 작업이지만, Main Queue 시점에서 보면 넘기고 나서 끝날 때까지 쭉 대기해서 늘어나 보임
- b가 끝났을 때 드디어 c를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
- 이제 aa를 Concurrent 큐에 Async로 던지고 바로 돌아오기
- 그리고 바로 bb도 Concurrent 큐에 Sync로 넘겼는데!! bb가 완료될 때까지 기다려야 함
- bb가 끝나면 이제 cc를 Concurrent 큐에 Async로 던지고 바로 돌아오기
비슷하게!! 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
결과는 이렇게 나오네요
그려 보면 이렇습니다
- 일단 a를 Serial 큐에 Async로 던지고 바로 돌아 옵니다
- 그리고 바로 b를 Serial 큐에 Sync로 넘기고, b가 완료될 때까지 기다립니다!!
- b가 끝났을 때 aa를 Concurrent 큐에 Async로 던지고 바로 돌아 오기
- 바로 돌아오자마자 c를 Serial 큐에 Async로 던지고 또 바로 돌아 오기
- 그리고 바로 bb도 Concurrent 큐에 Sync로 넘겼는데!! bb가 완료될 때까지 기다리기
- bb가 끝나면 이제 cc를 Concurrent 큐에 Async로 던지고 바로 돌아오기
- 마지막으로 d를 Serial 큐에 Sync로 넘기기.
- d는 실제로는 bb가 끝나고 cc를 넘기고 바로 돌아온 시점인 5초에 넘겨 줬지만, Serial 큐 때문에 대기해서 늘어나 보임
재밌네요…
Case 1, Case 2를 보면 작업 시간 1초짜리 태스크 d를 추가한 것 뿐인데, Serial 큐에서 기다려야 해서 끝나는 시간은 3초나 늘어나기도 하고ㅋㅋ 이런 거 보면 큐에 넘겨줄 땐 잘 생각하고 해줘야 할 것 같슴니다
댓글남기기