brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Mar 23. 2020

다트 isolate

플러터를 위한 다트 프로그래밍

다트의 비동기 프로그래밍 (1/3)

일반적인 프로그래밍은 순차적으로 작업을 처리한다. 즉 하나의 작업을 요청한 후 그 작업이 끝나면 다음 작업으로 넘어간다. 이런 경우 처리시간이 긴 작업(특히 UI와 관련된 상황)을 만나면 사용자는 프로그램이 멈춘 것처럼 느낄 수 있다.


이러한 문제는 비동기 프로그래밍(Asynchronous programming)으로 해결할 수 있다. 비동기 프로그래밍은 요청한 작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어감으로써 프로그램의 실행을 멈추지 않는다. 요청한 작업의 처리는 별도의 방식에 맡긴다.



비동기는 동시성(Concurrency)이나 병렬(Parallel)은 비교군이 될 수 없는 다른 개념이다.

또한 비동기를 정확히 이해하기 위해서는 블록킹(blocking)/논 블록킹(non-blocking)에 대해서도 알아야 한다. 관련해서는 따로 정리해봐야겠다.



다트는 future, stream을 통해서 자체적으로 비동기 프로그래밍을 지원한다. 그전에 isolate라는 다트의 독특한 구조부터 알아야 한다.


따라서 다음의 순서로 살펴본다.


1) isolate

2) future

3) stream


내용이 길어질 것 같으니 나눠서 포스팅해야 할 듯하다.


1) isolate

isolate라는 단어는 격리하다는 의미이다. 다트의 isolate도 그 의미와 연관이 깊다. isolate는 다트의 모든 코드가 실행되는 공간이다. 싱글 스레드를 가지고 있고 이벤트 루프를 통해 작업을 처리한다. 기본 isolate인 main isolate는 런타임에 생성된다.


isolate가 비록 싱글 스레드이지만 다트가 자체적인 비동기 프로그래밍을 지원하기 때문에 비동기 작업도 이벤트 루프에 의해서 적절히 처리된다. 또한 main isolate에서 무거운 작업으로 인해 반응성이 떨어진다면 추가로 isolate를 생성할 수 있다. 그러면 스레드가 2개가 되는 것이다. 다만 기존의 언어에서 사용하는 스레드와 차이점이 있다.


1.1) isolate 구조 및 기존 스레드와 차이점

자바 등의 다른 언어에서 사용하는 스레드는 다음과 같이 스레드가 서로 메모리를 공유하는 구조이다.

자바 스레드 구조

하지만 isolate의 스레드는 자체적으로 메모리를 가지고 있다. 따라서 새로운 isolate를 생성하면 해당 isolate에 별도의 고유한 메모리를 가진 스레드가 하나 더 생기는 것이다. 즉 메모리 공유가 되지 않는다.

다트 isolate 구조

따라서 두 isolate가 함께 작업하려면 message를 주고받아야만 가능하다. 이것이 불편하다고 생각 수도 있다. 하지만 멀티스레드 사용 시 늘 주의해야 하는 공유자원에 대한 컨트롤에 신경 쓰지 않아도 된다.


이벤트 루프는 이벤트 큐에 쌓여있는 작업들을 오래된 순으로 하나씩 가져와서 처리하도록 하는 역할을 한다.


1.2) 새로운 isolate 생성하기

새로운 isolate는 spawn을 통해서 만들 수 있다. 다음 예제를 보자.

main() 함수에서 isolate를 3개 spawn 하였다. 따라서 3개의 isolate가 만들어진다. 그런데 실행결과를 보면 이상하다. 출력이 2개밖에 없다. (isolate 생성 순서와 출력 순서가 다른 것은 thread 경쟁에 의해 순서가 보장되지 않기 때문이다.)



왜 그런지 알아보려고 고생한 결과는 다음과 같다. 여전히 모르겠고 납득이 되진 않는다.

여기에 올라온 버그에 대한 대처로 인해서 isolate의 print() 호출은 무시된다고 한다. 무려 7년 전에 발생한 문제이며 isolate의 print() 호출을 무시한다는 말이 정확히 뭔지 모르겠다. 그러면 모든 print() 호출이 무시되어야 하는 것 아닌가? 무시될 수도 있다라면 어느 정도 이해는 된다.


실제로 위 예제를 여러 번 실행하다 보면 출력이 랜덤하게 1개, 2개, 3개가 된다.



1.3) isolate 간 message 주고받기

앞선 예제에서 Line 4에 주석 처리된 부분이 있다. 해당 부분의 주석을 풀면 출력문이 기대했던 3개로 나온다. 단순히 ReceivePort 객체를 생성한 것만으로 원하던 동작이 되다니! 여전히 이유는 모르겠다.


ReceivePort는 isolate 간에 message를 주고받을 수 있는 역할을 한다. ReceivePort는 sendPort라는 getter를 통해서 SendPort를 리턴 받는다. 따라서 message를 보내고(send) 받기(receive)가 가능하다.


message를 보낼 때는 SendPort의 send를 이용하고 수신할 때는 ReceivePort의 listen을 이용한다.


다음 예제는 main isolate가 5개의 isolate와 message를 주고받는 예제이다.

Line 6 : main isolate에서 사용할 ReceivePort인 mainReceiverPort를 생성한다.

Line 8~14 : mainReceiverPort에서 message를 수신하는 listen를 선언한다. 만약 수신한 message가 SendPort 타입이면 해당 SendPort로 count 변수를 message로 하여 send 한다. 수신한 message가 SendPort 타입이 아니면 message를 출력한다.

Line 17 : 5개의 isolate를 생성한다.

Line 22 : 새로 생성된 isolate의 RecivePort인 fooReceivePort를 생성한다.

Line 23 : isolate 생성 시 전달받은 main isolate의 SendPort를 이용하여 main isolate에 새로 생성된 isolate의 SendPort를 전달한다.

Line25~27 : main isolate에서 받은 message(count 변숫값)을 'received: &msg' 형태의 String으로 만들어 다시 main isolate로 보낸다. 이 String이 Line 12의 print()를 통해서 출력되는 것이다.


이리저리 왔다 갔다 하는 코드라서 설명을 봐도 정신이 없을 수 있다. 다음과 같이 그림으로 표현해봤다.



이전 16화 다트 제네릭 (Dart Generic)
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari