이 글은 공부한 내용을 정리하는 글이오니 건전한 비판은 환영합니다.
비동기
https://luca-os.tistory.com/55
비동기와 쓰레드에 관한 내용은 위에 글에 있으니 참고 부탁드립니다.
먼저 쓰레드에 관해서 간단히 이야기 하자면, 하나의 공간에 두명의 작업자가 두개의 작업을 하는 것입니다.
메인 쓰레드와 global 쓰레드
스토리 보드에서 두개의 라벨이 있고 3개의 버튼이 있습니다.
버튼은 차례대로 action함수로 선언하였고, 2개의 라벨은 아울렛 변수로 설정하였습니다.
먼저 위에는 시간을 나타낼 수 있게 viewDidLoad에다가 타이머를 선언을 해주었습니다.
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var finishLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { time in
self.timeLabel.text = Date().timeIntervalSince1970.description
}
}
먼저 일반적으로 버튼을 눌렀을 경우에 어떻게 발생하는지 알려드리겠습니다.
@IBAction func action1(_ sender: Any) {
finishLabel.text = "끝"// 대략적으로 메인 쓰레드에서 하는 작업하는 것이다.
}
이와 같이 실행하면, 시간은 계속 돌아가면서 '끝'이라는 단어가 화면에 송출이 됩니다.
그리고 클로져 함수를 작성하여서 하나의 main에서 작업을 하면 어떤 현상이 벌어지는지 설명하겠습니다.
func simpleClosure(completion: () -> Void){
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
completion()
}
저 위에 Thread.sleep는 0.2초를 멈춘다는 이야기 입니다.
그리고 클로져를 사용하여서 버튼에다가 작업을 하면은
@IBAction func action1(_ sender: Any) {
simpleClosure {
finishLabel.text = "끝"}
}
위에 있는 시간이 멈추고 print하였던 index 번호가 끝날 때까지 기다리다가 끝이라는 단어와 같이 나오게 됩니다.
왜 이런 경우가 발생하는지? 그 이유는 단일 쓰레드에서 작업을 진행하기 때문입니다. 즉 하나의 작업자가 일을 처리하기 때문에 for문이 끝나야지 다음 작업이 이루어지기 때문입니다.
그러면 쓰레드를 만들어서 진행해보도록 해보면 되지 않을까요?
func simpleClosure(completion: @escaping() -> Void){
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
completion()
}
}
똑같이 돌려보겠습니다. 화면의 시간은 그대로 표출되고, index도 콘솔에 찍히게 되지만 에러가 발생합니다.
UILabel.text must be used from main thread only
이 뜻은 화면에 표출하는 것은 메인 쓰레드에서 하라는 이야기 입니다. 메인쓰레드가 아닌 global에서 진행을 하였기 때문에 앱이 죽었습니다.
그러면 에러가 발생하지 않게 메인 쓰레드에서 진행을 해볼까요?
func simpleClosure(completion: @escaping() -> Void){
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
DispatchQueue.main.async {
completion()
}
}
}
이러면 에러가 발생하지 않았습니다. 왜냐하면 메인 쓰레드만 호출해서 화면에 표출하였기 때문입니다.
이렇게 클로져 함수에서 main쓰레드를 감싸도 되고 아니면
func simpleClosure(completion: @escaping() -> Void){
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
completion()
}
}
액션함수에서 메인 쓰레드를 호출하면 에러가 발생하지 않습니다.
@IBAction func action1(_ sender: Any) {
simpleClosure {
DispatchQueue.main.async {//설계의 문제일 뿐 어디서 써도 상관없다
self.finishLabel.text = "끝"
}
}
DispatchGroup
디스패치그룹은 쓰레드를 하나의 작업으로 묶어서 진행하겠다는 이야기 입니다.
무슨 이야기냐면 예제로 한번 설명해 보겠습니다.
위에 스토리 보드처럼 이제는 두번째 버튼에다가 로직을 추가하여 보겠습니다.
@IBAction func action2(_ sender: Any) {
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "q1")
queue1.async(group: dispatchGroup){//Dispatch를 하나만으로 하였기 때문에, 어짜피 작업의 흐름대로 쭉 간다.
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
queue1.async(group: dispatchGroup){
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
queue1.async(group: dispatchGroup){
for index in 20..<30{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
디스패치큐를 하나를 만들고, 디스패치 그룹 인스턴스를 만든 후에 queue1에다가 그룹으로 만들어 준 후에 버튼을 클릭하면
위와 같이 순서대로 출력이 됩니다. 그 이유는 디스패치큐를 하나로만 만들어 주었기 때문에 쓰레드가 하나로만 작동이 되어서 순서대로 쭉 진행이 됩니다. 그래서 이번에는 디스패치큐를 3개로 만들어주고 한번 어떻게 진행하는지 보면
@IBAction func action2(_ sender: Any) {
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
let queue3 = DispatchQueue(label: "q3")
queue1.async(group: dispatchGroup){
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
queue2.async(group: dispatchGroup){
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
queue3.async(group: dispatchGroup){
for index in 20..<30{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
print("끝")
}
}
이번에 콘솔을 찍어보면
위와 같이 나오게 됩니다. 그 이유는 3개의 쓰레드가 진행하고 있기 때문입니다. 그리고 저 '끝'이라고 나오는 문구는 그룹으로 쓰레드를 묶어놨기 때문에, 끝이 나면 알려달라는 의미로 로직을 작성하였기 때문에 나오게 됩니다.
@IBAction func action2(_ sender: Any) {
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
let queue3 = DispatchQueue(label: "q3")
queue1.async(group: dispatchGroup){
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
queue2.async(group: dispatchGroup){
DispatchQueue.global().async {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
queue3.async(group: dispatchGroup){
DispatchQueue.global().async {
for index in 20..<30{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
print("끝")
}
}
위와 같이 작성을 하게 되면 끝이 먼저 나오게 됩니다. 먼저 이유를 설명하자면은 디스패치큐안에 또 디스패치큐 글로벌을 넣었습니다. 그러면 로직상으로 안에 있는 디스패치큐가 '이 작업이 끝나든 안끝나든 내 할일만 할꺼야'라고 하면서 바로 다음으로 던져버리게 됩니다.
그래서 3개의 디스패치큐는 작업이 진행되는 중에 다음으로 넘어가 버리고 '끝'이 먼저 나오게 됩니다.
근데 왜 이렇게 하는지 이유를 설명하자면, 로직의 차이일 수 있겠지만 네트워크상에서 먼저 작업을 하고 진행해야 할 수도 있기 때문입니다.
예를 들면, 유튜브의 댓글을 보고자 할 때 네트워크가 막혀있는데 댓글을 쓴 아이디의 사진이 나와야 하지만 먼저 회색으로 된 동그라미만 나올 경우가 간혹 발생할 때가 있다. 그럴 때 먼저 네트워크에서 느려질 때 진행은 되고 있다는 표시로 알려줘야 할 필요도 있지만, 내 생각에는 먼저 틀을 잡아 놓고 사진을 네트워크에서 받는 것이 로직 상에도 좋은 것 같다.
그러면 끝을 알 수 있게 어떻게 로직을 짜야 할까?
@IBAction func action2(_ sender: Any) {
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
let queue3 = DispatchQueue(label: "q3")
queue1.async(group: dispatchGroup){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
queue2.async(group: dispatchGroup){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
queue3.async(group: dispatchGroup){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 20..<30{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
print("끝")
}
}
디스패치큐가 시작하기 전에 dispatchGroup.enter()와 끝날 때 dispatchGroup.leave()를 남겨주면 끝이 나오게 된다
DispatchQoS
디스패치QoS는 그룹핑 되어 있는 큐에게 진행의 우선순위를 정하는 것이다.
물론 확신은 못해준다.
위의 그림과 같이 backgroud가 순위 중에 가장 낮으며, unspecified가 가장 높다. 그래서 만약에 우선순위를 안 정했을 때에는 'default'로 진행이 된다.
@IBAction func action2(_ sender: Any) {
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
let queue3 = DispatchQueue(label: "q3")
queue1.async(group: dispatchGroup, qos: .background){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
queue2.async(group: dispatchGroup, qos: .userInteractive){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
queue3.async(group: dispatchGroup){
dispatchGroup.enter()
DispatchQueue.global().async {
for index in 20..<30{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
print("끝")
}
}
그래서 작동을 해보면
물론 qos는 우선순위를 정하는 것이지, 순서를 보장해 주지는 않는다.
Sync
먼저 Sync를 쓰는 것은 내 작업이 우선순위이기 때문에, 다른 것은 내 작업이 먼저 끝난 후에 작업을 실행합니다.
먼저 예제를 보여주자면,
@IBAction func action3(_ sender: Any) {
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
queue1.sync {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
queue2.sync {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
위와 같이 작업을 하면, 화면에 시간이 표시 된것이 멈추고 싱크가 다 완료된 후에 시간이 돌아간다.
물론 작업도 동일 하게 흐름대로 진행이 된다.
deadlock
데드락이라 불리는 이것은 상대가 작업이 끝나기를 기다리는데, 계속 끝나지 않고 서로 교착 상태에 머물다가 앱이 죽어버린다.
예시를 들어보면
@IBAction func action3(_ sender: Any) {
let queue1 = DispatchQueue(label: "q1")
queue1.sync {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
queue1.sync {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
}
에러 메시지는
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
에러가 발생한 이유는 q1의 디스패치큐 안에 같은 디스패치큐가 있는데 밖에 디스패치큐가 작업을 하고 있으면서 for 문이 끝나고 그 안에 sync가 있는데 sync는 서로 끝나기를 바라는데, 서로 맞물려 있어서 계속 프로세스를 잡고 있다가 에러가 발생한다.
물론 위에 디스패치큐를 async를 해도 에러가 발생한다. 그 이유는 sync는 작업이 끝나는 것을 기다리기 때문에 발생한다. 밑에 예제
@IBAction func action3(_ sender: Any) {
let queue1 = DispatchQueue(label: "q1")
queue1.async {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
queue1.sync {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
}
}
그러면 안에 async로 만들면 되지 않을까? 당연히 된다. 왜냐하면 sync는 자신이 끝나는 것을 알아야 하기 때문에 안에를 async로 만들면 async는 '나는 작업을 할테니 진행하라는 이야기'니 에러가 발생 안한다.
queue1.sync {
for index in 0..<10{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
queue1.async {
for index in 10..<20{
Thread.sleep(forTimeInterval: 0.2)
print(index)
}
}
print("async")
}
그러면
이와 같이 나오게 된다.
mainSync
main.sync는 화면에서 표출해야 하는 sync이다.
그렇기 때문에 에러가 발생한다.
에러 내용은 위와 똑같은 메시지이다.
@IBAction func action3(_ sender: Any) {
DispatchQueue.main.sync {
print("sync")
}
}
main은 화면단을 보여주는 것이기 때문에 async로 해야한다.
이 상황은 main에서 작업을 하고 있기 때문에 sync로 하면 main이 작업이 끝날 때까지 기다려야 하는데 main은 계속 돌아가기 때문에 deadlock이 걸려버린다.
sync를 사용하는 이유는 작업이 비어있을때 사용하는 것이다. 또는 중요한 일이 첫 번째로 하여야 할 때 사용한다.
요즘 대부분은 sync로 작업을 많이 안하고, async로 작업을 한다.
sync를 쓸 때 이게 꼭 필요한 상황인지 인지하고 사용해야 한다.
그래서 위에 상황을 사용하기 위해서는 async로 해야한다.
@IBAction func action3(_ sender: Any) {
DispatchQueue.main.async {
print("sync")
}
}
'Mobile > IOS' 카테고리의 다른 글
ViewModel을 이용한 버튼 활성화 방법 (0) | 2022.10.18 |
---|---|
Objective-c xml 파싱, json 파싱 (0) | 2022.08.21 |
CocoaPods 에러 Cannot find 'Auth' in scope (0) | 2022.05.04 |
UINavigationController (0) | 2022.04.26 |
UserDefaults 및 싱글톤 (0) | 2022.03.04 |