網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
Flutter 是單線程架構(gòu),按道理理說(shuō),F(xiàn)lutter 不會(huì)出現(xiàn) Java 的多線程相關(guān)的問(wèn)題。
但在我使用 Flutter 過(guò)程中,卻發(fā)現(xiàn) Flutter 依然會(huì)存在數(shù)據(jù)操作原子性的問(wèn)題。
其實(shí) Flutter 中存在多線程的(Isolate 隔離池),只是 Flutter 中的多線程更像 Java 中的多進(jìn)程,因?yàn)?Flutter 中線程不能像 Java 一樣,可以兩個(gè)線程去操作同一個(gè)對(duì)象。
我們一般將計(jì)算任務(wù)放在 Flutter 單獨(dú)的線程中,例如一大段 Json 數(shù)據(jù)的解析,可以將解析計(jì)算放在單獨(dú)的線程中,然后將解析完后的 Map
返回到主線程來(lái)用。
Flutter單例模式
在 Java 中,我們一般喜歡用單例模式來(lái)理解 Java 多線程問(wèn)題。這里我們也以單例來(lái)舉例,我們先來(lái)一個(gè)正常的:
class FlutterSingleton { static FlutterSingleton? _instance; /// 將構(gòu)造方法聲明成私有的 FlutterSingleton._(); static FlutterSingleton getInstance() { if (_instance == null) { _instance = FlutterSingleton._(); } return _instance!; } }
由于 Flutter 是單線程架構(gòu)的, 所以上述代碼是沒有問(wèn)題的。
問(wèn)題示例
但是, 和 Java 不同的是, Flutter 中存在異步方法。
做 App 開發(fā)肯定會(huì)涉及到數(shù)據(jù)持久化,Android 開發(fā)應(yīng)該都熟悉 SharedPreferences,F(xiàn)lutter 中也存在 SharedPreferences 庫(kù),我們就以此來(lái)舉例。同樣實(shí)現(xiàn)單例模式,只是這次無(wú)可避免的需要使用 Flutter 中的異步:
class SPSingleton { static SPSingleton? _instance; String? data; /// 將構(gòu)造方法聲明成私有的 SPSingleton._fromMap(Mapmap) : data = map['data']; static Future _fromSharedPreferences() async { // 模擬從 SharedPreferences 中讀取數(shù)據(jù), 并以此來(lái)初始化當(dāng)前對(duì)象 Map map = {'data': 'mockData'}; await Future.delayed(Duration(milliseconds: 10)); return SPSingleton._fromMap(map); } static Future getInstance() async { if (_instance == null) { _instance = await SPSingleton._fromSharedPreferences(); } return _instance!; } } void main() async { SPSingleton.getInstance().then((value) { print('instance1.hashcode = ${value.hashCode}'); }); SPSingleton.getInstance().then((value) { print('instance2.hashcode = ${value.hashCode}'); }); }
運(yùn)行上面的代碼,打印日志如下:
instance1.hashcode = 428834223
instance2.hashcode = 324692380
可以發(fā)現(xiàn),我們兩次調(diào)用 SPSingleton.getInstance() 方法,分別創(chuàng)建了兩個(gè)對(duì)象,說(shuō)明上面的單例模式實(shí)現(xiàn)有問(wèn)題。
我們來(lái)分析一下 getInstance() 方法:
static FuturegetInstance() async { if (_instance == null) { // 1 _instance = await SPSingleton._fromSharedPreferences(); //2 } return _instance!; }
當(dāng)?shù)谝淮握{(diào)用 getInstance() 方法時(shí),代碼在運(yùn)行到 1 處時(shí),發(fā)現(xiàn) _instance 為 null, 就會(huì)進(jìn)入 if 語(yǔ)句里面執(zhí)行 2 處, 并因?yàn)?await 關(guān)鍵字掛起, 并交出代碼的執(zhí)行權(quán), 直到被 await 的 Future 執(zhí)行完畢,最后將創(chuàng)建的 SPSingleton 對(duì)象賦值給 _instance 并返回。
當(dāng)?shù)诙握{(diào)用 getInstance() 方法時(shí),代碼在運(yùn)行到 1 處時(shí),可能會(huì)發(fā)現(xiàn) _instance 還是為 null (因?yàn)?await SPSingleton._fromSharedPreferences() 需要 10ms 才能返回結(jié)果), 然后和第一次調(diào)用 getInstance() 方法類似, 創(chuàng)建新的 SPSingleton 對(duì)象賦值給 _instance。
最后導(dǎo)致兩次調(diào)用 getInstance() 方法, 分別創(chuàng)建了兩個(gè)對(duì)象。
解決辦法
問(wèn)題原因知道了,那么該怎樣解決這個(gè)問(wèn)題呢?
究其本質(zhì),就是 getInstance() 方法的執(zhí)行不具有原子性,即:在一次 getInstance() 方法執(zhí)行結(jié)束前,不能執(zhí)行下一次 getInstance() 方法。
幸運(yùn)的是, 我們可以借助 Completer 來(lái)將異步操作原子化,下面是借助 Completer 改造后的代碼:
import 'dart:async'; class SPSingleton { static SPSingleton? _instance; static Completer? _monitor; String? data; /// 將構(gòu)造方法聲明成私有的 SPSingleton._fromMap(Map map) : data = map['data']; static Future _fromSharedPreferences() async { // 模擬從 SharedPreferences 中讀取數(shù)據(jù), 并以此來(lái)初始化當(dāng)前對(duì)象 Map map = {'data': 'mockData'}; await Future.delayed(Duration(milliseconds: 10)); return SPSingleton._fromMap(map); } static Future getInstance() async { if (_instance == null) { if (_monitor == null) { _monitor = Completer (); _instance = await SPSingleton._fromSharedPreferences(); _monitor!.complete(true); } else { // Flutter 的 Future 支持被多次 await await _monitor!.future; _monitor = null; } } return _instance!; } } void main() async { SPSingleton.getInstance().then((value) { print('instance1.hashcode = ${value.hashCode}'); }); SPSingleton.getInstance().then((value) { print('instance2.hashcode = ${value.hashCode}'); }); }
我們?cè)俅畏治鲆幌?getInstance() 方法:
static FuturegetInstance() async { if (_instance == null) { // 1 if (_monitor == null) { // 2 _monitor = Completer (); // 3 _instance = await SPSingleton._fromSharedPreferences(); // 4 _monitor!.complete(true); // 5 } else { // Flutter 的 Future 支持被多次 await await _monitor!.future; //6 _monitor = null; } } return _instance!; // 7 }
當(dāng)?shù)谝淮握{(diào)用 getInstance() 方法時(shí), 1 處和 2 處都會(huì)判定為 true, 然后進(jìn)入執(zhí)行到 3 處創(chuàng)建一個(gè)的 Completer 對(duì)象, 然后在 4 的 await 處掛起, 并交出代碼的執(zhí)行權(quán), 直到被 await 的 Future 執(zhí)行完畢。
此時(shí)第二次調(diào)用的 getInstance() 方法開始執(zhí)行,1 處同樣會(huì)判定為 true, 但是到 2 處時(shí)會(huì)判定為 false, 從而進(jìn)入到 else, 并因?yàn)?6 處的 await 掛起, 并交出代碼的執(zhí)行權(quán);
此時(shí), 第一次調(diào)用 getInstance() 時(shí)的 4 處執(zhí)行完畢, 并執(zhí)行到 5, 并通過(guò) Completer 通知第二次調(diào)用的 getInstance() 方法可以等待獲取代碼執(zhí)行權(quán)了。
最后,兩次調(diào)用 getInstance() 方法都會(huì)返回同一個(gè) SPSingleton 對(duì)象,以下是打印日志:
instance1.hashcode = 786567983
instance2.hashcode = 786567983
由于 Flutter 的 Future 是支持多次 await 的, 所以即便是連續(xù) n 次調(diào)用 getInstance() 方法, 從第 2 到 n 次調(diào)用會(huì) await 同一個(gè) Completer.future, 最后也能返回同一個(gè)對(duì)象。
Flutter任務(wù)隊(duì)列
雖然我們經(jīng)常拿單例模式來(lái)解釋說(shuō)明 Java 多線程問(wèn)題,可這并不代表著 Java 只有在單例模式時(shí)才有多線程問(wèn)題。
同樣的,也并不代表著 Flutter 只有在單例模式下才有原子操作問(wèn)題。
問(wèn)題示例
我們同樣以數(shù)據(jù)持久化來(lái)舉例,只是這次我們以數(shù)據(jù)庫(kù)操作來(lái)舉例。
我們?cè)诓僮鲾?shù)據(jù)庫(kù)時(shí),經(jīng)常會(huì)有這樣的需求:如果數(shù)據(jù)庫(kù)表中存在這條數(shù)據(jù),就更新這條數(shù)據(jù),否則就插入這條數(shù)據(jù)。
為了實(shí)現(xiàn)這樣的需求,我們可能會(huì)先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒查詢到就插入,代碼如下:
class Item { int id; String data; Item({ required this.id, required this.data, }); } class DBTest { DBTest._(); static DBTest instance = DBTest._(); bool _existsData = false; Futureinsert(String data) async { // 模擬數(shù)據(jù)庫(kù)插入操作,10毫秒過(guò)后,數(shù)據(jù)庫(kù)中才有數(shù)據(jù) await Future.delayed(Duration(milliseconds: 10)); _existsData = true; print('執(zhí)行了插入'); } Future update(String data) async { // 模擬數(shù)據(jù)庫(kù)更新操作 await Future.delayed(Duration(milliseconds: 10)); print('執(zhí)行了更新'); } Future - selected(int id) async { // 模擬數(shù)據(jù)庫(kù)查詢操作 await Future.delayed(Duration(milliseconds: 10)); if (_existsData) { // 數(shù)據(jù)庫(kù)中有數(shù)據(jù)才返回 return Item(id: 1, data: 'mockData'); } else { // 數(shù)據(jù)庫(kù)沒有數(shù)據(jù)時(shí),返回null return null; } } /// 先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒查詢到就插入 Future
insertOrUpdate(int id, String data) async { Item? item = await selected(id); if (item == null) { await insert(data); } else { await update(data); } } } void main() async { DBTest.instance.insertOrUpdate(1, 'data'); DBTest.instance.insertOrUpdate(1, 'data'); }
我們期望的輸出日志為:
執(zhí)行了插入
執(zhí)行了更新
但不幸的是, 輸出的日志為:
執(zhí)行了插入
執(zhí)行了插入
原因也是異步方法操作數(shù)據(jù), 不是原子操作, 導(dǎo)致邏輯異常。
也許我們也可以效仿單例模式的實(shí)現(xiàn),利用 Completer 將 insertOrUpdate() 方法原子化。
但對(duì)于數(shù)據(jù)庫(kù)操作是不合適的,因?yàn)槲覀兛赡苓€有其它需求,比如說(shuō):調(diào)用插入數(shù)據(jù)的方法,然后立即從數(shù)據(jù)庫(kù)中查詢這條數(shù)據(jù),發(fā)現(xiàn)找不到。
如果強(qiáng)行使用 Completer,那么到最后,可能這個(gè)類中會(huì)出現(xiàn)一大堆的 Completer ,代碼難以維護(hù)。
解決辦法
其實(shí)我們想要的效果是,當(dāng)有異步方法在操作數(shù)據(jù)庫(kù)時(shí),別的操作數(shù)據(jù)的異步方法應(yīng)該阻塞住,也就是同一時(shí)間只能有一個(gè)方法來(lái)操作數(shù)據(jù)庫(kù)。我們其實(shí)可以使用任務(wù)隊(duì)列來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)操作的需求。
我這里利用 Completer 實(shí)現(xiàn)了一個(gè)任務(wù)隊(duì)列:
import 'dart:async'; import 'dart:collection'; /// TaskQueue 不支持 submit await submit, 以下代碼就存在問(wèn)題 /// /// TaskQueue taskQueue = TaskQueue(); /// Futuretask1(String arg)async{ /// await Future.delayed(Duration(milliseconds: 100)); /// } /// Future task2(String arg)async{ /// 在這里submit時(shí), 任務(wù)會(huì)被添加到隊(duì)尾, 且當(dāng)前方法任務(wù)不會(huì)結(jié)束 /// 添加到隊(duì)尾的任務(wù)必須等到當(dāng)前方法任務(wù)執(zhí)行完畢后, 才能繼續(xù)執(zhí)行 /// 而隊(duì)尾的任務(wù)必須等當(dāng)前任務(wù)執(zhí)行完畢后, 才能執(zhí)行 /// 這就導(dǎo)致相互等待, 使任務(wù)無(wú)法進(jìn)行下去 /// 解決辦法是, 移除當(dāng)前的 await, 讓當(dāng)前任務(wù)結(jié)束 /// await taskQueue.submit(task1, arg); /// } /// /// taskQueue.submit(task2, arg); /// /// 總結(jié): /// 被 submit 的方法的內(nèi)部如果調(diào)用 submit 方法, 此方法不能 await, 否則任務(wù)隊(duì)列會(huì)被阻塞住 /// /// 如何避免此操作, 可以借鑒以下思想: /// 以數(shù)據(jù)庫(kù)操作舉例, 有個(gè)save方法的邏輯是插入或者更新(先查詢數(shù)據(jù)庫(kù)select,再進(jìn)行下一步操作); /// sava方法內(nèi)部submit,并且select也submit, 就容易出現(xiàn)submit await submit的情況 /// /// 我們可以這樣操作,假設(shè)當(dāng)前類為 DBHelper: /// 將數(shù)據(jù)庫(kù)的增,刪,查,改操作封裝成私有的 async 方法, 且私有方法不能使用submit /// DBHelper的公有方法, 可以調(diào)用自己的私有 async 方法, 但不能調(diào)用自己的公有方法, 公有方法可以使用submit /// 這樣就不會(huì)存在submit await submit的情況了 class TaskQueue { /// 提交任務(wù) Future submit(Function fun, A? arg) async { if (!_isEnable) { throw Exception('current TaskQueue is recycled.'); } Completer result = new Completer (); if (!_isStartLoop) { _isStartLoop = true; _startLoop(); } _queue.addLast(_Runnable( fun: fun, arg: arg, completer: result, )); if (!(_emptyMonitor?.isCompleted ?? true)) { _emptyMonitor?.complete(); } return result.future; } /// 回收 TaskQueue void recycle() { _isEnable = false; if (!(_emptyMonitor?.isCompleted ?? true)) { _emptyMonitor?.complete(); } _queue.clear(); } Queue<_Runnable> _queue = Queue<_Runnable>(); Completer? _emptyMonitor; bool _isStartLoop = false; bool _isEnable = true; Future _startLoop() async { while (_isEnable) { if (_queue.isEmpty) { _emptyMonitor = new Completer(); await _emptyMonitor!.future; _emptyMonitor = null; } if (!_isEnable) { // 當(dāng)前TaskQueue不可用時(shí), 跳出循環(huán) return; } _Runnable runnable = _queue.removeFirst(); try { dynamic result = await runnable.fun(runnable.arg); runnable.completer.complete(result); } catch (e) { runnable.completer.completeError(e); } } } } class _Runnable { final Completer completer; final Function fun; final A? arg; _Runnable({ required this.completer, required this.fun, this.arg, }); }
由于 Flutter 中的 future 不支持暫停操作, 一旦開始執(zhí)行, 就只能等待執(zhí)行完。
所以這里的任務(wù)隊(duì)列實(shí)現(xiàn)是基于方法的延遲調(diào)用來(lái)實(shí)現(xiàn)的。
TaskQueue 的用法示例如下:
void main() async { Futuretest1(String data) async { await Future.delayed(Duration(milliseconds: 20)); print('執(zhí)行了test1'); } Future test2(Map args) async { await Future.delayed(Duration(milliseconds: 10)); print('執(zhí)行了test2'); return 'mockResult'; } TaskQueue taskQueue = TaskQueue(); taskQueue.submit(test1, '1'); taskQueue.submit(test2, { 'data1': 1, 'data2': '2', }).then((value) { print('test2返回結(jié)果:${value}'); }); await Future.delayed(Duration(milliseconds: 200)); taskQueue.recycle(); } /* 執(zhí)行輸出結(jié)果如下: 執(zhí)行了test1 執(zhí)行了test2 test2返回結(jié)果:mockResult */
值得注意的是: 這里的 TaskQueue 不支持 submit await submit, 原因及示例代碼已在注釋中說(shuō)明,這里不再贅述。
為了避免出現(xiàn) submit await submit 的情況,我代碼注釋中也做出了建議(假設(shè)當(dāng)前類為 DBHelper):
將數(shù)據(jù)庫(kù)的增、刪、查、改操作封裝成私有的異步方法, 且私有異步方法不能使用 submit;
DBHelper 的公有方法, 可以調(diào)用自己的私有異步方法, 但不能調(diào)用自己的公有異步方法, 公有異步方法可以使用 submit;
這樣就不會(huì)出現(xiàn) submit await submit 的情況了。
于是,上述的數(shù)據(jù)庫(kù)操作示例代碼就變成了以下的樣子:
class Item { int id; String data; Item({ required this.id, required this.data, }); } class DBTest { DBTest._(); static DBTest instance = DBTest._(); TaskQueue _taskQueue = TaskQueue(); bool _existsData = false; Future_insert(String data) async { // 模擬數(shù)據(jù)庫(kù)插入操作,10毫秒過(guò)后,數(shù)據(jù)庫(kù)才有數(shù)據(jù) await Future.delayed(Duration(milliseconds: 10)); _existsData = true; print('執(zhí)行了插入'); } Future insert(String data) async { await _taskQueue.submit(_insert, data); } Future _update(String data) async { // 模擬數(shù)據(jù)庫(kù)更新操作 await Future.delayed(Duration(milliseconds: 10)); print('執(zhí)行了更新'); } Future update(String data) async { await _taskQueue.submit(_update, data); } Future - _selected(int id) async { // 模擬數(shù)據(jù)庫(kù)查詢操作 await Future.delayed(Duration(milliseconds: 10)); if (_existsData) { // 數(shù)據(jù)庫(kù)中有數(shù)據(jù)才返回 return Item(id: 1, data: 'mockData'); } else { // 數(shù)據(jù)庫(kù)沒有數(shù)據(jù)時(shí),返回null return null; } } Future
- selected(int id) async { return await _taskQueue.submit(_selected, id); } /// 先從數(shù)據(jù)庫(kù)表中查詢數(shù)據(jù),查詢到了就更新,沒查詢到就插入 Future
_insertOrUpdate(Map args) async { int id = args['id']; String data = args['data']; Item? item = await _selected(id); if (item == null) { await _insert(data); } else { await _update(data); } } Future - insertOrUpdate(int id, String data) async { return await _taskQueue.submit(_insertOrUpdate, { 'id': id, 'data': data, }); } } void main() async { DBTest.instance.insertOrUpdate(1, 'data'); DBTest.instance.insertOrUpdate(1, 'data'); }
輸出日志也變成了我們期望的樣子:
執(zhí)行了插入
執(zhí)行了更新
總結(jié)
Flutter 異步方法修改數(shù)據(jù)時(shí), 一定要注意數(shù)據(jù)操作的原子性, 不能因?yàn)?Flutter 是單線程架構(gòu),就忽略多個(gè)異步方法競(jìng)爭(zhēng)導(dǎo)致數(shù)據(jù)異常的問(wèn)題。
Flutter 保證數(shù)據(jù)操作的原子性,也有可行辦法,當(dāng)邏輯比較簡(jiǎn)單時(shí),可直接使用 Completer,當(dāng)邏輯比較復(fù)雜時(shí),可以考慮使用任務(wù)隊(duì)列。
另外,本文中的任務(wù)隊(duì)列實(shí)現(xiàn)有很大的缺陷,不支持 submit await submit,否則整個(gè)任務(wù)隊(duì)列會(huì)被阻塞住。
原文鏈接:https://juejin.cn/post/7070071427864477710
相關(guān)推薦
- 2022-07-02 C語(yǔ)言詳細(xì)講解指針數(shù)組的用法_C 語(yǔ)言
- 2022-04-22 最新版npm : 無(wú)法將“npm”項(xiàng)識(shí)別為 cmdlet、函數(shù)、腳本文件或可運(yùn)行程序的名稱。請(qǐng)檢查
- 2022-06-08 FreeRTOS任務(wù)控制API函數(shù)的功能分析_操作系統(tǒng)
- 2022-01-16 span設(shè)置寬高無(wú)效
- 2022-05-10 oracle如何創(chuàng)建或刪除臨時(shí)表空間和空間詳解
- 2022-09-23 Pandas時(shí)間類型轉(zhuǎn)換與處理的實(shí)現(xiàn)示例_python
- 2022-12-21 C++強(qiáng)制轉(zhuǎn)換與智能指針示例詳解_C 語(yǔ)言
- 2022-12-07 C++?兩個(gè)vector對(duì)象拼接方式_C 語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支