В тази статия ще разгледаме проблемите, които тя има за цел да реши, ще обясним как работи под капака, ще сравним новия модел с предишния и ще разгледаме по-отблизо модела Actor. Общ преглед на конкуренцията на Swift: проблеми и решения Писането на код, който изпълнява задачи едновременно, може да подобри производителността и отзивчивостта, но често въвежда сложност и фини грешки като състезателни условия, задръствания и проблеми с безопасността. Swift Concurrency, въведена в Swift 6, има за цел да опрости едновременното програмиране, като осигури ясен, сигурен и ефективен модел за справяне с асинхронни задачи. 💡Ако използвате Swift 5.x и планирате да мигрирате към Swift 6, можете да разрешите проверките на Swift Concurrency в настройките на вашия проект. Това ви позволява постепенно да приемете новия модел на съвпадение, като същевременно поддържате съвместимост с съществуващия код. Позволяването на тези проверки помага за ранно улавяне на потенциални проблеми с съвпадение, което прави прехода по-гладък и по-безопасен. 💡Ако използвате Swift 5.x и планирате да мигрирате към Swift 6, можете да разрешите проверките на Swift Concurrency в настройките на вашия проект. Това ви позволява постепенно да приемете новия модел на съвместимост, като същевременно поддържате съвместимост с съществуващ код. async/await Синтаксис и други функции на Swift Concurrency постепенно без пълно пренаписване. Some of the key problems Swift Concurrency addresses include: Състояние на състезанието: Предотвратяване на едновременния достъп до споделено изменчиво състояние, което може да предизвика непредсказуемо поведение. Callback hell: Simplifying asynchronous code that used to rely heavily on nested callbacks or completion handlers, making code easier to read and maintain. Сложност на управлението на нишките: Отстраняване на създаването и синхронизирането на ниско ниво, което позволява на разработчиците да се съсредоточат върху логиката, а не върху обработката на нишките. Координиране на съпътстващи задачи: Структурираната съвместимост позволява ясни йерархии на задачите с правилно отменяне и разпространение на грешки. Използвайки нови езикови характеристики като Swift 6 предоставя по-интуитивен и надежден начин за писане на съпътстващ код, подобрявайки както производителността на разработчиците, така и стабилността на приложенията. async/await Мултитаскинг Съвременните операционни системи и времето за изпълнение използват мултитаскинг, за да изпълняват единици работа едновременно. Swift Concurrency приема кооперативно мултитаскинг, което се различава фундаментално от модела за превантивно мултитаскинг, използван от нишките на ниво OS. Разбирането на разликата е ключът към писането на ефективен и сигурен асинхронни Swift код. Превантивен мултитаскинг Превантивният мултитаскинг е моделът, използван от операционните системи за управление на нишките и процесите. В този модел, планиращият на ниво система може принудително да прекъсне всяка нишка в почти всеки момент, за да извърши превключване на контекста и да разпредели времето на процесора към друга нишка. Превантивното мултитаскинг позволява истинска паралелност между няколко ядра на процесора и предотвратява неправилно поведение или дълготрайни нишки от монополизиране на системните ресурси.Въпреки това, тази гъвкавост идва с цена.Тъй като нишките могат да бъдат прекъснати по всяко време на тяхното изпълнение - дори в средата на критична операция - разработчиците трябва да използват синхронизационни примитиви като мутекси, семафори или атомни операции, за да защитят споделеното мутабилно състояние.Невъзможността да се направи това може да доведе до състезания с данни, сривове или фини грешки, които често са трудни за откриване и възпроизвеждане. Този модел предлага по-голям контрол и груба съвместимост, но също така поставя значително по-голяма тежест върху разработчиците.Осигуряването на безопасност на нишките в превантивна среда е склонно към грешки и може да доведе до не-детерминистично поведение - поведение, което варира от бягане до бягане - което е известно трудно да се разсъждава или надеждно да се тества. От техническа гледна точка, превантивното мултитаскинг разчита на операционната система, за да се справя с изпълнението на нишките. ОСът може да прекъсне нишка в почти всяка точка - дори в средата на една функция - и да премине към друга. За да направите това, системата трябва да извърши превключване на контекста, което включва запазване на цялото състояние на изпълнение на текущия нишка (като CPU регистри, указател за инструкции и указател за стак), и възстановяване на предварително запазеното състояние на друг нишка. Тези операции въвеждат значително време за изпълнение. Всеки контекстен превключвател отнема време и консумира системни ресурси – особено когато контекстните превключватели са чести или когато много нишки се конкурират за ограничени CPU ядра. Докато този модел осигурява максимална гъвкавост и истинска паралелност, той често е прекомерен за асинхрони работни потоци, където задачите обикновено прекарват по-голямата част от времето си в очакване на I/O, потребителски вход или мрежови отговори, вместо активно да използват CPU. Кооперативен мултитаскинг В този модел задачата се изпълнява, докато доброволно не получи контрол – обикновено в точка на изчакване или чрез изрично обаждане до За разлика от традиционните нишки, съвместните задачи никога не са принудително предотвратени.Това води до предсказуемо изпълнение: превключванията на контекста се случват само при ясно определени точки на окачване. Task.yield() Кооперативните задачи на Swift са планирани в лесен, управляван по време на работа съвместен поток от нишки – отделно от опашките на Grand Central Dispatch. Задачите, изпълнявани в този поток, се очаква да бъдат „добри граждани“, като по целесъобразност се дава контрол, особено по време на продължителна работа или интензивна работа с CPU. като ръчна точка за окачване, като се гарантира, че други задачи имат шанс да бъдат изпълнени. Task.yield() However, cooperative multitasking comes with a caveat: if a task never suspends, it can monopolize the thread it’s running on, delaying or starving other tasks in the system. Therefore, it is the developer’s responsibility to ensure that long-running operations include suspension points. В кооперативното многозадачно изпълнение основната единица за изпълнение не е нишка, а част от работата, често наричана продължение. Функцията се прекратява при , времето за изпълнение на Swift улавя текущото състояние на изпълнение в продължение, разпределено в купчина. Това продължение представлява точка на възобновяване и е включено за бъдещо изпълнение. async await Вместо да свързва нишка с дълготрайна задача, Swift runtime третира нишка като тръба от продължения. Всеки нишка изпълнява едно продължение след друго. Когато продължение завърши или се спира отново, нишката взема следващото готово продължение от опашката. Както бе споменато по-горе, този модел избягва традиционните превключватели на контекста на нивото на операционната система. Няма нужда да се записват и възстановяват регистрите на CPU или нишките; времето за изпълнение просто призовава за следващото продължение като затваряне. Ключовият компромис: използвате малко повече памет, но получавате драстично по-ниски надценки за управление на задачите.Кооперативното планиране дава тесен контрол над това кога се случват суспензии, което подобрява предсказуемостта и улеснява съвместното мислене. Introducing to Задача Задача В случай на бърза конкуренция, а асинхронна работа, за разлика от простото назоваване на функционалност, а е управляван обект, който се изпълнява едновременно с други задачи в кооперативен пул от нишки. Task async Task 💡 Задачите се управляват от кооперативен пул от нишки. Кооперативният пул от нишки е предназначен да управлява съвпадението ефективно, като позволява на задачите да произвеждат процесора, докато чакат асинхронните операции да завършат. 💡 Задачите се управляват от кооперативен пул от нишки. Кооперативният пул от нишки е предназначен да управлява съвпадението ефективно, като позволява на задачите да произвеждат процесора, докато чакат асинхронните операции да завършат. Задачите могат да бъдат създадени, за да се изпълняват едновременно, а също така могат да бъдат изчакани или отменени.Те осигуряват фин контрол върху асинхронното поведение и са неразделна част от структурираното съвпадение в Swift. Създаване на Task A task can be created using the , което незабавно стартира предоставената асинхронна операция: Task Инициализиране Инициализиране Task(priority: .userInitiated) { await fetchData() } Когато създавате а използване на стандартния инициализатор (т.е. не Това поведение е от решаващо значение за структурираното съвпадение и сигурността в съпътстващия код. Task Отделени Отделени 💡 Swift 6.2 въвежда значителна промяна в начина, по който се управлява съвпадението: по подразбиране, всички кодове се изпълняват на MainActor. За да изпълнявате код на фона, Swift 6.2 добавя нов атрибут @concurrent. Можете също да използвате неизолиран, ако кодът не изисква достъп до главния играч. 💡 Swift 6.2 въвежда значителна промяна в начина, по който се управлява съвпадението: по подразбиране, всички кодове се изпълняват на MainActor. За да изпълнявате код на фона, Swift 6.2 добавя нов атрибут @concurrent. Можете също да използвате неизолиран, ако кодът не изисква достъп до главния играч. WWDC обхваща конкуренцията на Swift WWDC обхваща конкуренцията на Swift Под капака, в по-ранните версии на Swift Concurrency, Swift runtime използва вътрешен механизъм, наречен Въпреки че това свойство не е част от публичния API, то играе ключова роля за гарантиране, че задачите, създадени в кода, изолиран от актьора, ще се изпълняват върху същия актьор, като се запази сигурността на състезанието с данни. @_inheritActorContext С напредъка в Swift, runtime е започнал да преминава от Това е нов механизъм, известен като , което сега се управлява по-изрично от компилатора и runtime. @_inheritActorContext sending 💡 Какво е изпращането? изпращането е нова ключова дума, въведена в Swift 6 като част от движението на езика към по-безопасно и по-изрично съвпадение. Тя се използва за маркиране на параметри на функцията и връщане на стойности, които се преместват през границите на съвпадението. Тя работи особено добре некопируеми типове, осигуряване на сигурността на паметта и предотвратяване на грешки след използване. Когато параметър е маркиран с изпращане, компилаторът налага, че оригиналната инстанция вече не е достъпна след прехвърлянето. func process(_ data: sending MyNonCopyableType) async { // `data` is moved here and can’t be used elsewhere after the call } Какво е sending ? sending е нова ключова дума, въведена в Swift 6 като част от движението на езика към по-безопасно и по-изрично съвпадение. Тя се използва за маркиране на параметри на функции и връщане на стойности, които се преместват през границите на съвпадение. Тя работи особено добре некопируеми типове, осигуряване на безопасност на паметта и предотвратяване на грешки след употреба. sending , компилаторът налага, че първоначалният екземпляр вече не е достъпен след прехвърлянето. func process(_ data: sending MyNonCopyableType) async { // `data` is moved here and can’t be used elsewhere after the call } Когато стартирате A , трябва да се уверите, че всички стойности, улавяни от задачата, са Компилаторът сега прилага това по време на компилацията, като използва Протоколът и Тип на функцията. Task.detached Sendable Sendable @Sendable Неспособност да се съобразяват може да доведе до грешка в времето за компилиране, особено в строг режим на съвпадение. Sendable Задачите също поддържат приоритети, подобно на това как опашките на Grand Central Dispatch ги обработват. vs Task Task.detached Когато работите с Swift Concurrency, важно е да разберете разликата между и , тъй като те определят как и къде се изпълнява асинхронната работа. Task Task.detached Task Задача Наследява текущия контекст на участниците (като например Обикновено се използва, когато искате да създадете нова асинхронна операция, която все още зачита текущото структурирано дърво за съвпадение или изолация на актьорите. Task MainActor Task { await updateUI() } В горния пример, ако се обадите от главния актьор, Ще се играе и на главния актьор, освен ако изрично не се премести другаде. Task Task.detached Задължение.отсечени създава напълно независима задача. Тя не наследява текущия контекст или приоритет на участниците. Това означава, че тя започва в глобален контекст и изисква управление на сигурността, особено при достъп до споделени данни. Task.detached Task.detached { await performBackgroundWork() } Използвайте когато трябва да изпълнявате операции на фона извън текущия структуриран контекст, като например дълги изчисления или избягване на изолацията на участник. Task.detached Thread Pool сътрудничество Cooperative Thread Pool в Swift Concurrency е механизъм, който управлява изпълнението на асинхрони задачи, като ги планира на ограничен брой нишки, обикновено съвпадащи с броя на CPU ядрата. Cooperative Thread Pool в Swift Concurrency е механизъм, който управлява изпълнението на асинхрони задачи, като ги планира на ограничен брой нишки, обикновено съвпадащи с броя на CPU ядрата. Swift Concurrency работи с помощта на кооперативен пул от нишки, предназначен за ефективно планиране и минимално прехвърляне на нишки. За разлика от традиционния модел за изпълнение на нишки по задачи, подходът на Swift подчертава структурирано съвместно планиране и съзнателно планиране. Едно често срещано опростяване е да се каже, че Swift Concurrency използва една нишка на ядро, което съответства на целта си да намали превключването на контекста и да увеличи максимално използването на CPU. Трейлър: Не е толкова просто Трейлър: Не е толкова просто На 16-ядрен Mac е възможно да се наблюдават до 64 нишки, управлявани само от Swift Concurrency - без участието на GCD. Това е така, защото съвместният поток от нишки на Swift картографира не само на ядро, но и на ядро на QoS бакет. официално : Max threads = (CPU cores) × (dedicated quality-of-service buckets) Thus, on a 16-core system: 16 cores × 4 QoS buckets = 64 threads Всяка QoS кутия е по същество специална нишка за група задачи, които споделят подобен приоритет на изпълнение. Те се управляват вътрешно от механизма за планиране на нишките на Дарвин и не са същите като редиците на GCD. QoS Buckets и приоритет на задачите QoS Buckets и приоритет на задачите Въпреки че Има шест константи, някои от които са псевдоними: TaskPriority Починали на високо Ниска полезност По подразбиране → вече е нанесено на средно От гледна точка на ядрото, това опростява до 4 основни приоритетни нива, всяка от които е картографирана в QoS кутия, което влияе на разпределението на нишките в кооперативния нишесте. Кога се случва свръхкомпетентността? Кога се случва свръхкомпетентността? При нормално натоварване Swift Concurrency спазва ограниченията на кооперативния басейн.Въпреки това, при спорове (напр. задачи с висок приоритет, които чакат за задачи с нисък приоритет), системата може да претовари нишките, за да запази отзивчивостта.Тази динамична настройка гарантира, че задачите, чувствителни към времето, не са блокирани за неопределено време зад работата с по-нисък приоритет. This behavior is managed by the Darwin kernel via Mach scheduling policies and high-priority ленти - не нещо, което изрично се контролира от вашия код. pthreads Task Priority Swift provides a priority system for tasks, similar to Grand Central Dispatch (GCD), but more semantically integrated into the structured concurrency model. You can set a task’s priority via the Инициализиране на: Task Task(priority: .userInitiated) { await loadUserData() } Наличните приоритети се определят от от ENUM: TaskPriority Priority Description / .high .userInitiated For tasks initiated by user interaction that require immediate feedback. .medium For tasks that the user is not actively waiting for. / .low .utility For long-running tasks that don’t require immediate results, such as copying files or importing data. .background For background tasks that the user is not directly aware of. Primarily used for work the user cannot see. / от .high .userInitiated За задачи, инициирани от потребителско взаимодействие, които изискват незабавна обратна връзка. .medium За задачи, за които потребителят не чака активно. / .low .utility За дълги задачи, които не изискват незабавни резултати, като например копиране на файлове или импортиране на данни. .background За задачи на заден план, за които потребителят не е пряко наясно.Първоначално се използва за работа, която потребителят не може да види. Създаване на задачи с различни приоритети Създаване на задачи с различни приоритети Когато създавате а в рамките на друга задача (по подразбиране Приоритет), можете изрично да зададете различен приоритет за всяка вградена задача. , а другият е .high. Това показва, че приоритетите могат да бъдат зададени индивидуално, независимо от родителя. Task .medium .low Task { // .medium by default Task(priority: .low) { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .high) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } // 1, thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}, priority: TaskPriority.low // 2, thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}, priority: TaskPriority.high Ако не зададете изрично приоритет за вградена задача, тя наследява приоритета на нейния непосредствен родител. и Блоковете наследяват съответните приоритети, освен ако не са превишени. .high .low Приоритетите на задачите могат да бъдат наследени Приоритетите на задачите могат да бъдат наследени Task { Task(priority: .high) { Task { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") Task { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } } // 2, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.low // 1, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.high // 3, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.low // 4, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.medium Ако изрично не зададете приоритет за вградена задача, тя наследява приоритета на нейния непосредствен родител. В този пример анонимните задачи в блоковете .high и .low наследяват съответните приоритети, освен ако не са пренаредени. Ескалация на приоритета Ескалация на приоритета Task(priority: .high) { Task { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } await Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") await Task { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") }.value Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } }.value } // 1, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 2, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 3, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 4, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.medium Този механизъм се нарича ескалация на приоритета – когато задача се очаква от задача с по-висок приоритет, системата може временно да повиши приоритета си, за да избегне затруднения и да осигури отзивчивост. В резултат на това: Задача 2, която е .low, се ескалира до .high, докато се чака. Task 3, which doesn’t have an explicit priority, inherits the escalated priority from its parent (Task 2) and is also executed with priority. .high Task 4 explicitly sets its priority to , so it is not affected by escalation. .medium Не наследява приоритет Задължение.отсечени Задължение.отсечени Задължения за изпълнение ( Те се държат като глобални задачи със собственото си планиране.Това е полезно за изолиране на позадинната работа, но може да доведе и до неочаквани несъответствия на приоритетите, ако не са зададени ръчно. Task.detached Task(priority: .high) { Task.detached { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") Task.detached { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } } // 1, thread: <NSThread: 0x60000174dec0>{number = 4, name = (null)}, priority: TaskPriority.medium // 2, thread: <_NSMainThread: 0x600001708180>{number = 1, name = main}, priority: TaskPriority.low // 3, thread: <NSThread: 0x60000174dec0>{number = 4, name = (null)}, priority: TaskPriority.medium // 4, thread: <_NSMainThread: 0x600001708180>{number = 1, name = main}, priority: TaskPriority.medium Точки за суспензия и как Swift управлява изпълнението на Async В Swift, всяко обаждане до Функция за използване е потенциална точка на суспензия - място във функцията, където изпълнението може да бъде прекъснато и да се възобнови по-късно. async await Here’s an example: func fetchData() async -> String { let result = await networkClient.load() return result } В този случай, Когато функцията достигне тази линия, тя може да спре изпълнението, да даде контрол на системата и по-късно да продължи веднъж. Зад сцените компилаторът превръща тази функция в станционна машина, която проследява нейния прогрес и вътрешни променливи. await networkClient.load() load() Под качулката: продължения и държавни машини Всеки Функцията в Swift е компилирана в държавна машина. преходни точки. преди да стигнете до , Swift: async await await Запазва текущото състояние на функцията - включително локалните променливи и текущия указател за инструкции. Спира изпълнението и планира продължение. След като асинхронната операция приключи, тя възобновява функцията от мястото, където е спряла. Това е подобно на стила на непрекъснато преминаване (CPS), използван в много функционални програми.В съвместния модел на Swift това се оркестрира от вътрешни типове като и състезателния график за изпълнение. ParticialAsyncTask Блокиране = Блокиране Когато ти нещо в Swift, текущата нишка не е блокирана, вместо това: await Текущата задача връща контрола на изпълнителя Други задачи могат да се изпълняват, докато чакат Когато очакваната операция приключи, суспендираната задача се възобновява при съответния изпълнител. Това прави фундаментално по-ефективни и мащабируеми от базирани на нишки блокиращи операции като . async/await DispatchQueue.sync Извършване на други задачи Task.yield() Задача.издаване на печалба() Това е статичен метод, предоставен от системата на Swift, която доброволно спира текущата задача, като дава възможност на системата да изпълнява други задания.Това е особено полезно при продължителни асинхрони операции или тесни вериги, които естествено не съдържат точки за окачване. Task.yield() func processLargeBatch() async { for i in 0..<1_000_000 { if i % 10_000 == 0 { await Task.yield() } } } без Това ще доведе до монополизиране на изпълнителя. Периодично, вие си сътрудничите със съвместното изпълнение на Swift, което ви позволява да поддържате отзивчивост и справедливост. await await Task.yield() Под капака Обаждане в очакване спира текущата задача и я презарежда в края на опашката за текущия изпълнител (например основен играч или глобален съпътстващ изпълнител). Task.yield() Това е част от кооперативния мултитаскинг модел на Swift: задачите се изпълняват до следващата точка на спиране и се очаква да бъдат справедливи.За разлика от превантивните системи (например нишките), задачите на Swift не се прекъсват принудително – те трябва доброволно да дават контрол. Резюме Swift 6 е важна стъпка напред в начина, по който се управлява съвместната търговия, като предлага на разработчиците по-голям контрол, предсказуемост и сигурност.Въпреки че кривата на обучението може да е стръмна в началото, разбирането на тези концепции отваря вратата за изграждане на високо отзивчиви и устойчиви приложения.Като продължаваме да изследваме по-дълбоките аспекти на новия модел на съвместната търговия на Swift, е ясно, че тези промени полагат основите за бъдещето на безопасното и мащабируемо разработване на приложения.