-
[iOS] - 직렬과 동시(4/4)iOS/iOS 2024. 3. 16. 18:03728x90반응형
이전 포스팅
[iOS] - 비동기와 동기(3/4)
이전 포스팅 [Swift] - GCD에 대해서(2/4) 이전 포스팅을 읽고 읽어주길 바란다. https://quarang.tistory.com/71 [Swift] - GCD를 알아야하는 이유(1/4) 오늘은 최근에 했던 컴퓨터구조 스터디를 기반으로 포스팅
quarang.tistory.com
저번에 이어서 바로 직렬괃 동시에 대해서 설명하겠다.
직렬과 동시
우리 모두 비동기와 동기의 차이를 숙지했다. 바로 메인스레드가 작업의 완료를 기다리냐 안기다리느냐의 차이였다. 근데 여기서 근본적인 문제는 바뀌지 않았다. 바로 이렇게 했다 하더라도 모든 작업을 각자에 맞는 스레드로 할당하지 않았다는 말이다. 이래서 우리는 스레드를 할당하는 방식 중 직렬과 동시에 대해 알아야한다.
직렬(Serial)
직렬처리방식을 쓰기 위해서는 우리가 저저번 포스팅에서 알게된 직렬전용 큐(Queue)를 사용해야한다. 그래서 직렬이 뭐냐?
이 그림과 같이 한 스레드에 작업들을 몰아서 보내주는 것이다.
여기서 생각이 하나 들었다. 메인스레드도 직렬처리를 써서 저렇게 혼자서 다 처리하고 있던건가? -> 정답이다.
메인스레드는 직렬큐를 사용하고 동기방식으로 작동하는 스레드이다. 그 이유는 저번에도 얘기했지만 UI의 동작이 여러 쓰레드에서 비동기적으로 작동하면, 컴포넌트가 엉망진창 짬뽕으로 이루어진 화면을 볼 수 있을 것이다. 그래서 iOS에서도 UI의 업데이트는 모두 메인스레드에서 작동하길 권장하고 있다.
근데 왜 강제가 아니라 권장이지? 애플은 앱의 성능을 위해 이런 스레드 관리를 개발자가 동적으로 하길 바라는 듯하다. 이말은 곧 우리가 자유롭게 관리가 가능해 프로젝트의 기능과 양에 따라 효율적으로 개발할수 있다는 장점이 될테고, 개발자에 따라서 성능을 크게 좌지우지할 수 있다는 말이다. 우리가 이걸 배우고 있는 이유가 되는것.(이 정보는 언뜻 어디서 들은 말이기 때문에 전적으로 신뢰할 수 없다.. 정확하게 아시는 분이 댓글을 달거나 새로 배우게 될 경우, 바로 수정하겠다.)
동시(concurrency)
솔직히 직렬에 대한 내용을 보면 어느정도 예상이 갈것이다. 그 예상대로 동시처리는 한 스레드에 작업을 몰아주는 것이 아닌, 여러 스레드에 분배 처리를 하는 것이다.
이렇게 말이다.
동시처리는 동시전용 큐를 사용한다. 이 큐에서는 지금 작업을 처리해줄 스레드를 각각 생성하고 할당해준다.
근데 그럼 병렬이 아니라 동시인 이유는 무엇일까?
일단 직렬과 동시는 싱글코어에서도 동작이 가능하다. 하지만 병렬은 아니다.
"? 싱글코어 CPU를 사용하는 경우 동시에 여러 스레드를 사용하는 것이 불가능하다 하지 않았나? 어떻게 스레드를 분산하는 것이 가능한거지? 얘 알지도 못하면서 똥글 싸지른다!"
라는 의문이 생기신 분은 아주 명석한 분이다.
한 코어는 실제로 하나의 스레드를 실행시킬 수 있다. 근데 제품들 보면 4코어 8스레드 이런말을 봤을 텐데 이건 실제로 1코어안에서 2개의 스레드를 실행 시킨다는 말인데, 뭔가 앞뒤가 안맞는다.
하지만 지극히 하드웨어 입장에서 생각해보면, 좀 애매한데 사실 코어가 작업을 잘게 쪼개어 일사 분란하게 스레드를 번갈아가며 실행시키는 것이다. 이걸 인간입장에서 보면 마치 진짜 동시에 한 코어에서 여러개의 스레드가 동시에 실행되는 것처럼 보이는 것인데, 이것을 시분할 스케쥴링이라고 부른다고 한다.
하지만 스레드에서 스레드로 작업을 계속 보내는 작업은 상당히 전력을 많이 잡아 먹는다. CPU가 어지간히 성능이 좋지 않으면 못할 것이다. 이렇게 힘들게 동시처리를 힘들게 하고 있었던 것이다.
이와 다르게, 병렬은 이것보다 조금더 넓은 개념이다.
각자 다른 코어에서 개별적으로 움직이며, 각각의 큐, 스레드를 사용한다. 그렇기 때문에 직렬과 동시 전부 동시다발적으로 작동될 수 있다. 이것이 병렬의 개념이다.
자 그럼 우린 4개의 동작 방식이 있다는 것을 알았고, 또 이것들이 각각과 조합이 되어 사용될 수 있다는 것을 알 수 있을 것이다.
한번 코드로 살펴보자
let concurrntQueue = DispatchQueue(label: "concurrntQueue",attributes: .concurrent) //concurrent - 병렬 let serialQueue = DispatchQueue(label: "SerialQueue") //serial - 직렬 // 명령어 라인 func performTask(taskNumber: Int) { print("Task \(taskNumber) 인출") sleep(1) print("Task \(taskNumber) 해석") sleep(1) print("Task \(taskNumber) 실행") sleep(1) print("Task \(taskNumber) 저장") sleep(1) } // 병렬 처리할 작업 개수 let numberOfTasks = 5 //1 - serial + sync print(Thread.main) for i in 1...numberOfTasks { serialQueue.sync { performTask(taskNumber: i) } } print(Thread.main) //2 - concurrency + sync print(Thread.main) for i in 1...numberOfTasks { concurrntQueue.sync { performTask(taskNumber: i) } } print(Thread.main) //3 - serial + async print(Thread.main) for i in 1...numberOfTasks { serialQueue.async { performTask(taskNumber: i) } } print(Thread.main) //4 - concurrency + async print(Thread.main) for i in 1...numberOfTasks { concurrntQueue.async { performTask(taskNumber: i) } } print(Thread.main)
손수 작성한 코드다. 각각의 큐를 생성하고 작업과 루프를 각각의 조합별로 만들었다. 한번 자세히 생각해보자.
1. 직렬 + 동기
전형적인 순차코드일 것이다. 메인스레드는 큐로 작업을 보내고 그 작업이 완료되었을때 하나씩 보내줄 것이고, 직렬큐로 한 스레드에서 모든 작업을 할것이다. 참고로 Thread.main은 메인스레드 정보를 출력하는 것이다.
<_NSMainThread: 0x600001704000>{number = 1, name = main} Task 1 인출 Task 1 해석 Task 1 실행 Task 1 저장 Task 2 인출 Task 2 해석 Task 2 실행 Task 2 저장 Task 3 인출 Task 3 해석 Task 3 실행 Task 3 저장 Task 4 인출 Task 4 해석 Task 4 실행 Task 4 저장 Task 5 인출 Task 5 해석 Task 5 실행 Task 5 저장 <_NSMainThread: 0x600001704000>{number = 1, name = main}
2. 동시 + 동기
스레드 자체는 분배해서 처리한다 해도, 어쩃든 동기처리기 때문에 메인스레드에서는 작업을 기다린다. 그래서 직렬큐를 사용하는 것과 별다른 차이는 없다.
<_NSMainThread: 0x600001704000>{number = 1, name = main} Task 1 인출 Task 1 해석 Task 1 실행 Task 1 저장 Task 2 인출 Task 2 해석 Task 2 실행 Task 2 저장 Task 3 인출 Task 3 해석 Task 3 실행 Task 3 저장 Task 4 인출 Task 4 해석 Task 4 실행 Task 4 저장 Task 5 인출 Task 5 해석 Task 5 실행 Task 5 저장 <_NSMainThread: 0x600001704000>{number = 1, name = main}
3. 직렬 + 비동기
메인스레드가 본인에게 할당된 작업을 빨리 빨리 큐로 이송하고 본인은 그 다음 일을 할것이다. 다만 직렬이기 때문에 루프안에 있는 작업은 모두 순차적으로 처리될 것이다. 다만 메인스레드만 본인일을 하는 것이고, 메인스레드 정보의 처음과 끝이 먼저 출력되고 나머지 루프는 한 스레드에서 실행 될 것이다.
<_NSMainThread: 0x600001704000>{number = 1, name = main} Task 1 인출 <_NSMainThread: 0x600001704000>{number = 1, name = main} 3초 안에 task 완료 성공 Task 1 해석 Task 1 실행 Task 1 저장 Task 2 인출 Task 2 해석 Task 2 실행 Task 2 저장 Task 3 인출 Task 3 해석 Task 3 실행 Task 3 저장 Task 4 인출 Task 4 해석 Task 4 실행 Task 4 저장 Task 5 인출 Task 5 해석 Task 5 실행 Task 5 저장
3. 동시 + 비동기
이제 스레드도 모두 분산되고 메인스레드도 작업을 기다리지 않으니 각 작업들은 일사분란하게 작동될 것이다. 참고로 인출,해석,실행,저장은 task단위로 묶여있기 때문에 무조건 항상 순서대로 나온다.
<_NSMainThread: 0x600001710000>{number = 1, name = main} Task 1 인출 Task 2 인출 Task 3 인출 Task 4 인출 Task 5 인출 <_NSMainThread: 0x600001710000>{number = 1, name = main} 3초 안에 task 완료 성공 Task 2 해석 Task 4 해석 Task 1 해석 Task 5 해석 Task 3 해석 Task 3 실행 Task 1 실행 Task 2 실행 Task 4 실행 Task 5 실행 Task 4 저장 Task 3 저장 Task 1 저장 Task 2 저장 Task 5 저장
모두 이해가 갔으면 좋겠다.
형편없는 말솜씨로 아는 지식을 끄적여 보았다. 이로써 개념은 확실히 다진 것같다. 긴 글을 모두 읽어주신 분은 감사드린다.
'iOS > iOS' 카테고리의 다른 글
[iOS] - protocol을 사용하는 이유 (1) 2024.08.31 [iOS] - protocol이란? (0) 2024.08.31 [iOS] - 비동기와 동기(3/4) (1) 2024.03.16 [iOS] - GCD에 대해서(2/4) (1) 2024.03.16 [iOS] - GCD를 알아야하는 이유(1/4) (0) 2024.03.16