번역 - Concurrent Programming With GCD in Swift 3(1/2)

okstring·2021년 9월 27일
0

본문은 Concurrent Programming With GCD in Swift 3을 변역, 정리한 글입니다

틀린 부분이 있다면 지적해주시면 감사하겠습니다 :)

어플리케이션의 main thread에서 대규모 작업(이미지 처리 등)을 하게 된다면 macOS에서는 spinning wheel appearing과 같이 UI가 느려지거나 완전히 중지될 수 있습니다. 이 문제는 Concurrent을 도입하는 것부터 시작해야 합니다.


우리 시스템에서는 thread를 만들어 concurrent한 환경을 만들 수 있습니다. concurrent한 환경을 얻는 것과 동시에 코드 불변성을 초래할 수 있습니다.

GCD는 저희 플랫폼의 concurrency 라이브러리입니다. 다중스레드 코드를 작성할 수 있게 도와주고 스레드 위에 몇 가지 추상적인 개념을 도입합니다.

대기열을 Dispatch하고 루프를 실행합니다. Dispatch는 thread의 모든 작업을 완료하면 thread를 해제할 수 있습니다. 또한 자신만의 thread를 만들 수도 있으며 thread안에서 Run Loop를 실행 할 수 있습니다. Main thread는 특별합니다. Main Run Loop와 Main Queue를 모두 가져옵니다.


디스패치 큐에는 작업을 제출할 수 있는 두 가지 주요 방법이 있습니다. 첫 번째 방법은 비동기 실행입니다. 여기서 여러 작업 항목을 디스패치 큐에 대기열에 올린 다음 디스패치가 해당 작업을 실행할 스레드를 불러올 수 있습니다. 디스패치는 해당 대기열에서 항목을 하나씩 제거하고 실행합니다.

그런 다음 대기열에 있는 모든 항목이 완료되면 시스템이 사용자를 위해 구입한 스레드를 회수합니다.



비동기 실행의 두 번째 모드입니다. 예를 들어, 이전과 동일한 설정을 가진 경우 일부 비동기 작업이 포함된 DispatchQueue 항목이 여기에 해당합니다. 하지만 여러분만의 Thread(Worker)가 있고, 그 Thread는 해당 대기열에서 코드를 실행하고 그것이 발생할 때까지 기다리려고 합니다. 해당 작업을 디스패치 큐에 제출하면 거기서 블록이 됩니다. 사용자가 실행하도록 요청한 항목이 완료될 때까지 기다립니다.해당 큐에 비동기 작업을 추가할 수 있으며, 그러면 디스패치가 해당 큐의 항목을 처리하기 위해 스레드를 불러올 것입니다. 다시, 비동기식 항목은 여기에서 실행되며, 실행하도록 요청한 동기식 항목을 실행할 시간이 되면 실행됩니다. 디스패치 큐는 제어 권한을 대기 중이던 스레드로 전달하여 해당 항목을 실행한 다음 디스패치 큐에 대한 제어 권한이 디스패치에 의해 제어되는 작업자 스레드로 돌아갑니다. 그러면 해당 대기열의 나머지 항목이 계속 소모되고 사용 중이던 스레드가 회수됩니다. 이제 작업물을 Dispatch하는 방법을 보여드렸습니다.

-> 이해가 잘 안가는 부분이긴 하지만 다른 스레드에 일을 처리시켜놓고 실행할 시점에 되면 해당 쓰레드에서 실행을 시키는 것으로 이해했습니다

Transform을 DispatchQueue를 사용하여 가져와 Transform을 할 수 있습니다. Data를 변환하려면 해당 데이터 값을 가지고와 Transform 한 후 Main Thread로 다시 전송할 수 있습니다.

이렇게 하면 주 스레드가 유휴 상태이고 이벤트를 처리하는동안 해당 작업을 수행할 수 있습니다.

실제 코드로는 어떻게 보일까요? 음, 정말 간단해요. 따라서 먼저 DispatchQueue 개체를 만들면 작업을 제출할 디스패치 대기열을 만들 수 있습니다. 레이블을 가져오면 해당 레이블이 프로그램을 작성할 때 디버거에 표시됩니다.

디스패치 큐는 사용자가 먼저 제공한 작업을 먼저 순서대로 실행합니다. 즉, 큐에 제출된 순서는 디스패치가 해당 순서를 실행하는 순서입니다.

그런 다음 디스패치 큐의 비동기 방법을 사용하여 작업을 해당 큐에 제출할 수 있습니다. 이제 실제로 크기 조정 작업을 다른 대기열에 제출했습니다. 그렇다면 어떻게 하면 Main Thread로 돌아갈 수 있을까요?

즉, DispatchQueue main을 호출한 다음 해당 기본 큐에서 async를 호출하면 해당 코드가 실행되고 거기에서 사용자 인터페이스를 업데이트할 수 있습니다. 다소 비용이 듭니다.

스레드 풀은 동시성을 제한합니다. 차단하는 작업자 스레드가 더 많이 생성될 수 있습니다. 사용할 적절한 대기열 수를 선택하는 것이 중요합니다.

응용 프로그램의 동시성을 제어해야 합니다. 디스패치가 사용하는 스레드 풀은 장치의 모든 호출을 사용하기 위해 사용자가 달성하는 동시성을 제한합니다.

그러나 해당 스레드를 차단할 때 애플리케이션의 다른 부분을 기다리거나 시스템 호출을 기다리는 경우 차단된 작업자 스레드로 인해 더 많은 작업자 스레드가 생성될 수 있습니다. Dispatch는 코드 실행을 계속할 수 있는 새 스레드를 제공하여 합당한 동시성을 제공하려고 합니다. 즉, 코드를 실행하는 데 사용할 적절한 수의 디스패치 대기열을 선택하는 것이 매우 중요합니다. 그렇지 않으면 하나의 스레드를 차단할 수 있습니다. 다른 스레드가 올라오고 다른 스레드를 차단하는 식입니다. 그리고 이 패턴은 우리가 thread explosion이라고 부릅니다.

그림과 같이 이렇게 하면 큐가 너무 많고 스레드가 너무 많은 문제를 겪지 않고 각 서브시스템에 독립적으로 작업을 실행할 수 있는 대기열이 제공됩니다. 그리고 우리는 몇 장의 슬라이드를 보았습니다. 얼마나 쉽게 서로 연결시킬 수 있는지요. 즉, 한 블록을 다른 블록으로 비동기화하고, 다른 큐로 비동기화하고, 다시 주 큐로 돌아갈 수 있습니다. 하지만 여러분께 보여드리고 싶은 두번째 패턴이 있습니다.


여러 개의 다른 작업 항목을 생성하고, 해당 작업 항목이 완료되었을 때만 작업을 진행하고자 하는 경우, 그렇게 할 수 있습니다.

그래서 우리가 전에 했던 다이어그램으로 되돌아가서, 사용자 인터페이스가 여기에 세 개의 분리된 작업 항목을 생성한다면, 여러분은 디스패치 그룹을 만들 수 있습니다.

파견 그룹은 작업 추적을 돕기 위해 여기에 있으며, Swift에서 쉽게 만들 수 있습니다. DispatchGroup 개체만 만들면 됩니다.

이제 디스패치에 작업을 제출할 때 비동기 호출에 그룹을 선택적 매개 변수로 추가할 수 있습니다.

해당 그룹에 작업을 더 추가할 수 있으며, 다른 대기열에 작업을 수행할 수 있지만 동일한 그룹에 연결할 수 있습니다. 그리고 여러분이 그룹에 작업을 제출할 때마다, 그룹은 완성될 것으로 예상되는 항목의 카운터를 증가시킵니다.

마지막으로, 모든 작업을 제출했으면 해당 작업이 완료되면 그룹에 알려달라고 요청할 수 있으며 선택한 대기열에 작업을 완료했다고 말할 수 있습니다. 이제 이 항목들이 하나씩 실행되기 시작하고, 그룹 내 카운트를 실행하면 작업 항목이 완료될 때마다 줄어듭니다. 마지막으로 마지막 작업 항목이 완료되면 그룹이 알림 블록을 요청한 대기열에 제출합니다. 그래서 여기서, 우리는 그룹을 제출했고, 블록을 다시 주 대기열에 올렸습니다. 그러면 그것은 주 스레드에서 실행될 것입니다. 이제 보여드려야 할 세번째 패턴이 있습니다. 이 두 가지는 비동기 실행이며, 세 번째는 동기 실행을 처리하는 것입니다. 동기 실행을 사용하여 하위 시스템 간의 상태를 직렬화할 수 있습니다.

직렬 대기열, 디스패치 대기열은 자연스럽게 직렬화됩니다. 그리고 이것을 mutual exclusion properties으로 사용할 수 있습니다. 즉, 작업을 해당 큐에 동기식으로 제출하면 하위 시스템이 해당 큐에서 동시에 실행되지 않는 작업을 알 수 있습니다.

이를 통해 다른 위치에서 하위 시스템의 속성에 액세스하는 매우 간단한 스레드 세이프 액세스를 구축할 수 있습니다. 예를 들어, 여기서 대기열 동기화를 호출하면 대기열 동기화에서 벗어난 값을 반환할 수 있습니다. 그러면 대기열에서 해당 값을 캡처한 다음 작업 항목이 완료되면 사용자에게 반환됩니다. 그러나 하위 시스템 간에 잠금 순서 그래프를 도입하기 시작하므로 이 패턴을 도입할 때는 주의해야 합니다. 이제 그게 무슨 뜻인가요? 음, 만약 여러분이 우리가 전에 가지고 있던 서브시스템을 가지고 있다면, 한 장소에서 다른 곳으로 그리고 나서 다른 곳으로 동기화하면, 결국, 여러분은 처음 것과 다시 동기화하게 됩니다. 그리고 이제 우리는 교착 상태에 빠졌어요.

이제 우리는 애플리케이션 전체에서 디스패치 사용을 구조화하는 방법을 조금 보았습니다. 하위 시스템 내부의 사용에 어떻게 적용할 수 있습니까? 디스패치를 사용하여 제출한 작업을 분류할 수 있으며 그렇게 하려면 Quality of Service를 도입해야 합니다. 이들은 Dispatch를 위해 제출하는 작업의 명시적 분류를 제공하는 클래스입니다. 따라서 개발자는 디스패치를 위해 제출하는 코드의 의도를 나타낼 수 있습니다. 그리고 디스패치는 당신이 우리에게 준 코드를 실행하는 방법에 영향을 주기 위해 그것을 사용할 수 있습니다. 즉, 코드는 다른 CPU 우선순위, 다른 IO 스케줄링 우선순위 등으로 실행될 수 있습니다. 사용은 간단합니다. QoS 클래스를 다른 선택적 매개변수로 비동기식으로 전달할 수 있습니다. 여기에서 백그라운드 작업을 대기열에 제출합니다.

나중에 더 높은 QoS에서 큐아웃 작업을 제출하면 디스패치가 생성된 우선 순위 역전 문제를 해결하는 데 도움이 됩니다. 즉, 디스패치 대기열의 작업 앞에 있는 항목을 더 높은 QoS로 올려 더 빨리 실행하고 예상한 만큼 빠르게 항목을 실행할 수 있습니다.

그러나 이 시점에서 유의해야 할 점은 이것이 여러분의 작업이 한계를 뛰어넘는 데 도움이 되지 않는다는 것입니다. 여기서 수행하는 모든 작업은 앞에 있는 모든 작업을 업그레이드하여 제출한 작업만큼 빠르게 실행됩니다.

그런 다음 특정 QoS 클래스가 있는 디스패치 대기열을 만들 수도 있습니다. 이것은 매우 유용합니다. 예를 들어 항상 백그라운드에서 실행하려는 백그라운드 작업이 있는 경우 모든 작업을 백그라운드로 실행하는 대기열을 만들 수 있습니다. 그리고 해당 대기열에 작업을 제출하면 이것이 우리가 얻게 될 QoS입니다. 따라서 보다 세분화된 수준에서 디스패치 큐에 비동기화하면 비동기화되는 지점에서 execution context를 캡처합니다.

execution context는 QoS와 같은 것을 의미합니다. 또한 현재 가지고 있는 로그인 컨텍스트를 의미합니다.

그러나 이 항목을 더 제어하려면 DispatchWorkItem을 사용하여 실행 방법을 더 제어할 수 있는 항목을 만들 수 있습니다.

DispatchWorkItem: Concurrency를 직렬로 활용 가능하게끔 캡슐화

예를 들어 assignCurrentContext를 사용하여 작업 항목을 만들고 있습니다. 이 작업 항목은 디스패치 큐에 제출하는 시간이 아니라 작업 항목을 작성할 때 실행 컨텍스트의 QoS를 사용합니다.

즉, 해당 항목을 만들고 나중에 사용할 수 있도록 저장할 수 있으며, 마지막으로 실행할 때 항목을 만들 때의 속성과 함께 디스패치에 제출합니다. 이제 작업 항목에 대해 이야기하는 동안 DispatchWorkItem에 매우 유용한 또 다른 부분이 있습니다. 이 부분은 완료되기를 기다리고 있습니다. DispatchWorkItem의 wait 메서드를 사용하여 진행하기 전에 해당 작업 항목을 완료해야 함을 디스패치에 표시할 수 있습니다.

Dispatch는 이전의 우선 순위 반전과 마찬가지로 QoS까지 작업 우선 순위를 높여 응답합니다. 그리고 DispatchWorkItem이 제출된 위치와 실행하려는 대기열을 알고 있기 때문에 이 작업을 수행할 수 있습니다. 따라서 디스패치는 작업 항목을 완료하기 위해 올려야 하는 대기열을 알고 있습니다.waiting on Semaphores and Groups은 이 소유권 정보를 저장하지 않기 때문에 매우 중요합니다. 즉, 세마포어를 기다리는 경우 세마포어 신호 앞에 있는 항목이 더 빨리 실행되지 않습니다.

profile
step by step

0개의 댓글