美文网首页
多线程网络

多线程网络

作者: 蔚尼 | 来源:发表于2018-07-26 14:58 被阅读40次

下文中,前面介绍一些简单的概念。虽然简单,但还是需要了解。
后面介绍的pthread简单了解即可。GCD和NSOperation需要熟悉掌握如何运用。

不喜欢说很多引入的话,习惯用记笔记的方式,让文章满满干货不影响学习。首先是基本概念:

一.基本概念

1.基本概念

1.1进程

  • 在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,均运行在其专用且受保护的内存空间内

1.2线程

在线程中真正执行任务的

1.3进程和线程的关系

包含关系:进程内有多条线程,至少有一条

1.4多线程

一个进程中可以开启多条线程,每条线程可以并行执行不同的任务。

多线程原理

  • 同一时间,CPU只能处理1条线程,只有1条线程在工作;
  • 多线程并发执行,其实是CPU快速的在多条线程之间调度(切换)
  • 如果CPU调度线程足够快,就有多线程并发执行的假象

注意多线程数量不少越多越好,一般控制在3-5条。太多反而会降低每条线程的调度次数,降低效率。

优点

适当的提高程序的执行效率,CPU、内存的利用率。

缺点

  • 创建线程有开销:内存方面、时间方面都有
  • 如果开启大量线程,CPU会频繁调度线程,降低程序的性能

1.5主线程/UI线程、子线程

一个程序运行后,会默认开启一条线程,称为主线程/UI线程。

  • 主线程作用:显示/刷新UI、处理事件(点击、滚动)

  • 主线程实用注意:
    1.不要将耗时操作放到主线程,会导致界面卡顿;
    2.所有UI操作必须在主线程中执行;

  • 子线程/后台线程/非主线程:只有一条主线程,主线程以外的就是子线程/后台线程

二.线程的状态

线程状态有:新建、就绪、运行、阻塞、死亡 5个状态。

  • 如图【就绪状态、运行状态切换】当线程新建,开启线程后就进入“就绪状态”,CPU调度当前线程,就进入“运行状态”;

  • 当CPU调度其他线程到时候,当前线程就恢复到就绪状态;


    就绪状态、运行状态切换
  • 如图【阻塞状态】,当调用了sleep方法、或者等待同步锁的时候,运行中的线程切换为阻塞状态;线程对象移出可调度线程池。

  • 如图【所有状态】,阻塞状态的线程sleep到时、得到同步锁的时候,状态就由阻塞状态切换为就绪状态;线程状态回到可调度线程池,可让CPU调度当前线程;
    注意:阻塞状态需要切换到就绪状态后,才可以切换到运行状态。阻塞状态不能直接转换为运行状态;

  • 如图【所有状态】,当现场任务执行完毕,就会进入死亡状态

阻塞状态 所有状态

2.多线程

  • 如下图,多线程有这几种实现方案:pthread、NSThread、GCD、NSOperation。
  • GCD和NSOperation线程的生命周期是自动管理的,线程内部的任务执行完毕的时候,线程会释放,使用频率也高一些。
  • pthread几乎不用,所以pthread只是简单的介绍。
多线程的实现方案

三.pthread的使用

pthread使用较少,简单了解就行。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //phread创建线程
    /*
     01 包含头文件
     02 创建线程对象
     */
    pthread_t thread = nil;
    //03 创建线程
    /*
     第一个参数:线程对象的地址
     第二个参数:线程的属性(优先级)
     第三个参数:指向函数的指针   void *(*)(void *)
     第四个参数:传递给函数的参数
     */
    
    pthread_create(&thread, NULL, run, @"str");
    
    
}
//技巧:(*)改写成函数的名称,再补全参数
void *run(void * str){
 
    NSLog(@"%@,str:%@",[NSThread currentThread],str);
    return NULL;
}

四.NSThread

NSThread创建

1.动态创建

     //创建线程对象
    /*
     第一个参数:目标对象
     第一个参数:方法选择器
     第一个参数:传递给线程方法的参数
     */
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"123"];
    //启动线程
    [thread start];

2.静态创建

    //静态创建
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"123"];

3.直接分离出一条子线程

  • 当前线程执行操作
  
   [self performSelector:@selector(threadRun)];
   [self performSelector:@selector(threadRun) withObject:nil];
   [self performSelector:@selector(threadRun) withObject:nil afterDelay:2.0];
  • (在其他线程中)指定主线程执行操作
 [self performSelectorOnMainThread:@selector(threadRun) withObject:nil waitUntilDone:YES];
  • (在主线程中)指定其他线程执行操作
 //这里指定为某个线程
 [self performSelector:@selector(threadRun) onThread:newThread withObject:nil waitUntilDone:YES];

//这里指定为后台线程
 [self performSelectorInBackground:@selector(threadRun) withObject:nil];

NSThread常用方法

  • 获取当前线程
 [NSThread currentThread];
  • 获取主线程
 [NSThread mainThread];
  • 进入sleep状态
 [NSThread sleepForTimeInterval:1.0];

 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
  • 取消线程
 [newThread cancel];
  • 所有线程都停止
 [NSThread exit];
  • 设置名称
 thread1.name = @"thread1";
  • 设置优先级
    范围0.0-1.0,默认0.5
    优先级更高的线程,被CPU调度到的概率会更高
 thread1.threadPriority = 1.0;

五.线程安全

当多个售票员在售同一趟火车的票时,为了避免售出同一张票,我们需要使用互斥锁来避免这个问题。

  @synchronized(self){
        锁住的代码块
  }
  • 互斥锁使用的前提:多条线程抢夺同一块资源;
  • 锁定1份代码只用1把锁,用多把锁是无效的;
  • 互斥锁的优点:让多条线程按顺序执行任务。防止因多线程抢夺资源造成的数据安全问题;
  • 互斥锁的缺点:需要消耗大量的CPU资源;

互斥锁使用实例

创建3个线程代表3个售票员,可以售出的总票数totalCount为100。
3个线程都调用-(void)saleTicket{}方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //设置总票数
    self.totalCount = 100;
       
    self.thread01 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread02 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread03 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];

    self.thread01.name = @"售票员1";
    self.thread02.name = @"售票员2";
    self.thread03.name = @"售票员3";

    //线程启动
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];

}

saleTicket方法里面,while循环减少toltalCount,直到totalCount = 1。
3个线程都在调用saleTicket方法,为了避免重复减少就给减少部分的代码添加互斥锁。

@synchronized(self){}加锁的位置需要注意,saleTicket方法里面,如果加锁在while之前,就是要一个线程把while循环跳出去,即totalCount数量都减为0,下一个线程才能调用。那就变成了一个售票员把所有的票卖完了。

如果把锁加在while之后,一个线程循环了一遍,另一个线程立马就可以来进行下一遍循环了。不会导致一个售票员把所有的票卖完了。

  • 注意:加多把锁是无效的;加锁的位置;多个线程抢夺资源的时候才需要加锁;
    加锁的代码块,就是我在执行这部分的时候,你就不要执行这里。等我执行完这里你再来
-(void)saleTicket{
   
    //为代码添加互斥锁(互斥锁)
    //token:锁对象(要使用全局的对象),建议直接使用self
    //{}要加的代码块(就是我在执行这部分的时候,你就不要执行这里。等我执行完这里你再来)

    //当锁打开之后,会主动唤醒排队的线程
        while (1) {
            
        @synchronized(self){
            //售票
            //检查余票,如果有票就卖出,否则提示用户
            if (self.totalCount >0) {
                self.totalCount --;
                
                NSLog(@"%@售出一张,剩余:%ld",[NSThread currentThread].name,(long)self.totalCount);
            }else{
                NSLog(@"%@已卖完",[NSThread currentThread].name);
                break;
            }
        }
    }
   
}

六.GCD

GCD中增加了任务队列两个概念。

  • 任务:即要做的事情;
  • 队列:用于存放任务;

封装任务的函数:同步、异步

封装任务的函数有同步函数、异步函数。

  1. 同步函数:dispatch_sync
    1)不具备开线程的能力,不能开线程
    2)执行任务的方式:同步
    3)会阻塞当前线程并等待block中的任务执行完毕,然后当前线程才会继续往下运行。

  2. 异步函数:dispatch_async
    1)具备开线程的能力,可以开启线程
    2)执行任务方式:异步
    3)使用异步操作,当前线程会直接往下执行,不会阻塞当前线程。

GCD中的队列:并发、串行

  1. 并发队列:任务可以同时执行

GCD会FIFO先进先出的取出任务,只要第一个任务取出来之后,不用等待执行完,就可以接着取出第二个任务执行。CPU在多个线程之间切换调度,如果速度够快,看起来所有任务都是一起执行的。

  • 有两种并发队列:
    1)自己创建: dispatch_queue_create方法、DISPATCH_QUEUE_CONCURRENT类型
    2)全局并发队列:dispatch_get_global_queue
  1. 串行队列:任务必须一个接着一个的执行

GCD会FIFO先进先出的方式取出任务,第一个任务取出来之后,必须等待该任务执行完,才可以接着执行第二个任务

  • 串行队列有以下两种类型:
    1)自己创建:dispatch_queue_create方法、DISPATCH_QUEUE_SERIAL类型
    2)主队列:特殊的串行队列(和主线程相关联的)
    放在主队列中的任务,都会放在主线程中执行

不同任务、不同队列组合

不同的任务和不同的队列方式组合效果如下,原因后续分析:

异步 同步
串行 开启一条子线程,在子线程按顺序一个个执行 当前线程,按顺序一个个执行
并发 开启多条子线程,多个子线程同时执行(开启线程的个数不等于子线程的个数,由GCD自己决定的) 当前线程,按顺序一个个执行
主队列 主线程按顺序一个个串行执行 死锁

总结:
可以开线程的情况:异步函数,且是非主队列;
开线程条数:看队列,并行队列开N条,串行开1条;

1.异步+串行

GCD开启线程都是先创建队列,然后把任务添加到队列里面。

  • 自己创建队列:
    dispatch_queue_create第一个参数是:队列名称;
    第二个参数是队列类型:
    DISPATCH_QUEUE_CONCURRENT 并行
    DISPATCH_QUEUE_SERIAL 串行

  • 异步任务:dispatch_async

-(void)asyncSerial{
    
    //01-创建队列
    dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_SERIAL);
    
    //02-封装任务,把任务添加到队列
    dispatch_async(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
}

串行+异步

当number = 1,name = main的时候是主线程。
上面异步+串行的方式,从打印可以看出:开启了一个线程,在这个线程中按顺序串行执行任务;

2.异步+并发

-(void)asyncConcurent{
    
    //01-创建队列
    NSLog(@"--start---");
    dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_CONCURRENT);
    
    
    //02-封装任务,把任务添加到队列
    dispatch_async(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
    
    NSLog(@"--end---");

}

异步+并发

如上图:异步+并发 开启多条子线程,所有任务是并发执行的

3.异步+主队列

-(void)asyncMain{
    
    dispatch_queue_t que = dispatch_get_main_queue();
    
    //02-封装任务,把任务添加到队列
    dispatch_async(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
    
}
异步+主队列

主队列是串行的,添加在主队列中的任务都是在主线程中执行的。从上图也可以看出,异步+主队列 是在主线程按顺序一个个执行的。

4. 同步+ 串行

-(void)syncSerial{

    NSLog(@"--start---");

    dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_SERIAL);

    //02-封装任务,把任务添加到队列
    dispatch_sync(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
    
    NSLog(@"--end---");
}
同步+串行

同步不具备开启线程的能力,所以不开启线程;
同步会阻塞任务,需要一个任务执行完才能执行下一个;
从上图也可以看出,同步+串行 是在当前线程按顺序一个个执行任务。

5. 同步+ 并发

-(void)syncCurrent{
    
    //01-创建队列
    NSLog(@"--start---");
    dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_CONCURRENT);

    //02-封装任务,把任务添加到队列
    dispatch_sync(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
    
    NSLog(@"--end---");

}
同步+并发

从上图也可以看出,同步+ 并发和 同步串行一样, 是在当前线程按顺序一个个执行任务。

6. 同步+ 主队列

-(void)syncMain{
    
    //01-创建队列
    
    dispatch_queue_t que = dispatch_get_main_queue();
    
    NSLog(@"---------");
    
    //02-封装任务,把任务添加到队列
    dispatch_sync(que, ^{
        NSLog(@"1----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"2----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"3----%@",[NSThread currentThread]);
    });
    dispatch_sync(que, ^{
        NSLog(@"4----%@",[NSThread currentThread]);
    });
}
同步+主队列-->死锁

如上图,同步+主队列造成了死锁。
前提:
首先,同步会造成阻塞,要当前任务执行完之后才能执行下一个任务。
再次,主队列是串行的,一个任务执行完才能执行下一个。
主队列上面添加的任务,都是在主线程执行。

  1. 代码执行到257行的时候,因为是串行的,会把257行的任务添加到主队列中。等待主队列中当前的任务执行完后执行257行的任务。即到270行syncMain方法执行完后,执行257行添加的任务。

  2. 257行的任务是同步的,会阻塞当前线程。syncMain要执行完的前提是257行的任务执行完才可以。

所以257行的任务和syncMain都在等待对方执行完之后再执行。造成了死锁。

6.1 假设把上面同步+主队列的方法syncMain放到后台线程去调用,会怎么样?
- (void)viewDidLoad {
    [super viewDidLoad];
       
    //直接调用同步+主队列的方法:死锁
    //[self syncMain];
    
    //把方法放到后台线程执行:不会死锁
    [self performSelectorInBackground:@selector(syncMain) withObject:nil];
    
}
同步+主队列 放到后台线程调用

执行结果如上图,串行执行任务。
我们是在后台线程,对主线程添加同步任务。

  • 当我们添加257行的同步任务到主线程时,syncMain方法不在主线程,不需要等待syncMain方法执行完后再执行257行的任务。
    主线程此时没有在执行的任务,就执行了257行这个任务。所以syncMain方法就可以执行完成。就不会造成死锁。
6.2 为什么“3.异步+主队列”没有造成死锁呢?

3中是异步方式,不能对线程造成阻塞,所以不会造成死锁。

上面就介绍完同步、异步和串行、并发各种组合的结果。下面开始查看GCD的其他用法。

GCD-栅栏函数

  • 栅栏函数如下:
 dispatch_async(que, ^{
  });
  • 栅栏函数作用:
    【并发队列+异步】的情况下,开启多条线程并发执行任务的时候,栅栏函数可以设定某几个任务并发执行后,再并发执行某几个任务。

  • 举例
    例如,有4个任务是并发+异步的,那么会开启多条线程来并发执行这4个任务。要求,1、2并发执行完成,再并发执行任务3、4.

   //开启并发队列
    dispatch_queue_t que = dispatch_queue_create("que", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(que, ^{
        NSLog(@"1--------%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"2--------%@",[NSThread currentThread]);
    });
    //添加栅栏函数
    dispatch_barrier_sync(que, ^{
        NSLog(@"++++++++");
    });
    dispatch_async(que, ^{
        NSLog(@"3--------%@",[NSThread currentThread]);
    });
    dispatch_async(que, ^{
        NSLog(@"4--------%@",[NSThread currentThread]);
    });

首先,我们看未添加栅栏函数的打印结果:
任务执行顺序是3、1、2、4。是开启多条线程并发执行的。


未添加栅栏函数的打印结果

再看添加栅栏函数后的打印结果:
任务执行顺序是1、2、栅栏函数的任务、4、3。
达到目的任务1、2并发执行后再让3、4并发执行。


添加栅栏函数的打印结果

从上面可以看出,使用栅栏函数:
栅栏函数前面的任务执行完之后,执行栅栏任务,再执行栅栏函数后面的任务;

注意栅栏函数使用的时候,不能使用全局并发队列dispatch_get_global_queue(0, 0);;否则拦截不到任务

思考:栅栏函数要求是【异步+并发】,那为什么不能【同步+并发】呢,因为【同步+并发】组合就是在当前线程顺序执行了,没有必要使用栅栏函数来限制任务的执行顺序。同理,【异步+串行】、【同步+串行】添加栅栏函数没有意义。

GCD-快速迭代

平时我们会使用for循环来进行遍历,这相当于在当前线程串行执行。
GCD提供了一个快速迭代函数,让多个线程并发遍历。提高效率。

  • 快速迭代函数:
   dispatch_apply(10, que, ^(size_t i) {
    });

第一个参数:上面的10代表遍历10次
第二个参数:在哪个队列;我们使用并发队列,达到开启多个线程的效果
第三个参数:上面的i相当于for循环里面的i,遍历到第几个

  • 举例
    dispatch_queue_t que = dispatch_get_global_queue(0, 0);//全局并发
   // dispatch_queue_t que = dispatch_queue_create("queName", DISPATCH_QUEUE_CONCURRENT);

    dispatch_apply(10, que, ^(size_t i) {//i相当于执行到第几次
        
        NSLog(@"%zu----%@",i,[NSThread currentThread]);
    });
并发队列+快速遍历函数

上面例子中【并发+快速迭代】是开启多个子线程,和主线程一起并发执行任务。
注意如果使用【主队列+快速迭代】会造成死锁
注意如果使用【串行队列+快速迭代】:在主线程串行执行,和for循环一样的效果

GCD-队列组

上面讲述了GCD中的两大概念,任务、队列。现在介绍的队列组,就是来管理队列中的任务。

  • 使用步骤
    1.创建队列组
    2.创建队列
    3.创建队列组异步函数,关联对应的队列、队列组

  • 作用:监听任务的完成
    当一个组中的所有任务执行完成之后,可以通过监听函数得到。 dispatch_group_notify(group, que2, ^{ });

    //01 创建队列组
    dispatch_group_t group = dispatch_group_create();
    
    //02 创建队列
    dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);

    //03 封装任务、并监听任务的队列情况
    dispatch_group_async(group, que2, ^{
        NSLog(@"1--------%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, que2, ^{
        NSLog(@"2--------%@",[NSThread currentThread]);

    });
    dispatch_group_async(group, que2, ^{
        NSLog(@"3--------%@",[NSThread currentThread]);

    });
    
    
    //拦截通知:
    //这个函数里面的队列决定这里的block块在哪个线程执行(主线程、非主线程)
    //dispatch_group_notify:异步执行的(如下,先打印的end,再执行dispatch_group_notify内容的)
    dispatch_group_notify(group, que2, ^{
       
        NSLog(@"队列组中的任务都执行完毕-%@",[NSThread currentThread]);
    });
    
    NSLog(@"--end--");
  • 了解--队列组其他可以达到监听效果的写法
    也可以使用:dispatch_group_enter、dispatch_group_leave组合起来,达到监听的作用。
   dispatch_group_t group = dispatch_group_create();
    
    dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);
   

    //在该方法后面的任务会被队列组监听
    //dispatch_group_enter、dispatch_group_leave要成对出现
    dispatch_group_enter(group);
    dispatch_group_async(group, que2, ^{
        NSLog(@"2--------%@",[NSThread currentThread]);
   
        //监听到该任务已经执行完毕
        dispatch_group_leave(group);
        
    });
    
    dispatch_group_notify(group, que2, ^{
        
        NSLog(@"队列组中的任务都执行完毕-%@",[NSThread currentThread]);
    });
    
  • 了解--队列组调用函数方法
    队列组里面的任务可以是block的,也可以是调用函数的,如下:
-(void)other{
    
    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);

    //封装任务 (block方法)
    dispatch_group_async(group, que2, ^{
        NSLog(@"1--------%@",[NSThread currentThread]);
    });
    
    //封装任务(函数方法)
    dispatch_group_async_f(group, que2, NULL, run);
    
}
void run(void * name){
    
    NSLog(@"2--------%@----%@",[NSThread currentThread],name);

}

非队列组的任务封装,也可以是函数的方式

    //封装任务(函数的方式)
    dispatch_async_f(que2, NULL, run);

七. NSOperation和NSOperationQueue

NSOperation和NSOperationQueue配合使用,也可以实现多线程编程。

1.NSOperation

NSOperation是一个抽象类,不具备封装操作的能力,需要使用其子类NSInvocationOperation、NSBlockOperation、自定义NSOperation实现内部响应的方法。

使用步骤
1.封装操作
2.执行操作(start方法)

1.1 NSInvocationOperation

  //01 封装操作对象
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];
    //02 执行操作
    [op1 start];
    

1.2 NSBlockOperation

    //01 封装操作对象
     NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block2----%@",[NSThread currentThread]);
    }];
    
    //添加任务
    [op2 addExecutionBlock:^{
       
        NSLog(@"block3----%@",[NSThread currentThread]);

    }];
    //02 执行操作
    [op2 start];

操作、任务

GCD中有任务、队列。对于NSOpration来说,有操作、队列。其中:

  • 操作:一个NSOperation对象就是一个操作
  • 任务:一个block就是一个任务。
    上面的NSBlockOperation例子中就有1个操作,2个任务。
  • 当一个操作中的任务数量大于1的时候,就会开启子线程来和当前的线程一起完成任务。
    上面的例子打印结果如下:
    1个操作中有两个任务,开启了子线程和主线程一起执行任务。
    1个操作、2个任务打印结果

1.3 自定义操作

参考下文的【4.4自定义操作、以及注意事项】

2.NSOperationQueue

上面讲到NSOperation和NSOperationQueue实现多线程。

使用步骤如下:
1.创建队列NSOperationQueue
2.封装操作对象NSOperation
3.把操作添加到NSOperationQueue中
(系统会自动将NSOperationQueue中的操作NSOperation取出来,放到线程中执行)

2.1 NSOperationQueue和NSInvocationOperation的使用

    //01 创建队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
    //02 封装操作
    NSInvocationOperation * op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    NSInvocationOperation * op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    NSInvocationOperation * op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
    //03 把操作放到队列
    [que addOperation:op1];
    [que addOperation:op2];
    [que addOperation:op3];
  -(void)download1{
      NSLog(@"download1:%@",[NSThread currentThread]);
  }
  -(void)download2{
      NSLog(@"download2:%@",[NSThread currentThread]);
  }
  -(void)download3{
      NSLog(@"download3:%@",[NSThread currentThread]);
  }
NSOperationQueue和NSInvocationOperation使用打印

上面打印结果可看出,开启了多条线程同时执行。为并发。

2.2 NSOperationQueue和NSBlockOperation的使用

    //01 创建队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
    
    //02 封装操作
    NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
       
        NSLog(@"op1:%@",[NSThread currentThread]);
    }];
    
     //03 把操作添加到队列
     [que addOperation:op1];//该方法内部会自动执行start方法

2.3 addOperationWithBlock

上面封装操作、把操作添加到队列这两部,可以简化为一步,用addOperationWithBlock代替,如下:

     //01 创建队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
   //blockque的简便方法
    [que addOperationWithBlock:^{
        
        //该方法内部,会把block内部的任务封装成一个操作(operation),然后把操作添加到队列
      
        NSLog(@"add in que:%@",[NSThread currentThread]);
    }];

3. maxConcurrentOperationCount

3.1并发、串行

GCD的队列:
回顾GCD的队列,有以下两种:
1).并发:自己创建、全局并发
2).串行:自己创建、主队列

NSOperationQueue队列
1).并发:NSOperationQueue中的队列有两种,上面我们一直使用的都是自定义队列。默认为并行。即开启多条子线程同时执行。
2)串行:当我们需要串行队列的时候,maxConcurrentOperationCount = 1即可。

队列类型 创建方法 特点
自定义队列 [[NSOperationQueue alloc] init] 默认为并发队列,可以设置通过maxConcurrentOperationCount为串行队列
主队列 [NSOperationQueue mainQueue] 串行队列,和主线程相关

注意:开启几条子线程并不是由操作数量决定,由系统自己决定

3.2maxConcurrentOperationCount举例

  1. 先封装几个操作添加到队列,查看到打印结果为:开启多条线程,并发执行。
    //01 创建队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
    
    //02 封装操作
    NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
       
        NSLog(@"op1:%@",[NSThread currentThread]);
    }];
    NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
        
        NSLog(@"op2:%@",[NSThread currentThread]);
    }];
    NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"op3:%@",[NSThread currentThread]);
    }];

    //03 把操作添加到队列
     [que addOperation:op1];//该方法内部会自动执行start方法
     [que addOperation:op2];
     [que addOperation:op3];
    
封装多个操作的打印结果
  1. 我们再上面添加maxConcurrentOperationCount = 1,设置为串行。看打印结果:开启多个线程,但是串行执行。
  que.maxConcurrentOperationCount = 1;
串行打印结果
  1. 在上面的基础上,给op2添加两个任务。查看maxConcurrentOperationCount = 1是否还能串行执行任务。

查看下面的打印界面,即时maxConcurrentOperationCount = 1了,但是对于任务数量大于1的操作是无效的。

    [op2 addExecutionBlock:^{

        NSLog(@"op2 add1111111111111:%@",[NSThread currentThread]);
    }];
    [op2 addExecutionBlock:^{

        NSLog(@"op2 add22222222:%@",[NSThread currentThread]);
    }];
给op2添加两个任务

总结:
1.设置最大并发数,对于任务数量大于1的操作是无效的
即时maxConcurrentOperationCount = 1,任务数量大于1的操作也不会串行执行。
2.que.maxConcurrentOperationCount = 1;只能决定是串行还是并发,不能决定开几条子线程。开几条线程,还是由系统决定。
3.que.maxConcurrentOperationCount = 0; 设置为0,就是不执行
4.que.maxConcurrentOperationCount = -1;默认为-1,就是最大值,系统根据情况自己开线程。

4. NSOperationQueue的其他操作

NSOperationQueue还要GCD没有的功能,例如:暂停、取消。

4.1暂停

 [self.que setSuspended:YES];

4.2取消暂停

 [self.que setSuspended:NO];

4.3取消操作

这是取消全部操作。

  [self.que cancelAllOperations];

暂停:只能暂停当前操作后面的操作;当前操作不可分割,必须执行完毕
操作是有状态的,有正在执行的、在等待的。
取消:只能取消正在等待的操作。已经执行的操作是不能取消的

4.4自定义操作、以及注意事项

自定义步骤:
1.新建操作类继承NSOperationQueue
2.子类里面重写-(void)main方法。把操作的任务写在这个方法里面。(把操作通过 [que addOperation:op]添加到队列,会自动调用 [op start];方法,start方法会自动调用这个main方法)

  • 自定义操作好处:代码复用

注意事项:
在自定义操作的时候,每执行完一个耗时操作就判断一下当前操作是否被取消,如果被取消就直接返回。

去判断是否被取消的原因是,队列使用了自定义的操作。即时调用取消方法,也不会被取消。所以需要判断状态,被取消之后强制退出任务。

如下: LYTOperation : NSOperation,main方法里面进行状态判断。

.h:
@interface LYTOperation : NSOperation

@end

.m:
-(void)main{
    
    for (int i = 0 ; i< 10000; ++i) {
        NSLog(@"1:----%d:%@",i,[NSThread currentThread]);
    }
    //官方建议:在自定义操作的时候,每执行完一个耗时操作就判断一下当前操作是否被取消,如果被取消就直接返回
    if (self.cancelled) {
        
        return;
    }
    for (int i = 0 ; i< 10000; ++i) {
        NSLog(@"2:----%d:%@",i,[NSThread currentThread]);
    }
     
}

viewcontroller里面使用这个自定义操作,按钮点击可以取消操作:

#import "LYTOperation.h"
@interface ViewController ()
@property(nonatomic,strong)NSOperationQueue * que;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    //使用自定义队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
    
    //封装操作
    LYTOperation * op = [[LYTOperation alloc] init];
    
    //把操作添加到队列
    [que addOperation:op];//内部会调用start方法,start方法会调用main方法
      
    self.que = que;
}
//取消
- (IBAction)cancel:(id)sender {
    
    [self.que cancelAllOperations];
}

4. NSOperation添加依赖、监听

添加依赖

  1. 依赖:
    如有多个任务,需要设置B执行完之后再执行A。我们就需要使用依赖。
    如下:依赖B,B执行之后执行A
[A addDependency:B]; 
  1. 注意:
    1)设置依赖要写在添加到队列之前
    2)不能设置循环依赖。例如,操作A依赖操作B,不能再设置操作B依赖操作A。否则不会执行操作A和B。
    3)可以跨队列添加依赖
  • 举例:
    队列1有操作:op1、op2、op3
    队列2有操作:op4
    要求执行顺序如下:op3、op2、op1、op4。
  //01 创建队列
    NSOperationQueue * que = [[NSOperationQueue alloc] init];
    NSOperationQueue * que2 = [[NSOperationQueue alloc] init];

    //封装对象
    NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
       
        NSLog(@"1:%@",[NSThread currentThread]);
    }];
    NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
        
        NSLog(@"2:%@",[NSThread currentThread]);
    }];
    NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{
        
        NSLog(@"3:%@",[NSThread currentThread]);
    }];
    NSBlockOperation * op4 = [NSBlockOperation blockOperationWithBlock:^{
        
        NSLog(@"4:%@",[NSThread currentThread]);
    }];

    //设置操作依赖
    //如下,要求执行顺序为op3、op2、op1、op4
    //[A addDependency:B]; 即A依赖B,B执行之后执行A
    [op2 addDependency:op3];
    [op1 addDependency:op2];
    [op4 addDependency:op1];//跨队列依赖
        
    //把操作添加到队列
    [que addOperation:op1];
    [que addOperation:op2];
    [que addOperation:op3];
    
    [que2 addOperation:op4];//放到了队列2
添加依赖执行效果

监听

监听某一个操作执行完,可以使用如下方法:

     op3.completionBlock = ^{
    };
  • 举例:给上面例子中的op3添加监听:
    //监听任务执行完毕
    op3.completionBlock = ^{//op3执行完毕就来执行block这里

        NSLog(@"执行完毕");
    };
监听打印结果

GCD和NSOperation对比

GCD:

是C语言的API,block任务块表示。是轻量级的数据结构
1.监听队列中所有任务的完成

NSOperation:

NSOperation对于GCD是更加重量级的OC对象
1.可以为操作添加依赖
2.可以为操作添加监听是否完成
3.可以对操作取消、暂停
4.可以通过KVO对NSOperation对象精细控制。如监听当前操作是否已经被取消。
5.可以指定操作的优先级
6.可以自定义NSOperation的子类实现操作重用。

相关文章

  • iOS知识体系

    UI 网络 多线程

  • 多线程、网络-整理中

    多线程、网络-整理中

  • 多线程网络

    第一节 1.基本概念 2.线程安全 3.线程间通信 4.GCD GCD基本使用【重点】 GCD的栅栏函数 在使用栅...

  • 多线程网络

    下文中,前面介绍一些简单的概念。虽然简单,但还是需要了解。后面介绍的pthread简单了解即可。GCD和NSOpe...

  • 多线程网络

    1.多线程的底层实现 1> 首先搞清楚什么是线程,什么是多线程 说起多线程,那么就不得不说什么是线程,而说起线程,...

  • 网络多线程

    怎么保证线程安全?只在主线程刷新UI。防止线程资源抢夺,要用@synchronized进行加锁保护。异步操作保证线...

  • 网络多线程

    第1天 1 基本概念 2 pthread 3 NSThread (1)NSThread的基本使用 (2)设置线程的...

  • 网络多线程

    Future的错误和状态 让异步耗时操作处理完成之后,再执行下面代码,Future前面需要添加await 下面我们...

  • 多线程面试

    首先分析多线程的使用环境: 多线程处理包括Core Data的多线程访问,UI的并行绘制,异步网络请求以及一些在运...

  • Redis各版本特性

    Redis6.0 多线程IO Redis 6引入多线程IO,但多线程部分只是用来处理网络数据的读写和协议解析,执行...

网友评论

      本文标题:多线程网络

      本文链接:https://www.haomeiwen.com/subject/rcgtmftx.html