Ранее в целях упрощения взаимодействиями с изолятами в Dart я начал разрабатывать акторный фреймворк - Theater.
Однако не всем может прийти по вкусу как сама акторная модель которую реализуют изоляты в Dart так и акторный фреймворк.
Помимо Dart-а я так же пишу на C# и там есть, как по мне, удобная обертка для работы с многопоточностью - Task.
Я решил сделать в Dart нечто максимально похожее на Task в C#, однако с некоторыми ньюансами из за того что все же под капотом используются изоляты.
Предоставляет удобную обертку для работы с изолятами похожую на Task в C#.
Добавьте Ossa в ваш pubspec.yaml файл:
dependencies:
ossa: ^1.0.4
Импортируйте ossa в файле где он должен использоваться:
import 'package:ossa/ossa.dart';
Каждая задача запускается и работает в отдельном изоляте, задачи могут возвращать некий результат.
Как уже было сказанно в введении я пытался сделать задачи максимально похожими на Task в C#. Однако есть и различия.
К примеру отсутствие вместо пула изолятов (в C# пул потоков) к которым переадресуются задачи и затем выполняются, в Ossa каждая создаваемая вами задача имеет собственный изолят.
Каждая задача управляет жизненным циклом своего изолята.
Задачи бывают двух типов:
- одноразовые задачи (по умолчанию);
- переиспользуемые задачи.
Различие между этими двумя типами состоит в том что одноразовые задачи после завершения выполнения задачи (в том числе если выполнение завершилось ошибкой) - вызывают свой метод dispose, уничтожают свой изолят и закрывают все используемые ими StreamController-ы, то есть освобождает все используемые ей ресурсы. Переиспользуемые задачи этого не делают, расчитанно это на то что вы создав задачу будете использовать её неоднократно.
Зачем использовать переиспользуемую задачу, а не создавать в будущем, при необходимости новую задачу? - чтобы каждый раз не создавать новый изолят.
Запустить задачу можно двумя способами:
- при помощи метода run;
- создав задачу, проиницализировав её и самостоятельно её запустив.
Пример запуска при помощи метода run:
void main() async {
// Create and run Task using run method
var task = await Task.run((context) {
print('Hello, from task!');
});
// Wait when task is completed
await task.result();
}
Пример запуска создавая задачу, проинициализировав её и запустив:
void main() async {
// Create task
var task = Task((context) {
print('Hello, from task!');
});
// Initialize task before work with him
await task.initialize();
// Start task
await task.start();
// Wait when task is completed
await task.result();
}
Задача может возвратить результат выполнения. При создании задачи самостоятельно, либо создании и запуске при помощи метода run вы можете указать Generic тип, который должен вернуть Task.
Можно обработать получение результата как ассинхронно при помощи onDone обработчика, так и дождаться получения результата при помощи Future полученного при вызове метода result.
Метод result в случае когда задача выполняется подождет результата из изолята, а затем вернет его. Если же задача на момент вызова метода result уже завершена и не запущенна повторно, то он вернет результат от предыдущего запуска задачи.
Получение результата из Future полученного при вызове метода result:
void main() async {
// Create and run Task with double return type using run method
var task = await Task.run<double>((context) => 3 * 7);
// Wait result from Task
var result = await task.result();
print(result);
}
Ожидаемый вывод:
21
Ассинхронное получение результата при помощи onDone обработчика:
void main() async {
// Create and run task with int return type, set onDone handler
var task = await Task.run<int>((context) {
return 5 * 10;
}, onDone: (value) async {
print(value);
});
}
Ожидаемый вывод:
50
Каждая задача выполняется в своем собственном изоляте. Изоляты не имеют разделяемой друг с другом памяти. Поэтому те данные с которыми должна работать задача предаются в неё при помощи параметра data в run и start методов.
Пример передачи данных в задачу:
void main() async {
// We have some data
var number = 100;
// Create and run task using run method, passing data to task
var task = await Task.run<int>((context) {
var number = context.get<int>('number');
return number * 10;
}, data: {'number': number});
// Wait when task is completed
var result = await task.result();
print(result);
}
Ожидаемый вывод:
1000
Задача в ходе своего жизненного цикла имеет различные статусы:
- не инициализированна (notInitialized);
- ожидает запуска (waitingToRun);
- выполняется (running);
- приостановлена (paused);
- очищена (disposed).
Не инициализированна задача имеет при создании без помощи метода run, до выполнения метода initialize. В этот момент изолят задачи еще не создан и требует инициализации прежде чем задача сможет выполнится.
После запуска метода initialize задача находится в ожидании запуска. Она готова выполнится и ждет выполнения метода start.
В статусе выполнения задача находится после выполнения метода start, может быть приостановлена (приостанавливается изолят задачи).
Если задача приостановлена, то её выполнение может быть возобновлено.
После очистки задачи её изолят уничтожается, все StreamController-ы закрываются. Дальнейшее использование этого экземпляра задачи невозможно.
Во время выполнения задачи может возникнуть исключение.
Есть варианта его обработки:
- обработка при помощи onError обработчика;
- заключение части кода с ожиданием результата в try/catch.
Пример обработки исключения при помощи onError обработчика, ассинхронно обрабатывая результат выполнения задачи:
void main() async {
late Task task;
// Create and run Task using run method, set onError handler
task = await Task.run<void>((context) {
throw FormatException();
}, onError: (error) async {
print(error.object.toString());
task.dispose();
});
}
Если при старте задачи не был задан onError обработчик, то есть 2 сценария что произойдет с ним:
- если вы ожидаете результат задачи ассинхронно, то есть при помощи onDone обработчика, то ничего не произойдет, исключение не будет никак обработанно. Задача из статуса выполнения (running) перейдет в статус готова к выполнению (waitingToRun);
- если вы ожидаете результат задачи при помощи метода result, то исключение будет вызванно повторно вызванно уже в методе result.
Пример обработки исключения без onError обработчика, ожидая результат при помощи метода result:
void main() async {
// Create and run Task using run method
var task = await Task.run((context) {
throw FormatException();
});
try {
// Wait when task is completed
await task.result();
} catch (object) {
// Handle error
if (object is TaskCompleteException) {
print(object);
await task.dispose();
}
}
}