iOS多线程知识总结

多线程基本概念

单核CPU,同一时间cpu只能处理1个线程,只有1个线程在执行 。多线程同时执行:是CPU快速的在多个线程之间的切换。如果线程数非常多,线程切换会消耗大量的cpu资源。相同时间内,每个线程被调度的次数会降低,线程的执行效率降低 。

优点:
能适当提高程序的执行效率
能适当提高资源的利用率(CPU&内存)
线程上得任务执行完后自动销毁

缺点:
开启线程需要占用一定的内存空间(默认情况下,每一个线程都占512KB)
如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,cpu在调用线程上的开销就越大
程序设计更加复杂,比如线程间的通信、多线程的数据共享

主线程:
一个程序运行后,默认会开启1个线程,称为“主线程”或“UI线程”
主线程一般用来 刷新UI界面 ,处理UI事件(比如:点击、滚动、拖拽等事件)
主线程使用注意 �别将耗时的操作放到主线程中�耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种卡的坏体验

iOS中目前有四种实现多线程的方案:

1.pthread
2.NSThread
3.GCD
4.NSOperation

pthread

一套在多操作系统上通用的多线程API,跨平台、可移植性强,基于C语言。线程的生命周期需要程序员自己管理,基本不怎么使用。

简单使用:

创建一个子线程并在子线程中执行一个函数。

#import <pthread.h>

#pragma mark --pthread
- (void)pthreadDemo1{
    pthread_t PID;
    //参数1:pthread_t 线程的标示
    //参数2:pthread_attr_t 线程的属性
    //参数3:void*  (*)  (void *)  函数签名, void *大约可以理解为oc中的id
    //      返回值  函数名 参数
    //参数4:给函数(参数3)的参数
    //返回值:0 成功,非0 失败
    int result = pthread_create(&PID, NULL, task, NULL);
    if (result == 0) {
        NSLog(@"ok");
    }else{
        NSLog(@"fail");
    }
}
void * task(void *param){
    NSLog(@"task is running %@",[NSThread currentThread]);
    return NULL;
}
- (IBAction)clicked:(id)sender {
    NSLog(@"main thread:%@",[NSThread currentThread]);
    [self pthreadDemo1];
}
pthreadDemo1.png
- (void)pthreadDemo2{
    pthread_t PID;

    NSString *str = @"str";
//    char * str2 = "str2";   
//    int result = pthread_create(&PID, NULL, task, str2);
    int result = pthread_create(&PID, NULL, task, (__bridge void *)(str));
    if (result == 0) {
        NSLog(@"ok");
    }else{
        NSLog(@"fail");
    }
}
void * task(void *param){
//    printf("%s\n",param);
    
    NSString *str = (__bridge NSString *)(param);
    NSLog(@"正在运行的线程: %@,  ->%@",[NSThread currentThread],param);
    NSLog(@"正在运行的线程:%@,  ->%@",[NSThread currentThread],str);
    return NULL;
}

注意:ARC默认下对OC对象进行内存管理,不对C变量管理,桥接的作用是让C变量在合适的时候进行释放。在ARC中使用到和C语言对应的数据类型,就应该使用__bridge进行桥接,在MRC中则不需要。

pthreadDemo2.png

使用pthread需要自己管理线程的生命周期,上面的代码中创建了线程但是没有销毁。

NSThread

面向对象,可以直接操作线程对象,基于OC语言。线程的生命周期仍然需要程序员自己进行管理,使用频率不高。可以通过[NSThread currentThread]获取当前线程,以此获取线程的各种属性,便于调试。

创建和启动线程

  • 需要手动启动
    //参数1:对象
    //参数2:方法
    //参数3:参数2方法需要的参数
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task2) object:nil];
    [thread start];//线程启动后在thread中执行task2方法
  • 自动启动
[NSThread detachNewThreadSelector:@selector(task2) toTarget:self withObject:nil];

或者:

//隐式创建并自动启动线程
[self performSelectorInBackground:@selector(task2) withObject:nil];

后面这两种创建线程并自动启动线程的方法有一个缺点:没有办法对线程的一些属性进行设置。

线程的状态

  1. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task3) object:nil];线程新建状态。在内存中创建出一个线程对象。

  2. [thread start];线程就绪状态。线程对象被放入可调度线程池,池中还有别的其他线程对象。等待CPU调度。

  3. CPU调度当前线程,线程进入运行状态。当CPU调度其他线程,线程回到就绪状态。

  4. [NSThread sleepForTimeInterval:1];线程阻塞状态。当调用了sleep方法,或者在等待同步锁。线程对象移出可调度线程池

  5. 当sleep时间到时或得到了同步锁,回到就绪状态。

  6. 线程死亡。线程任务执行完毕,自然死亡;强制退出,手动杀死。线程一旦死亡了,就不能再次开启任务。


- (void)stateDemo{
    //新建状态
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task3) object:nil];
    //就绪状态
    [thread start];
}
- (void)task3{
    //运行
    NSLog(@"正在运行的线程:%@",[NSThread currentThread]);
    //阻塞
    NSLog(@"线程即将进入阻塞状态");
    [NSThread sleepForTimeInterval:1];
//    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];//线程休眠时间
    //从阻塞->就绪->运行。被cpu调用,进入运行状态
    NSLog(@"线程唤醒");
    //线程死亡。可以自然死亡也可以被手动杀死,当线程任务执行完毕,自动进入死亡状态
    //手动杀死
    [NSThread exit];
    NSLog(@"dead");//这句话将不会被打印出来
 
    //一旦线程死亡,就不能再次开启任务
}
stateDemo.png

常用属性、方法

更多的属性、方法请参阅文档

名字属性。
@property (nullable, copy) NSString *name;
设置线程名:
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task4) object:nil];
thread.name = @"thread";

获得当前线程
+ (NSThread *)currentThread;
获得主线程
+ (NSThread *)mainThread;
是否为主线程
- (BOOL)isMainThread; 
线程优先级
+ (double)threadPriority;
设置优先级:
- (void)attrDemo2{
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task4) object:nil];
    thread.name = @"thread";
    thread.threadPriority = 0;
    [thread start];    
    NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(task4) object:nil];
    thread2.name = @"thread2";
    [thread2 start];
}
- (void)task4{
    for (int i = 0; i<20; i++) {
        NSLog(@"当前线程:%@,%d",[NSThread currentThread],i);
    }
}

线程优先级取值范围0.0-1.0,默认为0.5,值越大优先级越高。设置优先级并不意味着优先级高的线程要比优先级低的线程先运行(运行反映的结果就是,打印结果里面显示的第一条是哪个thread),只是更可能被CPU执行到。先执行哪个线程是由cpu调度决定的。


线程间通信

常用方法:NSThread类
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

GCD

旨在替代NSThread等的线程技术,充分利用多核,基于C语言。线程的生命周期是自动管理的,不需要程序员手动管理线程的创建/销毁/复用过程。经常使用。

任务、队列

任务和队列是GCD中的两个核心概念。
任务就是要执行什么操作,在GCD中通过block来指定要执行的代码。队列用来存放任务。

GCD使用的两个步骤:
1.定制任务:确定想做的事情
2.将任务添加到队列中,指定运行方式。
GCD会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO原则,先进先出。

    //创建任务
    //dispatch_block_t的定义typedef void (^dispatch_block_t)(void);任务实际上是一个Block
    dispatch_block_t task = ^{
        NSLog(@"task %@",[NSThread currentThread]);
    };
    //获取队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);  
    //把任务放到队列中
    //参数1:队列  参数2:任务
    dispatch_async(queue, task);    
    //简化
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"task %@",[NSThread currentThread]);
    });

不需要管理线程的生命周期;线程能够复用

    for (int i = 0; i < 20; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"task %@ %d",[NSThread currentThread] , i);
        });
    }
GCD复用线程.png
  • 队列
    队列的类型:并发队列、串行队列。
    并发队列:可以让多个任务并发(同时)执行(自动开启多个线程“同时”执行任务)。并发功能只有在异步时才有效。
    串行队列:无论是同步异步,任务都是一个接一个地执行。
    无论是串行队列还是并发队列,队列里面的任务取出都遵循FIFO原则。并发队列取出任务就分发到可用的线程里,取出的动作很快,就相当于是所有任务都是一起执行的。
    全局并发队列:供整个应用使用,GCD有函数可以获得,不需要手动创建
    主队列:它是特殊的串行队列,又叫全局串行队列,代表着主线程。

队列的创建:
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);第一个参数是唯一标识符,也可以当做是队列的名称,用于debug。第二个参数是队列的属性,表示创建的是并行队列还是串行队列,DISPATCH_QUEUE_SERIAL或者NULL为串行,DISPATCH_QUEUE_CONCURRENT为并行。
dispatch_get_main_queue()获得主队列
dispatch_get_global_queue(long identifier, unsigned long flags);获得全局并发队列.参数1为队列的优先级。参数2暂时没有用,填0即可。

全局队列 、 并发队列的区别:
全局队列没有名称,无论 MRC & ARC 都不需要考虑释放.
并发队列有名字,和 NSThread 的 name 属性作用类似;如果在 MRC 开发时,需要使用 dispatch_release(q); 释放相应的对象;dispatch_barrier 必须使用自定义的并发队列;开发第三方框架时,建议使用并发队列

  • 任务
    任务的执行方式:同步、异步。同步异步的区别是,是否会阻塞当前线程
    同步执行会阻塞当前线程,等到block任务执行完毕,然后当前线程再继续往下运行。异步执行不会阻塞当前线程。通常的表现是:同步是在当前线程中执行,异步是在另一条线程中执行,但也有例外。
    通过打断点来看同步异步的运行过程来理解会更直观。

看下面的例子:
串行队列,同步执行

- (void)demo1{
    //创建串行队列
    //参数1:队列的名字  参数2:队列的属性
    dispatch_queue_t serialQueue =  dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 10 ;i++ ){
        dispatch_sync(serialQueue, ^{
            NSLog(@"serialQueue %@ %d",[NSThread currentThread],i);
        });
    }
    //用途:在多个线程时,要确保在一个线程执行完任务再去执行另一个线程上的任务    
}
串行队列,同步执行-多任务.png

运行结果显示:不开线程,在当前线程下执行(阻塞当前线程);任务是有序执行的。

串行队列,异步执行

- (void)demo2{
    dispatch_queue_t serialQueue =  dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 10 ;i++ ){
        dispatch_async(serialQueue, ^{
            NSLog(@"serialQueue %@ %d",[NSThread currentThread],i);
        });
    }    
}
串行队列,异步执行-多任务.png

运行结果显示:另外开一个线程(不会阻塞当前线程);任务是有序执行的。
修改demo1 demo2不进行循环创建,通过打断点来看同步异步的区别:异步,当代码块执行时demo2:方法已经退出了;同步,demo1:一直等到代码块执行完才退出

并行队列,异步执行

- (void)demo3{
    //创建并行队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    for (int i =0; i<10; i++) {
        dispatch_async(concurrentQueue, ^{
            NSLog(@"concurrent %@ %d",[NSThread currentThread],i);
        });
    }
}

运行结果显示:开多个线程(不会阻塞当前线程),任务无序执行。效率最大。每次开启多少个线程是不固定的(线程数由GCD来决定)

并行队列,同步执行

- (void)demo4{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    for (int i =0; i<10; i++) {
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"concurrent %@ %d",[NSThread currentThread],i);
        });
    }
}

运行结果显示:不开线程,在当前线程下执行(当前线程不一定是主线程);任务是有序执行的。这种情况等同于,串行队列同步执行

主队列,异步执行

- (void)demo5{
    //得到主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    for (int i =0; i<10; i++) {
        dispatch_async(mainQueue, ^{
            NSLog(@"mainQueue %@",[NSThread currentThread]);
        });
    }
}
主队列,异步执行.png

主队列是特殊的串行队列,永远在主线程执行。运行结果显示:任务顺序执行,由于是异步,因此并不会阻塞当前线程(主线程)。

主队列,同步执行

- (void)demo6{
     NSLog(@"开始");
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        NSLog(@"mainQueue %@",[NSThread currentThread]);//这个任务要等待-demo6执行完成才能继续执行
    });
    NSLog(@"结束");
}

运行的结果显示,只会打印出“开始”这一句,然后后面主线程就卡死了。程序死锁。
原因:同步执行会阻塞当前线程。当前-demo6方法在主线程中执行,它把block任务放到主队列中执行,即执行到dispatch_sync()的时候,主线程就被阻塞了。主线程要等到任务执行完成之后才会继续往下执行,而block任务要等到主线程中的-demo6方法执行完之后才能执行。由此造成死锁,主线程卡死。

总结:


线程间通信

dispatch_async(dispatch_get_global_queue(0, 0), ^{  //也叫后台执行
    // 执行耗时的异步操作...
      dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主线程,执行UI刷新操作
        });
});

队列组

队列组可以将很多队列添加到一个组里,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。
假设现在有这样一个需求:分别异步执行两个耗时操作;等两个异步操作都执行完毕,再回到主线程执行操作。这时可以使用队列组来高效实现。

-(void)demo{
    NSLog(@"begin");
    //创建组
    dispatch_group_t group = dispatch_group_create();
    //开启异步任务,参数1:队列组;参数2:队列;参数3:任务
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        NSLog(@"下载 文件1.zip %@",[NSThread currentThread]);
    }); 
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        NSLog(@"下载 文件2.zip %@",[NSThread currentThread]);
    });
    //完成队列组的任务后进行通知,参数1:队列组;参数2:队列;参数3:任务
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"下载完成 %@",[NSThread currentThread]);
    });
}

一次性执行

被调用多次,但只会执行一次。它是线程安全的

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//    for (int i = 0; i < 20; i++) {
//        [self demo];
//    }    
    //多线程测试
        for (int i = 0; i < 20; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self demo];
            });
        }  
}
-(void)demo{
    NSLog(@"这句话执行多次");
    static dispatch_once_t onceToken;
    NSLog(@"%ld",onceToken);
    dispatch_once(&onceToken, ^{
        NSLog(@"once");
    });
}

dispatch_once_t 实际上被定义为long类型,demo方法中打印出onceToken的值中可以看出,在代码第一次被调用的时候,onceToken的值为0(即使在多线程下可能会有多个0值打印出来),此时block里面的代码会被执行而且只会执行一次。当代码再次被调用,onceToken的值为非0,block代码不会被执行。onceToken值相当于是一个flag,用来标记block里面的代码是否已经被执行过。

单例模式

OC里面实现单例在此不进行详述,用到了 GCD 的 dispatch_once方法。
实现单例还可以用互斥锁,但相比GCD性能要差得多(没有得到锁的线程一直在等待。)

@implementation Singleton
+(instancetype)configSync{
    static Singleton *instance;    
    @synchronized(self) {
        if(instance == nil){
            instance = [[Singleton alloc] init];
        }
    }    
    return instance;    
}
@end

NSOperation

面向对象,基于GCD封装,比GCD多一些简单的功能,基于OC语言。线程的生命周期是自动管理的,经常使用。
NSOperationNSOperationQueue就相当于是GCD 的任务和队列。

NSOperation实现多线程的具体步骤
1.将需要执行的任务封装到一个NSOperation对象中
2.将NSOperation对象添加到NSOperationQueue中
3.系统会自动将NSOperationQueue中的NSOperation取出来
4.NSOperation封装的任务被放到一条新线程中执行

任务NSOperation

NSOperation是一个抽象类,不具备封装任务的能力,不可以直接使用,它只是约束子类都具有共同的属性和方法。因此必须使用它的子类:
1.NSInvocationOperation
2.NSBlockOperation
3.自定义子类,继承NSOperation,实现内部相应的方法

子类创建operation使用的方法虽不尽相同,但最后都需要调用start方法来启动执行任务。默认情况下,调用start方法后不会新开一个线程执行任务,而是在当前线程同步执行,只有将NSOperation放入一个NSOperationQueue中,才会异步执行操作。

  • NSInvocationOperation比较少用
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
 //创建operation
 NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(demo1) object:nil];//任务在主线程(当前线程)运行
  //启动任务
  [op start];
}

 - (void)demo1{
    NSLog(@"任务1当前线程----%@",[NSThread currentThread]);//在主线程运行
}
  • NSBlockOperation
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//创建operation
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"当前线程----%@",[NSThread currentThread]);
    }];//还是主线程
    //额外的任务在子线程执行
    [op addExecutionBlock:^{
        NSLog(@"当前线程----%@",[NSThread currentThread]);
    }];
    [op start];
}

红框 并发执行

addExecutionBlock方法给operation添加额外的任务,这时operation中的所有任务并发执行(当前线程和其他子线程)
只要NSBlockOperation封装的任务数大于1,就会异步执行?

  • 自定义子类
    当以上两个子类无法满足需求时,又或者需要封装任务等等,就需要自定义operation。
    自定义operation需要继承NSOperation类,并实现main 方法,因为在调用start方法的时候,内部会调用main方法完成相关逻辑。重写main方法的注意点:
    1.自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)【这个不太理解】
    2.经常通过-(BOOL)isCancelled方法检测操作是否被取消,对取消作出响应
    具体举例下面用到再细说。

队列NSOperationQueue

NSOperation可以调用start方法来执行任务,但因为是同步执行,会占用当前线程。但如果把NSOperation添加到NSOperationQueue中,就可以异步执行任务。

  • NSOperationQueue队列类型
    1.主队列 [NSOperationQueue mainQueue]添加到主队列中的任务都会放到主线程中执行
    2.其他队列(包括串行、并发),使用alloc init方式创建。任务只要添加到队列,系统就会自动异步执行任务(自动调用start方法)
    用maxConcurrentOperationCount(最大并发数)属性控制是串行还是并发队列

  • 添加任务的方法:

 - (void)addOperation:(NSOperation *)op;
 - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
 - (void)operationQueueDemo1{
    //将NSOperation添加到NSOperationQueue中,系统会自动**异步执行**任务
    //创建队列
    NSOperationQueue *opqueue = [[NSOperationQueue alloc]init];
    //创建operation
    NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(demo1) object:nil];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务2当前线程----%@",[NSThread currentThread]);
    }];
    
    [op2 addExecutionBlock:^{
        NSLog(@"任务3当前线程----%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务4当前线程----%@", [NSThread currentThread]);
    }];
    
    CustomOperation *op4 = [[CustomOperation alloc] init];//自定义operation
    
    // 添加任务到队列中
    [opqueue addOperation:op];//一旦添加,就会执行(自动start),并发,且每添加一个operation开一条子线程
    [opqueue addOperation:op2];
    [opqueue addOperation:op3];
    [opqueue addOperation:op4];//添加自定义operation,自动调用start,start调用main
    //添加任务的另一种更简洁的方式,和上面是等价的
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,block当前线程----%@",[NSThread currentThread]);
    }];
}

ps:在这里使用了自定义operation

 #import "CustomOperation.h"
@implementation CustomOperation
//封装自定义任务,自动调用这个方法
 - (void)main
{
    NSLog(@"自定义operation");
}
@end
  • 队列最大并发数
    @property NSInteger maxConcurrentOperationCount;这个属性用来设置最多可以让多少个任务同时执行。所以,把maxConcurrentOperationCount的值设为1,那么队列就是串行的了!
 - (void)operationQueueDemo2{
    NSOperationQueue *opqueue = [[NSOperationQueue alloc]init];    
//    opqueue.maxConcurrentOperationCount = 2;
    //串行队列
    opqueue.maxConcurrentOperationCount = 1;
    
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,任务1当前线程----%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,任务2当前线程----%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,任务3当前线程----%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,任务4当前线程----%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
    [opqueue addOperationWithBlock:^{
        NSLog(@"block添加任务,任务5当前线程----%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
}

maxConcurrentOperationCount=2时运行结果:



maxConcurrentOperationCount=1时运行结果:



任务执行在哪个线程不是固定的。比如当前最大并发数为2,在同一时间并发执行任务的线程的确有2个,但并不是固定一直都是这两个线程去处理任务。当一个线程执行完任务后,由系统来决定销毁它另开线程还是继续使用这个线程来执行任务
  • 队列的暂停、恢复、取消
    暂停、恢复:suspended属性
    要执行suspended=yes时候,队列挂起,但如果有任务还没执行完,那么这个任务将会继续执行到完成。而队列中余下的任务就会被挂起不执行。
    当suspended属性设为No的时候,再继续执行余下的任务。

队列取消:- (void)cancelAllOperations;
cancelAllOperations取消队列所有的任务,取消了就不会再恢复。This method calls the cancel method on all operations currently in the queue.同样,如果有任务还没执行完,那么这个任务将会继续执行到完成。
如果自定义operation中有多个耗时的操作,建议在main方法中在每个耗时操作后判断任务是否已经被取消了,如果取消了,余下的耗时操作将不再执行

#import "CustomOperation.h"
@implementation CustomOperation
 - (void)main
{
    for (NSInteger i = 0; i<1000; i++) {
        NSLog(@"download1 -%zd-- %@", i, [NSThread currentThread]);
    }
    if (self.isCancelled) return;
    
    for (NSInteger i = 0; i<1000; i++) {
        NSLog(@"download2 -%zd-- %@", i, [NSThread currentThread]);
    }
    if (self.isCancelled) return;
    
    for (NSInteger i = 0; i<1000; i++) {
        NSLog(@"download3 -%zd-- %@", i, [NSThread currentThread]);
    }
    if (self.isCancelled) return;
}
@end
  • 依赖
    NSOperation之间可以设置依赖来保证执行顺序。[op1 addDependency:op2]可以让任务op1在任务op2之后执行。
    这种任务依赖是可以跨队列的。不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。
 - (void)operationQueueDemo4{
    NSOperationQueue *opqueue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *op = [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]);
    }];
    //监听任务是否执行完毕,这个block在任务完成之后执行,而且它不在主线程上,也与它监听的任务不一定在同一个线程上
    op4.completionBlock = ^{
        NSLog(@"op4执行完毕---%@", [NSThread currentThread]);
    };
    
    // 设置依赖,而且这种依赖是可以跨队列的
    [op2 addDependency:op3];
    
    //添加完依赖再把任务加到队列上
    [opqueue addOperation:op];
    [opqueue addOperation:op2];
    [opqueue addOperation:op3];
    [opqueue addOperation:op4];
}

线程间通信

[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        .......
        // 回到主线程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            ......
        }];
}];

其他

线程同步:

为了防止多个线程可能会访问同一块资源,引发数据错乱和数据安全问题,所采取的措施。

  • 互斥锁:给需要同步的代码块加一个互斥锁,就可以保证每次只有一个线程访问此代码块。
    售票问题:
@property (nonatomic , assign)int tickets;

 - (void)viewDidLoad {
    [super viewDidLoad];
    self.tickets = 5;
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(sellTicket) object:nil];
    thread.name = @"窗口1";
    [thread start];    
    NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(sellTicket) object:nil];
    thread2.name = @"窗口2";
    [thread2 start];
}
 - (void)sellTicket{
    while (YES) {
        //模拟网络延时
        [NSThread sleepForTimeInterval:1];
        if (self.tickets > 0) {
            self.tickets = self.tickets - 1;
            NSLog(@"%@ 剩余票数%d",[NSThread currentThread],self.tickets);
            continue;
        }
        NSLog(@"卖完了");
        break;
    }
}

运行结果:产生数据错乱问题


解决方法:使用互斥锁(同步锁)
格式:@synchronized(锁对象){需要锁定的代码}
优缺点:能有效防止因多线程抢夺资源造成的数据安全问题。但是需要消耗大量的CPU资源。没有得到锁的线程一直在等待。
锁可以是任意对象,默认锁是开着的。

@property (nonatomic , strong) NSObject *obj;
 - (void)viewDidLoad {
    [super viewDidLoad];
    self.tickets = 5;
    self.obj = [[NSObject alloc]init];//obj要初始化,锁才有效
    ......(创建线程)
}
 - (void)sellTicket{
    while (YES) {
        //模拟网络延时
        [NSThread sleepForTimeInterval:1];
        //锁是一个任意对象(任意对象都有一把锁),默认锁是开着的     
        @synchronized(self.obj) {
            if (self.tickets > 0) {
                self.tickets = self.tickets - 1;
                NSLog(@"%@ 剩余票数%d",[NSThread currentThread],self.tickets);
                continue;
            }
            NSLog(@"卖完了");
            break;
        }
    }
}

运行结果:是线程安全的


注意:锁定一份代码只能用一把锁,使用多把锁无效。如果把上面的代码改成下面这样,同样会产生数据错乱问题。因为当前while循环中每次循环都创建一把新的锁,锁默认都是开着的。

while (YES) {
    [NSThread sleepForTimeInterval:1];
    NSObject *obj = [[NSObject alloc]init];  
    @synchronized(obj) {
        ......
        }
        NSLog(@"卖完了");
        break;
    }
}

atomic 和nonatomic:
nonatomic非原子属性:多个线程可以同时赋值同时读取。非线程安全的,适合内存小的移动设备。开发中建议把属性声明为nonatomic
atomic原子属性:多个线程中只有一个线程能够对变量赋值(为setter加锁),但多个线程可以同时读取。线程安全,但需要消耗大量资源。原子属性有自旋锁
互斥锁:如果发现其他线程正在执行锁定的代码,线程休眠(就绪状态),等其他线程开锁后,线程被唤醒。
自旋锁:如果发现其他线程正在执行锁定代码,线程会用死循环的方式一致等待锁定代码完成。自旋锁适合执行不耗时的操作。

不使用互斥锁,如果把tickets声明为atomic,再执行第一段“售票问题”代码的话会怎么样呢?@property (nonatomic , assign)int tickets;
运行结果显示,自旋锁仍然会有线程安全问题。因为它只对setter加锁,对Getter不加锁。

延时执行:

方式1:NSTimer

 NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(task) userInfo:nil repeats:NO];

方式2:

[self performSelector:@selector(task) withObject:nil afterDelay:1];

方法3:GCD

    //参数1:延时的时间 dispatch_time生成时间 纳秒为计时单位 精度高
    //参数2:队列
    //参数3:任务
    //异步执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"task");
});

相关文章:
Cocoa深入学习:NSOperationQueue、NSRunLoop和线程安全
iOS多线程你看我就够

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容