Конкурентное программирование

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

NSThread

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

Grand Central Dispatch

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

Одна из таких технологий — Grand Central Dispatch, сокращенно GCD. Ее особенностью является портированная как есть Си-подобная стилистика: все реализовано на функциях со специфичными названиями и типами.

Работая с GCD, надо знать ряд вещей.

  1. Хотя очереди можно создавать, лучше воспользоваться готовыми системными.
  2. Есть два вида очередей: последовательные — задания в них выполняются по одному в порядке добавления, и конкурентные (все системные такие, кроме исключения ниже) — включающие распараллеливание.
  3. Существует единственная главная последовательная очередь, в которой должны производиться изменения, связанные с графическим интерфейсом — это нельзя забывать.
  4. Задания представляют собой блоки. Добавлять их в очередь можно либо синхронно, с ожиданием завершения блока, либо асинхронно, с возвращением контроля сразу.
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 используется в качестве инструмента.

Relationships between operations, queues, GCD and threads

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

import Foundation

let queue = NSOperationQueue()
let operation = NSBlockOperation {
    print(NSOperationQueue.currentQueue())
}

queue.addOperation(operation)

// Or equivalently
queue.addOperationWithBlock {
    print(NSOperationQueue.currentQueue())
}

Так и сложных, требующих создания дочерних классов от NSOperation. И здесь важны моменты.

  1. Стартовая логика должна находиться в методе start операций. Он будет вызываться очередью.
  2. У операций есть несколько статусных свойств:
    • ready — готовность к началу выполнения. По умолчанию — true.
    • executing — нахождение в процессе работы. По умолчанию — false.
    • finished — окончание, вследствие завершения или отмены. По умолчанию — false.
    • cancelled — отмена. По умолчанию — false.
    В подклассах необходимо вовремя и верно задавать как минимум finished и executing, потому что они участвуют в работе очередей и зависимостей. Остальные необязательно: ready является лишь дополнительным условием старта операции, основное — завершение всех зависимостей (finished у них должно быть true), если они есть; cancelled устанавливается при вызове метода cancel. Также, при изменении значений этих свойств, потребуется рассылка уведомлений методами willChangeValueForKey и didChangeValueForKey механизма KVO для соответствующих ключей, иначе очереди и зависимые операции не узнают об этом.
  3. Все операции в очереди выполняются конкурентно. Если же нужна определенная последовательность, это делается через зависимости (по-простому — ожидания одними операциями завершения других).
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)
    }
}

Скачать Concurrency.zip

Дополнительные материалы: