-
IsolateFW, Lib 공부/Flutter 2023. 6. 27. 23:18
Flutter, dart는 싱글 스레드 환경이다.
즉, 화면 그리기, api 통신, 계산 등 모두 하나의 스레드에서 처리된다.
물론 비동기 처리를 지원하기 때문에 api 통신을 함에 있어,
데이터가 들어올 때 까지 화면이 그려지지 않는다거나 하는 문제는 발생하지 않는다.
하지만, 복잡한 연산을 수행하는 동기 연산의 경우에는 화면이 버벅이게 되는 문제가 발생한다.
이를 해결하기 위해 복잡하거나 무거운 연산을 수행하는 로직을 별도의 Isolate에서 처리하면 화면이 부드러워진다.
Isolate
일반적으로 모든 언어는 스레드 단위로 처리된다.
스레드는 같은 메모리와 코드를 공유하기 때문에 뮤텍스, 락과 같은 race condition을 고려해야만 한다.
하지만, dart는 Isolate 단위로 처리된다.
Isolate는 스레드와 다르게, 메모리 공간을 공유하지 않는다. 따라서 race condition을 고려할 필요가 없다.
(엄밀히 말하면 고유한 메모리와 이벤트 루프를 작동시키는 단일 스레드를 갖는 것이 Isolate이다)
간단하게 말하면, Isolate는 임계 영역 걱정을 할 필요가 없는 스레드(처리 단위)라고 생각하면 된다.
멀티 스레딩?
앞서 언급했 듯, flutter는 싱글 스레드, 즉 싱글 Isolate에서 동작한다.
<Fig. 01> Isolate그렇기에 동기 연산이 복잡하거나 큰 루프를 도는 등 오버헤드가 크다면 프로그램이 일시 정지하는 문제가 생긴다.
이는 결국 앱이 버벅이게 되는 현상을 야기한다.
이런 복잡한 연산을 별도의 워커 Isolate로 옮겨 처리하면 무거운 연산이 별도의 스레드에서 동작하게 되는데,
일반적으로 이러한 isolate를 백그라운드 워커 라고 한다.
즉, Main isolate에서는 화면을 그리는 동작을 동기적으로 하고,
Worker isolate에서는 무거운 연산을 동기적으로 수행하기 때문에,
Main isolate의 화면 그리는 동작은 부드럽게 수행될 수 있는 것이다.
코드 및 테스트
Future<void> _enemyListInitEventHandler( EnemyListInitEvent event, Emitter<EnemyListState> emit, ) async { emit(const EnemyListLoadingState()); try { final t1 = DateTime.now(); List<EnemyListModel> result = []; String jsonString = await rootBundle .loadString('${getGameDataRoot()}excel/enemy_handbook_table.json'); final t2 = DateTime.now(); print('delta: ${t2.difference(t1).inMilliseconds}ms'); Map<String, dynamic> jsonData = await json.decode(jsonString); for (var enemyData in jsonData.entries) { var enemy = EnemyModel.fromJson(enemyData.value); if (enemy.hideInHandbook ?? true) continue; result.add(EnemyListModel( enemyKey: enemy.enemyId!, enemyIndex: enemy.enemyIndex!, name: enemy.name!, level: enemy.enemyLevel!, tags: enemy.tags, )); } final t3 = DateTime.now(); print('delta: ${t3.difference(t2).inMilliseconds}ms'); emit(EnemyListLoadedState( enemyList: result, filteredEnemyList: result, selectedFilterOption: const [true, true, true], searchQuery: "", )); } catch (e) { emit(EnemyListErrorState(message: e.toString())); } }
// IOS
flutter: delta: 145ms
flutter: delta: 12ms
// Android
I/flutter (30246): delta: 201ms
I/flutter (30246): delta: 19msFuture<void> _enemyListInitEventHandler( EnemyListInitEvent event, Emitter<EnemyListState> emit, ) async { emit(const EnemyListLoadingState()); try { final t1 = DateTime.now(); String jsonString = await rootBundle .loadString('${getGameDataRoot()}excel/enemy_handbook_table.json'); final t2 = DateTime.now(); print('delta: ${t2.difference(t1).inMilliseconds}ms'); ReceivePort port = ReceivePort(); await Isolate.spawn( _deserializeEnemyListModel, [port.sendPort, jsonString], ); var result = await port.first; port.close(); final t3 = DateTime.now(); print('delta: ${t3.difference(t2).inMilliseconds}ms'); emit(EnemyListLoadedState( enemyList: result, filteredEnemyList: result, selectedFilterOption: const [true, true, true], searchQuery: "", )); } catch (e) { emit(EnemyListErrorState(message: e.toString())); } } // Isolate static void _deserializeEnemyListModel(List<dynamic> args) { SendPort sendPort = args[0]; String jsonString = args[1]; List<EnemyListModel> result = []; Map<String, dynamic> jsonData = jsonDecode(jsonString); for (var enemyData in jsonData.entries) { var enemy = EnemyModel.fromJson(enemyData.value); if (enemy.hideInHandbook ?? true) continue; result.add(EnemyListModel( enemyKey: enemy.enemyId!, enemyIndex: enemy.enemyIndex!, name: enemy.name!, level: enemy.enemyLevel!, tags: enemy.tags, )); } Isolate.exit(sendPort, result); }
// IOS
flutter: delta: 142ms
flutter: delta: 134ms
// Android
I/flutter (31355): delta: 166ms
I/flutter (31355): delta: 164msisolate는 spawn 메서드를 통해 생성할 수 있으며,
SendPort, ReceivePort를 통해 데이터를 주고 받을 수 있다.
spawn 메서드의 인자는 다른 언어에서 스레드를 생성하는 메서드와 유사하게 생겼다.
첫 번째 인자는 해당 isolate에서 수행할 함수가 들어간다.
이 때, 이 함수는 static 형태이거나 클래스 외부에 글로벌로 위치해야 한다.
즉, 메모리 상에서 힙 영역에 위치하면 안되고, data 영역에 있어야만 한다.
두 번째 인자는 스레드 함수의 인자가 들어간다.
인자는 배열 형태로 전달할 수 있으며 dynamic 형태이기에 어떠한 자료형, 클래스도 전달할 수 있다.
단, 소켓은 전달할 수 없다.
오래 걸리는 동기 연산을 worker isolate로 돌리면 확실히 화면은 부드러워 보인다.
하지만, 위의 테스트 결과에서도 알 수 있듯 isolate의 생성 오버헤드 때문에 실제 처리시간은 더 길어진다.
(평균 15.5ms -> 149ms)
그렇기에 화면을 부드럽게 하기 위해서는 isolate를 사용하는 것이 도움이 되겠지만,
그에 따라 로딩 화면도 이쁘게 만들어야 사용자가 조금 기다리는 맛이 있을 것이다.
'FW, Lib 공부 > Flutter' 카테고리의 다른 글
bloc 디자인 패턴 (0) 2023.06.27