Перейти к содержимому

Практика 15. Практика к лекции 13

Цель: освоить модель структурированной конкурентности Swift — async/await, Task, async let, TaskGroup, кооперативную отмену и акторы — для написания безопасного и эффективного асинхронного кода.

Рекомендации по выполнению:

  • Создавайте отдельный .swift-файл для каждого задания.
  • Компилируйте и запускайте через swift <файл>.swift (Linux, без Xcode).
  • Для замера времени используйте ContinuousClock или Date.
  • Помните: точка входа @main struct App или обёртка в Task нужна для вызова async-функций на верхнем уровне.

A. Разминка — async/await

  1. Напишите функцию delay(_ seconds: Double) async -> String, которая приостанавливается на указанное время через try? await Task.sleep(nanoseconds:) и возвращает "Ожидание \(seconds) сек завершено". Вызовите с await и выведите результат.

Пример ожидаемого поведения:

@main
struct App {
static func main() async {
let message = await delay(1.5)
print(message) // Ожидание 1.5 сек завершено
}
}
  1. Напишите функцию 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

  1. Используя функцию 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 секунды
  1. Напишите функцию fetchDashboard() async throws -> String, которая параллельно загружает три ресурса через async let:

    • fetchUserName(id: 1);
    • fetchOrders(userId: 1) async -> [String] (имитация);
    • fetchNotifications(userId: 1) async -> Int (имитация).

    Объедините результаты в одну строку дашборда. Если любой вызов выбрасывает ошибку, вся функция должна пробросить её наверх.


C. TaskGroup

  1. Создайте функцию 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"]
  1. Перепишите 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. Акторы

  1. Напишите актор 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
  1. Создайте актор 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. Кооперативная отмена

  1. Реализуйте кооперативную отмену:
    • Функция 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. Мини-проект

  1. «Параллельный агрегатор данных»:
  • Актор 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 внутри задачи);
    • вывести итоговый отчёт.

Пример ожидаемого поведения:

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).