【OC多线程】iOS中的多线程实现方案

目录
一、进程、线程、多线程
二、iOS中的多线程实现方案
  1、pthread
  2、NSThread
  3、GCD
  4、NSOperation


一、进程、线程、多线程


进程就是指操作系统上正在运行的应用程序。

线程就是指一段代码从头到尾的执行路径,具体地说我们编写的OC代码最终都会被编译成二进制代码供CPU执行,那么CPU在执行这些二进制代码时是从上往下一行一行串行执行的,当遇见if语句或for语句等控制语句时,CPU会偏离当前地址处的二进制代码去执行其它地址处的二进制代码,但执行完后又会返回来执行当前地址处的二进制代码,直到二进制代码执行完毕,这样一段代码从头到尾的执行路径就被称为一个线程。

而开辟一个新线程就是指把某段代码的执行路径和另一段代码的执行路径给完全独立开来,成为CPU的一个单独调度单位,所以当一个进程中有多个这样独立的代码的执行路径时,就是多线程。使用多线程的好处是可以提高程序的执行效率,使用多线程的坏处是开辟过多的线程会占用大量内存和CPU资源,而且还会存在数据竞争问题,因此通常开三到五个线程就差不多了。我们都知道一个CPU一次只能调度一个线程,那苹果是怎么实现多线程并发的呢?原来苹果会让CPU一会儿执行线程1,一会执行线程2,当线程之间的切换时间足够短时,就让我们感觉CPU是在一次执行多个线程,而现在设备都是多核的了,就不仅仅是“感觉”了,而是真得可以利用多个CPU来实现多线程并发。主线程主要用来显示/刷新UI界面和处理UI事件(如点击事件、滚动事件等),子线程主要用来做耗时操作。


二、iOS中的多线程实现方案


实现方案 简介 语言 线程生命周期 使用频率
pthread 是一套跨平台的多线程API,但使用难度较大 C 程序员管理 几乎不用
NSThread 是对pthread的OC封装,使用更加面向对象,轻量级、灵活 OC 程序员管理 偶尔使用
GCD 充分利用设备的多核,旨在替代NSThread等实现方案 C 自动管理 经常使用
NSOperation 是对GCD的OC封装,使用更加面向对象,同时它提供了更加简单的API来实现线程同步和任务同步 OC 自动管理 经常使用

1、pthread

#import "PthreadViewController.h"
#import <pthread.h>

@interface PthreadViewController ()

@end

@implementation PthreadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"pthread";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    // ❌主线程做耗时操作...
//    for (int i = 0; i <= 10000; i++) {
//        NSLog(@"%d---%@", i, [NSThread currentThread]);
//    }
    
    // 1、子线程对象
    pthread_t pthread;
    
    // 2、创建子线程
    //
    // 第一个参数:子线程对象的地址
    // 第二个参数:子线程的属性,可以传个nil
    // 第三个参数:子线程对应函数的指针
    // 第四个参数:子线程对应函数的参数,可以传个nil
    pthread_create(&pthread, nil, subThreadAction, nil);
}

void *subThreadAction(void *params) {
    // ✅子线程做耗时操作...
    for (int i = 0; i <= 10000; i++) {
        NSLog(@"%d---%@", i, [NSThread currentThread]);
    }
    
    return nil;
}

@end

2、NSThread

#import "NSThreadViewController.h"

@interface NSThreadViewController ()

@end

@implementation NSThreadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"NSThread";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    // ❌主线程做耗时操作...
//    for (int i = 0; i <= 10000; i++) {
//        NSLog(@"%d---%@", i, [NSThread currentThread]);
//    }
    
    [self createSubThread1];
//    [self createSubThread2];
//    [self createSubThread3];
}

/// 创建子线程的方式1
- (void)createSubThread1 {
    // 1、创建子线程(此时线程处于新建状态)
    //
    // NSThread的生命周期是由我们程序员管理的,但我们也只需要负责创建线程就可以了
    // 至于线程的销毁时机,它有点特别,你别看这里subThreadA是个局部变量,出了createSubThread1的作用域就会销毁,但是它指向的线程对象却不会销毁,线程的销毁时机是它内部的任务执行完后(此时线程处于销毁状态),也就是subThreadAction这个方法执行完后,系统会做这件事,我们不用管
    // 当然我们也可以通过[NSThread exit]手动退出这个线程,也就是把这个线程销毁掉
    NSThread *subThreadA = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
    
    // 设置子线程的名字
    subThreadA.name = @"我是线程A";
    // 设置子线程的优先级(0~1,默认值0.5),可以改变CPU调度该子线程的概率
    subThreadA.threadPriority = 1;
    
    // 2、启动子线程(此时线程会被放入线程池,处于运行状态)
    [subThreadA start];
    
    
    NSThread *subThreadB = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
    subThreadB.name = @"我是线程B";
    subThreadB.threadPriority = 0.5;
    [subThreadB start];
    
    NSThread *subThreadC = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
    subThreadC.name = @"我是线程C";
    subThreadC.threadPriority = 0.25;
    [subThreadC start];
}

/// 创建子线程的方式2
- (void)createSubThread2 {
    // 创建子线程,会自动启动
    [NSThread detachNewThreadSelector:@selector(subThreadAction) toTarget:self withObject:nil];
}

/// 创建子线程的方式3
- (void)createSubThread3 {
    // 创建子线程,会自动启动
    [self performSelectorInBackground:@selector(subThreadAction) withObject:nil];
}

- (void)subThreadAction {
    // ✅子线程做耗时操作...
    for (int i = 0; i <= 10000; i++) {
        NSLog(@"%d---%@", i, [NSThread currentThread]);
    }
}

@end

3、GCD

详见这篇文章

4、NSOperation

GCD里有两对儿重要的概念是同步追加任务/异步追加任务和队列,NSOperation是对GCD的OC封装,所以它也有两对儿重要的概念是同步追加任务/异步追加任务和队列。

4.1 异步追加

GCD里有dispatch_syncdispatch_async之分,要想实现多线程开发就必须得用dispatch_asyncdispatch_sync使用不当还会造成死锁。而NSOperation里没有同步追加,只有异步追加,只要我们创建一个任务,把任务追加到队列里时就是异步追加任务。

  • 通过selector创建一个任务
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
  • 通过block创建一个任务
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    
    // 任务...
}];
  • 把任务追加到队列中
[queue addOperation:operation];
4.2 并发队列和主队列

GCD里有串行队列和并发队列之分,如果想让多个任务并发执行,就必须得用并发队列,而如果想让多个任务串行执行,就必须得用串行队列。NSOperation里没有串行队列(当然主队列是个串行队列),只有并发队列(当然我们把并发队列的线程最大并发数设置为1时也能实现串行队列的效果),只要我们创建一个队列,它就是并发队列。

  • 并发队列
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  • 主队列
// 获取主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
4.3 简单使用
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    for (int i = 0; i < 10; i++) {
        
        // 创建任务
        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
        // 把任务追加到队列中
        [queue addOperation:operation];
    }
}

- (void)threadAction {
    
    NSLog(@"执行任务,%@", [NSThread currentThread]);
}

@end


// 控制台打印:
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
执行任务,<NSThread: 0x600001925780>{number = 4, name = (null)}
执行任务,<NSThread: 0x600001909a00>{number = 6, name = (null)}
执行任务,<NSThread: 0x600001927000>{number = 5, name = (null)}
执行任务,<NSThread: 0x600001916080>{number = 7, name = (null)}
4.4 其它常用API
  • 设置线程最大并发数,可以用来实现线程同步(等同于GCD里使用信号量设置线程最大并发数)

当我们设置NSOperationQueue的线程最大并发数为1时,这个队列就实现串行队列的效果了,需要注意的是设置线程最大并发数为1 != 只开一条线程,而是指线程同步。

#import "NSOperationViewController.h"

@interface NSOperationViewController ()

@end

@implementation NSOperationViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"NSOperation";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 设置线程最大并发数
    queue.maxConcurrentOperationCount = 1;

    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]);
    }];
    NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"5---%@", [NSThread currentThread]);
    }];
    NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"6---%@", [NSThread currentThread]);
    }];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    [queue addOperation:op4];
    [queue addOperation:op5];
    [queue addOperation:op6];
}

@end


// 控制台打印(可见开了两条线程,但线程是同步执行的):
1---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
2---<NSThread: 0x60000332f000>{number = 5, name = (null)}
3---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
4---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
5---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
6---<NSThread: 0x6000033382c0>{number = 3, name = (null)}
  • 设置任务之间的依赖关系,可以用来实现任务同步(等同于GCD里使用队列组来控制任务的执行顺序,但要比GCD队列组强大得多得多)

NSOperation设置任务之间的依赖关系功能真得很强大,即便是不同队列里的任务也可以设置依赖关系,需要注意的是不要设置循环依赖就可以了。

#import "NSOperationViewController.h"

@interface NSOperationViewController ()

@end

@implementation NSOperationViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"NSOperation";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSOperationQueue *queue = [[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]);
    }];
    NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"5---%@", [NSThread currentThread]);
    }];
    NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"6---%@", [NSThread currentThread]);
    }];
    
    // 比如我们希望任务的执行顺序为:6、3、5、2、4、1,那么可以设置任务之间的依赖关系
    [op3 addDependency:op6];
    [op5 addDependency:op3];
    [op2 addDependency:op5];
    [op4 addDependency:op2];
    [op1 addDependency:op4];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    [queue addOperation:op4];
    [queue addOperation:op5];
    [queue addOperation:op6];
}

@end


// 控制台打印(可见任务是按我们指定的顺序执行的):
6---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
3---<NSThread: 0x6000011de240>{number = 2, name = (null)}
5---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
2---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}
4---<NSThread: 0x6000011de240>{number = 2, name = (null)}
1---<NSThread: 0x6000011dd5c0>{number = 3, name = (null)}

这里我们再解释一下线程同步和任务同步,举个例子:假设火车站出站口有100个人在等着打车回家,有10个站台,每个站台上等了10个人,出租车公司一共就安排了4辆出租车来拉这批人。那么我们可以把一辆出租车看成是一个线程,一个站台看成是一个队列,一个人看成是一个任务,也就是说我们开辟了4条线程来做事情,有10个队列,每个队列里塞了10个任务。

开始拉人了,出租车1从站台1把排第1位的人拉走,与此同时,出租车2从站台2把排第1位的人拉走,出租车3从站台3把排第1位的人拉走,出租车4从站台4把排第1位的人拉走,也就是说4辆车同时发射,几分钟后,出租车2先回来了,它又去站台5把排第1位的人拉走,又隔了几分钟,出租车4也回来了,它又去站台6把排第1位的人拉走,又隔了几分钟,出租车1也回来了,它又去站台7把排第1位的人拉走,又隔了几分钟,出租车3也回来了,它又去站台8把排第1位的人拉走,……,这就是多线程并发。

而我们所说的线程同步主要是指开辟了多个线程的情况下、同一时间只能有一个线程在执行任务,也就是说虽然安排了4辆车来拉人,但同一时间只能有一辆车真正在路上穿梭,下一辆车想拉人出发就必须得等上一辆车返回来之后才行,这也很简单,只要我们制定规则一次只能发出一辆车就好了,也就是设置线程最大并发数为1。这个用GCD和NSOperation都可以很简单地实现。

而任务同步则主要是指任务是按顺序执行的,当然这些任务可以是在同一个队列里也可以是在多个队列里,也就是说100个人你得按他们先出站的顺序一个一个地送到家。方案一:你可以把他们都安排在一个站台上排队,也就是只开一个队列,而且这个队列得是个串行队列,然后你可以只开一个线程,也就是只安排一辆车来拉他们,这样必然就是一个一个地按顺序送到家,当然你也可以开多个线程 + 线程同步来拉他们,这样也能完成任务同步,这个用GCD和NSOperation都可以很简单地实现。方案二:你可以把他们安排在10个站台上等,但是你得有一种机制知道第二个站台上的第一个人应该在第一个站台最后一个人后面(即依赖关系),这样才能保证车不优先去二站台拉人,然后同样你可以只开一个线程,也就是只安排一辆车来拉他们,这样必然就是一个一个地按顺序送到家,当然你也可以开多个线程 + 线程同步来拉他们,这样也能完成任务同步,这个就得用NSOperation来实现了,只有它才可以设置跨队列之间任务的依赖关系。

可见线程同步的实现很简单,就是在开辟多个线程的情况下,设置一下线程的最大并发数为1就行了(当然还有加锁、把多个线程放到串行队列里等其它方案),用GCD和NSOperation都行。

而任务同步的实现则比较复杂,你得首先把任务放在一个或多个都可以串行队里,然后决定开辟一个线程或开辟多个线程 + 线程同步来实现,当遇到需要做任务同步的场景时推荐使用NSOperation。

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

推荐阅读更多精彩内容