Используйте одну долгоживущую очередь Foundation OperationQueue и задайте её maxConcurrentOperationCount до отправки вычислений. Для утилиты Mac, получающей один запрос при каждом нажатии кнопки, это ограничит число одновременных задач, а последующие запросы смогут ждать.
В этом руководстве создаются вычислительный механизм для такой настольной задачи и отдельная программа для проверки его поведения. Каждый запрос содержит синтетические целые числа, а результаты хранятся в памяти. Вычисление выполняется синхронно внутри операции, поэтому срок жизни операции охватывает фактическую работу.
Какой инструмент конкурентности подходит для отдельных входящих запросов?
OperationQueue принимает операции по одной, а её блочный метод addOperation(_:) оборачивает каждое переданное замыкание в операцию. Храните очередь в объекте, который существует на протяжении обработки запросов, а затем отправляйте по одному замыканию для каждого входящего элемента. В документации Apple по addOperation(_:) описан этот метод отправки.
Установите для maxConcurrentOperationCount положительный предел, например 2. Apple определяет это свойство как максимальное число операций данной очереди, которые могут выполняться одновременно. Ограничение относится к конкретной очереди: создание отдельной очереди для каждого запроса лишит вас общего ограничения. Если впоследствии уменьшить это значение, уже выполняющиеся операции продолжат работу. Эти границы описаны в документации Apple об ограничении конкурентности.
У группы задач Swift другой срок жизни. Она существует внутри области withTaskGroup, её нельзя использовать за пределами создавшей её задачи, и она дожидается завершения дочерних задач перед возвратом. Поэтому хранить группу задач в свойстве для обработки будущих независимых нажатий кнопки нельзя. Для этого синхронного вычислительного механизма долгоживущая очередь операций обеспечивает нужный срок жизни и явную настройку конкурентности. Эти ограничения описаны в документации стандартной библиотеки Swift о группах задач.
| Организация работы | Подходящий вариант |
|---|---|
| Независимые синхронные вычисления поступают по одному | Храните одну OperationQueue и отправляйте по одной операции на вычисление. |
| Родительская асинхронная операция управляет конечным набором дочерних задач | Используйте группу задач в области видимости родительской операции; если требуется числовое ограничение, явно контролируйте допуск задач. |
| Работа должна обновлять интерфейс настольного приложения | Выполняйте вычисление отдельно, а затем передавайте результат коду, изолированному актором MainActor. |
Последняя строка касается передачи результата. В руководстве по языку Swift главный актор указан как место для состояния интерфейса, а также показаны замыкания с явной изоляцией на главном акторе. Не выполняйте ресурсоёмкую арифметику на этом этапе передачи. Изоляция на главном акторе описана в документации «Язык программирования Swift: конкурентность».
Определите вычисление и условия завершения
Пример принимает целочисленный идентификатор и число итераций от 1 до 1_000_000. Для числа итераций n программа складывает квадраты всех целых чисел от 1 до n. Она возвращает идентификатор вместе с результатом типа UInt64. Идентификатор обозначает результат и не влияет на арифметику.
Это примерные ограничения, выбранные для данной программы. При максимально допустимом входном значении результат равен 333333833333500000 и помещается в UInt64. Проверка выполняется до отправки, поэтому при неверном запросе сразу возникает ошибка, а операция не ставится в очередь. Ограничение входных данных также гарантирует корректность диапазона цикла.
После завершения операции для каждого принятого запроса сохраняется один результат. Результаты накапливаются в массиве, поэтому повторяющиеся идентификаторы не перезаписывают предыдущие результаты. В демонстрации используются различные возрастающие идентификаторы, а для удобства чтения вывод сортируется по идентификатору; такой порядок вывода ничего не говорит о порядке выполнения.
Этот контракт предполагает, что очередь остаётся доступной, а её операции завершаются. В примере отмена не предусмотрена. Если добавить отмену, потребуется отдельно определить исход незавершённых запросов, поскольку Apple описывает отмену как запрос на остановку, которому выполняющаяся работа должна следовать. Срок жизни и отмена операций описаны в документации Apple по OperationQueue.
Создайте полноценную программу с общим ограничением
Используйте Mac с компилятором Swift 6.2 или новее и SDK macOS, доступным через Xcode или его инструменты командной строки. Сохраните приведённую программу целиком в файл main.swift. В командах ниже явно задана изоляция по умолчанию без изоляции, чтобы вспомогательные функции вычислений не зависели от настроек изоляции целевого приложения. Эта настройка компилятора описана в SE-0466: управление выводом изоляции актора по умолчанию.
Журнал результатов использует NSLock для защиты массива и счётчиков активности. Соответствие @unchecked Sendable возлагает ответственность за синхронизацию на реализацию: каждый доступ к изменяемому состоянию выполняется под одной и той же блокировкой. Эта ответственность описана в документации Apple по Sendable, а защита критических секций — в документации по NSLock.
import Foundation
enum InputError: Error {
case invalidIterations
}
struct Calculation: Sendable {
let id: Int
let iterations: Int
}
func sumOfSquares(_ job: Calculation) -> UInt64 {
var sum: UInt64 = 0
for integer in 1...job.iterations {
let value = UInt64(integer)
sum += value * value
}
return sum
}
final class ExecutionLog: @unchecked Sendable {
private let lock = NSLock()
private var active = 0
private var peak = 0
private var rows: [(id: Int, value: UInt64)] = []
func begin() {
lock.lock()
defer { lock.unlock() }
active += 1
peak = max(peak, active)
}
func finish(id: Int, value: UInt64) {
lock.lock()
defer { lock.unlock() }
rows.append((id: id, value: value))
active -= 1
}
func snapshot() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
lock.lock()
defer { lock.unlock() }
return (rows, peak)
}
}
final class CalculationPool {
private let queue = OperationQueue()
private let log = ExecutionLog()
init(limit: Int) {
precondition(limit > 0)
queue.name = "Synthetic calculations"
queue.maxConcurrentOperationCount = limit
}
func submit(id: Int, iterations: Int) throws {
guard (1...1_000_000).contains(iterations) else {
throw InputError.invalidIterations
}
let job = Calculation(id: id, iterations: iterations)
queue.addOperation { [log] in
log.begin()
let value = sumOfSquares(job)
log.finish(id: job.id, value: value)
}
}
// Call only after the producer has stopped submitting work.
// This blocking method is for the command-line demonstration.
func finishForDemo() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
queue.waitUntilAllOperationsAreFinished()
return log.snapshot()
}
}
@main
struct Demo {
static func main() throws {
let limit = 2
let pool = CalculationPool(limit: limit)
try pool.submit(id: 1, iterations: 100_000)
try pool.submit(id: 2, iterations: 200_000)
try pool.submit(id: 3, iterations: 300_000)
let report = pool.finishForDemo()
let rows = report.rows.sorted { $0.id < $1.id }
precondition(rows.map { $0.id } == [1, 2, 3])
precondition(rows.map { $0.value } == [
UInt64(333338333350000),
UInt64(2666686666700000),
UInt64(9000045000050000)
])
precondition(report.peak >= 1 && report.peak <= limit)
for row in rows {
print("Job \(row.id): \(row.value)")
}
print("Observed peak: \(report.peak)")
}
}
Скомпилируйте и запустите программу из каталога, содержащего main.swift:
swiftc -swift-version 6 -default-isolation nonisolated \
-parse-as-library main.swift -o calculation-demo
./calculation-demo
Должны появиться три строки с результатами:
Задача 1: 333338333350000
Задача 2: 2666686666700000
Задача 3: 9000045000050000
В последней строке будет указан наблюдавшийся пик: 1 или 2. Ограничение допускает перекрытие выполнения, но не гарантирует его. Ожидаемые суммы независимо проверены целочисленной арифметикой и формулой n * (n + 1) * (2 * n + 1) / 6. Код на Swift был проверен, но не скомпилирован в исследовательской среде, поскольку там не установлен исполняемый файл Swift.
Почему это ограничение охватывает фактические вычисления?
Каждая операция увеличивает счётчик активности, полностью выполняет вычисление синхронно, записывает результат и затем возвращает управление. Из замыкания не запускаются дополнительные асинхронные вычисления. Поэтому ограничение числа выполняющихся операций ограничивает и число этих вычислений.
Сохраняйте эту взаимосвязь при адаптации программы. Если операция лишь создаёт отдельную Task и сразу возвращает управление, очередь считает операцию завершённой, хотя задача может продолжать вычисление. Тогда ограничение числа операций будет регулировать лишь короткие замыкания, запускающие задачи. Это следует из описанного срока жизни операций и API отправки блоков. Эти компоненты определены в обзоре OperationQueue от Apple и документации по addOperation(_:).
Блокировка журнала охватывает только короткие участки учёта. Если удерживать её во время sumOfSquares(_:), доступ к вычислению будет сериализован, что помешает нужному перекрытию выполнения. Каждое получение и снятие блокировки происходит в рамках одного синхронного метода, что соответствует требованию Apple снимать NSLock в том потоке, который её получил. Это требование объяснено в документации Apple по NSLock.
Чтобы проверить адаптацию, запустите те же три запроса, установив limit в 1, а затем восстановите 2. В обоих запусках должны сохраниться все три точных результата. При последовательном запуске должен быть указан пик 1; при втором запуске значение не должно превышать 2. Эти проверки подтверждают сохранность результатов и соблюдение наблюдаемого ограничения числа вычислений, не предполагая определённого порядка планирования.
Обрабатывайте действия пользователя на Mac, не блокируя интерфейс
Храните один экземпляр CalculationPool в компоненте, который отвечает за вычисления в настольном приложении. Для каждого входящего действия один раз вызывайте submit(id:iterations:). Не создавайте пул заново в обработчике действий: у каждого нового экземпляра своя очередь и своё ограничение.
В демонстрации finishForDemo() вызывается только после последней отправки, чтобы процесс командной строки успел собрать результаты до завершения. Этот метод блокирует вызывающий поток. В настольном приложении используйте передачу каждого результата отдельно и не вызывайте этот метод из обработчика нажатия кнопки или из главного актора. Блокирующее поведение waitUntilAllOperationsAreFinished() прямо описано Apple.
Для интеграции с интерфейсом расширьте API отправки, добавив обратный вызов результата, изолированный актором MainActor и помеченный @Sendable. После вычисления результата внутри операции запланируйте Task с явным замыканием @MainActor, которое вызовет этот обратный вызов. Захватите неизменяемое значение результата и обратный вызов. Внутри этого замыкания выполняйте только обновление интерфейса; арифметику оставьте в операции. В Swift описаны и синтаксис замыканий главного актора, и захват значений в передаваемых замыканиях.
Для такой версии с интерфейсом потребуется дополнительно определить, что считается завершением: завершение вычисления и отображение результата — разные события. Программа командной строки дожидается вычислений и синхронного сохранения результатов. Отдельно запланированный обратный вызов интерфейса может выполниться позже.
Что происходит, когда запросы поступают быстрее, чем выполняются вычисления?
Настройка конкурентности ограничивает число выполняемых операций, а дополнительная отправленная работа может оставаться в очереди. При этом массив завершённых результатов в примере может расти без ограничений. Таким образом, пример ограничивает конкурентность вычислений, но не общий объём памяти и не размер очереди. Согласно документации Apple, очередь хранит операции до их завершения. Удержание операций описано в обзоре OperationQueue.
Если функция настольного приложения может создавать длительные всплески запросов, перед расширением выберите политику допуска: принимайте только фиксированное число ожидающих запросов и сообщайте об отказе, приостанавливайте новые отправки или заменяйте ожидающий синтетический предварительный просмотр более новым запросом, если такое поведение предусмотрено явно. Счётчики допуска и обновления после завершения должны координироваться одним синхронизированным владельцем. Одно лишь ограничение выполняющейся работы не позволяет реализовать эти политики.
Для отдельных синхронных запросов необходима простая схема: храните одну очередь, один раз настройте её ограничение, выполняйте каждое вычисление внутри своей операции и синхронизируйте общие результаты. Сохраните эти границы при подключении действий пользователя, а явную политику для очереди добавьте только в случае, если этого требует характер поступления запросов.
Источники
- OperationQueue — Apple.
- maxConcurrentOperationCount — Apple.
- addOperation(_:) — Apple.
- waitUntilAllOperationsAreFinished() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- Concurrency — The Swift Programming Language — Swift.org.
- TaskGroup.swift — исходный код и документация стандартной библиотеки Swift — Swift.org.
- SE-0466: управление выводом изоляции актора по умолчанию — Swift.org.