Конкурентное программирование
Мобильные устройства уже давно содержат многоядерные процессоры, открывающие доступ к многопоточности. Но не все так просто. Так как система сама не в состоянии верно, с учетом всех зависимостей разделить код по потокам, эта возможность вынесена в API и доступна разработчикам. Существует несколько способов распараллеливания задач.
NSThread
Низкоуровневый подход, наиболее сложный и наименее предпочтительный из всех — прямая работа с потоками через NSThread. В подавляющем большинстве случаев этот способ нецелесообразен, лучше сразу перейти к технологиям более высокого уровня, основанным на очередях.
Grand Central Dispatch
Преимущества конкурентного программирования с применением очередей огромны. Разработчик избавляется от ручного управления потоками, учета системных ресурсов, доступности, а также приоритезации и распределения задач.
Одна из таких технологий — Grand Central Dispatch, сокращенно GCD. Ее особенностью является портированная как есть Си-подобная стилистика: все реализовано на функциях со специфичными названиями и типами.
Работая с GCD, надо знать ряд вещей.
- Хотя очереди можно создавать, лучше воспользоваться готовыми системными.
- Есть два вида очередей: последовательные — задания в них выполняются по одному в порядке добавления, и конкурентные (все системные такие, кроме исключения ниже) — включающие распараллеливание.
- Существует единственная главная последовательная очередь, в которой должны производиться изменения, связанные с графическим интерфейсом — это нельзя забывать.
- Задания представляют собой блоки. Добавлять их в очередь можно либо синхронно, с ожиданием завершения блока, либо асинхронно, с возвращением контроля сразу.
import UIKit
class ViewController: UIViewController {
var label: UILabel!
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collision: UICollisionBehavior!
override func viewDidLoad() {
super.viewDidLoad()
label = UILabel(frame: CGRect(x: 100, y: 50, width: 100, height: 50))
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [self.label])
collision = UICollisionBehavior(items: [self.label])
collision.translatesReferenceBoundsIntoBoundary = true
view.addSubview(label)
// Asynchronously submit long calculations to the system global concurrent queue
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
let result = "\(self.complexCalculation())"
// Back to the main serial queue for update GUI
dispatch_async(dispatch_get_main_queue()) {
self.label.text = result
self.animator.addBehavior(self.gravity)
self.animator.addBehavior(self.collision)
}
}
}
// Long running method
func complexCalculation() -> Double {
return M_PI
}
}
NSOperationQueue
Другая технология — очереди операций. В отличие от GCD, она построена по модели ООП: очереди — объекты NSOperationQueue, задания — объекты NSOperation. Но в то же время GCD используется в качестве инструмента.
Эти очереди хорошо подходят как для простых случаев, когда операциями могут выступать банальные объектные обертки блоков.
import Foundation
let queue = NSOperationQueue()
let operation = NSBlockOperation {
print(NSOperationQueue.currentQueue())
}
queue.addOperation(operation)
// Or equivalently
queue.addOperationWithBlock {
print(NSOperationQueue.currentQueue())
}
Так и сложных, требующих создания дочерних классов от NSOperation. И здесь важны моменты.
- Стартовая логика должна находиться в методе start операций. Он будет вызываться очередью.
- У операций есть несколько статусных свойств:
- ready — готовность к началу выполнения. По умолчанию — true.
- executing — нахождение в процессе работы. По умолчанию — false.
- finished — окончание, вследствие завершения или отмены. По умолчанию — false.
- cancelled — отмена. По умолчанию — false.
- Все операции в очереди выполняются конкурентно. Если же нужна определенная последовательность, это делается через зависимости (по-простому — ожидания одними операциями завершения других).
import UIKit
// Utility class for image data
class Picture {
var url: NSURL
var originalImage: UIImage!
var filteredImage: UIImage!
init(url: String) {
self.url = NSURL(string: url)!
}
}
// Base image operations class
class ImageOperation: NSOperation {
// MARK: - Properties
// Private executing and finished properties with KVO notifications
private var isExecuting = false {
willSet {
willChangeValueForKey("isExecuting")
}
didSet {
didChangeValueForKey("isExecuting")
}
}
private var isFinished = false {
willSet {
willChangeValueForKey("isFinished")
}
didSet {
didChangeValueForKey("isFinished")
}
}
override var executing: Bool {
get {
return isExecuting
}
set {
isExecuting = newValue
}
}
override var finished: Bool {
get {
return isFinished
}
set {
isFinished = newValue
}
}
}
class ImageDownloadOperation: ImageOperation {
// MARK: - Properties
private var picture: Picture!
lazy var imageDownloadTask: NSURLSessionDataTask = {
return NSURLSession.sharedSession().dataTaskWithURL(self.picture.url,
completionHandler:
{
(data: NSData?,
response: NSURLResponse?,
error: NSError?) -> Void in
if error == nil {
self.picture.originalImage = UIImage(data: data!)!
self.executing = false
self.finished = true
} else {
print(error)
self.executing = false
self.finished = true
self.cancel()
}
})
} ()
// MARK: - Initialization
init(picture: Picture) {
super.init()
self.picture = picture
}
override func start() {
executing = true
// If operation is cancelled, stop process
guard !cancelled else {
executing = false
finished = true
return
}
imageDownloadTask.resume()
}
}
class ImageFilterOperation: ImageOperation {
// MARK: - Properties
private var picture: Picture!
// MARK: - Initialization
init(picture: Picture) {
super.init()
self.picture = picture
}
override func start() {
executing = true
// If operation is cancelled, stop process
guard !cancelled else {
executing = false
finished = true
return
}
// Make filtered image
let inputImage = CIImage(image: picture.originalImage)
let context = CIContext(EAGLContext: EAGLContext(API: EAGLRenderingAPI.OpenGLES2))
let filter = CIFilter(name: "CIPhotoEffectMono")!
filter.setValue(inputImage, forKey: kCIInputImageKey)
let outputImage = filter.outputImage
let outputCGImage = context.createCGImage(outputImage!, fromRect: outputImage!.extent)
guard !cancelled else {
executing = false
finished = true
return
}
picture.filteredImage = UIImage(CGImage: outputCGImage)
executing = false
finished = true
}
}
class ViewController: UIViewController {
let queue = NSOperationQueue()
let picture = Picture(url:
"http://valery.bashkatov.org/images/concurrent-programming/sample.jpg")
let originalImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
let filteredImageView = UIImageView(frame: CGRect(x: 0, y: 200, width: 300, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(originalImageView)
view.addSubview(filteredImageView)
/*
Create operations with dependencies: originalImageDownload - originalImageShow
|
originalImageFilter - filteredImageShow
*/
let originalImageDownload = ImageDownloadOperation(picture: picture)
let originalImageShow = NSBlockOperation {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.originalImageView.image = self.picture.originalImage
}
}
originalImageShow.addDependency(originalImageDownload)
let originalImageFilter = ImageFilterOperation(picture: picture)
originalImageFilter.addDependency(originalImageDownload)
let filteredImageShow = NSBlockOperation {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.filteredImageView.image = self.picture.filteredImage
}
}
filteredImageShow.addDependency(originalImageFilter)
queue.addOperations([originalImageDownload,
originalImageShow,
originalImageFilter,
filteredImageShow], waitUntilFinished: false)
}
}
Дополнительные материалы:
- Threading Programming Guide
- Concurrency Programming Guide
- Advanced NSOperations
- NSOperation
- Grand Central Dispatch Tutorial for Swift: Part 1
- Grand Central Dispatch Tutorial for Swift: Part 2
- NSOperation and NSOperationQueue Tutorial in Swift
- iOS Concurrency: Getting Started with NSOperation and Dispatch Queues