Лучшая практика многопоточности внутри функции, которая возвращает значение, Swift

У меня есть вопрос, который может быть не конкретно о реализации, а скорее о совете/лучшей практике.

Я работаю над классом в Swift, который получает данные из онлайн-источника в формате JSON. Я хочу иметь в этом классе определенные методы, которые подключаются к онлайн-источнику и возвращают результат в виде словаря. Функция может быть такой:

func getListFromOnline()->[String:String]{

    var resultList : [String:String]=[:]
    ...
    /*
    Some HTTP request is sent to the online source with NSURLRequest

    Then I parse the result and assign it to the resultList
    */
    ...

    return resultList
}

То, что я получаю прямо сейчас в этой реализации, без многопоточности, resultList, по-видимому, возвращается до выполнения выборки из онлайн-источника. Неудивительно, что это приводит к сбою в программе.

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

Или дело вовсе не в многопоточности и я не вижу здесь очевидного решения?


person lakeoffm    schedule 27.06.2015    source источник
comment
Вы можете использовать обработчик завершения, который вызывается после получения результатов.   -  person ABakerSmith    schedule 27.06.2015


Ответы (3)


В разработке Swift/Objective-C есть три простых и полностью ожидаемых подхода к этой проблеме, и ни одно из этих решений не включает метод, возвращающий значение напрямую. Вы можете написать код, который ожидает завершения асинхронной части (блокируя поток), а затем возвращает значение, и в некоторых случаях это делается в некоторых собственных библиотеках Apple, но я не собираюсь рассказать о подходе, потому что на самом деле это не очень хорошая идея.


Первый подход включает в себя блоки завершения.

Когда наш метод будет выполнять некоторый асинхронный код, мы можем передать блок кода для выполнения всякий раз, когда выполняется асинхронная работа. Такой метод будет выглядеть следующим образом:

func asynchStuff(completionHandler: ([String:String]) -> Void) {
   // do asynchronous stuff, building a [String:String]
   let result: [String: String] = // the result we got async
   completionHandler(result)
}

Не забудьте вызвать completionHandler() в том же потоке, в котором был вызван asynchStuff. (Этот пример не демонстрирует этого.)


Второй подход включает делегатов и протоколы.

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

@objc protocol AsyncDelegate {
    func complete(result: [String:String]
}

Теперь наш асинхронный рабочий:

class AsyncWorker {
    weak var delegate: AsyncDelegate?

    func doAsyncWork() {
        // like before, do async work...
        let result: [String: String] = // the result we got async
        self.delegate?.complete(result)
    }
}

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


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


Какой подход вы используете, полностью зависит от вашего конкретного варианта использования. В Objective-C я часто предпочитал делегирование блочному подходу (хотя иногда блоки правильны), но Swift позволяет нам передавать обычные функции/методы в качестве нашего блочного аргумента, поэтому он немного склоняет меня к использованию блочного подхода для Swift. , но тем не менее, используйте правильный инструмент для правильной работы, как всегда.


Я хотел расширить этот ответ, чтобы обратиться к обратному вызову (будь то блоки или делегаты) в соответствующем потоке. Я не уверен, есть ли способ сделать это с помощью GCD, но мы можем сделать это с помощью NSOperationQueues.

func asyncStuff(completionHandler: ([String:String]) -> Void) {
    let currentQueue = NSOperationQueue.currentQueue()
    someObject.doItsAsyncStuff(someArg, completion: { 
        result: [String:String] in
        currentQueue.addOperationWithBlock() {
            completionHandler(result)
        }
    }
}
person nhgrif    schedule 27.06.2015
comment
Зависит от того, используете ли вы GCD или NSOperations. Я не могу обновить ответ прямо сейчас (по телефону), но я могу обновить его позже. @GoZoner - person nhgrif; 28.06.2015
comment
GCD (я думал, что функция «получить текущую очередь» устарела. См.: stackoverflow.com/a/17678556/1286639) . - person GoZoner; 28.06.2015
comment
Вы всегда можете использовать NSOperationQueues по-прежнему... Я обновлю этот ответ в любом случае, когда у меня будет возможность провести небольшое собственное исследование. - person nhgrif; 28.06.2015
comment
@GoZoner GCD dispatch_get_current_queue устарел, но currentQueue из NSOperationQueue не устарел. Сказав это, NSURLSession и AFNetworking полностью избегают этой проблемы за счет (а) наличия параметра/свойства, для которого очередь запускать блоки завершения; и (b) иметь некоторое предопределенное поведение, если эта очередь nilNSURLSession по умолчанию будет собственная очередь, с AFNetworking по умолчанию будет основная очередь). - person Rob; 29.06.2015
comment
Спасибо за комментарии! Я, вероятно, буду использовать замыкание для кода, но у меня все еще есть вопрос к этому. Если я использую замыкание в классе, который подключается к онлайн-источнику, как мне передать полученные данные в класс, который вызвал этот класс с помощью асинхронных методов? Например, если у меня есть класс ViewController, который вызывает класс Async, как передать данные обратно в ViewController? - person lakeoffm; 29.06.2015
comment
Я имею в виду, что я мог бы легко иметь эти методы подключения в своем ViewController и отображать результаты в пользовательском интерфейсе асинхронно, но что, если я хочу сохранить эти методы в отдельном классе? - person lakeoffm; 29.06.2015
comment
Я не понимаю, о чем вы спрашиваете, но вы не можете обновлять пользовательский интерфейс асинхронно — вы можете обновлять пользовательский интерфейс только из основного потока. - person nhgrif; 29.06.2015
comment
@lakeoffm Если вы спрашиваете, как обновлять пользовательский интерфейс по мере получения и обработки данных, то в дополнение к блоку завершения или методу делегата, как сказал nhgrif, у вас также может быть блок выполнения или метод делегата. См. AFNetworking для примера на основе блоков. См. протоколы делегирования для NSURLConnection или NSURLSession для примера примера на основе протокола делегирования. Кстати, вам следует рассмотреть возможность принятия ответа nhgrif. Кроме того, не задавайте новых вопросов в комментариях, а (а) попробуйте; и (b) опубликовать новый вопрос, если это необходимо. - person Rob; 29.06.2015
comment
Не забудьте вызвать завершениеHandler() в том же потоке, в котором был вызван asynchStuff — почему это полезно? В вашем примере completionHandler(result) будет вызываться в любом потоке, в котором выполняется asynchStuff. Предположительно, это какой-то частный поток, предоставленный GCD - например, если asynchStuff был отправлен с dispatch_async(). Я бы подумал, что явно НЕ использует эту очередь (потому что это может быть какая-то выделенная очередь, особенно подходящая для выполнения asyncStuff, например очередь, связанная с вводом-выводом), но вместо этого одна из глобальных очередей GCD является гораздо лучшим подходом. - person CouchDeveloper; 04.07.2015
comment
Фрагмент кода моего примера не показывает код, необходимый для выполнения того, что я комментировал (отсюда и комментарий). Последний фрагмент, в котором использовалось NSOperationQueue, — это то, что я имел в виду под этим комментарием. Я постараюсь сделать это более ясным. - person nhgrif; 04.07.2015
comment
В вашем последнем примере с использованием NSOperationQueue мы ДОЛЖНЫ предположить, что сайт вызова представляет собой NSOperation, выполняющийся на NSOperationQueue. В противном случае ваш код не будет работать, так как этот метод класса currentQueue() возвращает необязательный параметр, который будет nil. И тем не менее, я не вижу преимущества выполнения завершения в NSOperationQueue call-site: call-site всегда может легко отправить в любую подходящую очередь, которая выполняет фактический код обработчика завершения. Опять же, если выбрать глобальную очередь GCD, она будет работать даже в тех случаях, когда сайт вызова не является NSOperation. - person CouchDeveloper; 04.07.2015
comment
Вы можете опубликовать свой собственный ответ. Ты прав. Завершение может отправляться в любую очередь, которую оно хочет. Речь идет не о том, чтобы предотвратить это, а о том, чтобы сделать поведение несколько ожидаемым. Асинхронный вызов очереди должен вернуться туда, откуда он был отправлен (если не указано иное). Альтернативой может быть отправка в глобальную очередь (и документирование этого поведения) или принятие очереди для обратного вызова (и обратный вызов в этой очереди). Я использовал все эти подходы в прошлом. - person nhgrif; 04.07.2015
comment
Я с вами - большинство людей могут ожидать, что обработчик завершения будет вызываться из любого контекста выполнения, который выполняет call-site. К сожалению, это возможно только (как в вашем примере) только при использовании NSOperations, отправленного в NSOperationQueues. У нас нет способа сделать это при использовании очередей GCD. :( - person CouchDeveloper; 04.07.2015

Краткий ответ: вы не можете. Это не то, как работает асинхронная обработка. Результат НИКОГДА не будет доступен во время возврата вашего метода. На самом деле ваш метод возвращает даже до того, как асинхронная обработка будет завершена.

Как говорит ABakerSmith в своем комментарии, вам следует добавить замыкание обработчика завершения (он же блок) в качестве параметра функции. Это блок кода, который передает вызывающая сторона. Вы пишете свой метод, чтобы после завершения загрузки асинхронных данных JSON он вызывал блок завершения.

Затем в вызывающей программе вы пишете свой код для передачи кода, который вы хотите выполнить после завершения загрузки В БЛОКЕ ЗАВЕРШЕНИЯ. Вы должны структурировать свою программу так, чтобы она могла работать без ваших данных (например, отображая пустое табличное представление) и само обновляться после поступления данных. Вы можете написать свой блок завершения, чтобы установить данные в модель данных для табличного представления, а затем вызвать reloadData для табличного представления.

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

person Duncan C    schedule 27.06.2015
comment
Нет. Ваш блок завершения должен выполняться в потоке, в котором был вызван ваш метод. - person nhgrif; 28.06.2015
comment
@nhgrif и Дункан: я не согласен: блок завершения следует вызывать либо в частном контексте выполнения (поток, очередь GCD, NSOperationQueue и т. д.), который недоступен с сайта вызова, либо в контексте выполнения, который был явно указан по call-site - если для этого есть параметр. Как правило, это позволяет избежать потенциальных взаимоблокировок и проблем с производительностью. Использование контекста выполнения, который был текущим на месте вызова, невозможно достичь в Mac OS/iOS, поскольку нет неявно заданного абстрактного определения текущего контекста выполнения. - person CouchDeveloper; 04.07.2015

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

    let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
    dispatch_async(dispatch_get_global_queue(priority, 0)) {

        // do some asynchronous work

        dispatch_async(dispatch_get_main_queue()) {

            // use returned data to update some UI on the main thread
        }
    }
person Ivan Rigamonti    schedule 28.06.2015