iOS多线程的那些事儿

474 阅读27分钟

要想掌握多线程,就有必要了解一些相关的基本概念,比方说什么是线程?要了解线程肯定会牵涉到进程,那什么又是进程?明白这些之后,还要明白多线程是怎么回事?为什么要用多线程?瞬时有点懵逼,一堆的概念。但要了解多线程,并且要会用它,这些玩意还是跳不过。既然事情比繁琐,那就拆分其化繁为简,一个一个地去了解。

  • 进程
    进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

上面是百科的概念,翻译成人话就是:进程是系统正在运行的应用程序,一个应用程序即一进程,进程与进程间是相互独立的,且运行在其专用且受保护的内存空间中。

  • 线程

    线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。

正常的理解是进程要想执行任务,得有线程。这样一来,进程中得至少有一个线程,这线程即为主线程。

线程的任务可以多个,但同一时间线程只能执行一个任务,即多任务的执行是按顺序的,执行完了一个任务再接着执行下一个任务。

关于进程与线程间的区别,知乎上有个大牛解释地非常到位,传送进程与线程的区别

  • 多线程

    多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

    而事实上呢,CPU在同一时间只能处理一个线程,不能多个线程同时处理。那多线程一说从何而来?那是因为CPU在切换调度不同线程比较快,时间很短。所以才会给我们的感觉就是多个任务同时在并发进行着的假象。

    合理地利用多线程,可以提高应用程序的运行效率,及提高CPU/内存的利用率。但凡事都有个度,过度使用多线程也会导致占用过高的内存(默认情况下,主线程占用1M,子线程占用512K内存空间)及增加CPU的开销,从而降低应用程序的性能。

接下来是多线程的主要内容,iOS开发中多线程的常使用方式有三种,分别是:

  • NSThread

    每个NSThread的对象对应着一个线程,轻量级别,比较简单。但需要程序员手动管理线程的所有活动,如线程的生命周期、睡眠、同步。且管理多个线程比较困难,所以NSThread开发上相对比较少使用

  • GCD

    apple官方推荐的高效多线程编程解决方案,基于C。使用block定义任务,使用起来灵活方便。线程生命周期自动管理,编码时可更注重逻辑实现。

  • NSOperation/NSOPerationQueue

    NSOperation基于GCD实现的一套Objective-C的API,是面向对象的线程技术,线程的生命周期自动管理,程序员可更加注重逻辑;较于GCD更容易实现限制最大并发量,操作之间的依赖关系。

综合上述三种多线程实现的方式,NSThread由于在处理多个线程上,存在困难,较少使用。NSOperation、GCD线程生命周期自动管理,程序员只需要注重业务逻辑即可。GCD基于C实现,性能最为高效。NSOperation基于GCD封装的一套API,使用更加面向对象,更为简单,且在控制任务最大并发、处理依赖关系上更为方便。

NSThread

NSThread创建的方式有三种,分别如下:

//thread创建方式1
- (void)threadCreateMethod1 { 
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadImageDownload:) object:opImageUrl];
    [thread1 start];
}

//thread创建方式2
- (void)threadCreateMethod2 {
    [NSThread detachNewThreadSelector:@selector(threadImageDownload:) toTarget:self withObject:opImageUrl2];
}

//thread创建方式3
- (void)threadCreateMethod3 {
    [self performSelectorInBackground:@selector(threadImageDownload:) withObject:opImageUrl3];
}

#pragma mark - downloadMethod

- (void)threadImageDownload:(NSString *)imageUrl {
    //如果当前线程被取消,则return
    if ([[NSThread currentThread] isCancelled]) {
        return;
    }
    //
    NSURL *requestUrl = [NSURL URLWithString:imageUrl];
    NSData *imageData = [NSData dataWithContentsOfURL:requestUrl];
    if (imageData) {
        [self performSelectorOnMainThread:@selector(refreshUiImage:) withObject:imageData waitUntilDone:YES];
    }
}

- (void)refreshUiImage:(NSData *)imageData {
    UIImage *downloadImage = [UIImage imageWithData:imageData];
    self.contentImageView.image = downloadImage;
}

NSThread还有一些属性:

//是否在执行中
thread1.isExecuting;
//是否为主线程
thread1.isMainThread;
//是否被取消
thread1.isCancelled;
//是否已经完成
thread1.isFinished;
//线程的堆栈大小,线程执行前堆栈大小为512K,线程完成后堆栈大小为0K 值得一提的是线程在执行完毕后,由于内存空间被释放,不能再次启动
thread1.stackSize;
//线程优先级
thread1.threadPriority;

实例方法

//线程开始,线程加入线程池等待CPU调度(并非真正开始执行,只是通常等待时间都非常短,看不出效果)
[thread1 start];
//标记线程被取消执行
[thread1 cancel]

类方法

//是否为多线程
+ (BOOL)isMultiThreaded;
//线程阻塞方法
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//线程退出,释放内存空间,线程不可再启动。(cancel方法,只是标志线程取消执行,并非退出,exit为线程退出)
+ (void)exit;
//设置线程优先级,值区间0~1,默认为0.5。这里要注意的是,在多线程的执行中,某线程设置优先级为1,并不是CPU会先调度完了该线程再去调度其它线程的意思,只是优先级高的线程在调度频率上会高于基于优先级低的线程。
+ (BOOL)setThreadPriority:(double)p;

GCD

Grand Central Dispatch (GCD) 是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。——百度百科

这是apple官方推荐基于C实现的一套高效的多线程解决方案,使用方便,优点多:

1、GCD可用于多核的并行运算

2、GCD会自动利用更多的CPU内核(比如双核、四核)

3、GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

4、程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

只需要把要执行的任务扔进去即可,简直6到不行啊。平时的开发中也经常使用,不仅因为它功能强大,简单易用,且它是以block方式呈现出来的。代码集中度也高,更为直观。说了这么多,来看看它具体怎么用吧。

老规则,先了解一些概念性的东西:任务、队列

  • 任务:任务就是要执行的操作,简单来说就是一段要处理某个业务的逻辑代码。在GCD中来看的话,任务就是block里面的代码。执行任务的时候有两方式:同步执行异步执行,这两者的区别就在于开辟新线程的能力。

    • 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力
    • 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力,但在主线程执行异步操作,是不会开辟新线程的。
  • 队列:这里的队列指任务队列,即用来存放及管理任务的队列。采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。在GCD中有两种队列:串行队列并发队列

    • 并发队列:系统调度时,同时创建多个线程,执行多个任务,且并发功能只有在异步(dispatch_async)函数下才有效;
    • 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

GCD使用的步骤:第一步:创建队列,第二步执行任务。

创建队列

//label参数为队列标识,用于debug调试使用;attr为队列类型,DISPATCH_QUEUE_SERIAL为串行队列,DISPATCH_QUEUE_CONCURRENT为并发队列
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

//串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", DISPATCH_QUEUE_SERIAL);
//并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrent", DISPATCH_QUEUE_CONCURRENT);
//特殊的串行队列,主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//identifier优先级,flags未使用的保留值,为0(iOS8 开始使用 QOS(服务质量) 替代了原有的优先级。获取全局并发队列时,直接传递 0,可以实现 iOS 7 & iOS 8 later 的适配。)
dispatch_get_global_queue(long identifier, unsigned long flags); 
//全局队列,apple为方便开发者设立的全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//iOS8以后可以直接 
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

创建任务

上述讲到任务的执行方式有两种,分别是『同步执行』与『异步执行』,那两种不同的执行方式在不同队列类型下的表现如何?先看串行队列下的同步执行与异步执行:

串行队列下的任务同步执行

- (void)serialQueueSyncMethod {
    NSLog(@"_____serialQueueSyncMethod start_____");
    //NULL为串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", NULL);
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        };
    });
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____serialQueueSyncMethod end_____");
}

打印结果为:

2018-02-10 14:49:25.849676+0800 NCImageDownloader[2004:172084] _____serialQueueSyncMethod start_____
2018-02-10 14:49:25.849876+0800 NCImageDownloader[2004:172084] thread1 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850013+0800 NCImageDownloader[2004:172084] thread1 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850124+0800 NCImageDownloader[2004:172084] thread2 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850209+0800 NCImageDownloader[2004:172084] thread2 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850325+0800 NCImageDownloader[2004:172084] thread3 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850455+0800 NCImageDownloader[2004:172084] thread3 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850566+0800 NCImageDownloader[2004:172084] _____serialQueueSyncMethod end_____

结果显示串行队列下同步执行任务,不会开启新线程,任务一个完成后接下一个任务执行,串行执行方式。

串行队列下的任务异步执行

- (void)serialQueueAsyncMethod {
    NSLog(@"_____serialQueueAsyncMethod start_____");
    //NULL为串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", NULL);
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____serialQueueAsyncMethod end_____");
}

打印结果为:

2018-02-10 14:49:25.850664+0800 NCImageDownloader[2004:172084] _____serialQueueAsyncMethod start_____
2018-02-10 14:49:25.850758+0800 NCImageDownloader[2004:172084] _____serialQueueAsyncMethod end_____
2018-02-10 14:49:25.850837+0800 NCImageDownloader[2004:172141] thread1 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.850964+0800 NCImageDownloader[2004:172141] thread1 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851080+0800 NCImageDownloader[2004:172141] thread2 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851209+0800 NCImageDownloader[2004:172141] thread2 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851318+0800 NCImageDownloader[2004:172141] thread3 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851426+0800 NCImageDownloader[2004:172141] thread3 is <NSThread: 0x604000463c40>{number = 4, name = (null)}

结果显示串行队列下异步执行任务,会开启新线程,任务一个完成后接下一个任务执行,串行执行方式。

并发队列下的任务同步执行

- (void)concurrentQueueSyncMethod {
    NSLog(@"_____concurrentQueueSyncMethod start_____"); 
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrentS", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____concurrentQueueSyncMethod end_____");
}

打印结果为:

2018-02-10 15:02:50.870721+0800 NCImageDownloader[2065:180191] _____concurrentQueueSyncMethod start_____
2018-02-10 15:02:50.870914+0800 NCImageDownloader[2065:180191] thread1 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871023+0800 NCImageDownloader[2065:180191] thread1 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871121+0800 NCImageDownloader[2065:180191] thread2 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871243+0800 NCImageDownloader[2065:180191] thread2 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871329+0800 NCImageDownloader[2065:180191] thread3 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871419+0800 NCImageDownloader[2065:180191] thread3 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871512+0800 NCImageDownloader[2065:180191] _____concurrentQueueSyncMethod end_____

结果显示并发队列下同步执行任务,不会开启新线程,任务一个完成后接下一个任务执行,串行执行方式。

并发队列下的任务异步执行

- (void)concurrentQueueAsyncMethod {
    NSLog(@"_____concurrentQueueAsyncMethod start_____"); 
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrentA", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____concurrentQueueAsyncMethod end_____");
}

打印结果为:

2018-02-10 15:05:39.025175+0800 NCImageDownloader[2080:181771] _____concurrentQueueAsyncMethod start_____
2018-02-10 15:05:39.025341+0800 NCImageDownloader[2080:181771] _____concurrentQueueAsyncMethod end_____
2018-02-10 15:05:39.025576+0800 NCImageDownloader[2080:182352] thread2 is <NSThread: 0x604000279c40>{number = 5, name = (null)}
2018-02-10 15:05:39.025583+0800 NCImageDownloader[2080:182364] thread3 is <NSThread: 0x60c0002722c0>{number = 6, name = (null)}
2018-02-10 15:05:39.025606+0800 NCImageDownloader[2080:181994] thread1 is <NSThread: 0x600000279ac0>{number = 4, name = (null)}
2018-02-10 15:05:39.025775+0800 NCImageDownloader[2080:182364] thread3 is <NSThread: 0x60c0002722c0>{number = 6, name = (null)}
2018-02-10 15:05:39.025780+0800 NCImageDownloader[2080:181994] thread1 is <NSThread: 0x600000279ac0>{number = 4, name = (null)}
2018-02-10 15:05:39.025783+0800 NCImageDownloader[2080:182352] thread2 is <NSThread: 0x604000279c40>{number = 5, name = (null)}

结果显示并发队列下异步执行任务,会开启新线程,任务交替同时执行的,并行执行方式。

还有一特殊的串行队列:主队列

主队列下的任务同步执行

- (void)mainQueueSyncMehtod {
    NSLog(@"_____mainQueueSyncMehtod start_____");
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____mainQueueSyncMehtod end_____");
}

运行崩溃:

打印情况:

2018-02-10 15:25:10.070468+0800 NCImageDownloader[2143:192405] _____mainQueueSyncMehtod start_____
(lldb) 

只打印了_____mainQueueSyncMehtod start_____就崩溃了。 为什么,往主队伍添加同步任务,会运行崩溃?原因啊,是因为往主队列添加同步任务后,任务会立马执行。但主线程现在正在调用mainQueueSyncMehtod方法,必须等mainQueueSyncMehtod方法执行完了之后,才会执行后面的任务。但mainQueueSyncMehtod必须等任务1执行完了之后,再执行后面的两个任务才算执行完毕。所以现在的情况是mainQueueSyncMehtod等任务1执行完,任务1等mainQueueSyncMehtod方法执行完,互相等待,没完没了。

主队列下的任务异步执行

- (void)mainQueueAsyncMehtod {
    NSLog(@"_____mainQueueAsyncMehtod start_____");
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____mainQueueAsyncMehtod end_____");
}

打印结果为:

2018-02-10 15:38:07.394448+0800 NCImageDownloader[2184:200006] _____mainQueueAsyncMehtod start_____
2018-02-10 15:38:07.394573+0800 NCImageDownloader[2184:200006] _____mainQueueAsyncMehtod end_____
2018-02-10 15:38:07.417503+0800 NCImageDownloader[2184:200006] thread1 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.417755+0800 NCImageDownloader[2184:200006] thread1 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.417865+0800 NCImageDownloader[2184:200006] thread2 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418003+0800 NCImageDownloader[2184:200006] thread2 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418122+0800 NCImageDownloader[2184:200006] thread3 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418227+0800 NCImageDownloader[2184:200006] thread3 is <NSThread: 0x600000260a40>{number = 1, name = main}

结果显示主队列下异步执行任务,并不会开启新线程,任务执行一个后再执行下一个任务,串行执行方式。

GCD的线程间的通信

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //耗时操作,如什么下载,上传,大量数据存储、文件写入,读取等
    // 回归主线程
    dispatch_async(dispatch_get_main_queue(), ^{
        //刷新UI,同步实时状态
    });
}); 

看上去没那么难,对吧。

GCD一次性执行

最常用的单例,或者是有什么只要执行一次代码段:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    //write your code
});

GCD的延时执行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //2s后面执行这里的代码
    });

GCD的循环

dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
        //循环10次
    });

GCD栅栏方法

dispatch_barrier_sync 与 dispatch_barrier_Async 按字面来理解,这方法起到一堵障碍的作用。如果开发中有需要异步执行两组操作,第二组操作须在第一组操作完成之后再执行,那这堵墙就有作用了。

dispatch_barrier_sync

- (void)gcdBarrier
{
    dispatch_queue_t queue = dispatch_queue_create("gcdBarrier", NULL);
    
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_sync(queue, ^{
        //让它睡1s
        sleep(1);
        
        for (int i = 0; i < 2; ++i) {
            NSLog(@"----barrier-----%@", [NSThread currentThread]);
        }
    });
    NSLog(@"_______________________这是一条线");
    NSLog(@"_______________________这TM也是一条线");
    
    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

打印结果为:

2018-02-10 16:18:06.531521+0800 NCImageDownloader[2406:228747] ----2-----<NSThread: 0x608000072c00>{number = 1, name = main}
2018-02-10 16:18:06.531534+0800 NCImageDownloader[2406:228808] ----1-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:06.531668+0800 NCImageDownloader[2406:228747] _______________________这是一条线
2018-02-10 16:18:06.531762+0800 NCImageDownloader[2406:228747] _______________________这TM也是一条线
2018-02-10 16:18:07.532328+0800 NCImageDownloader[2406:228808] ----barrier-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532620+0800 NCImageDownloader[2406:228808] ----barrier-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532858+0800 NCImageDownloader[2406:228808] ----3-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532912+0800 NCImageDownloader[2406:228807] ----4-----<NSThread: 0x60000046db00>{number = 5, name = (null)}

当把 dispatch_barrier_sync 换成 dispatch_barrier_async的时候,打印结果为:

2018-02-10 16:23:30.229972+0800 NCImageDownloader[2425:232456] ----2-----<NSThread: 0x604000062e40>{number = 1, name = main}
2018-02-10 16:23:30.229973+0800 NCImageDownloader[2425:232510] ----1-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:30.230147+0800 NCImageDownloader[2425:232456] _______________________这是一条线
2018-02-10 16:23:30.230261+0800 NCImageDownloader[2425:232456] _______________________这TM也是一条线
2018-02-10 16:23:31.232162+0800 NCImageDownloader[2425:232510] ----barrier-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:31.232432+0800 NCImageDownloader[2425:232510] ----barrier-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:31.232687+0800 NCImageDownloader[2425:232509] ----4-----<NSThread: 0x6000000686c0>{number = 3, name = (null)}
2018-02-10 16:23:31.232689+0800 NCImageDownloader[2425:232510] ----3-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}

看那两条线的位置变化,可以得出dispatch_barrier_sync同步会阻塞线程,dispatch_barrier_async则不会。

GCD队列组

当需要异步执行两组操作任务后,回归主线程刷新UI:

- (void)gcdGroup {
    dispatch_group_t gcdGroup = dispatch_group_create();
    
    dispatch_group_async(gcdGroup, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"第一组操作");
    });
    
    dispatch_group_async(gcdGroup, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"第二组操作");
    });
    //当队列组操作执行完毕之后,发出通知回到主线程刷新UI
    dispatch_group_notify(gcdGroup, dispatch_get_main_queue(), ^{
        //刷新UI
    });
}

像上面一条dispatch_barrier_sync 与 dispatch_barrier_async的例子也可以通过 dispatch_group_sync 及 dispatch_group_async,但实现方式不够前者优雅,直观。

NSOperation/NSOperationQueue

NSOperation是基于GCD封装的面向对象使用的一套Objcet C的API,它既然基于GCD,显然GCD有的优点NSOperation也一样持有,同时NSOperation还有一些GCD不那么容易实现的功能,如控制最大并发数。它比GCD更适合面向对象编程人员的使用习惯,简单易用。

NSOperation它实际上是一个抽象类,不能直接实现,要实现它的功能就要使用它的子类NSIvocationOperation、NSBlockOperation及实现NSOperation自定义的子类。

NSOperation实际上是任务,而NSOperationQueue实际上就是队列。对比于GCD,NSOperation就相当于GCD block里面的执行任务,而NSOperationQueue则是dispatch_queue_t队列。NSOperation一般的使用都要搭配NSOperationQueue队列,单独使用NSOperation则是『同步』运行在『当前』线程的任务,它是没有开启新线程的能力的。当NSOperation搭配NSOperationQueue的时候,就是把任务放到任务队列中去,等待系统的调度,再由系统开辟线程去抽取队列中的任务执行。

依上的描述,NSOperation/NSOperationQueue的使用步骤就是: 1、新建任务封于NSOperation; 2、新建NSOperationQueue队列,并把NSOpertion添加到新建的NSOperationQueue队列中; 3、系统自动调度NSOperationQueue队列中的NSOperation任务,开辟新线程异步执行。 PS:如果NSOperation单独使用,则是在当前的线程上同步执行。

子类NSInvocationOperation与NSBlockOperation

NSOperation两子类的应用代码:

//子类NSInvocationOperation
- (void)invocationOperationMethod {
    //新建队列
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:img1];
    //创建队列
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    //将任务加入队列等待系统调度
    [operationQueue addOperation:invocationOperation];
}

//子类NSBlockOperation
- (void)blockOperationMethod {
    //新建任务
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self downloadImage:img1];
    }];
    //创建队列
    NSOperationQueue *blockOperationQueue = [[NSOperationQueue alloc] init];
    //将任务加入队列等待系统调度
    [blockOperationQueue addOperation:blockOperation];
}

- (void)downloadImage:(NSString *)imageUrl{
    NSURL *imageRequestUrl = [NSURL URLWithString:imageUrl];
    NSData *imageData = [NSData dataWithContentsOfURL:imageRequestUrl];
    if (imageData) {
        NSInvocationOperation *mainOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(refresh:) object:imageData];
        [[NSOperationQueue mainQueue] addOperation:mainOperation];
    }
}

- (void)refresh:(NSData *)imageData {
    UIImage *image = [UIImage imageWithData:imageData];
    [self.imageView1 setImage:image];
} 

看上去的确是挺简单,也符合我们平时面向对象的编码方式。

下面来看看上述关于NSOperation与NSOperationQueue搭配与否的区别,及它们之间开辟线程的能力。

NSOpertaion单独使用:

-(void)operationWithoutOperationQueue {
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [invocationOperation start];
    
    NSLog(@"这是可能是一条分割线——————————————————————");
    
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self run];
    }];
    [blockOperation start];
}

- (void)run {
    NSLog(@"当前线程:%@",[NSThread currentThread]);
}

打印结果:

2018-02-14 09:10:12.921660+0800 NCImageDownloader[6504:1921474] 当前线程:<NSThread: 0x60c00006c640>{number = 1, name = main}
2018-02-14 09:10:12.921847+0800 NCImageDownloader[6504:1921474] 这是可能是一条分割线——————————————————————
2018-02-14 09:10:12.922125+0800 NCImageDownloader[6504:1921474] 当前线程:<NSThread: 0x60c00006c640>{number = 1, name = main}

在当前的线程下(也就是主线程)顺序执行.

NSOpertaion搭配NSOperationQueue使用:

-(void)operationWithOperationQueue {
    NSOperationQueue *blockOperationQueue = [[NSOperationQueue alloc] init];
    //
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self run];
    }];
    //
    [blockOperationQueue addOperation:invocationOperation];
    [blockOperationQueue addOperation:blockOperation];
    NSLog(@"这是可能是一条分割线——————————————————————");
}

- (void)run {
    NSLog(@"当前线程:%@",[NSThread currentThread]);
}

打印结果:

2018-02-14 09:14:12.989780+0800 NCImageDownloader[6526:1925078] 这是可能是一条分割线——————————————————————
2018-02-14 09:14:12.989920+0800 NCImageDownloader[6526:1925146] 当前线程:<NSThread: 0x60400026cb40>{number = 3, name = (null)}
2018-02-14 09:14:12.989921+0800 NCImageDownloader[6526:1925144] 当前线程:<NSThread: 0x608000276340>{number = 4, name = (null)}

由结果可以看出来:两任务分别由系统开辟两条新的线程异步执行。

此外,NSBlockOperation还有个更简洁的用法:

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
    //要执行的任务
}];

自定义Operation子类

自定义子类的实现:

@interface NCYOperation : NSOperation

@end

@implementation NCYOperation 

- (void)main {
    NSLog(@"当前线程:%@",[NSThread currentThread]);
} 
@end

具体的使用:

NSOperationQueue *ncyOperationQueue = [[NSOperationQueue alloc] init];
NCYOperation *ncyOperation = [[NCYOperation alloc] init];
[ncyOperationQueue addOperation:ncyOperation];

NSOperationQueue的一些东西

主队列

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

其他队列(上面的例子中其实已经使用到了)

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

最大并发

NSOperationQueue有个属性叫maxConcurrentOperationCount译为最大并发数,这个属性尤为关键。经过上面的GCD了解什么是并发,串行之后,那接下来就更容易懂得NSOperationQueue的maxConcurrentOperationCount属性的使用了。先看段代码:

- (void)operationQueue {
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    NSLog(@"maxConcurrentOperationCount : %ld",operationQueue.maxConcurrentOperationCount);
    
    [operationQueue addOperationWithBlock:^{
        NSLog(@"1______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"2______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"3______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"4______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"5______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
}

打印结果为:

2018-02-14 11:39:23.368948+0800 NCImageDownloader[9078:2021062] maxConcurrentOperationCount : -1
2018-02-14 11:39:23.369646+0800 NCImageDownloader[9078:2021116] 5______<NSThread: 0x60400007bfc0>{number = 3, name = (null)}
2018-02-14 11:39:23.369650+0800 NCImageDownloader[9078:2021117] 2______<NSThread: 0x600000273d80>{number = 6, name = (null)}
2018-02-14 11:39:23.369654+0800 NCImageDownloader[9078:2021118] 3______<NSThread: 0x60c000270bc0>{number = 5, name = (null)}
2018-02-14 11:39:23.369671+0800 NCImageDownloader[9078:2021115] 4______<NSThread: 0x60400026fc00>{number = 7, name = (null)}
2018-02-14 11:39:23.369671+0800 NCImageDownloader[9078:2021119] 1______<NSThread: 0x60c000271040>{number = 4, name = (null)}

结果可以看出:maxConcurrentOperationCount默认的值为:-1,接下来看看打印的顺序:5、2、3、4、1完全是乱来的。说明队列默认的情况下是『并发队列』。

其实NSOperationQueue是通过maxConcurrentOperationCount属性来控制队列的类型的:

maxConcurrentOperationCount -1,默认并发队列
maxConcurrentOperationCount 1,为串行队列
maxConcurrentOperationCount 大于1,并发队列

为了验证,将上述代码的operationQueue.maxConcurrentOperationCount设置为1,再打印:

2018-02-14 11:57:18.465118+0800 NCImageDownloader[9357:2036692] 1______<NSThread: 0x6080004610c0>{number = 4, name = (null)}
2018-02-14 11:57:18.968538+0800 NCImageDownloader[9357:2036691] 2______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:19.470083+0800 NCImageDownloader[9357:2036691] 3______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:19.975773+0800 NCImageDownloader[9357:2036691] 4______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:20.480337+0800 NCImageDownloader[9357:2036691] 5______<NSThread: 0x60c000261e80>{number = 5, name = (null)}

妥妥地按顺序执行:1、2、3、4、5,再将其operationQueue.maxConcurrentOperationCount设置为2时,打印:

2018-02-14 11:58:45.773876+0800 NCImageDownloader[9392:2038259] 1______<NSThread: 0x60400026db00>{number = 5, name = (null)}
2018-02-14 11:58:45.773903+0800 NCImageDownloader[9392:2038256] 2______<NSThread: 0x60800026d440>{number = 4, name = (null)}
2018-02-14 11:58:46.276428+0800 NCImageDownloader[9392:2038260] 4______<NSThread: 0x604000463200>{number = 6, name = (null)}
2018-02-14 11:58:46.276428+0800 NCImageDownloader[9392:2038257] 3______<NSThread: 0x60400026ad80>{number = 3, name = (null)}
2018-02-14 11:58:46.781921+0800 NCImageDownloader[9392:2038257] 5______<NSThread: 0x60400026ad80>{number = 3, name = (null)}

并发执行。

这里要注意的是,maxConcurrentOperationCount值是有上限的,即使你设置了一个很变态的数,系统也会为之设置调整。

队列挂起

- (void)suspendQueue:(NSOperationQueue *)operationQueue {
    //当队列没有了操作,直接return
    if (operationQueue.operationCount == 0) {
        return;
    }
    //只是队列的挂起与恢复,不会影响正在执行的NSOperation
    operationQueue.suspended = !operationQueue.isSuspended;
    if (operationQueue.isSuspended) {
        NSLog(@"队列挂起");
    }else {
        NSLog(@"队列恢复执行");
    }
}

队列取消所有操作

- (void)cancelAllOperation:(NSOperationQueue *)operationQueue {
    //只能取消所有队列的里面的操作,正在执行的无法取消
    [operationQueue cancelAllOperations];
    //
    NSLog(@"取消队列所有操作");
    //取消操作并不会影响队列的挂起状态,需要手动操作队列操作
    //只要是取消了队列的操作,我们就把队列处于不挂起状态,以便于后续队列的开始)
    operationQueue.suspended = NO;
}

如果要单个任务取消,可以使用NSOperation的cancel操作。

依赖

日常的开发中会遇到从服务器下载zip包,解压包的文件,读取文件内容呈现到用户界面上。此过程包括三组操作:下载-解压-刷新UI,示例:

- (void)dependency {
    //download
    NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载.zip包");
    }];
    //unzip
    NSBlockOperation *unzipOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"解压.zip包");
    }];
    //refresh
    NSBlockOperation *refreshOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"刷新UI");
    }];
    
    //创建队列
    NSOperationQueue *handleZipOperationQueue = [[NSOperationQueue alloc] init];
    //解压的操作依赖于下载操作的完成
    [unzipOperation addDependency:downloadOperation];
    //刷新的操作依赖于解压操作的完成
    [refreshOperation addDependency:unzipOperation];
    [handleZipOperationQueue addOperations:@[downloadOperation,unzipOperation] waitUntilFinished:YES];
    //
    [[NSOperationQueue mainQueue] addOperation:refreshOperation];
}

打印结果:

2018-02-14 15:31:45.792174+0800 NCImageDownloader[12401:2188305] 下载.zip包
2018-02-14 15:31:45.792347+0800 NCImageDownloader[12401:2188308] 解压.zip包
2018-02-14 15:31:45.816914+0800 NCImageDownloader[12401:2188237] 刷新UI

这样就可以让业务能正常运行,执行的顺序有条不紊。

还有部分多线程安全部分的内容,抽时间再整理整理。