zoukankan      html  css  js  c++  java
  • NSOperation、NSOperationQueue(II)

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

    NSOperationQueue 创建的自定义队列同时具有串行、并发功能

    这里有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

    最大并发操作数:maxConcurrentOperationCount
    • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
    • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
    • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
     1 //设置 MaxConcurrentOperationCount(最大并发操作数)
     2 - (void)setMaxConcurrentOperationCount {
     3     
     4     // 1.创建队列
     5     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
     6     
     7     // 2.设置最大并发操作数
     8     queue.maxConcurrentOperationCount = 1; // 串行队列
     9     // queue.maxConcurrentOperationCount = 2; // 并发队列
    10     // queue.maxConcurrentOperationCount = 8; // 并发队列
    11     
    12     // 3.添加操作
    13     [queue addOperationWithBlock:^{
    14         for (int i = 0; i < 2; i++) {
    15             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    16             NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    17         }
    18     }];
    19     [queue addOperationWithBlock:^{
    20         for (int i = 0; i < 2; i++) {
    21             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    22             NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    23         }
    24     }];
    25     [queue addOperationWithBlock:^{
    26         for (int i = 0; i < 2; i++) {
    27             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    28             NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    29         }
    30     }];
    31     [queue addOperationWithBlock:^{
    32         for (int i = 0; i < 2; i++) {
    33             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    34             NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    35         }
    36     }];
    37 }
    View Code

    打印结果:

    1 <-------------------------NSOperationAndQueue 线程结束-------------------------->
    2 2018-03-31 16:14:45.902770+0800 StruggleSwift[14468:604379] 1---<NSThread: 0x60c000076e40>{number = 5, name = (null)}
    3 2018-03-31 16:14:49.192405+0800 StruggleSwift[14468:604379] 1---<NSThread: 0x60c000076e40>{number = 5, name = (null)}
    4 2018-03-31 16:14:54.864788+0800 StruggleSwift[14468:597389] 2---<NSThread: 0x60000007f200>{number = 3, name = (null)}
    5 2018-03-31 16:14:56.866890+0800 StruggleSwift[14468:597389] 2---<NSThread: 0x60000007f200>{number = 3, name = (null)}
    6 2018-03-31 16:14:58.869083+0800 StruggleSwift[14468:597389] 3---<NSThread: 0x60000007f200>{number = 3, name = (null)}
    7 2018-03-31 16:15:00.874565+0800 StruggleSwift[14468:597389] 3---<NSThread: 0x60000007f200>{number = 3, name = (null)}
    8 2018-03-31 16:15:02.875308+0800 StruggleSwift[14468:604379] 4---<NSThread: 0x60c000076e40>{number = 5, name = (null)}
    9 2018-03-31 16:15:04.877348+0800 StruggleSwift[14468:604379] 4---<NSThread: 0x60c000076e40>{number = 5, name = (null)}

    结论:当最大并发操作数为1时,操作是按顺序串行执行的,并且一个操作完成之后,下一个操作才开始执行。当最大操作并发数为2时,操作是并发执行的,可以同时执行两个操作。而开启线程数量是由系统决定的,不需要我们来管理。

    NSOperation 操作依赖

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

    1 - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
    2 
    3 - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
    4 
    5 @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。

    当然,我们经常用到的还是添加依赖操作。现在考虑这样的需求,比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。

    如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A。具体代码如下:

     1 /操作依赖
     2 //使用方法:addDependency:
     3 - (void)addDependency {
     4     
     5     // 1.创建队列
     6     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
     7     
     8     // 2.创建操作
     9     NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    10         for (int i = 0; i < 2; i++) {
    11             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    12             NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    13         }
    14     }];
    15     NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    16         for (int i = 0; i < 2; i++) {
    17             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    18             NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    19         }
    20     }];
    21     
    22     // 3.添加依赖
    23     [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
    24     
    25     // 4.添加操作到队列中
    26     [queue addOperation:op1];
    27     [queue addOperation:op2];
    28 }
    View Code

    打印结果:

    1 <-------------------------NSOperationAndQueue 线程结束-------------------------->
    2 2018-03-31 23:44:21.656997+0800 StruggleSwift[17855:844153] 1---<NSThread: 0x60c0000777c0>{number = 3, name = (null)}
    3 2018-03-31 23:44:24.756931+0800 StruggleSwift[17855:844153] 1---<NSThread: 0x60c0000777c0>{number = 3, name = (null)}
    4 2018-03-31 23:44:30.036394+0800 StruggleSwift[17855:875561] 2---<NSThread: 0x60800007a9c0>{number = 4, name = (null)}
    5 2018-03-31 23:44:33.012677+0800 StruggleSwift[17855:875561] 2---<NSThread: 0x60800007a9c0>{number = 4, name = (null)}
    View Code

    NSOperation 优先级

    NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
    1 // 优先级的取值
    2 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    3     NSOperationQueuePriorityVeryLow = -8L,
    4     NSOperationQueuePriorityLow = -4L,
    5     NSOperationQueuePriorityNormal = 0,
    6     NSOperationQueuePriorityHigh = 4,
    7     NSOperationQueuePriorityVeryHigh = 8
    8 };

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

    那么,什么样的操作才是进入就绪状态的操作呢?

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

    举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作: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 属性的作用对象。

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

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

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

    NSOperation、NSOperationQueue 线程间的通信

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

     1 #pragma mark -  线程间通信
     2 - (void)communication {
     3     
     4     // 1.创建队列
     5     NSOperationQueue *queue = [[NSOperationQueue alloc]init];
     6     
     7     // 2.添加操作
     8     [queue addOperationWithBlock:^{
     9         // 异步进行耗时操作
    10         for (int i = 0; i < 2; i++) {
    11             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    12             NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    13         }
    14         
    15         // 回到主线程
    16         [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    17             // 进行一些 UI 刷新等操作
    18             for (int i = 0; i < 2; i++) {
    19                 [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    20                 NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    21             }
    22         }];
    23     }];
    24 }
    View Code

    打印结果:

    <-------------------------NSOperationAndQueue 线程结束-------------------------->
    2018-04-01 08:57:11.278897+0800 StruggleSwift[824:13773] 1---<NSThread: 0x608000265d00>{number = 3, name = (null)}
    2018-04-01 08:57:17.168299+0800 StruggleSwift[824:13773] 1---<NSThread: 0x608000265d00>{number = 3, name = (null)}
    2018-04-01 08:57:28.123412+0800 StruggleSwift[824:13640] 2---<NSThread: 0x60400007bd40>{number = 1, name = main}
    2018-04-01 08:57:31.052426+0800 StruggleSwift[824:13640] 2---<NSThread: 0x60400007bd40>{number = 1, name = main}

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

    NSOperation、NSOperationQueue 线程同步和线程安全

    线程安全如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

    线程同步可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

    举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

    下面,我们模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
    场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

    NSOperation、NSOperationQueue 非线程安全

     1 #pragma mark -  NSOperation、NSOperationQueue 线程同步和线程安全
     2 //非线程安全:不使用 NSLock
     3 //初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
     4 - (void)initTicketStatusNotSave {
     5     NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
     6     
     7     self.ticketSurplusCount = 10;
     8     
     9     // 1.创建 queue1,queue1 代表北京火车票售卖窗口
    10     NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    11     queue1.maxConcurrentOperationCount = 1;
    12     
    13     // 2.创建 queue2,queue2 代表上海火车票售卖窗口
    14     NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    15     queue2.maxConcurrentOperationCount = 1;
    16     
    17     // 3.创建卖票操作 op1
    18     __weak typeof(self) weakSelf = self;
    19     NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    20         [weakSelf saleTicketNotSafe];
    21     }];
    22     
    23     // 4.创建卖票操作 op2
    24     NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    25         [weakSelf saleTicketNotSafe];
    26     }];
    27     
    28     // 5.添加操作,开始卖票
    29     [queue1 addOperation:op1];
    30     [queue2 addOperation:op2];
    31 }
    32 
    33 /**
    34  * 售卖火车票(非线程安全)
    35  */
    36 - (void)saleTicketNotSafe {
    37     while (1) {
    38         
    39         if (self.ticketSurplusCount > 0) {
    40             //如果还有票,继续售卖
    41             self.ticketSurplusCount--;
    42             NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
    43             [NSThread sleepForTimeInterval:0.2];
    44         } else {
    45             NSLog(@"所有火车票均已售完");
    46             break;
    47         }
    48     }
    49 }
    View Code

    打印结果:

     1 2018-04-01 09:13:03.617873+0800 StruggleSwift[1373:28962] currentThread---<NSThread: 0x60800006b880>{number = 1, name = main}
     2 <-------------------------NSOperationAndQueue 线程结束-------------------------->
     3 2018-04-01 09:13:19.360537+0800 StruggleSwift[1373:30935] 剩余票数:9 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
     4 2018-04-01 09:13:19.360562+0800 StruggleSwift[1373:31568] 剩余票数:8 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
     5 2018-04-01 09:13:19.565970+0800 StruggleSwift[1373:31568] 剩余票数:7 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
     6 2018-04-01 09:13:19.565970+0800 StruggleSwift[1373:30935] 剩余票数:7 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
     7 2018-04-01 09:13:19.767245+0800 StruggleSwift[1373:31568] 剩余票数:6 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
     8 2018-04-01 09:13:19.767245+0800 StruggleSwift[1373:30935] 剩余票数:6 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
     9 2018-04-01 09:13:19.972287+0800 StruggleSwift[1373:31568] 剩余票数:5 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
    10 2018-04-01 09:13:19.972537+0800 StruggleSwift[1373:30935] 剩余票数:4 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
    11 2018-04-01 09:13:20.175821+0800 StruggleSwift[1373:30935] 剩余票数:3 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
    12 2018-04-01 09:13:20.175823+0800 StruggleSwift[1373:31568] 剩余票数:3 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
    13 2018-04-01 09:13:20.381388+0800 StruggleSwift[1373:31568] 剩余票数:2 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
    14 2018-04-01 09:13:20.381398+0800 StruggleSwift[1373:30935] 剩余票数:1 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
    15 2018-04-01 09:13:20.587009+0800 StruggleSwift[1373:31568] 剩余票数:0 窗口:<NSThread: 0x604000260d80>{number = 8, name = (null)}
    16 2018-04-01 09:13:20.587009+0800 StruggleSwift[1373:30935] 剩余票数:0 窗口:<NSThread: 0x60c00007e740>{number = 7, name = (null)}
    17 2018-04-01 09:13:20.791763+0800 StruggleSwift[1373:31568] 所有火车票均已售完
    18 2018-04-01 09:13:20.791763+0800 StruggleSwift[1373:30935] 所有火车票均已售完

     结论:在不考虑线程安全,不使用 NSLock 情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题

     NSOperation、NSOperationQueue 线程安全

    线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。

    考虑线程安全的代码:

     1 //线程安全
     2 //初始化火车票数量、卖票窗口(线程安全)、并开始卖票
     3 - (void)initTicketStatusSave {
     4     NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
     5     
     6     self.ticketSurplusCount = 10;
     7     
     8     self.lock = [[NSLock alloc] init];  // 初始化 NSLock 对象
     9     
    10     // 1.创建 queue1,queue1 代表北京火车票售卖窗口
    11     NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    12     queue1.maxConcurrentOperationCount = 1;
    13     
    14     // 2.创建 queue2,queue2 代表上海火车票售卖窗口
    15     NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    16     queue2.maxConcurrentOperationCount = 1;
    17     
    18     // 3.创建卖票操作 op1
    19     __weak typeof(self) weakSelf = self;
    20     NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    21         [weakSelf saleTicketSafe];
    22     }];
    23     
    24     // 4.创建卖票操作 op2
    25     NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    26         [weakSelf saleTicketSafe];
    27     }];
    28     
    29     // 5.添加操作,开始卖票
    30     [queue1 addOperation:op1];
    31     [queue2 addOperation:op2];
    32 }
    33 
    34 /**
    35  * 售卖火车票(线程安全)
    36  */
    37 - (void)saleTicketSafe {
    38     while (1) {
    39         
    40         // 加锁
    41         [self.lock lock];
    42         
    43         if (self.ticketSurplusCount > 0) {
    44             //如果还有票,继续售卖
    45             self.ticketSurplusCount--;
    46             NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
    47             [NSThread sleepForTimeInterval:0.2];
    48         }
    49         
    50         // 解锁
    51         [self.lock unlock];
    52         
    53         if (self.ticketSurplusCount <= 0) {
    54             NSLog(@"所有火车票均已售完");
    55             break;
    56         }
    57     }
    58 }
    View Code

    打印结果:

     1 2018-04-01 09:49:16.070431+0800 StruggleSwift[1941:58765] currentThread---<NSThread: 0x60000006d880>{number = 1, name = main}
     2 (lldb) po self.lock
     3 <NSLock: 0x6040000b19a0>{name = nil}
     4 
     5 <-------------------------NSOperationAndQueue 线347250213结束-------------------------->
     6 2018-04-01 09:49:44.761455+0800 StruggleSwift[1941:58823] 剩余票数:9 窗口:<NSThread: 0x604000269940>{number = 3, name = (null)}
     7 2018-04-01 09:50:02.260390+0800 StruggleSwift[1941:59749] 剩余票数:8 窗口:<NSThread: 0x60000046af00>{number = 4, name = (null)}
     8 2018-04-01 09:50:10.237070+0800 StruggleSwift[1941:58823] 剩余票数:7 窗口:<NSThread: 0x604000269940>{number = 3, name = (null)}
     9 2018-04-01 09:50:10.442618+0800 StruggleSwift[1941:59749] 剩余票数:6 窗口:<NSThread: 0x60000046af00>{number = 4, name = (null)}
    10 2018-04-01 09:50:10.645708+0800 StruggleSwift[1941:58823] 剩余票数:5 窗口:<NSThread: 0x604000269940>{number = 3, name = (null)}
    11 2018-04-01 09:50:10.851185+0800 StruggleSwift[1941:59749] 剩余票数:4 窗口:<NSThread: 0x60000046af00>{number = 4, name = (null)}
    12 2018-04-01 09:50:11.056340+0800 StruggleSwift[1941:58823] 剩余票数:3 窗口:<NSThread: 0x604000269940>{number = 3, name = (null)}
    13 2018-04-01 09:50:11.259532+0800 StruggleSwift[1941:59749] 剩余票数:2 窗口:<NSThread: 0x60000046af00>{number = 4, name = (null)}
    14 2018-04-01 09:50:11.463973+0800 StruggleSwift[1941:58823] 剩余票数:1 窗口:<NSThread: 0x604000269940>{number = 3, name = (null)}
    15 2018-04-01 09:50:11.665998+0800 StruggleSwift[1941:59749] 剩余票数:0 窗口:<NSThread: 0x60000046af00>{number = 4, name = (null)}
    16 2018-04-01 09:50:11.870597+0800 StruggleSwift[1941:58823] 所有火车票均已售完
    17 2018-04-01 09:50:11.870597+0800 StruggleSwift[1941:59749] 所有火车票均已售完

    结论:考虑了线程安全,使用 NSLock 加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。

  • 相关阅读:
    LeetCode偶尔一题 —— 617. 合并二叉树
    《剑指offer》 —— 链表中倒数第k个节点
    《剑指offer》 —— 青蛙跳台阶问题
    《剑指offer》—— 二维数组中的查找
    《剑指offer》—— 替换空格
    《剑指offer》—— 合并两个排序的链表
    《剑指offer》—— 礼物的最大价值
    生成Nuget 源代码包来重用你的Asp.net MVC代码
    Pro ASP.Net Core MVC 6th 第四章
    Pro ASP.NET Core MVC 6th 第三章
  • 原文地址:https://www.cnblogs.com/EchoHG/p/8684895.html
Copyright © 2011-2022 走看看