Fonctionnement du développement multi-thread iOS

Une série d'articles sur le développement multi-thread iOS

Le concept de développement multithread iOS

Thread pour le développement multithread iOS

GCD pour le développement multithread iOS

Fonctionnement du développement multi-thread iOS

Sécurité des threads du développement multithread iOS

Le développement multithread est un élément indispensable des tâches de développement quotidiennes. Les technologies de développement multithread couramment utilisées dans le développement iOS incluent GCD、Operation, Thread. Cet article explique Operationprincipalement les connaissances pertinentes et les détails d'utilisation de .

Introduction

OperationIl s'agit d'un ensemble complet de solutions multi-threading fournies par Apple, en fait, il est basé sur une encapsulation de niveau GCDsupérieur et est complètement orienté objet. Comparé à GCD, il est plus simple à utiliser et le code est plus lisible. De nombreuses tâches multithread, y compris les requêtes réseau et la compression d'image, utilisent bien Operation. Bien sûr, Operation doit également être utilisé en conjonction avec OperationQueuece rôle important. Operation est une classe abstraite et ses sous-classes doivent être utilisées pour l'utiliser. Vous pouvez l'implémenter ou utiliser ses sous-classes définies : BlockOperation. Créez un objet de la sous-classe Operation et ajoutez l'objet à la file d'attente OperationQueue pour exécution.

BlockOperation

tâche unique

let op = BlockOperation{
    print("单任务:(Thread.current)")
}
op.start()

résultat de l'opération :

单任务:<NSThread: 0x6000008e03c0>{number = 1, name = main}
let op = BlockOperation()
op.addExecutionBlock {
    print("任务一:(Thread.current)")
}
op.start()

résultat de l'opération :

任务一:<NSThread: 0x600002e28080>{number = 1, name = main}

Résumé :
Dans le cas de l'utilisation de BlockOperation seul pour effectuer une opération dans le thread principal, l'opération est effectuée dans le thread actuel et aucun nouveau thread n'est ouvert.

Multitâche

private func testOperationBlock() {

    let op = BlockOperation()

    ///任务一
    op.addExecutionBlock {
        print("任务一:\(Thread.current)")
    }

    ///任务二
    op.addExecutionBlock {
        print("任务二:\(Thread.current)")
    }

    ///任务三
    op.addExecutionBlock {
        print("任务三:\(Thread.current)")
    }
    op.start()
    print("结束")
}

résultat:

任务二:<NSThread: 0x600003ee8900>{number = 6, name = (null)}
任务一:<NSThread: 0x600003ead6c0>{number = 7, name = (null)}
任务三:<NSThread: 0x600003ea4900>{number = 1, name = main}
结束

Résumer:

  1. BlockOperation fournit également une méthode addExecutionBlock, via addExecutionBlock : des opérations supplémentaires peuvent être ajoutées à BlockOperation. Ces opérations (y compris celles de blockOperationWithBlock) peuvent être exécutées simultanément (simultanément) dans différents threads. Il est considéré comme terminé uniquement lorsque toutes les opérations associées ont terminé leur exécution.
  2. 使用子类 BlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。
  3. 一般情况下,如果一个 BlockOperation 对象封装了多个操作。BlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

自定义 Operation

非并发 Operation

对于非并发的操作,在子类的 Operation 中,只需要复写 main() 就可以了。
在 main() 方法中,写一些任务执行的代码 等,另外在子类中可能还需要写一些初始化方法,以及一些访问读取数据的方法等等。
至于 Operation 的几种状态,我们是不需要关心的,当 main() 方法执行完毕,即 Operation 任务结束。
Operation 类中还提供了 cancel() 的方法,所以在 Operation 执行的时候需要判断是否已经取消了,因为取消操作可能在开始之前就执行了,也可能在任务执行过程中,所以代码中需要加入 isCancelled 的判断。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let op = MyOpertaion(data: "自定义Operation")
        op.start()
    }
}

class NonConcurrentOperation: Operation {
    var data: Any?
    init(data: Any) {
        super.init()
        self.data = data
    }
    
    override func main() {
        var isDone = false
        while !isCancelled && !isDone {
            // 在此方法中做一些相关业务操作等,并在完成后将isDone设置为true
            print("自定义Operation:\(Thread.current)")
            // ......
            isDone = true
        }
    }
}

运行结果:

自定义Operation:<NSThread: 0x6000030781c0>{number = 1, name = main}

并发 Operation

Operation 对象默认以同步方式执行,也就是说,在调用 start() 方法的线程中执行任务。但是,由于 OperationQueue 为非并发操作提供线程,所以大多数 Operation 仍然是异步运行的。但是,如果我们手动使用 Operation,不用 OperationQueue,并且仍然希望它们异步运行,则必须采取适当的操作来确保能够做到这一点。我们可以通过将 Operation 对象定义为并发操作来实现这一点。至于并发的 Operation,稍微有些复杂了,因为这里面的状态需要开发人员来管控。

创建一个并发的 Operation,则至少需要复写以下的方法和属性:

  • start()
  • isAsynchronous
  • isExecuting
  • isFinished
import Foundation

public class ConcurrentOperation: Operation {

    public var completedBlock: (() -> Void)?

    public override var isExecuting: Bool{
        return _executing
    }

    public override var isFinished: Bool{
        return _finished
    }
    
    public override var isAsynchronous: Bool{
        return true
    }

    // MARK: -

    // MARK: 利用 KVO 来通知 Operation 的 isExecuting(是否正在进行中),以及 isFinished(是否已经完成)

    //指定用于记录任务是否执行
    private var _executing:Bool = false{
        // kvo isExecuting
        willSet{
            willChangeValue(forKey: ModifyState.isExecuting.rawValue)
        }
        didSet{
            didChangeValue(forKey: ModifyState.isExecuting.rawValue)
        }
    }

    // 指定用于记录任务是否完成
    private var _finished:Bool = false{
        // kvo isFinished
        willSet{
            willChangeValue(forKey: ModifyState.isFinished.rawValue)
        }
        didSet{
            didChangeValue(forKey: ModifyState.isFinished.rawValue)
        }
    }
    
    // MARK: -

    /// 修改状态枚举(重写状态的字段标识)
    private enum ModifyState: String{
        case isExecuting = "isExecuting"
        case isFinished = "isFinished"

    }
    
    // MARK: 重写 start() 方法,开辟新的线程执行需要的耗时工作。
    // MARK: 注意! 不能调用父类的 start() 方法。
    // MARK: 因为 Operation 有 cancel() 方法来取消操作,而且我们并不知道在何时取消,所以我们需要在几个地方注意是否取消了操作.
    public override func start() {
        // 检测取消状态
        if isCancelled {
            done()
            return
        }
        // 修改状态 -> 执行
        _executing = true
        // 开启任务->并行,完成回调测试
        startTask()
    }

    // 开启任务(模拟耗时任务)
    private func startTask(){
        DispatchQueue.global().async { [weak self] in
            print("线程:",Thread.current)
            // 耗时
            sleep(2)
            for i in 0...2{
                print("\(i)")
            }

            // 检测状态
            if self?.isCancelled ?? false{
                self?.done()
                return
            }

            DispatchQueue.main.async { [weak self] in
                // 完成
                self?.completedBlock?()
                self?.done()
            }
        }
    }

    // 重写取消
    public override func cancel() {
        // 加锁保证线程安全
        objc_sync_enter(self)
        done()
        objc_sync_exit(self)
    }
    
    // 自定义 cancel
    private func done() {
        super.cancel()
        print("done start",isCancelled)
        if(_executing) {
            _finished = true
            _executing = false
        }
        print("done end",isCancelled)
    }
}

创建队列

OperationQueue 一共有主队列、自定义队列两种队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

主队列

凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执行)。

主队列获取方法:

let mainQueue = OperationQueue.main

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let mainQueue = OperationQueue.main
        let operation1 = BlockOperation {
            print("任务一:\(Thread.current)")
        }
        
        let opertaion2 = BlockOperation {
            print("任务二:\(Thread.current)")
        }
        
        let opertaion3 = BlockOperation {
            print("任务三:\(Thread.current)")
        }
        
        let opertaion4 = BlockOperation {
            print("任务四:\(Thread.current)")
        }
        
        mainQueue.addOperation(operation1)
        mainQueue.addOperation(opertaion2)
        mainQueue.addOperation(opertaion3)
        mainQueue.addOperation(opertaion4)
    }
}

运行结果:

任务一:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务二:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务三:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务四:<NSThread: 0x6000009bc280>{number = 1, name = main}

自定义队列

添加到这种队列中的操作,就会自动放到子线程中执行。

自定义队列创建方法

let queue = OperationQueue()

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let queue = OperationQueue()
        // 最大并发数
        queue.maxConcurrentOperationCount = 4
        let operation1 = BlockOperation {
            print("任务一 = \(Thread.current)")
        }
        let operation2 = BlockOperation {
            print("任务二 = \(Thread.current)")
        }
        let operation3 = BlockOperation {
            print("任务三 = \(Thread.current)")
        }
        let operation4 = BlockOperation {
            print("任务四 = \(Thread.current)")
        }
        queue.addOperation(operation1)
        queue.addOperation(operation2)
        queue.addOperation(operation3)
        queue.addOperation(operation4)
    }
}

运行结果:

任务三 = <NSThread: 0x600000bd6980>{number = 4, name = (null)}
任务二 = <NSThread: 0x600000ba1dc0>{number = 7, name = (null)}
任务一 = <NSThread: 0x600000bf6500>{number = 6, name = (null)}
任务四 = <NSThread: 0x600000bf7100>{number = 5, name = (null)}

OperationQueue 控制串行执行、并发执行

最大并发操作数:maxConcurrentOperationCount

  • maxConcurrentOperationCount 默认情况下为 -1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 等于 1 时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于 1 时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min {自己设定的值,系统设定的默认最大值}。

maxConcurrentOperationCount = 1 时

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print("start")
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1; // 串行队列
        queue.addOperation {
            sleep(2)
            print("0---\(Date())\(Thread.current)")
        }
        queue.addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
        }
        let operation1 = BlockOperation.init {
            sleep(2)
            print("2---\(Date())\(Thread.current)")
        }
        operation1.addExecutionBlock {
            sleep(2)
            print("3---\(Date())\(Thread.current)")
        }
        queue.addOperation(operation1)
        print("end")
    }
}

运行结果:

start
end
0---2021-07-21 08:30:46 +0000<NSThread: 0x600003f23140>{number = 6, name = (null)}
1---2021-07-21 08:30:48 +0000<NSThread: 0x600003f2d540>{number = 5, name = (null)}
2---2021-07-21 08:30:50 +0000<NSThread: 0x600003f23140>{number = 6, name = (null)}
3---2021-07-21 08:30:50 +0000<NSThread: 0x600003f2d540>{number = 5, name = (null)}

maxConcurrentOperationCount > 1 时

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print("start")
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 4; // 串行队列
        queue.addOperation {
            sleep(2)
            print("0---\(Date())\(Thread.current)")
        }
        queue.addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
        }
        let operation1 = BlockOperation.init {
            sleep(2)
            print("2---\(Date())\(Thread.current)")
        }
        operation1.addExecutionBlock {
            sleep(2)
            print("3---\(Date())\(Thread.current)")
        }
        queue.addOperation(operation1)
        print("end")
    }
}

运行结果:

start
end
0---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa89c0>{number = 2, name = (null)}
1---2021-07-21 08:33:23 +0000<NSThread: 0x600001aace00>{number = 6, name = (null)}
2---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa07c0>{number = 4, name = (null)}
3---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa85c0>{number = 3, name = (null)}

Operation 操作依赖

Operation、OperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。Operation 提供了 3 个接口供我们管理和查看依赖。

添加依赖

func addDependency(_ op: Operation); 添加依赖,使当前操作依赖于操作 op 的完成。

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        testDependency()
    }
    
    //测试op依赖关系
    //A,B - C
    //C,D - E
    func testDependency(){
        let opA = BlockOperation()
        let opB = BlockOperation()
        let opC = BlockOperation()
        let opD = BlockOperation()
        let opE = BlockOperation()
        ///创建任务
        opA.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("A--\(i)")
                }
            }
        }
        
        opB.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("B--\(i)")
                }
            }
        }
        
        opC.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("C--\(i)")
                }
            }
        }
        
        opD.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("D--\(i)")
                }
            }
            
        }
        
        opE.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("E--\(i)")
                }
            }
        }
        
        ///添加依赖
        opC.addDependency(opA)
        opC.addDependency(opB)
        opE.addDependency(opC)
        opE.addDependency(opD)
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 6
        queue.addOperations([opA,opB,opC,opD,opE], waitUntilFinished: false)
        print("end")
    }
}

移除依赖

func removeDependency(_ op: Operation); 移除依赖,取消当前操作对操作 op 的依赖。

获取依赖

var dependencies: [Operation] { get }; 在当前操作开始执行之前完成执行的所有操作对象数组。

小心死锁: 如果Operation之间互相依赖,比如队列 A 中的 Operation1 依赖Operation2, 而 Operation2 依赖 Operation3Operation3 依赖 Operation1, 这就会陷入互相等待的死锁。

Operation 优先级

Operation 提供了 queuePriority(优先级)属性,queuePriority 属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是 normal。但是我们可以通过 setQueuePriority: 方法来改变当前操作在同一队列中的执行优先级。

优先级的取值:

public enum QueuePriority : Int {
  case veryLow = -8
  case low = -4
  case normal = 0
  case high = 4
  case veryHigh = 8
}

上边我们说过:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。 

那么,什么样的操作才是进入就绪状态的操作呢?
当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有 4 个优先级都是 normal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这 4 个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。

  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。

  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。

  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        testQueuePriority()
    }
    
    func testQueuePriority(){
        let opA = BlockOperation()
        let opB = BlockOperation()
        let opC = BlockOperation()
        opA.addExecutionBlock {
            print("任务一:\(Thread.current)")
        }
        opB.addExecutionBlock {
            print("任务二:\(Thread.current)")
        }
        opC.addExecutionBlock {
            print("任务三:\(Thread.current)")
        }
        opA.queuePriority = .low
        opB.queuePriority = .high
        opC.queuePriority = .normal
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 3
        queue.addOperations([opA, opB, opC], waitUntilFinished: false)
    }
}

Operation、OperationQueue 线程间的通信

在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        communication()
    }
    ///线程间通信
    func communication() {
        OperationQueue().addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
            OperationQueue.main.addOperation({
                sleep(2)
                print("2---\(Date())\(Thread.current)")
            })
        }
    }
}

可以看到:通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作。

Operation、OperationQueue 线程同步和线程安全

线程同步

  • Synchronisation des threads :
    on peut comprendre que le thread A et le thread B coopèrent ensemble. Lorsque A s'exécute dans une certaine mesure, cela dépend d'un certain résultat du thread B, il s'arrête donc et indique que B est en cours d'exécution ; B s'exécute selon les mots , puis donne le résultat à A ; Continuer.

  • Si chaque thread n'a que des opérations de lecture sur les variables globales et les variables statiques, mais pas d'opérations d'écriture, de manière générale, cette variable globale est thread-safe ; si plusieurs threads effectuent des opérations d'écriture (changement de variables) en même temps, il est généralement nécessaire de considérez la synchronisation des threads, sinon la sécurité des threads pourrait être compromise.

sécurité des fils

  • Sécurité des threads :
    si votre code est dans un processus avec plusieurs threads exécutés en même temps, ces threads peuvent exécuter ce code en même temps. Si le résultat de chaque exécution est le même que le résultat d'une exécution à un seul thread et que les valeurs des autres variables sont les mêmes que prévu, il est thread-safe.

  • Solution thread-safe :
    vous pouvez verrouiller le thread et lorsqu'un thread effectue l'opération, les autres threads ne sont pas autorisés à fonctionner. Il existe de nombreuses façons d'implémenter le verrouillage de thread dans iOS. @synchronized, NSLock, RecursiveLock, NSCondition, NSConditionLock, pthread_mutex, dispatch_semaphore, OSSpinLock, atomic(property) set/get, etc. Ici, nous utilisons l'objet NSLock pour résoudre le problème de synchronisation des threads. Les objets NSLock peuvent assurer la sécurité des threads en appelant la méthode lock lors de la saisie du verrou et en appelant la méthode unlock lors du déverrouillage.

Considérez le code thread-safe :

class ViewController: UIViewController {
    var ticketSurplusCount = 50
    override func viewDidLoad() {
        super.viewDidLoad()
        
        ///1.1 创建代表北京火车票售卖窗口
        let operationForBeiJing = OperationQueue()
        operationForBeiJing.maxConcurrentOperationCount = 1;
        ///1.2 创建卖票操作 op1
        let op1 = BlockOperation{ 
            self.saleTicketSafe()
        }
        ///1.3 添加操作
        operationForBeiJing.addOperation(op1)
        
        
        ///2.1创建代表上海火车票售卖窗口
        let operationForShangHai = OperationQueue()
        operationForShangHai.maxConcurrentOperationCount = 1;
        ///2.2创建卖票操作 op2
        let op2 = BlockOperation{
            self.saleTicketSafe()
        }
        ///2.3 添加操作
        operationForShangHai.addOperation(op2)
    }
    
    
    private func saleTicketSafe(){
        while true {
            objc_sync_enter(self)
            if self.ticketSurplusCount > 0 {
                self.ticketSurplusCount-=1;
                print("剩余票数:\(self.ticketSurplusCount) 窗口:\(Thread.current)")
                sleep(2)
            }
            objc_sync_exit(self)
            
            if self.ticketSurplusCount <= 0 {
                print("所有火车票均已售完")
                break
            }
        }
    }
}

Guess you like

Origin juejin.im/post/7116167016653783077