iOS开发之多线程编程总结(一)

背景

前段时间锻炼身体胃出血的事情,每天想着这个事情,害怕有什么大问题!这周一才预约下周一去做胃镜检查,又要担心好几天啊!我在思考如果真有大病,医院这种制度得耽误多少病人,因为这种制度而耽误多少人的最佳治疗时间?

我们还是要多读书的,因为最近动物考古学家一行在饭店吃饭,点的羊腿中赫然发现猪骨,愤而与店家对质(你们知道我们是干什么的吗?我们连一根毛都知道出自什么动物身上),最后店家给免单了(我怎么就没这本事呢_)!

iOS开发之多线程编程总结(一)_第1张图片
真TM好看.png

为什么要读书?
你失恋时…你会说:“人生若只如初见,何事秋风悲画扇。等闲变却故人心,却道故人心易变。”而不是千万遍呼喊:“蓝瘦,香菇!”

当你看到夕阳余晖…你脑海里浮现的是:“落霞与孤鹜齐飞,秋水共长天一色。”而不是:“卧槽,这多鸟,这鸟真肥啊,真好看,真他妈太好看了!”

所以我们要多读书,多学习,这样才能出去装X!

前言

说到多线程编程大家肯定不陌生,也是面试和工作中经常碰到的,所以网上各种博客层出不穷,光在上搜索iOS 多线程就有1w+的文章,所以我也来凑凑热闹混个脸熟(不要脸啊)!
谈到多线程就到说到线程和进程,就要说到NSThread、GCD、NSOperation!就要提到RunLoop,你既然提到了RunLoop了,他兄弟Runtime肯定要提到吧(他俩不是一回事),这些我都会在后面一一道来啊!我能从诗词歌赋谈到人生哲学,从人生哲学谈到诗词歌赋。下面就跟我一起来装X!

基本知识

1. 进程(process)

  • 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程。
  • 每个进程之间是相互独立的, 每个进程均运行在其专用且受保护的内存空间内。
  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。
  • 进程状态:进程有三个状态,就绪,运行和阻塞。就绪状态其实就是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行。运行态就是获取了处理器分配的资源,程序开始执行,阻塞态,当程序条件不够时,需要等待条件满足时候才能执行,如等待I/O操作的时候,此刻的状态就叫阻塞态。

2. 线程(thread)

  • 一个进程要想执行任务,必须要有线程,至少有一条线程
  • 一个进程的所有任务都是在线程中执行
  • 每个应用程序想要跑起来,最少也要有一条线程存在,其实应用程序启动的时候我们的系统就会默认帮我们的应用程序开启一条线程,这条线程也叫做'主线程',或者'UI线程'

3. 进程和线程的关系

  • 线程是进程的执行单元,进程的所有任务都在线程中执行!
  • 线程是 CPU 调用的最小单位
  • 进程是 CPU 分配资源和调度的单位
  • 一个程序可以对应过个进程,一个进程中可有多个线程,但至少要有一条线程
  • 同一个进程内的线程共享进程资源

举个例子:进程就好比公司中的一个个部门,线程则代表着部门中的同事,而主线程当然是我们的老板了,一个公司不能没有老板,一个程序不能没有线程其实都是一个道理.

  • 相同点:进程和线程都是有操作系统所提供的程序运行的基本单元,系统利用该基本单元实现系统对应用程序的并发性。

  • 不同点:

  • 进程和线程的主要差别在于他们是不同的操作系统资源管理方式。

  • 进程有独立的地址空间,一个进程crash后,在保护模式下不会对其他进程产生影响。

  • 而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间。一个线程crash就等于整个进程crash

  • 多进程的程序比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  • 优缺点:

    • 进程执行开销大,但利于资源的管理和保护。
    • 线程执行开销小,但不利于资源的管理和保护。线程适合于在SMP(多核处理机)机器上运行。

4. 多线程

iOS开发之多线程编程总结(一)_第2张图片
CPU命令列.jpg

Mac、iPhone的操作系统OS X、iOS根据用户的指示启动应用程序后,首先便将包含在应用程序中的CPU命令列配置到内存中。CPU从应用程序知道的地址开始,一个一个执行CPU命令列。

在OC的if或for语句等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置迁移)。但是,由于一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧。

iOS开发之多线程编程总结(一)_第3张图片
通过CPU执行的CPU命令列.png

这里所说的“1个CPU执行的CPU命令列为一条无分叉路径”,即为“线程”

这种无分叉路径不只1条,存在有多条时即为“多线程”。 1个CPU核执行多条不同路径上的不同命令。

iOS开发之多线程编程总结(一)_第4张图片
在多线程中执行CPU命令列.png

OS X和iOS的核心XNU内核在发生操作系统事件时(如每隔一定时间,唤起系统调用等情况)会切换执行路径。例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这被称为“上下文切换

由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是“看上去像”了,而是真的提供了多个CPU核并行执行多个线程的技术。

这种利用多线程编程的技术就被称为“多线程编程

但是,多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互等待(死锁)、使用太多线程会消耗大量内存等。


iOS开发之多线程编程总结(一)_第5张图片
多线程编程易发问题.png

4.多线程的优点和缺点

  • 优点

    • 能适当的提高程序的执行效率
    • 能适当提高资源利用率(CPU 内存利用率)
  • 缺点

    • 开启线程需要占用一定的内存空间,如果开启大量的线程,则会占用大量的内存空间,降低程序的性能
    • 线程越多,CPU 在调度线程上的开销就越大
    • 程序设计更加复杂: 比如线程之间的通信, 多线程的数据共享

5.多线程实际应用

  • 使用单例模式时,可以使用GCD
  • 耗时操作放入子线程处理,完成后回主线程显示
  • 从数据库读取大量数据,可开辟子线程操作
  • 处理音频、视频数据时,在子线程处理
  • 数据同步操作,如百度云,可在子线程进入后台后开始同步

6.主线程

  • 也就是应用程序启动的时候,系统默认帮我们创建的线程,称之为'主线程'或者是'UI线程';
  • 主线程的作用一般都是用来显示或者刷新UI界面例如:点击,滚动,拖拽等事件

7.串行(Serial)和 并行(Parallelism)

串行和并行描述的是任务和任务之间的执行方式. 串行是任务A执行完了任务B才能执行, 它们俩只能顺序执行. 并行则是任务A和任务B可以同时执行.

8.同步(Synchronous) 和 异步(Asynchronous)

同步和异步描述的其实就是函数什么时候返回. 比如用来下载图片的函数A: {download image}, 同步函数只有在image下载结束之后才返回, 下载的这段时间函数A只能搬个小板凳在那儿坐等... 而异步函数, 立即返回. 图片会去下载, 但函数A不会去等它完成. So, 异步函数不会堵塞当前线程去执行下一个函数!

9.并发(Concurrency) 和 并行(Parallelism)

用Ray大神的示意图和说明来解释一下:

并发是程序的属性(property of the program), 而并行是计算机的属性(property of the machine).

iOS开发之多线程编程总结(一)_第6张图片
Concurrent_vs_Parallelism.png

还是很抽象? 那我再来解释一下, 并行和并发都是用来让不同的任务可以"同时执行", 只是并发是伪同时, 而并行是真同时. 假设你有任务T1和任务T2(这里的任务可以是进程也可以是线程):

a. 首先如果你的CPU是单核的, 为了实现"同时"执行T1和T2, 那只能分时执行, CPU执行一会儿T1后马上再去执行T2, 切换的速度非常快(这里的切换也是需要消耗资源的, context switch), 以至于你以为T1和T2是同时执行了(但其实同一时刻只有一个任务占有着CPU).

b. 如果你是多核CPU, 那么恭喜你, 你可以真正同时执行T1和T2了, 在同一时刻CPU的核心core1执行着T1, 然后core2执行着T2, great!

其实我们平常说的并发编程包括狭义上的"并行"和"并发", 你不能保证你的代码会被并行执行, 但你可以以并发的方式设计你的代码. 系统会判断在某一个时刻是否有可用的core(多核CPU核心), 如果有就并行(parallelism)执行, 否则就用context switch来分时并发(concurrency)执行.

Parallelism requires Concurrency, but Concurrency does not guarantee Parallelism!(并行要求并发性,但并发并不能保证并行性)

iOS中的多线程

类型 | 简介| 实现语言| 线程生命周期|使用频率|
---- |----- | ----- |-----
pthread| 1. 一套通用的多线程API
2. 适用于 Unix / Linux / Windows 等系统
3. 跨平台\可移植
4. 使用难度大|C|程序员管理|几乎不用
NSThread |1. 使用更加面向对象
2. 简单易用,可直接操作线程对象|OC|程序员管理|偶尔使用
GCD|1. 旨在替代NSThread等线程技术
2. 充分利用设备的多核
3. 基于 C 的底层的 API|C|自动管理|经常使用
NSOperation|1. 是基于 GCD 实现的 Objective-C API
2. 比GCD多了一些更简单实用的功能
3. 使用更加面向对象|OC|自动管理|经常使用

小结:上面只是介绍了多线程涉及的基本概念和基本知识(要理解啊),防止在后面学习的过程中混淆。下面才刚刚踏上多线程编程的道路:NSThread、GCD、NSOperation,pthread就不介绍了(有兴趣的自行学习),因为我在开发中没用过,用到再补充吧(哪那么多借口,不会就不会呗)。下面先介绍简单的NSThread。GCD和NSOperation会单独拿出来介绍(东西可能比较多)!

NSThread的使用

创建线程的方式

1、 通过NSThread的对象方法

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);

2、 通过NSThread的类方法

+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

3、通过NSObject的分类方法

- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

NSThread代码Demo

- (IBAction)downloadAction:(UIButton *)sender {
//    [self categoryNSthreadMethod];
//    [self classNSthreadMethod];
    [self objectNSthreadMethod];
    
}

//通过NSObject的分类方法开辟线程
- (void)categoryNSthreadMethod{
    [self performSelectorInBackground:@selector(downloadImage) withObject:nil];
}

//通过NSThread类方法开辟线程
- (void)classNSthreadMethod{
    //异步1
//    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
    
    //异步方式2
    [NSThread detachNewThreadWithBlock:^{
        [self downloadImage];
    }];
}

//通过NSThread对象方法去下载图片
- (void)objectNSthreadMethod{
    //创建一个程序去下载图片
    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(downloadImage) object:nil];
    //开启线程
    [thread start];
    thread.name = @"imageThread";
}

//下载图片
- (void)downloadImage{
    NSURL *url = [NSURL URLWithString:@"https://p1.bpimg.com/524586/475bc82ff016054ds.jpg"];
    
    //线程延迟10s
    [NSThread sleepForTimeInterval:5.0];
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    NSLog(@"downLoadImage:%@",[NSThread currentThread]);//在子线程中下载图片
    //在主线程更新UI
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];

}

//更新imageView
- (void)updateImage:(NSData *)data{
    NSLog(@"updateImage:%@",[NSThread currentThread]);//在主线程中更新UI
    //将二进制数据转换为图片
    UIImage *image=[UIImage imageWithData:data];
    //设置image
    self.imageView.image=image;
}

线程状态

  • 新建状态

    • 通过上面3中方法实 例化线程对象
    • 程序还没有开始运行线程中的代码
  • 就绪状态

    • 向线程对象发送 start 消息,线程对象被加入 可调度线程池 等待 CPU 调度

    • detachNewThreadSelector 方法
      detachNewThreadWithBlock
      performSelectorInBackground 方法
      会直接实例化一个线程对象并加入 可调度线程池

    • 处于就绪状态的线程并不一定立即执行线程里的代码,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。

  • 运行状态

    • CPU 负责调度可调度线程池中线程的执行
    • 线程执行完成之前(死亡之前),状态可能会在就绪和运行之间来回切换
    • 就绪和运行之间的状态变化由 CPU 负责,程序员不能干预
  • 阻塞状态

    • 所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
    • 线程通过调用sleep方法进入睡眠状态
    • 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者
    • 线程试图得到一个锁,而该锁正被其他线程持有;
    • 线程在等待某个触发条件
      + (void)sleepUntilDate:(NSDate *)date;//休眠到指定日期
      + (void)sleepForTimeInterval:(NSTimeInterval)ti;//休眠指定时长
      @synchronized(self):互斥锁
      sleep(unsigned int) __DARWIN_ALIAS_C(sleep);
    
  • 死亡状态

    • 正常死亡
      • 线程执行完毕
    • 非正常死亡
      • 当满足某个条件后,在线程内部自己中止执行(自杀)
      • 当满足某个条件后,在主线程给其它线程打个死亡标记(下圣旨),让子线程自行了断.(被逼着死亡)

线程通信

线程在运行过程中,可能需要与其它线程进行通信,如在主线程中修改界面等等,可以使用如下接口:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

线程的属性

  • thread.name = @"imageThread";给线程添加一个名字,方便后面出现问题的追踪!
  • [NSThread currentThread];获取当前的进程,打印出来name和number,如果Number为1,则为主线程
  • thread.isMainThread;判断当前线程是否是主线程
  • [NSThread isMultiThreaded];判断进程当前是否是多线程
  • thread.threadPriority = 0.5;threadPriority是线程的优先级,最高是1.0,最低为0.0;默认我们创建的优先级是0.5;
  • thread.stackSize默认情况下主线程和子线程在栈区大小都是512k
  • 线程执行状态
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);//是否正在执行
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);//是否完成
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);//是否取消

线程的同步与锁

线程的同步与锁什么时候会遇到?就是我们在线程公用资源的时候,导致的资源竞争。举个例子:多个窗口同时售票的售票系统!

#import "SellTicketsViewController.h"

@interface SellTicketsViewController (){
    NSInteger tickets;//总票数
    NSInteger count;//当前卖出去票数
}

@property (nonatomic, strong) NSThread* ticketsThreadOne;
@property (nonatomic, strong) NSThread* ticketsThreadTwo;
@property (nonatomic, strong) NSLock *ticketsLock;

@end

@implementation SellTicketsViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    tickets = 100;
    count = 0;
    
    //锁对象
    self.ticketsLock = [[NSLock alloc] init];

    self.ticketsThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(sellAction) object:nil];
    self.ticketsThreadOne.name = @"thread-1";
    [self.ticketsThreadOne start];
    
    self.ticketsThreadTwo = [[NSThread alloc] initWithTarget:self selector:@selector(sellAction) object:nil];
    self.ticketsThreadTwo.name = @"thread-2";
    [self.ticketsThreadTwo start];
    
}

- (void)sellAction{
    while (true) {
        //上锁
        [self.ticketsLock lock];
        if (tickets > 0) {
            [NSThread sleepForTimeInterval:0.5];
            count = 100 - tickets;
            NSLog(@"当前总票数是:%ld----->卖出:%ld----->线程名:%@",tickets,count,[NSThread currentThread]);
            tickets--;
        }else{
            break;
        }
        //解锁
        [self.ticketsLock unlock];
    }
}

@end

通过上面的Demo应该理解线程的同步以及锁的使用问题

[myLock lock]

资源处理....

[myLock unLock];

总结:又到了一篇最后的总结问题了,这篇主要讲解了一些基本概念、基本知识点、以及简单的NSThread。一些基本知识点在后面的博客中也会用到,希望你们能理解,更好的理解多线程!今天的博客就写到这里了。

如果我的文章对你有帮助,请点个喜欢,关注我(楼主真是厚颜无耻啊),如果还没看过瘾那就期待下期的GCD的知识吧_

参考资料:
http://www.jianshu.com/p/9f2fc08f9947
http://www.jianshu.com/p/ebb3e42049fd
http://www.jianshu.com/p/7ce30a806c51
书籍:Objective-C高级编程 iOS与OS X多线程和内存管理

你可能感兴趣的:(iOS开发之多线程编程总结(一))