안녕하세요. 슬로우의 Shawn입니다.
슬로우톡 앱에서 주소록 친구 추가를 하기 위해, 주소록을 불러오고 연락처를 파싱하는 작업을 진행하는데, 시간이 너무 오래 걸리는 문제가 있었습니다.
이에 Background로 주소록 데이터를 불러와서 작업을 하면 어떨까 하는 생각에 플러터에서 Background 작업을 지원하는 것을 찾게 되었습니다.
플러터 공식 홈페이지와 여러 블로그들을 찾아 보니, Isolate라는 동시성을 지원하는 기능이 있었습니다. 이 Isolate라는 것에 대해 알아보려 합니다.
플러터는 기본적으로 단일 스레드(Single Thread)에서 동작을 합니다. 멀티 스레드와 같은 것을 지원하기 위한 이 Isolate에 대해 플러터 공식 홈페이지에는 아래와 같이 설명하고 있습니다.
Isolate |
---|
Dart 코드의 모든 작업은 고립된 메모리를 가진 아이솔레이트(isolate) 에서 실행됩니다. 아이솔레이트는 스레드와 유사하지만, 상태를 공유하지 않으며 메시지를 통해서만 통신할 수 있다는 점에서 다릅니다. 기본적으로 Flutter 앱은 모든 작업을 단일 아이솔레이트(메인 아이솔레이트)에서 실행합니다. 대부분의 경우, 이 모델은 프로그래밍을 더 간단하게 만들어주며, 애플리케이션의 UI가 반응하지 않게 되는 상황을 방지할 만큼 충분히 빠릅니다. (링크) |
Dart에서는 스레드 대신 모든 코드가 Isolate 안에서 실행됩니다. Isolate를 사용하면 Dart 코드가 여러 독립적인 작업을 동시에 수행할 수 있으며, 가능한 경우 추가 프로세서 코어를 활용할 수 있습니다. Isolate는 스레드나 프로세스와 비슷하지만, 각 Isolate는 고유한 메모리를 가지며, 이벤트 루프를 실행하는 단일 스레드로 작동합니다. (링크) |
위 설명에서 보듯이 스레드와 Isolate는 같은 것이 아니며, 차이점은 아래와 같이 정리할 수 있습니다.
SendPort
와 ReceivePort
를 사용하여 구현됩니다.Isolate를 언제 사용해야 하는지에 대한 규칙은 없지만, 다음과 같은 상황에서 Isolate가 유용할 수 있습니다:
Isolate는 2가지 방법으로 사용할 수 있습니다.
Isolate.run() 함수를 호출하여 Main Isolate에서 New Isolate(Background worker)를 생성하고, main()는 결과를 기다립니다.
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final jsonData = await Isolate.run(() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
});
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
앞의 Simple Worker Isolate(단기 Isolate)의 경우, 새로운 Isolate를 생성하고 객체를 다른 Isolate로 복사하는데 성능 오버헤드가 발생할 수 있습니다. 코드에서는 Isolate.run
을 사용하는 대신에 즉시 종료되지 않는 장기 Isolate을 생성하여 성능을 개선할 수 있습니다.
이를 위해 Isolate.run
이 추상화한 몇 가지 격리 관련 API를 사용할 수 있습니다:
Isolate.spawn()
과 Isolate.exit()
ReceivePort
와 SendPort
SendPort.send()
메서드장기 Isolate 간의 통신을 설정하려면 두 가지 클래스가 필요합니다 (Isolate 외에도):
ReceivePort
: 다른 격리로부터 메시지를 처리하는 객체입니다.SendPort
: 메시지를 보내는 데 사용됩니다.새로 생성된 Isolate는 Isolate.spawn
호출을 통해 받은 정보만을 가지고 있습니다. 메인 Isolate와 생성된 Isolate 간에 초기 생성 이후에도 계속해서 통신하려면, 메시지를 주고받을 수 있는 통신 채널을 설정해야 합니다. Isolate들은 메시지 전달을 통해서만 통신할 수 있습니다.
이 양방향 통신을 설정하려면, 먼저 메인 격리에서 ReceivePort
를 생성하고, 이를 새로운 격리에서 Isolate.spawn
을 사용할 때 SendPort
로 전달합니다. 생성된 격리는 자신의 ReceivePort
를 만들고, 이를 통해 메인 격리로 SendPort
를 다시 보냅니다. 메인 격리는 이 SendPort
를 수신하게 되며, 이제 양쪽 모두 메시지를 주고받을 수 있는 개방된 채널을 갖게 됩니다.
위 그림들의 포트 설정과 메시지 주고 받기에 대한 예제를 보겠습니다.
import 'dart:isolate';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Isolate Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _message = 'Result will be displayed here';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Isolate Demo'),
),
body: Center(
child: Text(_message),
),
floatingActionButton: FloatingActionButton(
onPressed: _startBackgroundTask,
tooltip: 'Start',
child: Icon(Icons.play_arrow),
),
);
}
// 백그라운드 작업을 실행하는 함수
void _startBackgroundTask() async {
// 1. Main Isolate의 ReceivePort 생성
ReceivePort receivePort = ReceivePort();
// 백그라운드 작업으로부터 메시지를 받습니다.
receivePort.listen((data) {
if (data is SendPort) {
// 6. Worker Isolate의 sendPort를 받으면, 작업 개시 명령어 전달
data.send('Start work');
} else if (data is String) {
// 9. Worker Isolate로부터 받은 데이터를 처리함
setState(() {
_message = data;
});
// 10.
isolate.exit();
isolate.kill();
}
});
// 2. Worker Isolate 생성
// 3. Main의 ReceivePort.sendPort 참조 전달
// sendPort 이외의 정보를 Worker Isolate로 전달 시, params class를 만들어 전달
Isolate.spawn(_backgroundWork, receivePort.sendPort);
}
// 백그라운드에서 실행될 작업 - 인자 값은 Main Isolate의 sendPort
static void _backgroundWork(SendPort sendPort) {
// 4. Worker Isolate에서 ReceivePort 생성
final backgroundIsolateStream = ReceivePort();
// 5. Worker Isolate의 ReceivePort.sendPort의 참조를 Main Isolate에게 전달
sendPort.send(backgroundIsolateStream.sendPort);
await for (final message in backgroundIsolateStream) {
// 7. Main Isolate의 작업 개시 명령어 수신하면, 작업 시작
if (message == 'Start work') {
// Main에서 하기에 복잡한 계산 또는 작업을 수행
// 뭔가 복잡복잡한... 작업들...
String result = "Background work completed";
// 8. 계산 결과를 메인 Isolate로 전달
sendPort.send(result);
}
}
}
}
주석을 보시면, Main과 Worker Isolates간의 메시지를 주고 받는 것을 주석으로 표시했습니다.
https://dart.dev/language/isolates https://dart.dev/language/isolates
https://dart.dev/language/concurrency https://dart.dev/language/concurrency
https://docs.flutter.dev/get-started/flutter-for/dart-swift-concurrency https://docs.flutter.dev/get-started/flutter-for/dart-swift-concurrency
https://velog.io/@ximya_hf/flutter-isolate https://velog.io/@ximya_hf/flutter-isolate
https://lucasblog.kr/flutter-isolate-간단한-설명/ https://lucasblog.kr/flutter-isolate-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%84%A4%EB%AA%85/
https://kimtaewookdeveloper.tistory.com/41 https://kimtaewookdeveloper.tistory.com/41
https://rlg1133.tistory.com/143 https://rlg1133.tistory.com/143