Практика 15. Практика к лекции 13
Цель: освоить модель структурированной конкурентности Swift — async/await, Task, async let, TaskGroup, кооперативную отмену и акторы — для написания безопасного и эффективного асинхронного кода.
Рекомендации по выполнению:
- Создавайте отдельный
.swift-файл для каждого задания. - Компилируйте и запускайте через
swift <файл>.swift(Linux, без Xcode). - Для замера времени используйте
ContinuousClockилиDate. - Помните: точка входа
@main struct Appили обёртка вTaskнужна для вызоваasync-функций на верхнем уровне.
A. Разминка — async/await
- Напишите функцию
delay(_ seconds: Double) async -> String, которая приостанавливается на указанное время черезtry? await Task.sleep(nanoseconds:)и возвращает"Ожидание \(seconds) сек завершено". Вызовите сawaitи выведите результат.
Пример ожидаемого поведения:
@mainstruct App { static func main() async { let message = await delay(1.5) print(message) // Ожидание 1.5 сек завершено }}-
Напишите функцию
fetchUserName(id: Int) async throws -> String, которая:- при
id < 0выбрасывает ошибкуAPIError.invalidID; - при
id == 0выбрасываетAPIError.notFound; - иначе имитирует задержку 0.5 сек и возвращает
"User_\(id)".
Вызовите для
idравных1,0,-1и обработайте ошибки вdo-catch. - при
B. Параллельные вызовы — async let
- Используя функцию
delayиз задания 1, запустите три вызова параллельно сasync let:delay(1.0),delay(2.0),delay(3.0).- Замерьте общее время через
ContinuousClock. - Убедитесь, что оно составляет ~3 секунды, а не ~6.
Пример ожидаемого поведения:
let clock = ContinuousClock()let elapsed = await clock.measure { async let a = delay(1.0) async let b = delay(2.0) async let c = delay(3.0) let results = await [a, b, c] print(results)}print("Время: \(elapsed)") // ~3 секунды-
Напишите функцию
fetchDashboard() async throws -> String, которая параллельно загружает три ресурса черезasync let:fetchUserName(id: 1);fetchOrders(userId: 1) async -> [String](имитация);fetchNotifications(userId: 1) async -> Int(имитация).
Объедините результаты в одну строку дашборда. Если любой вызов выбрасывает ошибку, вся функция должна пробросить её наверх.
C. TaskGroup
-
Создайте функцию
processItems(_ items: [String]) async -> [String], которая с помощьюwithTaskGroupпараллельно переводит каждую строку в верхний регистр с имитацией задержки (0.1 сек на элемент). Верните результат в том же порядке, что и входной массив.Подсказка: добавляйте в группу кортежи
(index, result)для сохранения порядка.
Пример ожидаемого поведения:
let items = ["swift", "kotlin", "rust", "go"]let result = await processItems(items)print(result) // ["SWIFT", "KOTLIN", "RUST", "GO"]- Перепишите
processItemsсwithThrowingTaskGroup: строки короче 3 символов должны выбрасывать ошибкуProcessError.tooShort(String). При ошибке одного элемента вся группа должна быть отменена.
Пример ожидаемого поведения:
do { let result = try await processItemsThrowing(["swift", "go", "rust"]) print(result)} catch ProcessError.tooShort(let s) { print("Слишком короткая строка: '\(s)'") // Слишком короткая строка: 'go'}D. Акторы
-
Напишите актор
WordCounter:- Внутренний словарь
private var counts: [String: Int] = [:]; - Метод
count(word: String)— увеличивает счётчик слова на 1; - Метод
topWords(n: Int) -> [(String, Int)]— возвращаетnсамых частых слов; - Метод
total() -> Int— общее количество подсчитанных слов.
Тестируйте из нескольких параллельных задач, каждая из которых добавляет слова.
- Внутренний словарь
Пример ожидаемого поведения:
let counter = WordCounter()
await withTaskGroup(of: Void.self) { group in let words = ["swift", "python", "swift", "rust", "swift", "python"] for word in words { group.addTask { await counter.count(word: word) } }}
print(await counter.topWords(n: 2)) // [("swift", 3), ("python", 2)]print(await counter.total()) // 6-
Создайте актор
BankAccountсprivate var balance: Doubleи методами:deposit(_ amount: Double);withdraw(_ amount: Double) throws(выбрасывает при недостатке средств);nonisolated var description: String— возвращает описание безawait.
Добавьте метод
nonisolated func accountType() -> String, возвращающий"Standard". Объясните, почемуnonisolatedметоды не требуютawaitпри вызове.
E. Кооперативная отмена
- Реализуйте кооперативную отмену:
- Функция
processLargeArray(_ items: [Int]) async throws -> [Int]обрабатывает элементы по одному (возводит в квадрат), проверяяtry Task.checkCancellation()перед каждым элементом. - Запустите задачу с массивом из 1000 элементов.
- Отмените задачу через 0.3 секунды с помощью
task.cancel(). - Поймайте
CancellationErrorи выведите, сколько элементов успело обработаться.
- Функция
Пример ожидаемого поведения:
let task = Task { try await processLargeArray(Array(1...1000))}
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 секtask.cancel()
switch await task.result {case .success(let values): print("Обработано: \(values.count)")case .failure(let error): print("Отменено: \(error)") // Отменено: CancellationError()}F. Мини-проект
- «Параллельный агрегатор данных»:
- Актор
DataAggregator:- хранит результаты из нескольких «источников данных»;
- метод
addResult(source: String, data: [String]); - метод
allResults() -> [String: [String]]; - метод
summary() -> String— текстовый отчёт.
- Функция
fetchFromSource(_ name: String) async throws -> [String]— имитирует загрузку данных с задержкой (0.5–2 сек). Один из источников ("flaky") выбрасывает ошибку с вероятностью 50%. - Основная логика:
- запуск 5 источников через
withThrowingTaskGroup; - кооперативная отмена: если задача отменена — прекратить работу;
- сбор результатов в
DataAggregator; - при ошибке одного источника — логировать и продолжить (переключитесь на
withTaskGroupс обработкойResultвнутри задачи); - вывести итоговый отчёт.
- запуск 5 источников через
Пример ожидаемого поведения:
let aggregator = DataAggregator()await collectData(into: aggregator, sources: [ "users", "orders", "products", "reviews", "flaky"])print(await aggregator.summary())// Источник 'users': 5 записей// Источник 'orders': 3 записи// Источник 'products': 4 записи// Источник 'reviews': 6 записей// Источник 'flaky': ошибка — данные не полученыG. Критерии оценивания
- Корректное использование
async/awaitиasync let: 0–4 балла TaskGroupиwithThrowingTaskGroupс сохранением порядка: 0–4 балла- Акторы,
nonisolated, безопасность данных: 0–4 балла - Кооперативная отмена и мини-проект: 0–4 балла
Максимум: 16 баллов. Бонус до +2 за дополнительные задания.
H. Дополнительно (по желанию)
- Реализуйте
AsyncStream<String>, генерирующий строки с задержкой, и потребляйте его черезfor await. - Добавьте в
BankAccount(задание 8) методtransfer(to other: BankAccount, amount: Double) async throwsи протестируйте из нескольких задач. - Сравните
Task { }иTask.detached { }— напишите пример, где поведение отличается (наследование приоритета и actor context).