Здравствуй, Хабр. Меня зовут Даниил, я iOS-инженер в Ситимобил. Около нескольких месяцев прошло с того момента, как стало возможным погонять на Xcode 13 beta новую свифтовую асинхронность. Заинтересовавшись, я провел немного времени в изучении нюансов работы механизма, и сейчас хотел бы поделиться своим структурированным пониманием async/await. Не буду лезть в дебри, но уверен, что стоит показать не только внешние преимущества, но и некоторые внутренние улучшения.  

Не могу не напомнить, что продукт всё ещё в стадии бета, и он достаточно отличается от того, что в итоге будет в релизной версии. Для получения самого свежего API лучше использовать последний тулчейн.

Для тех, кто хочет покопаться самостоятельно, вот материалы для изучения:

  • WWDC

  • Proposals — расширенная версия того, что можно увидеть в сессиях WWDC.

  • swift forum evolution — вообще, советую читать этот форум с небольшой периодичностью, клад для iOS-разработчика. 

  • Блог коллеги из Spotify — разобрано устройство акторов, но в кишки async/await тоже скоро появятся.

Итак, приступим.

Есть ли нюансы?

Есть.

  • Начнём с грустного: async/await поддерживается начиная с 15-й iOS и не является обратно-развертываемым. (стало известно, что поддержка будет с iOS 13!). Этот факт вызвал бурную дискуссию, в рамках которой инженеры Apple пояснили, что фича требует поддержки нового рантайма (подробнее об этом поговорим ниже). К слову, котлиновские корутины такой необходимости не имеют, что, видимо, свидетельствует о разном техническом стеке в реализации механизмов.

    Предположу, что какая-никакая совместимость с предыдущими версиями технически все же возможна, но не гарантирует ожидаемого перфоманса - такой расклад инженеров, вероятно, не устроил. 

  • Поддержка async/await - функций интегрирована в некоторые системные SDK, например, URLSession, HealthKit или NSDocument. Пока что выглядит скудновато, но  радует, что уже есть некая точка входа новой технологии в существующий проект: ничего не мешает начать строить свой транспортный слой с новой свифтовой многопоточностью.

Какие преимущества над уже имеющимися решениям, тем же самым GCD?

Вполне резонный вопрос. Их можно разделить на несколько составляющих - на фантик от конфетки и на саму конфетку.

Фантик” во многих статьях был уже досконально разобран - это визуальная эстетичность кода. Читать код стало на порядок легче, отчасти от того, что мы избегаем callback hell’ов - на мой взгляд, существенный плюс. Это, в свою очередь, снижает вероятность допустить ошибку - забыть вызвать completionHandler, из-за нарушить логику работы программы, теперь нельзя. Да и что тут говорить, код, с закосом под синхронный, стал намного элегантнее. Вот это:

func obtainFirstCarsharing(
  completionHandler: @escaping (CarsharingCarDetail?) -> Void) 
{
   fetchCars { [weak self] cars in
       guard let self = self, let firstCar = cars.first else {
           completionHandler(nil)
           return
       }
       self.fetchCarDetail(withId: firstCar.id) { detail in
           completionHandler(detail)
       }
   }
}

Теперь может выглядеть так:

func obtainFirstCarsharing() async throws -> CarsharingCarDetail {
   let allCars = try await fetchCars()
   guard let firstCarId = allCars.first?.id else { throw NSError() }
  
   return try await fetchCarDetail(with: firstCarId)
}

Замечу, что асинхронная функция, помимо результирующей модели, может вернуть ошибку - это нормальное поведение, которое разработчик может закладывать. В таком случае у нас есть возможность обрабатывать ошибки посредством try/catch.

Теперь о “конфетке”. Один из существенных плюсов в том, что async/await является неблокирующим механизмом. Сразу отмечу, что неблокирующий тут не равно непрерывный / синхронный. На это слово надо взглянуть под другим ракурсом - неблокирующим механизм является для потока. Что это значит?

Взглянем на примеры:

   let queue = DispatchQueue(label: "citymobil.queue.com")
    
   queue.sync { /* Execute WorkItem */ }

   // ----------------------------
      
   let semaphore = DispatchSemaphore(value: 0)
    
   semaphore.wait()

   // ----------------------------

   let _ = try await service.fetchCars()

Рассмотрим поведение потока с очередью, которая вызывает sync-метод — синхронно выполняет какую-нибудь WorkItem-задачу. В месте вызова sync поток блокируется и доступ к нему возвращается только после исполнения sync-замыкания.

С семафорами ситуация схожа, если не хуже: они, очевидно, находятся вне философии очередей - могут заблокировать какой-либо поток, в котором выполняется WorkItem, отданный очереди.

В случае с async/await синхронное выполнение метода приостанавливается: точкой приостановки является await, при этом сам поток не простаивает в ожидании. Как это возможно? 

Вернемся к уже упомянутому участку кода:

func obtainFirstCarsharing() async throws -> CarsharingCarDetail {
   let allCars = try await fetchCars()
   guard let firstCarId = allCars.first?.id else { throw NSError() }
  
   return try await fetchCarDetail(with: firstCarId)
}

Здесь поток, на котором выполняется метод, доходит до вызова fetchCars. Сразу после него приостанавливается дальнейшее синхронное выполнение инструкций: метод перестает владеть потоком и отдает «владение» им системе, тем самым сообщая ей, что он временно освобожден от работы и может перейти к выполнению более приоритетных задач. Если такие задачи есть, то система направляет поток на их выполнение. Когда выяснится, что более приоритетных задач уже нет, то система направит поток на выполнение fetchCars. Замечу, что приостановок может быть несколько. Когда fetchCars в конечном счете выполнится, некоторый поток продолжит выполнять дальнейшие инструкции в теле метода.

Тут стоит держать в голове пару моментов:

  • Поток, в котором выполнялся код до await, и который подхватил дальнейшее выполнение после, не обязательно будет одним и тем же. 

  • Несмотря на то, что в сниппете кода нет коллбеков, глобальное состояние приложения во время приостановки (там, где await), может кардинально поменяться - это обязательно нужно держать в голове.

Хочется дополнить механизм работы еще одним примером и сравнить разницу в поведении между новым и старых механизмом.

Ниже видим код, в котором с использованием GCD мы асинхронно, в бэкгруанде, запускаем 32 задачи, в каждой из которых синхронно исполняем еще какой-нибудь блок для работы:

 let syncQueue = DispatchQueue(
    label: "queue.sync.com", 
    attributes: .concurrent
 )

 for i in 1...32 {
    DispatchQueue.global().async {
      syncQueue.sync { /* do some work */ }
    }
 }

Здесь включаются в работу большое количество потоков. При этом каждое переключение между ними (context switch) становится все более ресурсоемким для системы при большом его количестве. Несмотря на то, что чего-то критичного в этих переключениях нет - context switch внутри одного процесса в общем то происходит достаточно быстро, и в большинстве случаев мы можем себе позволить не задумываться о нем - заблокированный поток, де факто, держит свой стек и занимает память. В довесок, мы можем легко воспроизвести ситуацию, где исчерпаем рабочие потоки, тем самым воссоздав thread explosion (взрыв потоков).

Мы можем избежать такой ситуации, грамотно спроектировав работу с многопоточным кодом - например, использовать здесь concurrentPerform или ненулевые семафоры. Но Apple, кажется, "встроил" подобные оптимизации в систему:

Аналогичный код, переписанный с async/await, на условном двухъядерном устройстве будет гонять по одному потоку, которые не станут простаивать в ожидании, а стало быть и переключения между ними не будет. Вместо этого, переключение будет происходить преимущественно внутри одного потока между continuation — объектами (чуть ниже вернемся к ним), и будет сводиться к переключению между методами. Apple заявляет, что это на порядок легче для системы.

Все выше - это следствие, которое возникает благодаря причине — новому пулу потоков (cooperative thread pool). Новый пул ограничивает параллелизм, тем самым, обеспечивая состояние как на картинке выше. Можно выработать правило - количество “работающих” одновременно потоков < количество ядер

Пазл с минимальной поддерживаемой iOS 15, кажется, сложился.

AsyncSequence

Тривиальный пример асинхронной последовательности
Тривиальный пример асинхронной последовательности

Вместе с async/await была представлена асинхронная последовательность, подобная обычной Sequence, с тем условием, что каждый элемент последовательности здесь достается асинхронно. Не хочется особо останавливаться на нем, скажу лишь, что при создании такой структуры необходимо по аналогии с обычной последовательностью реализовать async методы makeIterator и nextElement . Также заметим, что исполнение тела цикла в примере выше последовательное.

Все замечательно, но как совмещать async/await с привычным нам интерфейсом?

У нас достаточно много обращений в сеть, и каждый такой запрос, равно как и любой другой асинхронный, построен на коллбэках. Допустим, наш транспортный слой остался нетронутым, но мы хотим перевести API такого сервиса на async/await, под капотом используя коллбэки. Как это сделать?

Примерно так
Примерно так

Apple предоставляет глобальную функцию WithCheckedThrowingContinuation (аналогично есть API без возможности бросить ошибку - WithCheckedContinuation) которая является асинхронной, и при этом имеет кложур с параметром continuation (уже знакомый нам) в качестве аргумента.

В примере выше, внутри кложура, мы вызываем метод fetchCars уже с коллбэком, и после получения результата (результат получаем вызовом resume метода у continuation) возобновляем дальнейшее выполнение async-метода fetchCars. Отмечу, что resume необходимо вызвать обязательно, и исключительно один раз, иначе нас ждет краш. Аргумент метода resume, в свою очередь, может быть структурой типа Error или Result<Model, Error>

Есть и противоположная сторона интеграции асинхронного паттерна с не асинхронным. Разработчику определенно нужна возможность вызывать асинхронные функции из синхронного контекста, например, во viewModel выполнить сетевой запрос и обновить UI после. Сделать это можно внутри блока Task (в ранних версиях бетки - async) и TaskDetached (в ранних версиях бетки - asyncDetached):

@MainActor
func syncMethodUpdate() {
    Task {
        print("step 1 \(Thread.current)")
        let cars = try await service.fetchCars()
        print("step 2 \(Thread.current)")
        await updateUI(with: cars)
    }
    print("step 3 \(Thread.current)")
}

@MainActor
func syncMethodUpdateDetached() {
    TaskDetached {
        print("step 1 \(Thread.current)")
        let cars = try await service.fetchCars()
        print("step 2 \(Thread.current)")
        await updateUI(with: cars)
    }
    
    print("step 3 \(Thread.current)")
}

На минуту абстрагируемся от разницы между ними, и заметим, что возвращаемый тип функций — Task . Итак, мы приходим к еще одной важной сущности — Task (задача).

Task - Это базовый юнит многопоточности, каждая async-функция выполняется в Task. Попросту говоря, Task для асинхронной функции — то же самое, что поток для синхронной. Они, безусловно, заслуживают отдельной статьи, но я попытаюсь вкратце передать их суть. Задачи имеют приоритет, их можно отменять, и они могут находиться в трёх состояниях - приостановленные, выполняющиеся и завершенные. Но самое главное, что они могут быть структурированными и неструктурированными.

Структурированные — это задачи, которые находятся в иерархии (в дереве задач, состоящем из задач родительских и дочерних) и имеют ряд свойств: наличие родительской задачи; ограниченный жизненный цикл, который зависит от времени жизни родителя; более того, они наследуют приоритет родителя и отменяются по цепочке, если родительская задача была отменена. 

Неструктурированные задачи, напротив, не удовлетворяют полностью этим критериям, и нужны для того, чтобы управлять их временем жизни вручную. Например, если мы хотим создать задачу нажатием на кнопку в интерфейсе и отменить ее, если нажав на другую кнопку отмены. Более живой пример - когда у нас есть потребность создать задачу для ячейки коллекции, когда она появилась на экране, и отменить ее, если она быстро с этого экрана ушла.

Итак, Task и TaskDetached из сниппета выше — неструктурированные задачи. Чем они отличаются?

Результат выводов с Task
Результат выводов с Task
Результат выводов с TaskDetached
Результат выводов с TaskDetached

Видим, что Task наследует атрибуты контекста, из которого был вызван (isCancelled свойство, приоритет и тд) — все print были вызваны из главного потока.
TaskDetached, в свою очередь, не наследует ничего. Из сниппета видно, что каждый print тут был вызван из разных потоков, при том, два из них не на главном. Более того, заметим, что поток до вызова await, и после - различается, о чем ранее уже упоминалось. Profit.

Теперь поговорим про структурированные задачи. До сих пор все вызовы цепочек из await в теле метода выполнялись последовательно. При этом, конечно, параллелизм в работе с асинхронным кодом необходим. И достигается он посредством async let задач:

Task {
     async let detail1 = service.fetchCarDetail(with: "432")
     async let detail2 = service.fetchCarDetail(with: "231")  
     async let detail3 = service.fetchCarDetail(with: "123")

     let details = try await [detail1, detail2, detail3]
}

В данном случае создаются child-таски (detail1, detail2, detail3), которые начинают немедленно и параллельно выполняться; поток, при этом, продолжает исполнять дальнейшие инструкции внутри метода. На строчке с вызовом await поток приостанавливается по уже известному нам сценарию.

Несмотря на возможности async let - задач, мы можем ее использовать, имея фиксированное количество операций. Такой тип задач не подойдет, если количество  вызовов того же fetchCarDetail зависит от массива идентификаторов. Для этих целей Apple предоставляет группу - TaskGroup, которая создается вызовом функции withThrowingTaskGroup и в качестве аргумента кложура имеет свойство group:

let carsharingDetails = try await withThrowingTaskGroup(
     of: CarsharingCarDetail.self,
     returning: [CarsharingCarDetail].self
 ) { group in
        
     for id in ["id123", "id231", "id939", "id333", "id493"] {
         group.async(priority: .utility) {
            return try await carsharingDetail(id: id)
         }
     }
                        
     var details: [CarsharingCarDetail] = []
            
     for try await detail in group {
         details.append(detail)
     }
            
     return details
 }

С помощью вызова group.async можно асинхронно и параллельно запускать задачи. Когда дойдем до точки получения результата - циклом пройдемся по группе и получаем на выходе необходимый массив моделей. Внимательный читатель может заметить, что group конфирмит AsyncSequence.

Как переключиться на главный поток?

Вернемся к нашему примеру:

TaskDetached {
     let cars = try await service.fetchCars()
     await updateUI(with: cars)
 }

Для того, чтобы метод updateUI вызвался на главном потоке, его необходимо пометить атрибутом @MainActor:

@MainActor
func updateUI(with cars: [CarsharingCar]) {
  // update
}

Акторы - это зверь, который выходит за рамки этой статьи. Тем ни менее, вы можете легко найти информацию о них на приведенных в начале статьи ссылках - материала более чем достаточно.

Заключение

Мир многопоточности, как можем заметить, обширен, а с недавних пор и в iOS среде. Сегодня нам удалось познакомиться с еще одним механизмом, но если хочется пойти дальше, пожалуйста - на гитхабе есть репо с реализацией корутин.

Если же есть вопросы изи замечания по статье - велком в комментарии.

UPD:

Разработчики Apple реализуют обратное развертывание новой многопоточности, если получится, станет доступно с iOS 13 через какое то время.

Комментарии (31)


  1. haymob
    05.08.2021 12:01
    +3

    Лет через пять будет актуально.


    1. haymob
      05.08.2021 13:19
      -1

      Приятное сообщество, в посте про разработку под iOS минусую люди далекие от этого.


    1. creker
      05.08.2021 13:28
      -1

      Да, с учетом iOS 15, все это будет распространено очень нескоро


      1. haymob
        05.08.2021 14:33

        Большинство топ приложений которые делают кассу в AppStore, поддерживают iOS11/12, таким отношением эппл плюёт в колодец разработчикам этих приложений в лицо. Async/await нужно было внедрить ещё в swift 3 до stable abi, и не удивляться бурной дискуссии, ИМХО.


  1. creker
    05.08.2021 13:40
    +1

    Пазл с минимальной поддерживаемой iOS 15, кажется, сложился.

    Да как-то не особо. Ничего не мешало этот тред пул поставлять как библиотеку для старых iOS. Можно посмотреть на C#, где подобная практика повсеместна. Это ведь всего лишь тред пул, которому явно ничего хитрого не надо от самой iOS.

    Выглядит все прям под копирку с C#. Особенно с этими всеми контекстами и тасками. Не сказать, что рад видеть очередные асинки, но наверное лучше, чем ничего. И в этом плане более интересны как раз акторы. Там свифт пытается быть безопасными наподобие раста.


  1. SergeyMild
    05.08.2021 14:12
    -1

    Просто не понятно, почему они не смогли все это засунуть в более старые версии ios, тут конечно в этом плане kotlin выглядит победителем, на ios все это можно будет использовать ооочень не скоро


    1. haymob
      05.08.2021 14:53

      Очень понятно, что бы вы пошли и купили новый айфон, в случае если вы разработчик, пользователи для которых вы пишете приложения)


      1. pronvit
        06.08.2021 09:24

        Только вот iOS 15 будет поддерживать 6s, которому в этом году 6 лет.


        1. haymob
          06.08.2021 09:57

          Вы ставили iOS 15 на 6s? iPad Air 2 даже браузер не тянет, при этом на iOS 13 летал, на 14 приемлемо работает, 15 увы.


          1. pronvit
            06.08.2021 12:32
            +1

            Ожидаемо, но все же Эппл - не та компания, которая так уж прям заставляет покупать новое. Тестировать на старье по крайней мере можно:)


          1. GeoSD
            27.11.2021 18:26

            На старом SE стоит iOS 15. Работает вполне себе хорошо.


  1. Kanut
    05.08.2021 14:41

    Поток, в котором выполнялся код до await, и который подхватил дальнейшее выполнение после, не обязательно будет одним и тем же.

    То есть нет возможности "принудительно" заставить выполняться в том же потоке/ с тем же SynchronizationContext? По аналогу с ConfigureAwait(true) в шарпе?


    1. vorobyevthedeveloper Автор
      06.08.2021 23:45

      Нет, нельзя даже принудительно гарантировать исполнение в том же потоке / контексте . Если возникает необходимость в последовательном исполнении какой-либо секции (или нужно, например, не допустить гонку состояний), то эту секцию правильно будет поместить в актор


  1. Gorthauer87
    05.08.2021 16:10

    Интересно, а тут async функции начинают исполняться до того момента, когда кто-то решил их awaitнуть или нет?


    1. vorobyevthedeveloper Автор
      06.08.2021 21:51

      Да, в том случае, например, если была создана `async let` задача


  1. Gummilion
    05.08.2021 16:14

    Я читал, что вроде бы сам async/await будет работать в предыдущих версиях iOS, а вот для continuation нужна будет именно 15 версия - а теперь оказывается и сам async/await только на новой версии будет работать?


    1. haymob
      05.08.2021 16:42

      После SwiftUI vs Combine вы другого ожидали? Opaque Result Types была первая фича языка привязанная к версии ОС, и похоже поставили на поток, ждем babel для свифта.


      1. Gummilion
        05.08.2021 17:30

        Проще тогда уже PromiseKit использовать - это немного другой подход к асинхронности, но по крайней мере везде будет работать.


      1. creker
        05.08.2021 23:02

        Этого следовало ожидать после того, как они решили рантайм свифта поставлять с iOS, а не с каждым приложением. Я конечно все понимаю, но можно было и другое решение придумать для большого размера приложений.


    1. vorobyevthedeveloper Автор
      06.08.2021 00:10

      В предыдущих версиях iOS работать не будет, увы


      1. Insp1red
        24.09.2021 11:09
        +1

        upd: таки быть поддержке async/await в предыдщих версиях! https://github.com/apple/swift/pull/39051

        будет доступно для macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0


        1. vorobyevthedeveloper Автор
          24.09.2021 11:10

          Вы правы! Дополнил в статье этот момент


  1. kovserg
    05.08.2021 20:15
    -1

    Интересно swift сможет собрать все грабли которые собрал csharp с async/await или найдёт новые?


    1. creker
      05.08.2021 22:59
      -2

      Подозреваю, что сможет вагон своих еще подобрать. Мое мнение, проблемы происходят из самой сути асинков — разделение языка на синхронную и асинхронную части и необходимости аккуратно лавировать между ними. Обычно большинство проблем в шарпе именно от этого. Поэтому печально видеть, что очередной язык поддался этому. Да еще совершили туже самую ошибку — добавляют асинки в уже устоявшийся язык, где все синхронное или на колбэках. Если смотреть другие экземпляры, то там наверное все еще даже хуже. Питон — ужас сплошной. Раст — не имею личного опыта, но блог посты не внушают. Плюсы — с этими все и так понятно.


      1. Kanut
        06.08.2021 11:00

        Мое мнение, проблемы происходят из самой сути асинков — разделение языка на синхронную и асинхронную части и необходимости аккуратно лавировать между ними. Обычно большинство проблем в шарпе именно от этого.


        Если у вас проблемы только в этом, то кто мешает везде использовать только асинхронную часть? Ну то есть банально вместо функций возвращающиx void/Т сделать функции возвращающие Task/Task?


        1. creker
          06.08.2021 12:20

          Начнем с того, что это невозможно из-за внешних зависимостей. Даже фреймворк годами приводили в нормальный вид. Ну и проблема здесь фундаментальная, а не в том, какие где методы возвращаются. Это невозможно обойти, хоть пусть все методы будут асинхронные. Кооперативная многозадачность как никак. Код постоянно прыгает из синхронной в асинхронную части и в этих местах задействована куча внутренних компонентов фреймворка. Контексты, шедулеры, кодогенерация, вот это все. На этих стыках и возникают все проблемы, которые выше в видео описывают. Дедлоки, гонки, потерянные исключения, исключения, кладущие процесс и т.д. и т.п. Все происходит из-за разделения языка на две половины. Асинки работают просто в примитивных примерах, но в реальности это переусложненный и неочевидный подход к асинхронности. И если в плюса и расте я еще понимаю причины его использовать - stackless корутины таки дешевле для них и это важно, но свифт - он мог спокойно использовать stackful корутины.


          1. Kanut
            06.08.2021 12:55
            +1

            Начнем с того, что это невозможно из-за внешних зависимостей.

            В большинстве случаев элементарно решается wrapper'ом на стыке с этими самыми зависимостями.


            Код постоянно прыгает из синхронной в асинхронную части и в этих местах задействована куча внутренних компонентов фреймворка.

            Нет никаких синхронной или асинхронной частей. Есть выполнение в одном или разных потоках или в одном или разных контекстах синхронизации. И это всё вполне себе регулируется. Async/await в шарпах это просто "сахарная обёртка" вокруг ThreadPool. Никакой магии там нет.


            И если в плюса и расте я еще понимаю причины его использовать — stackless корутины таки дешевле для них и это важно, но свифт — он мог спокойно использовать stackful корутины.

            Я не сообо в курсе как оно там работает в плюса и расте. И отвечал в контексте шарпа.


            1. creker
              09.08.2021 12:14

              В большинстве случаев элементарно решается wrapper'ом на стыке с этими самыми зависимостями.

              Я надеюсь очевидно, что это плохое решение. Собственно, в этом и проблема. Чтобы код стал асинхронным надо его либо править, либо пытаться обернуть его во что-то, что все равно не сделает его нормальным асинхронным. Как минимум он не будет поддерживать CancellationToken, что чрезвычайно важно. Если он повиснет, то намертво заберет на себя поток. А если есть другие механизмы отмены, то начинается строительство других костылей, чтобы подружить это все. И в этом всем фундаментальная проблема асинков и деления языка на две половины.

              Нет никаких синхронной или асинхронной частей

              Еще как есть. Пока код выполняется синхронно его невозможно приостановить или как-то на него подействовать без явной реализации этих механизмов. Отсюда и приходят неочевидные дедлоки. Асинки это кооперативная многозадачность и это проблема.

              Async/await в шарпах это просто "сахарная обёртка" вокруг ThreadPool.

              Это не так. ThreadPool это всего лишь один из вариантов контекстов. Есть еще контексты с главным потоком, где многопоточности нет и можно огрести кучу проблем. Есть контексты asp.net свои хитрые.

              Я не сообо в курсе как оно там работает в плюса и расте. И отвечал в контексте шарпа.

              Зато я писал глобально о всем механизме, который примерно одинаково работает во всех языках с примерно одинаковыми проблемами, которые присущи stackless корутинам в общем. И мне не нравится, что в языки продолжают ткать этот механизм, когда есть решения лучше.


              1. Kanut
                09.08.2021 12:33
                +2

                Я надеюсь очевидно, что это плохое решение.

                Нет не очевидно. Как минимум мне.

                Чтобы код стал асинхронным надо его либо править, либо пытаться обернуть его во что-то, что все равно не сделает его нормальным асинхронным.

                Нет, не надо. Вы просто запускаете его асинхронно в вашем контексте синхронизации и это делает его «нормально асинхронным». При желании можете без контекста, но тут уже надо понимать когда это можно делать.

                Да и вообще у вас в любом асинхронном коде будет кусок «обычного» кода, который вы «оборачиваете» в асинхронную оболочку.

                Как минимум он не будет поддерживать CancellationToken, что чрезвычайно важно.

                Это важно если вам нужен CancellationToken. Или вообще подобный механизм. И если он вам нужен, а у сторонней библиотки он реализован как-то по другому, то это точно так же решется через wrapper. По крайней мере я ни разу не видел варианта чтобы это не решалось. Вы можете привести пример?

                Если он повиснет, то намертво заберет на себя поток.

                Это решается таймаутами и/или тем же WaitAny(Task[], TimeSpan).

                Это не так. ThreadPool это всего лишь один из вариантов контекстов. Есть еще контексты с главным потоком, где многопоточности нет и можно огрести кучу проблем.

                Что такое «главный поток» в вашем понимании? И ThreadPool это не вариант контекста. Это механизм работы с потоками на котором сверху пристроен async/await. Уберите ThreadPool и никакого async/await в шарпе у вас не будет.

                Зато я писал глобально о всем механизме, который примерно одинаково работает во всех языках с примерно одинаковыми проблемами

                Я очень сомневаюсь что он примерно одинаково работает во всех языках. Особенно учитывая что базируется это всё на очень разных вещах.


              1. Gargoni
                29.08.2021 18:17
                -1

                Похоже что у вас каша в голове в асинхронном программировании. Stackless корутины это круто и скоро захватят мир.


  1. lexd5
    27.09.2021 19:32

    Уже 7 лет использую полную поддержку async/await во всех iOS проектах. Не испытываю проблем ни с обратной совместимостью, ни с производительностью ​

    Всё что для этого надо сделать - использовать Xamarin.Native (не путать с Xamarin.Forms).

    А если серьезно, то если MS завезли весь свой NetStandart на все версии всех ОС от Apple, то странно, почему Apple ограничивает всё 15й версией.