iOS开发之多线程详解

多线程详解


多线程基础相关知识

进程:类似车间

系统正在运行的应用程序;每个进程都是独立的,都运行在其专用且受保护的内存空间中
每个进程可以开启多条线程

线程:类似车间里面的工人

一个iOS程序启动后会默认开启一条线程,即主线程/UI线程
是进程的执行单元,一个进程要想执行任务,只要要有一条线程;进程只会分配内存,并不会执行任务,所以执行任务必须要至少有一条线程。
例如:网易云音乐进程,那么播放音乐任务就要开启线程来执行了

一条线程中的的任务执行是串行的,也就是说在同一时间内只能执行一个任务;线程是进程中的一条执行路径

线程的状态

  • 1、新建状态(New)
    • 当线程被创建出来的时候就处于这种状态,此时会给线程对象分配内存空间,但线程此时并不能被执行
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:nil];
  • 2、就绪状态(Runnalbe)
    • 当线程调用start方法后,线程才具备可被调度的能力,此时线程处于就绪状态,并把线程对象放到可调度线程池可调度线程池里面还有其他的线程对象
[thread start];//线程此时具有可被调度的能力
  • 3、运行状态(Running)
    • 当CPU调度到当前线程的时候,此时线程是处于运行状态,如果当CPU调度可调度线程池里面的其他线程对象的时候,该线程又会进入就绪状态,所以线程对象是不断的在这两种状态中切换
  • 4、阻塞状态(Blocked)
    • 如果线程调用了sleep方法/等待同步锁,线程就会进入阻塞状态,因此线程将失去可被调度的能力,此时线程对象会从可调度线程池中移除,但还存在与内存中
//睡10秒
[NSThread sleepForTimeInterval:10];
//从现在起睡3秒
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
  • 5、如果sleep方法到时了/得到同步锁时,线程会从中“醒来”,处于就绪状态,因此再次放进可调度线程池里面
  • 6、死亡状态(Dead)
    • 如果线程在中途崩溃了,或者执行完任务,此时线程对象就会从内存中移除
    • 已经死亡的线程无法再次启动

多线程的原理

同一时间,CPU只能处理一条线程,但是如果如果CPU能快速在不同的线程之间进行调度(切换),就会造成多条线程同时执行的假象。
一个App运行之后,系统会默认开启一条线程,也就是“主线程”,也称为“UI线程”;
注意点:不要把耗时的任务放到主线程中,不然会影响UI的刷新,导致卡顿的现象,应该把耗时的任务放到子线程中


多线程的几种实现方案

  • 在iOS中,最常用的多线程实现方案是GCD和NSOperation,NSThread偶尔使用,pthread仅作了解即可
技术方案 简介 语言 线程生命周期 使用频率
pthread 一套通用的多线程API;适用于Unix\Linux\Windows等系统;跨平台\可移植;使用难度大 C 程序员管理 几乎不用
NSThread 使用更加面向对象;简单易用,可以直接操作线程对象 OC 程序员管理 偶尔使用
GCD 旨在替代NSThread等线程技术;充分利用设备的多核 C 自动管理 经常使用
NSOpreation 基于GCD(底层是GCD);比GCD多了一些更简单使用的功能;使用更加面向对象 OC 自动管理 经常使用

NSThread

//第一种创建方式(显式创建并手动启动):
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:nil];
//启动
[thread1 start];
//第二种创建方式(显式创建后自动启动,但无法对线程进行详细的设置,因为无法取得线程对象):
[NSThread detachNewThreadSelector:@selector(download:) toTarget:self withObject:@"www.baidu.com"];
//第三种创建方式(隐式创建并自动启动,但无法对线程进行详细的设置,因为无法取得线程对象):
[self performSelectorInBackground:@selector(download:) withObject:@"www.google.com.cn"];
//判断是否为主线程
+ (NSThread *)mainThread;//获得主线程
- (BOOL)isMainThread;//是否为主线程  
+ (BOOL)isMainThread;//是否为主线程 
//判断正在执行的方法是否在主线程中执行
[NSThread isMainThread];

GCD

  • 优势

    • GCD是苹果公司为多核并行运算提出的解决方案
    • GCD会自动利用更多的CPU内核(比如双核、四核)
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 两个核心概念:

    • 任务: 执行什么操作,即想要做什么
    • 队列:用来存放任务(并发,串行队列)
  • 执行步骤:

    • 1、确定要做的事情(制定任务)
    • 2、将任务添加到队列中

    1、GCD会自动将队列中的任务取出,放到对应的线程中执行
    2、无论是并发还是串行队列,里面的任务都遵循FIFO原则:先进先出,只不过在并发队列中,它会取出一个任务放到子线程中执行,再取出一个放到另外的子线程中执行,由于去任务的速度快,并且又是多条线程执行,所以看起来所有任务是一起并发执行;但是并发的数量由系统决定,所以当任务很多的时候,并不会让所有任务一起执行。

  • 主要执行任务的函数

//同步函数:不具备开启线程的能力,会阻塞当前线程,直到Block中的任务执行完毕,然后再执行当前线程的任务。
dispatch_sync(dispatch_queue_t queue, ^(void)block)
//异步函数:具备开启线程的能力,不会阻塞当前线程,即不用等待Block执行完毕才执行当前方法,当前方法执行完毕再回来执行Block里面的代码。
dispatch_async(dispatch_queue_t queue, ^(void)block)

队列:决定了任务的执行方式

  • 类型

    • 1、并发队列:可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    • 并发功能只有在异步函数下才有效
    • 2、串行队列:让任务一个接一个地执行(一个任务执行完毕后,再执行下一个任务)
    • 串行队列有:主队列;自己创建的串行队列
    • 并发队列有:全局并发队列;自己创建的并发队列
  • 全局并发队列

//两个参数填0即可
dispatch_queue_t queue = dispatch_get_global_queue(long identifier, unsigned long flags);
  • 全局并发队列的优先级
DISPATCH_QUEUE_PRIORITY_HIGH 2  //高
DISPATCH_QUEUE_PRIORITY_DEFAULT 0  //默认(中)
DISPATCH_QUEUE_PRIORITY_LOW (-2)  //底
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN  //后台

四个比较容易混淆的专业术语

  • 同步、异步:是否具备开启多线程的能力,会不会阻塞当前线程的执行,直到Block执行完毕
  • 并发、串行:决定任务的执行方式

同步,异步,并发,串行的组合

  • 1、同步并发(了解)
//全局并发队列(失去并发功能,因为并发的前提是多线程)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//同步函数(不会开子线程)
dispatch_sync(queue, ^{
//code
});
  • 2、同步串行(了解)
//手动创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myThread", NULL);
//同步函数(不会开子线程)
dispatch_sync(queue, ^{
//code
});
  • 3、异步并发(最常用)
//全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//异步函数(一般开多条子线程来同时执行任务)
dispatch_async(queue, ^{
//code
});
  • 4、异步串行(偶尔使用)
//手动创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myThread", NULL);
//异步函数(会开线程,但一般只开一条子线程)
dispatch_async(queue, ^{
//code
});
  • 5、异步函数中的主队列(常用,一般用在线程间通信)

    • 不会开线程
    • 是GCD自带的一种特殊串行队列,放在住队列中的任务都会放在主线程中执行;使用dispatch_get_main_queue()获得主队列
    • 只要是在主队列执行的任务都会自动放到主线程中去执行,即使是添加到异步函数中执行
  • 6、同步函数与主队列不能这样使用

- (void)syncMainQueue
{
    NSLog(@"syncMainQueue----begin--");
    // 1.主队列(添加到主队列中的任务,都会自动放到主线程中去执行)
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    // 2.添加 任务 到主队列中 异步 执行
    dispatch_sync(mainQueue, ^{
        NSLog(@"-----下载图片1---%@", [NSThread currentThread]);
    });
    NSLog(@"syncMainQueue----end--");
}
  • 卡死(死锁)的原因:因为syncMainQueue方法是在主线程执行的,但是执行到第5行的时候就卡死了,因为第5行把一个新的任务放到主线程的后面,而且线程的执行任务是遵循FIFO原则,所以只有等syncMainQueue方法执行完毕后才执行NSLog(@"-----下载图片1---%@", [NSThread currentThread]);但是syncMainQueue想执行完,必须要执行NSLog(@"-----下载图片1---%@", [NSThread currentThread]),因为NSLog是在同步函数中,所以需要立刻执行,所以就会出现你等我执行完,我等你执行完的现象,从而导致卡死状态。

队列组

  • 概念:把不同的任务放在队列里面,然后把该队列放进队列组里面
  • 优点:队列组会等待队列里面的所有任务执行完毕后才调用另一个函数
  • 使用场景举例:等下载图片1任务与现在图片2任务都完成后回到主线程进行合成照片
//创建队列组
dispatch_group_t group = dispatch_group_create();
//全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//任务1
dispatch_group_async(group, queue, ^{
    //下载图片1
});
dispatch_group_async(group, queue, ^{
    //下载图片2
});

//合并图片
dispatch_group_notify(group, queue, ^{
    //合并图片
});

//回到主线程刷新界面
dispatch_async(dispatch_get_main_queue(), ^{
    //刷新
});

GCD概览

同步/异步函数 全局并发队列 手动创建串行队列 主队列
同步函数 没有开启新的线程;串行执行任务 没有开启新的线程;串行执行任务 没有开启新的线程;串行执行任务
异步函数 开启新的线程;并发执行任务 开启新的线程;串行执行任务 开启新的线程;串行执行任务

NSOperation

  • NSOperation是抽象类,只能使用它的子类(NSInvocationOperation,NSBlockOperation,自定义继承NSOperation的子类)
  • NSInvocationOperation可以脱离队列来执行任务,NSInvocationOperation是用来封装任务的,如果调用start方法,则会把任务放到主线程调用;只有放到队列中才在子线程执行。
  • NSBlockOperation也可以脱离队列来执行任务,一个操作可以有多个任务,每个操作的第一个任务是在主线程执行;而且还可以追加任务,追加的任务是在子线程执行
- (void)cancel;//取消单个操作

NSOperationQueue

  • 只要是自己创建的队列,则任务就会在子线程中执行,而且是默认并发执行,除非最大并发数设置为1;

  • 将操作放到队列中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
  • 最大并发数

    • 表示同一时间最多能开多少条线程
  • 依赖

    • 需求:影响操作的执行顺序
//operationB依赖操作operationA
[operationB addDependency:operationA];
//移除依赖
[operationB removeDependency:operationA];
  • 注意点:不能相互依赖
  • 相对GCD的障栏,它可以在不同的队列创建依赖
  • 等操作A完成了再执行操作B,可以这样写
//@property (copy) void (^completionBlock)(void)
[opetationA setCompletionBlock:^{
    //opetationB
}]
  • 队列相关方法
- (void)cancelAllOperations;//取消所有操作,一般用在内存警告
方法里面
@property (getter=isSuspended) BOOL suspended;//暂停(或者恢复)操作,一般在开始(完成)刷新UI界面的时候调用

单例模式

ARC环境下得单例模式:

  • 概念:可以保证在程序运行的过程中,一个类只有一个实例(与懒加载不一样)

  • 单例模式的本质:为什么每次创建一个对象的内存地址不一样,本质上是因为alloc方法,此方法是为对象分配内存,所以我们应该重写alloc方法来控制一个类只有一个实例对象

  • alloc方法内部调用的其实是+(instancetype)allocWithZone:(struct _NSZone *)zone方法

  • 为了防止多线程的问题,所以应该为创建单例对象的方法添加互斥锁

//可以使用GCD实现,此处没采用
static id _instance;//全局变量
if (_instance == nil) { // 防止频繁加锁
    @synchronized(self) {//互斥锁
        if (_instance == nil) { // 防止创建多次
             _instance = [super allocWithZone:zone];
        }
    }
}
  • 防止实例对象调用copy产生新对象,我们应该重写-(id)copyWithZone:(NSZone *)zone-(id)mutableCopyWithZone:(NSZone *)zone来保证实例的唯一性,所以这两个方法直接返回以上的_instance即可;

MRC环境下得单例模式:

  • 目的:为了防止单例对象被release或者retain
  • 跟在ARC环境下类似,只需要重写几个方法即可
- (oneway void)release{
}
- (id)autorelease{
    return self;
}
- (id)retain{
    return _instance;//return self;
}
- (NSUInteger)retainCount{
    return 1;
}

简化单例模式代码

  • 把单例代码抽成宏,宏最好写在一个单独的.h文件,分两部分写,一个是.h文件,一个是.m文件的
  • 什么代码比较适合抽成宏?百年不变的代码,因为宏里面写错了,不会具体报哪行错了,所以不利于调试。(不一样的地方可以传参数解决)
  • 该宏应该做好环境适配(ARC/MRC)
#if __has_feature(objc_arc)
//arc环境
#else
//mrc环境
#endif

互斥锁

  • 使用场景:多线程抢夺同一资源的时候才用,并不是开了多线程就一定要加锁
  • 只要加上互斥锁,就会消耗性能,所以苹果不推荐使用,一般在服务端使用,移动端很少使用
  • 只要被@synchronized的{}包裹起来的代码,同一时刻只能被一个线程执行
  • 加锁必须要传递一个锁对象(任何对象都可以充当锁对象),用来作为锁,但是多个线程必须使用同一把锁才行,否则失效;开发过程中我们一般使用self来当做锁对象,因为很多时候self就是代表控制器,所以是唯一对象,如
@synchronized(self){
    /*
    code,需要被锁的代码
    */
}
  • 加锁的时候尽量缩小范围(作用域),因为范围越大性能越底,尽量只在影响线程安全(多线程同时抢夺同一资源)的代码处加锁。
  • 优点:能够有效防止因多线程抢夺资源造成的数据安全问题
  • 缺点:需要消耗大量的CPU资源
  • 相关专业术语:线程同步(多条线程在同一条线上执行,即按顺序执行任务,即只有一条线程执行完毕才到另外一条线程来执行)与线程并发相反;加锁就可以实现线程同步

原子性和非原子性

  • nonatomic: 非线性安全,不会为setter加锁,取而代之的可以在局部地方进行加锁,因为setter的调用频率比较高,所以会消耗大量的性能
  • atomic:线程安全,会为setter加锁,系统默认会加锁;需要消耗大量的性能;是系统自动给我们添加的锁,但不是互斥锁,是自旋锁;
    • 共同点:都能够保证多线程在同一时间内只能有一个线程操作锁定的代码;
    • 不同点:如果互斥锁,假如现在被锁住了,那么后面来的线程就会进入“休眠”状态,直到解锁之后,就会唤醒线程继续执行;如果是自旋锁,假如现在被锁住了,那么后面来的线程不会会进入“休眠”状态,到解锁之后,就会直接执行;
    • 自旋锁更加适合做一些时间较短的操作;

线程间通信

  • 举例:在子线程加载图片,在主线程刷新UI界面
//下载完成后将image传到主线程,并在downloadFinished:方法内刷新UI界面
//waitUntilDone参数:若传YES则表明:等待主线程里面的图片设置完成后再执行子线程之后的代码;传NO就不会等待主线程的操作,便直接执行子线程的代码
[self performSelectorOnMainThread:@selector(downloadFinished:) withObject:image waitUntilDone:NO];
//也可以这样优化
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];

//线程间通信的常用方法一(performSelector):
[self performSelector:@selector(downloadFinished:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];

//线程间通信的常用方法二(NSOperationQueue):
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    // 1.异步下载图片(耗时操作)
    NSURL *url = [NSURL URLWithString:@"图片URL"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    // 2.回到主线程,显示图片(刷新UI)
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.imageView.image = image;
    }];
}];
//线程间通信的常用方法三(GCD):
dispatch_sync(dispatch_get_main_queue(), ^{
        //操作
    });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容