지난 포스트에 이어 비동기 프로그래밍을 알아보는 시간을 가져보자.
큐의 QoS(Quality of Service)
이전 시간 DispatchQueue를 다루는 과정에서 QoS를 설정할 수 있는 큐들이 존재했다. (global, custom 큐가 가능했다!)
QoS의 속성들에 대한 이야기를 하지 않은 것 같아 간단히 표로 정리하자면 아래와 같다.
서비스품질 수준 | 사용 상황 | 소요 시간 |
.userInteractive | 유저와 직접적 인터렉티브: UI업데이트 관련(직접X), 애니메이션, UI반 응관련 어떤 것이든 (사용자와 직접 상호 작용하는 작업에 권장. 작업이 빨리 처리되지 않으면 상황이 멈춘 것처럼 보일만한) |
거의 즉시 |
.userInitiated | 유저가 즉시 필요하긴 하지만, 비동기적으로 처리된 작업 (ex. 앱내에서 pdf파일을 여는 것과 같은, 로컬 데이터베이스 읽기) |
몇초 |
.default | 일반적인 작업 | - |
.utility | 보통 Progress Indicator와 함께 길게 실행되는 작업, 계산 (ex. IO, Networking, 지속적인 데이터 feeds) |
몇초에서 몇분 |
.background | 유저가 직접적으로 인지하지 않고(시간이 안 중요한) 작업 (ex.데이터 미리가져오기, 데이터베이스 유지보수, 원격 서버 동기화 및 백업 수행) |
몇분이상 (속도보다는 에너지효율성 중시) |
.unspecified | legacy API 지원 (스레드를 서비스 품질에서 제외시키는) |
- |
GCD 사용시 주의사항
- 지난번 이야기한 주의사항을 제외한 추가적인 사항들에 대해 알아보자
3. weak, strong 캡처의 주의 - 객체 내에서 비동기코드 사용시
- 일반적으로 비동기 코드는 객체 내에서 사용하는 경우가 많다.
- 이때, [waek self]를 캡처리스트 안에서 선언하지 않으면 강한 참조가 발생한다.
- 강한 참조의 경우 객체 서로를 가르키는 경우 메모리 누수가 발생할 수 있고
- 발생하지 않더라도 클로저의 수명주기가 길어지는 현상이 발생할 수 있다!
- 따라서 대부분의 경우, 캡처리스트 안에서 weak self로 선언하는 것을 권장한다.
우선 코드로 어떤 현상이 발생하는지 보자
class ViewController: UIViewController {
var name: String = "뷰컨트롤러"
func doSomething() {
DispatchQueue.global().async {
sleep(3);
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction() {
let vc = ViewController()
vc.doSomething()
}
localScopeFunction()
// 글로벌큐에서 출력하기: 뷰컨트롤러
// 뷰컨트롤러 메모리 해제
- 위의 코드에서 localScopeFunction은 진작에 종료되었음에도 불구하고, 3초뒤에 “뷰컨트롤러 메모리 해제”라는 문자열이 출력된다.
- 비동기처리 코드에서 3초동안 강한 참조를 통해 ViewController 객체를 참조하기 때문이다!
- 즉, 클로저가 강하게 캡처하기 때문에, 뷰컨트롤러의 RC가 유지되어, 뷰컨트롤러가 해제되었음에도, 3초뒤에 출력하고 난 후에 해제된다.
- 강한 순환 참조가 일어나진 않지만, 뷰컨트롤러 객체가 필요없음에도 오래 머문다.
이젠 약한 참조로 설정해보면
class ViewController: UIViewController {
var name: String = "뷰컨트롤러"
func doSomething() {
DispatchQueue.global().async { [weak self] in
guard let weakSelf = self else { return }
sleep(3);
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction() {
let vc = ViewController()
vc.doSomething()
}
localScopeFunction()
// 뷰컨트롤러 메모리 해제
- 해당 코드는 “뷰컨트롤러 메모리 해제”만을 출력한 뒤 종료된다!
- guard let 구문에서 만약 weakSelf가 nil이면, 즉 ViewController를 참조하는 RC가 0이면 return을 한다.
- 뷰컨트롤러가 사라지면 출력하는 일을 계속하지 않도록 할 수 있다.
- if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록 해준다.
4. 동기함수를 비동기적으로 동작하는 함수로 변형하는 방법
- 아래 코드와 같이 동기 함수 내부에서 DispatchQueue.global().async를 사용하면 된다.
func normal() {
print("프린트 시작")
sleep(3)
print("프린트 종료")
}
func abnormal(com: @escaping (Void) -> Void) {
DispatchQueue.global().async {
print("프린트 시작")
sleep(3)
print("프린트 종료")
com()
}
}

추가예시를 보자
import Foundation
// 동기 함수
func syncFunction() {
print("동기 함수 시작")
sleep(3) // 3초간 대기
print("동기 함수 종료")
}
// 비동기 함수
func asyncFunction(completion: @escaping () -> Void) {
print("비동기 함수 시작")
DispatchQueue.global().async {
// 백그라운드 스레드에서 실행될 작업
sleep(3) // 3초간 대기
print("비동기 함수 종료")
completion() // 완료를 알리는 클로저 호출
}
}
// 동기 함수 호출
syncFunction()
// 비동기 함수 호출
asyncFunction {
print("비동기 함수의 작업 완료")
}
// 비동기 함수 호출 이후에도 다른 작업을 계속할 수 있음
print("비동기 함수 호출 이후에도 다른 작업을 수행할 수 있음")
→ asyncFunction이 완료되려면 3초의 시간이 소요되는데, 3초동안 밑의 print문 내용을 출력하는 것을 알 수 있다.
5. 비동기 함수/메서드의 이해
- URLSession과 같은 클래스의 메서드는 이미 내부적으로 GCD를 이용해서, 비동기적으로 처리하는 메서드(함수)이다.
- 이와 같이, 일반적으로 대부분의 네트워킹 등 오래걸리는 API들은 따로 비동기처리를 하지 않아도 내부적으로 비동기적으로 구현되어 있다.
그렇다면…
→ 사용할 때 DispatchQueue로 굳이 감쌀 필요가 없겠죠?!

그렇다면…
→ 내부적으로 비동기처리가 되어있지 않은 것들은 DispatchQueue로 클로저를 보내서 명시적으로 비동기처리가 필요한 API들도 있겠죠?
간단히 둘의 예시를 적어보면
비동기 처리가 되어있는 메서드 예시
- DispatchQueue 비동기 메서드
async(execute:)
- 주어진 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행.
asyncAfter(deadline:execute:)
: 주어진 시간 후에 지정된 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행합니다.
- 네트워크 요청 관련 메서드:
URLSession
클래스의dataTask(with:completionHandler:)
- 비동기적으로 네트워크 요청을 수행하고 완료 핸들러를 제공.
- Alamofire, URLSession 등의 네트워킹 라이브러리에서 제공되는 비동기 메서드들.
- 비동기 콜백을 사용하는 메서드:
- 클로저를 매개변수로 받아서 비동기적으로 실행되는 메서드들.
비동기 처리가 되어있지 않은 메서드 예시
- 동기적인 파일 쓰기/읽기 메서드:
write(to:atomically:encoding:)
- 파일에 동기적으로 데이터를 쓴다.
contentsOfFile(_:usedEncoding:)
- 파일에서 동기적으로 데이터를 읽어옵니다.
- 동기적인 데이터베이스 쿼리 메서드:
- CoreData 또는 Realm과 같은 데이터베이스 라이브러리에서 제공되는 동기적인 데이터 조회 메서드들.
- 동기적인 연산 메서드:
- 계산량이 많은 동기적인 연산을 수행하는 메서드들.

다 외워야되나요?
사실, 애플 공식 문서에서 확인할 수 있다.
- 아래에 Asynchronicity and URL sessions 항목에 설명되어있다.
https://developer.apple.com/documentation/foundation/urlsession
Async/await의 도입 / 스위프트 5.5 이후
- javascript 문법의 개념을 도입했다고 생각하자 (사실 잘 모름)
- 비동기처리를 하는 함수 설계를 리턴형이 아닌 클로저 방식으로 하게 된다면 사실…
- 컴플리션 블럭이 실행된다.
- 이건 또..
- 다른 컴플리션 블럭을 실행하고…
- 또…
- 다른 컴플리션 블럭을 실행하고…
- 즉, 실제로 비동기처리를 하는 함수가 여러개 이어져있을 때, 비동기함수의 일이 종료되는 시점을 연결하기 위해, 끊임없는 콜백함수의 연결과 들여쓰기를 해야만 한다.

했다가는…

이렇게 코드를 짤수도 있다.. 즉, 가독성의 파괴와 실수를 연발할 수 있다는 것
이를 해결하기 위해 async, await 개념을 도입했다고 생각하자.
아래와 같이 함수에서 async(비동기 함수)로 정의, 리턴방식을 사용할 수 있다.
func loadWebResource(_ path: String) async throws -> Resource {
...
}
실제로 사용되는 방식은 다음과 같다.
- 실제 사용시, async로 정의된 함수는 await 키워드를 통해 리턴시점을 기다릴 수 있다.
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataProfile.txt")
let imageResource = try await loadWebResource("imageData.txt")
le timageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}

추가적인 내용은 아래의 글을 참고하도록 하자. (너무 좋은 자료인듯)
https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f
동시성 프로그래밍과 관련된 문제점
오호 그럼 간단하게… 시간이 걸리는 작업에 대해선 비동기처리를 하는 함수를 만들고, 그냥 사용하면 되려나?
당연히 이에 대해서도 문제점이 발생한다 🥲 .
-> 바로 “Race Condition”, “Deadlock” 문제이다. 우선 Swift 코드로 확인하기 전에 아래의 내용을 확인해보자.
앞선 내용에서 Thread의 장점으로는…
- 같은 프로세스 내에 있으면 address space를 공유한다. (코드, 데이터, 힙)
- process를 공유하는 것보다는 훨씬 간편하게 데이터 자원들을 공유할 수 있다.
이러한 상황에서 아래의 코드를 보자.
void* do_loop1(void *data)
{
int i;
int *loop = (int *)data;
for (i = 0; i < *loop; i++)
ncount++;
return NULL;
}
void* do_loop2(void *data)
{
int i;
int *loop = (int *)data;
for (i = 0; i < *loop; i++)
ncount--;
return NULL;
}
volatile int ncount; // 0으로 초기화
int main(int argc, char *argv[])
{
void *thread_result;
int status, loop;
pthread_t thread_id[2];
ncount = 0;
loop = atoi(argv[1]);
status = pthread_create(&thread_id[0], NULL, do_loop1, &loop);
status = pthread_create(&thread_id[1], NULL, do_loop2, &loop);
/* FIXME: no sanity check */
pthread_join(thread_id[0], &thread_result);
pthread_join(thread_id[1], &thread_result);
printf("counter = %d", ncount);
return 0;
}
→ 간단하게 스레드를 2개 만들어서, 각각의 스레드에서 전역변수를 증가시키고, 감소시키는 과정이다.
→ 그렇다면 결과는 어떻게 될까?
일반적으로 우리는 계속 증가, 감소를 반복하니 0이 찍힐 거라고 생각할 거다.
하지만..
> gcc thrd4.c –o thrd4 -lpthread
> ./thrd4 10
counter = 0
> ./thrd4 1000
counter = 0
> ./thrd4 1000000
counter = 0
> ./thrd4 1000000000
counter = 199018 // Race Condition이 발생!
> ./thrd4 10000000000
counter = 0
> ./thrd4 10000000000
counter = 1824322 // Race Condition이 발생!

엥 이게 뭐시람… 우리가 처음 예상한 것처럼 값이 찍히지 않는다.
우선 간단하게 이러한 상황을 “Race Condition”이 발생했다고 한다.
간단하게 Race Condition이란
- 서로 다른 Context가 동일한 자료를 접근할 때 일관성이 보장되지 않는 문제
- 다른 말론, 멀티 쓰레드의 환경에서, 같은 시점에 여러개의 쓰레드에서 하나의 메모리에 동시접근 하는 문제이다.
Context란 앞선 강의에 정의한 바가 있다!
그렇다면 서로 다른 Context가 동일한 자료를 접근할 때 일관성이 보장되도록 하는 것이 중요한데.. 이를 강의 내용에선 Thread-Safe 처리를 해주는 거라고 했다.
[알아두면 좋은 용어]
- Critical Section
- thread들이 공유된 자료구조를 접근하는 코드 군을 의미한다.
- 어떤 공유되는 변수, 구조체, 자료구조(트리, 링크드리스트, 해쉬맵) 에 여러 개의 thread, process가 동시에 접근하면 안됨. 하나의 thread만이 실행되어야 함.
- 코드가 한 줄일수도, 여러 줄일 수도 있음.
- Mutual Exclusion
- 한 놈만 들어갈 수 있다.
- 못 들어가면 , 줄을 세움.
- 누군가 critical section에 들어가 있다면, 어떤 것에서 실행이 되어 있다는 것이므로, 줄을 세움.
- 그리고 cpu 자원을 양보 → 너가 먼저해(!) → 나중에 자리가 난다면 cpu 자원을 받아 실행함.
- 병렬화를 하려고 여러 개를 만들었는데, 줄을 서서 기다리다 보니 효율성이 떨어짐
Thread Safe?
이게 뭐지..? 아래 사이트에서 참고했다.
https://medium.com/@ranga.c222/thread-safety-in-swift-495a88d24d7d
THREAD SAFETY IN SWIFT
Thread safety refers to a programming concept where data or resources are accessed and modified in a way that ensures correct behavior and…
medium.com
https://siwon-code.tistory.com/34
- Thread safety refers to a programming concept where data or resources are accessed and modified in a way that ensures correct behavior and prevents conflicts when multiple threads (concurrent execution units) are working with the same data simultaneously
- 간단하게 말하자면 여러 스레드가 동일한 데이터를 동시에 작업할 때 올바른 동작을 보장하고 충돌을 방지하는 방식으로 코드를 짜는 거다.
- Swift에선 다양한 방법으로 Thread-Safe로 설정할 수 있다.
- 4가지 방법들에 대해 살펴본다.
Thread-Safe 하게 하는 방법
DispatchSemaphore
→ semaphore에 대해서 아직 학습을 안했기 때문에, 간단하게 이야기하자면 자원(Context)에 접근 가능한 스레드를 수적으로 제한하는 방식이다.
- 이때, 1개의 스레드만 접근할 수 있게 한다면, Race Condition이 발생할 수가 없겠죠?
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
for _ in 1...10 {
semaphore.wait()
...
semaphore.signal()
}
}
- signal에 대해서도 다루지 않았기 때문에 매우 간략히 설명을 하자면, 1개의 쓰레드만 접근 가능하도록 DispatchSemaphore 인스턴스를 만들어주고, 접근 전에 wait( )을 호출해 다른 스레드가 기다리게 한다.
- 만약 접근중인 스레드의 작업이 끝나면 signal( )을 보내 다른 스레드에게 방을 뺐다고 알린다.
Custom Serial Queue
DispatchQueue.global().async {
for _ in 1...10 {
serialQueue.sync {
...
}
}
}
- 간단히 보면 global스레드에서 직렬큐로 작업을 보낸다.
- 직렬큐로 보내면 들어온 순서대로 작업을 완료하겠죠?
NSLock

- 동일한 애플리케이션 내에서 여러 실행 스레드의 작업을 조정하는 객체
- POSIX 스레드를 이용하여 lock을 사용한다는데, POSIX에 대해서도 아직 다룬 적이 없으니 잠깐 묻혀두자.
- 우선 간단하게, lock, unlock메서드를 이용해 한 자원에 하나의 스레드가 접근할 수 있도록 한다.
- 1인용 놀이기구에 사람이 타면 다음 사람은 현재 사람이 나올 때까지 기다리는 것과 같다.
DispatchQueue.global().async {
for _ in 1...2 {
lock.lock()
...
lock.unlock()
}
}
- 주의할 점은 반드시 unlock을 호출해야한다는 거다!
- 만약 1인용 놀이기구에 탄 사람이 계속해서 나오지 않으면 다음 사람은 영원히 못타는 상황이 발생하는 것과 같다.
Dispatch Barrier
https://developer.apple.com/documentation/dispatch/dispatchworkitemflags/1780674-barrier
- 동시큐에서 직렬큐를 사용하는 것과 같이..
- concurrent queue가 사용하고 있는 여러 개의 thread 중에서, barrier block의 실행을 위한 하나의 스레드만 제외하고는 모든 스레드 사용을 막는 것
- async의 flags 파라미터를 .barrier로 설정하면, 클로저 내부의 코드는 순차적으로 실행된다.
var barrierQueue = DispatchQueue(
label: "barrierQueue",
attributes: .concurrent
)
barrierQueue.async(flags: .barrier) {
for _ in 1...2 {
let book = books.removeFirst()
print("손님1: \(book)을 샀습니다.")
}
}
아니 그러면 Race Condition이 발생할 수 있는 부분에 대해서 Thread-Safe하게 다 처리하면 되는 거 아닌가?

바로 “DeadLock”상황을 고려하면서 해야 한다.
DeadLock을 가볍게 받아들이고 알아보고 싶다면…
https://gwpaeng.tistory.com/125
DeadLock
이는 “2개이상의 쓰레드가 서로 배타적인 메모리의 사용으로 인해(서로 잠그고 점유하려고 하면서) 메서드의 작업이 종료도 못하고 일의 진행이 멈춰버리는 상태” 를 의미한다.
내가 생각한 간단한 비유는 다음과 같다.
- 내가 자원을 가지고 있는데, (자기가 lock한 상태), 그 채로 남이 갔고 있는 자원을 가지고 있는 경우
- 그 남은, 그 자원을 가지고 있는 채로, 내가 가지고 있는 자원을 원하는 경우
- 서로 상대방이 가지고 있는 자원을 원하는데, 각자가 서로의 자원을 홀딩하고 있는 경우
- 예로, 우리 집에 오빠, 여동생이 있는데, 오빠가 자동차를 쥔 채로, 여동생이 가지고 있는 인형을 달라하는 경우, 여동생은 그 역순
- 해당 상황은 해결할 수 없음.

- multiple한 자원을 공유하기 때문에, circular한 wait이 발생할 수 있음 → deadlock 상황
- 이를 해결할 수 있는 방안은 뭐가 있을까?
- 젓가락을 하나 더 둔다
- 철학자들이 젓가락을 드는 방향을 번갈아가면서 바꾼다…
- 등등이 있지만 근본적으로 모든 문제를 해결할 수는 없다.
아니 그러면 어떻게 해결하는데?
- 학교 수업 시간에도 그렇고, 여러 블로그도 그렇고…
- “잘 설계를 해서 이러한 상황이 발생하지 않도록 한다.” 또는 “직렬 큐로 보낸다” ?!
- 사실 전자로 해결해야 하는 경우가 다수다.
처음으로 신경써서 정리를 하며 글을 작성한 것 같아 뿌듯하다.
출처
https://medium.com/@ranga.c222/thread-safety-in-swift-495a88d24d7d
https://siwon-code.tistory.com/34
https://developer.apple.com/documentation/foundation/nslock
https://gwpaeng.tistory.com/125
https://sujinnaljin.medium.com/ios-차근차근-시작하는-gcd-15-3fef697f9aab
'iOS > Swift' 카테고리의 다른 글
Result Type에 대한 이해 (1) | 2024.02.19 |
---|---|
제네릭(Generics) (0) | 2024.02.18 |
비동기 프로그래밍(About Asynchronous) [1] (0) | 2024.02.16 |
Swift No.26 (0) | 2024.02.08 |
Swift No.25 (0) | 2024.02.03 |
지난 포스트에 이어 비동기 프로그래밍을 알아보는 시간을 가져보자.
큐의 QoS(Quality of Service)
이전 시간 DispatchQueue를 다루는 과정에서 QoS를 설정할 수 있는 큐들이 존재했다. (global, custom 큐가 가능했다!)
QoS의 속성들에 대한 이야기를 하지 않은 것 같아 간단히 표로 정리하자면 아래와 같다.
서비스품질 수준 | 사용 상황 | 소요 시간 |
.userInteractive | 유저와 직접적 인터렉티브: UI업데이트 관련(직접X), 애니메이션, UI반 응관련 어떤 것이든 (사용자와 직접 상호 작용하는 작업에 권장. 작업이 빨리 처리되지 않으면 상황이 멈춘 것처럼 보일만한) |
거의 즉시 |
.userInitiated | 유저가 즉시 필요하긴 하지만, 비동기적으로 처리된 작업 (ex. 앱내에서 pdf파일을 여는 것과 같은, 로컬 데이터베이스 읽기) |
몇초 |
.default | 일반적인 작업 | - |
.utility | 보통 Progress Indicator와 함께 길게 실행되는 작업, 계산 (ex. IO, Networking, 지속적인 데이터 feeds) |
몇초에서 몇분 |
.background | 유저가 직접적으로 인지하지 않고(시간이 안 중요한) 작업 (ex.데이터 미리가져오기, 데이터베이스 유지보수, 원격 서버 동기화 및 백업 수행) |
몇분이상 (속도보다는 에너지효율성 중시) |
.unspecified | legacy API 지원 (스레드를 서비스 품질에서 제외시키는) |
- |
GCD 사용시 주의사항
- 지난번 이야기한 주의사항을 제외한 추가적인 사항들에 대해 알아보자
3. weak, strong 캡처의 주의 - 객체 내에서 비동기코드 사용시
- 일반적으로 비동기 코드는 객체 내에서 사용하는 경우가 많다.
- 이때, [waek self]를 캡처리스트 안에서 선언하지 않으면 강한 참조가 발생한다.
- 강한 참조의 경우 객체 서로를 가르키는 경우 메모리 누수가 발생할 수 있고
- 발생하지 않더라도 클로저의 수명주기가 길어지는 현상이 발생할 수 있다!
- 따라서 대부분의 경우, 캡처리스트 안에서 weak self로 선언하는 것을 권장한다.
우선 코드로 어떤 현상이 발생하는지 보자
class ViewController: UIViewController {
var name: String = "뷰컨트롤러"
func doSomething() {
DispatchQueue.global().async {
sleep(3);
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction() {
let vc = ViewController()
vc.doSomething()
}
localScopeFunction()
// 글로벌큐에서 출력하기: 뷰컨트롤러
// 뷰컨트롤러 메모리 해제
- 위의 코드에서 localScopeFunction은 진작에 종료되었음에도 불구하고, 3초뒤에 “뷰컨트롤러 메모리 해제”라는 문자열이 출력된다.
- 비동기처리 코드에서 3초동안 강한 참조를 통해 ViewController 객체를 참조하기 때문이다!
- 즉, 클로저가 강하게 캡처하기 때문에, 뷰컨트롤러의 RC가 유지되어, 뷰컨트롤러가 해제되었음에도, 3초뒤에 출력하고 난 후에 해제된다.
- 강한 순환 참조가 일어나진 않지만, 뷰컨트롤러 객체가 필요없음에도 오래 머문다.
이젠 약한 참조로 설정해보면
class ViewController: UIViewController {
var name: String = "뷰컨트롤러"
func doSomething() {
DispatchQueue.global().async { [weak self] in
guard let weakSelf = self else { return }
sleep(3);
print("글로벌큐에서 출력하기: \(self.name)")
}
}
deinit {
print("\(name) 메모리 해제")
}
}
func localScopeFunction() {
let vc = ViewController()
vc.doSomething()
}
localScopeFunction()
// 뷰컨트롤러 메모리 해제
- 해당 코드는 “뷰컨트롤러 메모리 해제”만을 출력한 뒤 종료된다!
- guard let 구문에서 만약 weakSelf가 nil이면, 즉 ViewController를 참조하는 RC가 0이면 return을 한다.
- 뷰컨트롤러가 사라지면 출력하는 일을 계속하지 않도록 할 수 있다.
- if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록 해준다.
4. 동기함수를 비동기적으로 동작하는 함수로 변형하는 방법
- 아래 코드와 같이 동기 함수 내부에서 DispatchQueue.global().async를 사용하면 된다.
func normal() {
print("프린트 시작")
sleep(3)
print("프린트 종료")
}
func abnormal(com: @escaping (Void) -> Void) {
DispatchQueue.global().async {
print("프린트 시작")
sleep(3)
print("프린트 종료")
com()
}
}

추가예시를 보자
import Foundation
// 동기 함수
func syncFunction() {
print("동기 함수 시작")
sleep(3) // 3초간 대기
print("동기 함수 종료")
}
// 비동기 함수
func asyncFunction(completion: @escaping () -> Void) {
print("비동기 함수 시작")
DispatchQueue.global().async {
// 백그라운드 스레드에서 실행될 작업
sleep(3) // 3초간 대기
print("비동기 함수 종료")
completion() // 완료를 알리는 클로저 호출
}
}
// 동기 함수 호출
syncFunction()
// 비동기 함수 호출
asyncFunction {
print("비동기 함수의 작업 완료")
}
// 비동기 함수 호출 이후에도 다른 작업을 계속할 수 있음
print("비동기 함수 호출 이후에도 다른 작업을 수행할 수 있음")
→ asyncFunction이 완료되려면 3초의 시간이 소요되는데, 3초동안 밑의 print문 내용을 출력하는 것을 알 수 있다.
5. 비동기 함수/메서드의 이해
- URLSession과 같은 클래스의 메서드는 이미 내부적으로 GCD를 이용해서, 비동기적으로 처리하는 메서드(함수)이다.
- 이와 같이, 일반적으로 대부분의 네트워킹 등 오래걸리는 API들은 따로 비동기처리를 하지 않아도 내부적으로 비동기적으로 구현되어 있다.
그렇다면…
→ 사용할 때 DispatchQueue로 굳이 감쌀 필요가 없겠죠?!

그렇다면…
→ 내부적으로 비동기처리가 되어있지 않은 것들은 DispatchQueue로 클로저를 보내서 명시적으로 비동기처리가 필요한 API들도 있겠죠?
간단히 둘의 예시를 적어보면
비동기 처리가 되어있는 메서드 예시
- DispatchQueue 비동기 메서드
async(execute:)
- 주어진 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행.
asyncAfter(deadline:execute:)
: 주어진 시간 후에 지정된 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행합니다.
- 네트워크 요청 관련 메서드:
URLSession
클래스의dataTask(with:completionHandler:)
- 비동기적으로 네트워크 요청을 수행하고 완료 핸들러를 제공.
- Alamofire, URLSession 등의 네트워킹 라이브러리에서 제공되는 비동기 메서드들.
- 비동기 콜백을 사용하는 메서드:
- 클로저를 매개변수로 받아서 비동기적으로 실행되는 메서드들.
비동기 처리가 되어있지 않은 메서드 예시
- 동기적인 파일 쓰기/읽기 메서드:
write(to:atomically:encoding:)
- 파일에 동기적으로 데이터를 쓴다.
contentsOfFile(_:usedEncoding:)
- 파일에서 동기적으로 데이터를 읽어옵니다.
- 동기적인 데이터베이스 쿼리 메서드:
- CoreData 또는 Realm과 같은 데이터베이스 라이브러리에서 제공되는 동기적인 데이터 조회 메서드들.
- 동기적인 연산 메서드:
- 계산량이 많은 동기적인 연산을 수행하는 메서드들.

다 외워야되나요?
사실, 애플 공식 문서에서 확인할 수 있다.
- 아래에 Asynchronicity and URL sessions 항목에 설명되어있다.
https://developer.apple.com/documentation/foundation/urlsession
Async/await의 도입 / 스위프트 5.5 이후
- javascript 문법의 개념을 도입했다고 생각하자 (사실 잘 모름)
- 비동기처리를 하는 함수 설계를 리턴형이 아닌 클로저 방식으로 하게 된다면 사실…
- 컴플리션 블럭이 실행된다.
- 이건 또..
- 다른 컴플리션 블럭을 실행하고…
- 또…
- 다른 컴플리션 블럭을 실행하고…
- 즉, 실제로 비동기처리를 하는 함수가 여러개 이어져있을 때, 비동기함수의 일이 종료되는 시점을 연결하기 위해, 끊임없는 콜백함수의 연결과 들여쓰기를 해야만 한다.

했다가는…

이렇게 코드를 짤수도 있다.. 즉, 가독성의 파괴와 실수를 연발할 수 있다는 것
이를 해결하기 위해 async, await 개념을 도입했다고 생각하자.
아래와 같이 함수에서 async(비동기 함수)로 정의, 리턴방식을 사용할 수 있다.
func loadWebResource(_ path: String) async throws -> Resource {
...
}
실제로 사용되는 방식은 다음과 같다.
- 실제 사용시, async로 정의된 함수는 await 키워드를 통해 리턴시점을 기다릴 수 있다.
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataProfile.txt")
let imageResource = try await loadWebResource("imageData.txt")
le timageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}

추가적인 내용은 아래의 글을 참고하도록 하자. (너무 좋은 자료인듯)
https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f
동시성 프로그래밍과 관련된 문제점
오호 그럼 간단하게… 시간이 걸리는 작업에 대해선 비동기처리를 하는 함수를 만들고, 그냥 사용하면 되려나?
당연히 이에 대해서도 문제점이 발생한다 🥲 .
-> 바로 “Race Condition”, “Deadlock” 문제이다. 우선 Swift 코드로 확인하기 전에 아래의 내용을 확인해보자.
앞선 내용에서 Thread의 장점으로는…
- 같은 프로세스 내에 있으면 address space를 공유한다. (코드, 데이터, 힙)
- process를 공유하는 것보다는 훨씬 간편하게 데이터 자원들을 공유할 수 있다.
이러한 상황에서 아래의 코드를 보자.
void* do_loop1(void *data)
{
int i;
int *loop = (int *)data;
for (i = 0; i < *loop; i++)
ncount++;
return NULL;
}
void* do_loop2(void *data)
{
int i;
int *loop = (int *)data;
for (i = 0; i < *loop; i++)
ncount--;
return NULL;
}
volatile int ncount; // 0으로 초기화
int main(int argc, char *argv[])
{
void *thread_result;
int status, loop;
pthread_t thread_id[2];
ncount = 0;
loop = atoi(argv[1]);
status = pthread_create(&thread_id[0], NULL, do_loop1, &loop);
status = pthread_create(&thread_id[1], NULL, do_loop2, &loop);
/* FIXME: no sanity check */
pthread_join(thread_id[0], &thread_result);
pthread_join(thread_id[1], &thread_result);
printf("counter = %d", ncount);
return 0;
}
→ 간단하게 스레드를 2개 만들어서, 각각의 스레드에서 전역변수를 증가시키고, 감소시키는 과정이다.
→ 그렇다면 결과는 어떻게 될까?
일반적으로 우리는 계속 증가, 감소를 반복하니 0이 찍힐 거라고 생각할 거다.
하지만..
> gcc thrd4.c –o thrd4 -lpthread
> ./thrd4 10
counter = 0
> ./thrd4 1000
counter = 0
> ./thrd4 1000000
counter = 0
> ./thrd4 1000000000
counter = 199018 // Race Condition이 발생!
> ./thrd4 10000000000
counter = 0
> ./thrd4 10000000000
counter = 1824322 // Race Condition이 발생!

엥 이게 뭐시람… 우리가 처음 예상한 것처럼 값이 찍히지 않는다.
우선 간단하게 이러한 상황을 “Race Condition”이 발생했다고 한다.
간단하게 Race Condition이란
- 서로 다른 Context가 동일한 자료를 접근할 때 일관성이 보장되지 않는 문제
- 다른 말론, 멀티 쓰레드의 환경에서, 같은 시점에 여러개의 쓰레드에서 하나의 메모리에 동시접근 하는 문제이다.
Context란 앞선 강의에 정의한 바가 있다!
그렇다면 서로 다른 Context가 동일한 자료를 접근할 때 일관성이 보장되도록 하는 것이 중요한데.. 이를 강의 내용에선 Thread-Safe 처리를 해주는 거라고 했다.
[알아두면 좋은 용어]
- Critical Section
- thread들이 공유된 자료구조를 접근하는 코드 군을 의미한다.
- 어떤 공유되는 변수, 구조체, 자료구조(트리, 링크드리스트, 해쉬맵) 에 여러 개의 thread, process가 동시에 접근하면 안됨. 하나의 thread만이 실행되어야 함.
- 코드가 한 줄일수도, 여러 줄일 수도 있음.
- Mutual Exclusion
- 한 놈만 들어갈 수 있다.
- 못 들어가면 , 줄을 세움.
- 누군가 critical section에 들어가 있다면, 어떤 것에서 실행이 되어 있다는 것이므로, 줄을 세움.
- 그리고 cpu 자원을 양보 → 너가 먼저해(!) → 나중에 자리가 난다면 cpu 자원을 받아 실행함.
- 병렬화를 하려고 여러 개를 만들었는데, 줄을 서서 기다리다 보니 효율성이 떨어짐
Thread Safe?
이게 뭐지..? 아래 사이트에서 참고했다.
https://medium.com/@ranga.c222/thread-safety-in-swift-495a88d24d7d
THREAD SAFETY IN SWIFT
Thread safety refers to a programming concept where data or resources are accessed and modified in a way that ensures correct behavior and…
medium.com
https://siwon-code.tistory.com/34
- Thread safety refers to a programming concept where data or resources are accessed and modified in a way that ensures correct behavior and prevents conflicts when multiple threads (concurrent execution units) are working with the same data simultaneously
- 간단하게 말하자면 여러 스레드가 동일한 데이터를 동시에 작업할 때 올바른 동작을 보장하고 충돌을 방지하는 방식으로 코드를 짜는 거다.
- Swift에선 다양한 방법으로 Thread-Safe로 설정할 수 있다.
- 4가지 방법들에 대해 살펴본다.
Thread-Safe 하게 하는 방법
DispatchSemaphore
→ semaphore에 대해서 아직 학습을 안했기 때문에, 간단하게 이야기하자면 자원(Context)에 접근 가능한 스레드를 수적으로 제한하는 방식이다.
- 이때, 1개의 스레드만 접근할 수 있게 한다면, Race Condition이 발생할 수가 없겠죠?
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
for _ in 1...10 {
semaphore.wait()
...
semaphore.signal()
}
}
- signal에 대해서도 다루지 않았기 때문에 매우 간략히 설명을 하자면, 1개의 쓰레드만 접근 가능하도록 DispatchSemaphore 인스턴스를 만들어주고, 접근 전에 wait( )을 호출해 다른 스레드가 기다리게 한다.
- 만약 접근중인 스레드의 작업이 끝나면 signal( )을 보내 다른 스레드에게 방을 뺐다고 알린다.
Custom Serial Queue
DispatchQueue.global().async {
for _ in 1...10 {
serialQueue.sync {
...
}
}
}
- 간단히 보면 global스레드에서 직렬큐로 작업을 보낸다.
- 직렬큐로 보내면 들어온 순서대로 작업을 완료하겠죠?
NSLock

- 동일한 애플리케이션 내에서 여러 실행 스레드의 작업을 조정하는 객체
- POSIX 스레드를 이용하여 lock을 사용한다는데, POSIX에 대해서도 아직 다룬 적이 없으니 잠깐 묻혀두자.
- 우선 간단하게, lock, unlock메서드를 이용해 한 자원에 하나의 스레드가 접근할 수 있도록 한다.
- 1인용 놀이기구에 사람이 타면 다음 사람은 현재 사람이 나올 때까지 기다리는 것과 같다.
DispatchQueue.global().async {
for _ in 1...2 {
lock.lock()
...
lock.unlock()
}
}
- 주의할 점은 반드시 unlock을 호출해야한다는 거다!
- 만약 1인용 놀이기구에 탄 사람이 계속해서 나오지 않으면 다음 사람은 영원히 못타는 상황이 발생하는 것과 같다.
Dispatch Barrier
https://developer.apple.com/documentation/dispatch/dispatchworkitemflags/1780674-barrier
- 동시큐에서 직렬큐를 사용하는 것과 같이..
- concurrent queue가 사용하고 있는 여러 개의 thread 중에서, barrier block의 실행을 위한 하나의 스레드만 제외하고는 모든 스레드 사용을 막는 것
- async의 flags 파라미터를 .barrier로 설정하면, 클로저 내부의 코드는 순차적으로 실행된다.
var barrierQueue = DispatchQueue(
label: "barrierQueue",
attributes: .concurrent
)
barrierQueue.async(flags: .barrier) {
for _ in 1...2 {
let book = books.removeFirst()
print("손님1: \(book)을 샀습니다.")
}
}
아니 그러면 Race Condition이 발생할 수 있는 부분에 대해서 Thread-Safe하게 다 처리하면 되는 거 아닌가?

바로 “DeadLock”상황을 고려하면서 해야 한다.
DeadLock을 가볍게 받아들이고 알아보고 싶다면…
https://gwpaeng.tistory.com/125
DeadLock
이는 “2개이상의 쓰레드가 서로 배타적인 메모리의 사용으로 인해(서로 잠그고 점유하려고 하면서) 메서드의 작업이 종료도 못하고 일의 진행이 멈춰버리는 상태” 를 의미한다.
내가 생각한 간단한 비유는 다음과 같다.
- 내가 자원을 가지고 있는데, (자기가 lock한 상태), 그 채로 남이 갔고 있는 자원을 가지고 있는 경우
- 그 남은, 그 자원을 가지고 있는 채로, 내가 가지고 있는 자원을 원하는 경우
- 서로 상대방이 가지고 있는 자원을 원하는데, 각자가 서로의 자원을 홀딩하고 있는 경우
- 예로, 우리 집에 오빠, 여동생이 있는데, 오빠가 자동차를 쥔 채로, 여동생이 가지고 있는 인형을 달라하는 경우, 여동생은 그 역순
- 해당 상황은 해결할 수 없음.

- multiple한 자원을 공유하기 때문에, circular한 wait이 발생할 수 있음 → deadlock 상황
- 이를 해결할 수 있는 방안은 뭐가 있을까?
- 젓가락을 하나 더 둔다
- 철학자들이 젓가락을 드는 방향을 번갈아가면서 바꾼다…
- 등등이 있지만 근본적으로 모든 문제를 해결할 수는 없다.
아니 그러면 어떻게 해결하는데?
- 학교 수업 시간에도 그렇고, 여러 블로그도 그렇고…
- “잘 설계를 해서 이러한 상황이 발생하지 않도록 한다.” 또는 “직렬 큐로 보낸다” ?!
- 사실 전자로 해결해야 하는 경우가 다수다.
처음으로 신경써서 정리를 하며 글을 작성한 것 같아 뿌듯하다.
출처
https://medium.com/@ranga.c222/thread-safety-in-swift-495a88d24d7d
https://siwon-code.tistory.com/34
https://developer.apple.com/documentation/foundation/nslock
https://gwpaeng.tistory.com/125
https://sujinnaljin.medium.com/ios-차근차근-시작하는-gcd-15-3fef697f9aab
'iOS > Swift' 카테고리의 다른 글
Result Type에 대한 이해 (1) | 2024.02.19 |
---|---|
제네릭(Generics) (0) | 2024.02.18 |
비동기 프로그래밍(About Asynchronous) [1] (0) | 2024.02.16 |
Swift No.26 (0) | 2024.02.08 |
Swift No.25 (0) | 2024.02.03 |