Асинхронный цикл Swift в задаче

Вопрос или проблема

Я пытаюсь понять, как именно работает Task с циклом.

Я экспериментировал с выводом чисел в консоль и заметил, что получаю результат, который не соответствует моим ожиданиям.

актер ThreadSafeCollection<T> {
    приватная переменная коллекция: [T] = []

    функция add(_ элемент: T) {
        коллекция.append(элемент)
    }

    функция getAll() -> [T] {
        вернуть коллекцию
    }

    функция remove(at индекс: Int) {
        охранник коллекция.indices.contains(индекс) иначе { вернуть }
        коллекция.remove(at: индекс)
    }
}

переменная safeCollection: ThreadSafeCollection<Int> = ThreadSafeCollection()

@Sendable функция firstIterate() async -> Task<[Int], Never> {
    Task {
        для i в 0..<500 {
            await safeCollection.add(i)
        }
        вернуть await safeCollection.getAll()
    }
}

@Sendable функция secondIterate() async -> Task<[Int], Never> {
    Task {
        для i в 0..<500 {
            await safeCollection.add(i)
        }
        вернуть await safeCollection.getAll()
    }
}

Task {
    let result = await withTaskGroup(of: [Int].self, возвращая: [Int].self) { taskGroup в
        taskGroup.addTask { await firstIterate().value }
        taskGroup.addTask { await secondIterate().value }

        переменная collected = [Int]()

        для await value в taskGroup {
            collected.append(contentsOf: value)
        }

        вернуть собранное
    }
    print(result.sorted(by: <))
}

В этом примере я итерацию 2 раза по 500, вызывая методы firstIterate() и secondIterate(), в результате чего я ожидаю получить массив с числами, в котором каждое число будет повторяться 2 раза. Но вместо этого я вижу каждое число 4 раза в консоли.
[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4...

Кроме того, я заметил, что в конце массива числа повторяются не 4 раза, как в начале, а 3.

...494, 494, 494, 495, 495, 495, 496, 496, 496, 497, 497, 497, 498, 498, 498, 499, 499, 499]

Может кто-то объяснить, почему это происходит?

Ответ или решение

Для понимания проблемы, с которой вы столкнулись, важно рассмотреть, как работает асинхронный код в Swift и как одновременно выполняются задачи, используя Task и actor.

В вашем примере вы создаете экземпляр ThreadSafeCollection, который обеспечивает безопасный доступ к коллекции из нескольких потоков через актор. Однако, при использовании вашей функции add внутри циклов в двух задачах, вы сталкиваетесь с интересным эффектом.

  1. Проблема с добавлением элементов:
    Ваша реализация функции firstIterate и secondIterate создает Task, где каждая задача добавляет элементы от 0 до 499 в потокобезопасную коллекцию. Поскольку оба метода запускаются параллельно, оба цикла могут обращаться к массиву целиком одновременно. Это может привести к тому, что добавление значений происходит не только незащищенно, но и одновременно, вызывая иногда дубликаты.

  2. Избыточные значения:
    Основная причина появления четырех экземпляров каждого числа заключается в том, что add() выполняется, возможно, более одного раза для некоторых значений i за один цикл. Поскольку добавление выполняется асинхронно и может пересекаться между задачами, это может привести к тому, что один и тот же элемент будет добавлен несколько раз.

  3. Неполные значения:
    Появление неполных значений в вашей конечной коллекции (т.е. 3 раза вместо 4 для последних значений) может быть связано с состоянием гонки, когда одна задача завершает добавление значения и завершает выполнение перед тем, как другая задача завершит свою работу из-за особенностей управления задачами и очередей, что влияет на порядок выполнения.

Рекомендации по исправлению

Для устранения данной проблемы, рассмотрите следующие подходы:

  1. Синхронный доступ: Хотя вы используете actor, убедитесь, что добавление элементов происходит последовательно с учетом текущего состояния коллекции.
  2. Деблокировка: При добавлении элемента убедитесь, что он успешно добавляется и нет конфликта с другими задачами. Например, можно использовать временные отсрочки.
  3. Использование одного метода: Вместо двух отдельных операций добавления можно использовать один метод, который будет вызываться параллельно.

Вот пример исправленного кода:

actor ThreadSafeCollection<T> {
    private var collection: [T] = []

    func add(_ element: T) {
        collection.append(element)
    }

    func getAll() -> [T] {
        return collection
    }
}

var safeCollection = ThreadSafeCollection<Int>()

@Sendable func iterate(from start: Int, to end: Int) async {
    for i in start...end {
        await safeCollection.add(i)
    }
}

Task {
    await withTaskGroup(of: Void.self, returning: Void.self) { taskGroup in
        taskGroup.addTask { await iterate(from: 0, to: 499) }
        taskGroup.addTask { await iterate(from: 0, to: 499) }
    }

    let result = await safeCollection.getAll()
    print(result.sorted())
}

В данном варианте мы вызываем одну функцию iterate, что предотвращает конфликт добавления элементов и сохраняет порядок выполнения. Теперь вы должны получить ожидаемое количество повторяющихся чисел в коллекции.

Оцените материал
Добавить комментарий

Капча загружается...