[iOS/Swift] GCD 골든벨 - 다양한 상황에서의 동기/비동기처리 출력결과 예측하기

동기와 비동기, 동시와 직렬은 Swift 공부를 하면서 한번쯤은 들어봤을 개념일 것이다.

이번 글에서는 개념에 대해서는 간단하게만 짚고 넘어가고,

다양한 상황에서 동기/비동기 그리고 동시/직렬처리를 했을때 출력 결과가 어떻게 나올지 예측하고, 정답여부를 체크해보겠다.

 


동기와 비동기 vs 동시와 직렬

  • 동기 VS 비동기

동기와 비동기는 메인 쓰레드 관점에서의 구분이라고 생각하면 된다.

- 동기(Sync): 여러 Task가 있을 때, 몇 개의 Task가 Queue로 보내졌다면, 메인쓰레드 입장에서 Queue로 보내진 Task가 끝날때까지 기다렸다가 다음 Task를 하는것.

- 비동기(Async): Queue로 보내진 Task가 끝났는지의 여부와 상관없이, 바로 다음 Task를 진행하는 것

  • 동시 VS 직렬

동시와 직렬은 Queue의 관점에서의 구분이라고 생각하면 된다.

- 동시(Concurrent): Queue에 있는 여러 작업들을 여러 쓰레드에 골고루 분배하는 것. 보통 global이라고 칭함.

- 직렬(Serial): Queue에 있는 작업들을 여러 쓰레드에 분배하는게 아니라 하나의 쓰레드에 몰아서 주는것. 보통 main이라고 칭함.


코드의 동작 살펴보기

위의 경우의수로만 보면 총 4가지의 경우가 있을 것이다. 그중에서 실제 코드를 작성할 때 많이 사용하는 경우는 비동기+직렬, 그리고 비동기+동시인데, 코드로 나타내면 `DispatchQueue.main.async { }`, `DispatchQueue.global.async{ }` 이렇게 표현한다.

 

그럼, 이 두 경우의 동작을 살펴보자.

  • `DispatchQueue.main.async { }`
func example() {
    for i in 1...100 {
        print(i, terminator: " ")
    }
    
    DispatchQueue.main.async {
        for i in 101...200 {
            print(i, terminator: " ")
        }
    }
    
    
    for i in 201...300 {
        print(i, terminator: " ")
    }
}

example이라는 함수는 숫자를 print하는 동작을 하고있으며, 101~200을 출력하는 동작을 할 때 `DispatchQueue.main.async { }`을 통해 직렬, 그리고 비동기로 동작하고있다. 이 함수의 출력 결과는 어떻게될까?

1-100이 순서대로 출력된 이후에 201-300이 출력된다. 그리고 맨 마지막에 101-200이 출력된다.

어떤 과정을 거쳐서 이렇게 출력되는지 그림으로 살펴보자.

- 코드는 1-100을 출력하는것을 시작으로 진행하는데, 101-200은 main.async로 동작하도록 되어있으므로, Queue에 보내지고, 코드는 Queue로 보내진 작업이 끝났는지의 여부와 상관없이 다음 작업인 201-300을 실행한다.

- Queue의 입장에서는 자신이 가지고있는 101-200을 Serial하게 하나의 쓰레드로 보내주어야 하는데, 다른 쓰레드로 보내나 main쓰레드로 보내나 동작은 같기 때문에 main쓰레드의 마지막으로 작업을 보낸다.

- 그래서 201-300이 끝난 이후에 비로소 101-200 작업이 실행되는 것이다.

 

  • `DispatchQueue.global.async { }`
func example() {
    for i in 1...100 {
        print(i, terminator: " ")
    }
    
    DispatchQueue.global().async {
        for i in 101...200 {
            print(i, terminator: " ")
        }
    }
    

    for i in 201...300 {
        print(i, terminator: " ")
    }
}

그렇다면 이번에는 비동기 + 동시로 동작하는 위의 코드의 출력 결과는 어떻게될까?

1-100까지는 순서대로 출력이 되지만, 그 이후의 출력순서는 뒤죽박죽인것을 볼 수 있다.

마찬가지로 그림을 통해 동작을 알아보도록 하자

- 코드는 마찬가지로 1-100을 출력하는것을 시작으로 진행, 101-200은 global.async로 동작하도록 되어있으므로, Queue에 보내지고, 코드는 Queue로 보내진 작업이 끝났는지의 여부와 상관없이 다음 작업인 201-300을 실행한다.

- Queue의 입장에서는 자신이 가지고있는 101-200을 Concurrent하게 다른 쓰레드에 골고루 나눠줘야해서, n번째 쓰레드에게 101-200작업이 전달된다.

- 그래서 메인쓰레드가 201-300 작업을 함과 동시에 n번째 쓰레드는 101-200을 동시에 실행하기 때문에 출력 순서가 뒤죽박죽이 된다.


GCD 코드 출력 결과 예측하기

위의 예시와 비슷하게 다양한 경우의 GCD코드들을 작성해보았다.

그리고 어떻게 출력될지 예측했고, 그 정답 여부는 다음과 같다.

func example3() {
print("🔵START", terminator: " ")

// 1. serial sync => DeadLock이 생길 것 ✅
//        DispatchQueue.main.sync {
//            for item in 1...10 {
//                print(item, terminator: " ")
//            }
//        }

// 2. serial async => 맨 마지막에 실행될듯 ✅
DispatchQueue.main.async {
    for item in 11...20 {
        print(item, terminator: " ")
    }
}


// 3. concurrent sync => 출력순서는 serial sync와 같을것이고, main이 아무일도 안하기 때문에 잘 사용하지 않음 ✅
DispatchQueue.global().sync {
    for item in 21...30 {
        print(item, terminator: " ")
    }
}

// 4. 다른 스레드로 보낸 상태에서 concurrent sync => 순서가 섞여서 나올듯 ✅
// 이 숫자뭉치는 섞여서 나오지만(어느 시점에든 들어감), 31~40 순서는 유지됨
DispatchQueue.global().async {
    DispatchQueue.global().sync {
        for item in 31...40 {
            print(item, terminator: " ")
        }
    }
}

// 5. 이중 concurrent async => 숫자뭉치, 숫자뭉치 내 전부 섞여서 나올듯 ❌
// 숫자뭉치는 다른 숫자들 사이에 섞여서 나오지만, 41~50 사이의 숫자는 순서대로 나옴
DispatchQueue.global().async {
    DispatchQueue.global().async {
        for item in 41...50 {
            print(item, terminator: " ")
        }
    }
}

// 6. concurrent async => 숫자뭉치는 다른 숫자들 사이에 섞이고, 순서는 지켜질것 ✅
DispatchQueue.global().async {
    for item in 51...60 {
        print(item, terminator: " ")
    }
}

// 7. for문 두개를 감싼 concurrent async => 숫자뭉치는 다른 숫자들 사이에 섞이고, 순서(61~80)는 지켜질것 ✅
DispatchQueue.global().async {
    for item in 61...70 {
        print(item, terminator: " ")
    }

    for item in 71...80 {
        print(item, terminator: " ")
    }
}

// 8. concurrent async 안의 serial async => 91-100 숫자뭉치는 섞이지 않고 그대로 출력, 그러나 두 숫자뭉치 모두 그 위치 자체는 섞임 ❌
// 81-90은 순서는 지켜지고 위치가 섞인것은 맞지만, 91-100 뭉치는 순서는 유지, 그리고 위치는 항상 맨 마지막에 출력임
DispatchQueue.global().async {
    for item in 81...90 {
        print(item, terminator: " ")
    }

    DispatchQueue.main.async {
        for item in 91...100 {
            print(item, terminator: " ")
        }
    }
}

// 9. 이중 serial async => 이 두 숫자뭉치는 맨 마지막에 출력될것이고, 그중에서 101-110이 더 마지막에 출력될듯 ❌
// 일단 101~110이 맨 마지막에 출력되는건 맞지만, 111~120이 출력되고 그 다음에 91~100이 먼저 출력되고 101~110이 맨 마지막
// 111~120과 91~100의 순서는 어떻게 결정되지?
// 실행되면 이 두개의 반복문은 즉시 맨 뒤로감 -> 91-100반복문은 일단 다른 스레드로 갔다가 맨 뒤로 옴 -> 그중 101-110반목문은 맨 뒤로 감
// 그래서 111-120 -> 91-100 -> 101-110인듯
DispatchQueue.main.async {
    DispatchQueue.main.async {
        for item in 101...110 {
            print(item, terminator: " ")
        }
    }

    for item in 111...120 {
        print(item, terminator: " ")
    }
}

// attributes를 지정해주지 않는다면 기본적으로 Serial, qos도 지정할 수 있음
let customQueue = DispatchQueue(label: "serial async")

// 10. custom serial async 안의 serial sync => 맨 마지막에 실행 ✅
// 일단 커스텀 큐로 보내지고, 121-130은 다른 작업이 끝날때까지 기다릴것
customQueue.async {
    DispatchQueue.main.sync {
        for item in 121...130 {
            print(item, terminator: " ")
        }
    }
}

let customQueue2 = DispatchQueue(label: "concurrent async", attributes: .concurrent)

// 11. custom concurrent async 안의 serial sync => 얘도 아마 맨 마지막에 실행..? ✅
// 역시 일단 커스텀 큐로 보내지고, 131-140은 다른 작업이 끝날때까지 기다릴것
customQueue2.async {
    DispatchQueue.main.sync {
        for item in 131...140 {
            print(item, terminator: " ")
        }
    }
}

// 12. custom serial async안의 concurrent async => 151-160은 순서대로, 161-170은 순서는 지키되 위치는 무작위 ❌
// 151-170 전부 맨 마지막에 실행되고 순서도 지켜짐...
customQueue.async {
    for item in 151...160 {
        print(item, terminator: " ")
    }

    DispatchQueue.global().async {
        for item in 161...170 {
            print(item, terminator: " ")
        }
    }
}

// 13. custom concurrent async 안의 concurrent async => 두 숫자뭉치의 순서는 유지, 위치는 무작위 ❌
// 둘 다 순서 지켜서 차례대로
customQueue2.async {
    for item in 171...180 {
        print(item, terminator: " ")
    }

    DispatchQueue.global().async {
        for item in 181...190 {
            print(item, terminator: " ")
        }
    }
}

print("END🔴", terminator: " ")
}

 

동기/비동기 그리고 동시/직렬의 다양한 경우의수를 나타내보고 결과를 예측해보았다.

위의 코드를 실행한 출력 결과는 아래와 같다.

1차
2차
3차

 

 

결론적으로 총 13문제중에 5문제를 틀렸다 😭

틀린 이유는 이 아래에 추후 글을 수정하면서 작성해볼 예정이다.


5번의 경우 숫자뭉치 내의 순서가 무작위로 나올것이라고 예측했는데,

만약 GCD코드가  for문이 아니라 print문을 감싸고 있었다면 그랬을것이다.

DispatchQueue.global().async {
    for item in 41...50 {
        DispatchQueue.global().async {
        	print(item, terminator: " ")
        }
    }
}

=> 이렇게되면 41-50 숫자 하나하나가 Task가 되고 그 순서가 보장이 되지 않을 것이다.

다만, 문제에선 for문을 하나의 Task로 삼고있어서, 숫자뭉치 내에서는 순서대로 출력되는것이 맞다.