비동기 프로그래밍(About Asynchronous) [2]

2024. 2. 17. 13:36· iOS/Swift
목차
  1. 큐의 QoS(Quality of Service)
  2. GCD 사용시 주의사항
  3. Async/await의 도입 / 스위프트 5.5 이후
  4. 동시성 프로그래밍과 관련된 문제점
  5. 하지만..
  6. Thread Safe?
  7. Thread-Safe 하게 하는 방법
  8. DeadLock
  9. 출처

지난 포스트에 이어 비동기 프로그래밍을 알아보는 시간을 가져보자.


큐의 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로 굳이 감쌀 필요가 없겠죠?!

출처: https://overy.tistory.com/entry/개구리짤-개꿀-짤방

 

 그렇다면…

→ 내부적으로 비동기처리가 되어있지 않은 것들은 DispatchQueue로 클로저를 보내서 명시적으로 비동기처리가 필요한 API들도 있겠죠?

 

간단히 둘의 예시를 적어보면

 

비동기 처리가 되어있는 메서드 예시

  1. DispatchQueue 비동기 메서드
    • async(execute:)
      • 주어진 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행.
    • asyncAfter(deadline:execute:): 주어진 시간 후에 지정된 클로저를 지정된 Dispatch Queue에서 비동기적으로 실행합니다.
  2. 네트워크 요청 관련 메서드:
    • URLSession 클래스의 dataTask(with:completionHandler:)
      • 비동기적으로 네트워크 요청을 수행하고 완료 핸들러를 제공.
    • Alamofire, URLSession 등의 네트워킹 라이브러리에서 제공되는 비동기 메서드들.
  3. 비동기 콜백을 사용하는 메서드:
    • 클로저를 매개변수로 받아서 비동기적으로 실행되는 메서드들.

비동기 처리가 되어있지 않은 메서드 예시

  1. 동기적인 파일 쓰기/읽기 메서드:
    • write(to:atomically:encoding:)
      • 파일에 동기적으로 데이터를 쓴다.
    • contentsOfFile(_:usedEncoding:)
      • 파일에서 동기적으로 데이터를 읽어옵니다.
  2. 동기적인 데이터베이스 쿼리 메서드:
    • CoreData 또는 Realm과 같은 데이터베이스 라이브러리에서 제공되는 동기적인 데이터 조회 메서드들.
  3. 동기적인 연산 메서드:
    • 계산량이 많은 동기적인 연산을 수행하는 메서드들.

 

다 외워야되나요?

사실, 애플 공식 문서에서 확인할 수 있다.

  • 아래에 Asynchronicity and URL sessions 항목에 설명되어있다.

https://developer.apple.com/documentation/foundation/urlsession


Async/await의 도입 / 스위프트 5.5 이후

  • javascript 문법의 개념을 도입했다고 생각하자 (사실 잘 모름)
  • 비동기처리를 하는 함수 설계를 리턴형이 아닌 클로저 방식으로 하게 된다면 사실…
    • 컴플리션 블럭이 실행된다.
    • 이건 또..
      • 다른 컴플리션 블럭을 실행하고…
        • 또…
  • 즉, 실제로 비동기처리를 하는 함수가 여러개 이어져있을 때, 비동기함수의 일이 종료되는 시점을 연결하기 위해, 끊임없는 콜백함수의 연결과 들여쓰기를 해야만 한다.

 

뭐 일단 되기만 하면 되지!

 

했다가는…

출처: https://levelup.gitconnected.com/escape-the-pyramid-of-doom-c58edd326225

 

이렇게 코드를 짤수도 있다.. 즉, 가독성의 파괴와 실수를 연발할 수 있다는 것

이를 해결하기 위해 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
}

 

기존, async/await 코드의 차이(출처: 앨런 iOS 문법 강의자료)

 

 

추가적인 내용은 아래의 글을 참고하도록 하자. (너무 좋은 자료인듯)

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
  1. 큐의 QoS(Quality of Service)
  2. GCD 사용시 주의사항
  3. Async/await의 도입 / 스위프트 5.5 이후
  4. 동시성 프로그래밍과 관련된 문제점
  5. 하지만..
  6. Thread Safe?
  7. Thread-Safe 하게 하는 방법
  8. DeadLock
  9. 출처
'iOS/Swift' 카테고리의 다른 글
  • Result Type에 대한 이해
  • 제네릭(Generics)
  • 비동기 프로그래밍(About Asynchronous) [1]
  • Swift No.26
ParkSeongGeun
ParkSeongGeun
- 2000.08.01 - KU CSE 20
반응형
ParkSeongGeun
Foden's Blog
ParkSeongGeun
전체
오늘
어제
  • 분류 전체보기 (76)
    • iOS (49)
      • Swift (33)
      • SwiftUI (0)
      • UIKit (7)
    • Project (0)
      • Google Solution Challenge (0)
    • PS (3)
    • University (10)
      • 자료구조 (7)
      • 알고리즘 (0)
      • 운영체제 (2)
      • 데이터베이스 (1)
    • 일상 (6)
    • 백엔드 (7)
      • Spring&SpringBoot (7)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • Swift #iOS #UIKit
  • Limited Direct Execution
  • 의존성
  • 컴퓨터공학과
  • 운영체제
  • 네이버부스트캠프
  • MVVM
  • 데이터베이스 #수업 정리
  • Swift #iOS #Date
  • iOS #Swift
  • 어케해
  • KUIT
  • UIkit
  • ios
  • 제주도 #여행
  • iOS #Swift #비동기 #동기 #스레드
  • Swift #iOS #Result Type
  • 네부캠
  • Swift #iOS
  • Swift
  • Swift #iOS #비동기 프로그래밍
  • ViewController
  • 여행 #제주도 #나혼자만 여행
  • 선언형
  • 의존성 주입
  • 자료구조
  • input/output
  • UIKit #iOS
  • 코딩
  • #Delegate Pattern

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
ParkSeongGeun
비동기 프로그래밍(About Asynchronous) [2]
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.