iOS多线程 基础

iOS 多线程

1.线程与进程

1.1 线程的定义

  1. 线程是进程的基本执行单元,一个进程的所有任务大偶在线程中执行
  2. 进程要想执行任务,必须得有线程,进程至少要有一条线程
  3. 程序启动会默认开启一条线程,这条线程被称为主线程或UI线程

1.2进程

  1. 进程是系统进行资源和调度的基本单位
  2. 在移动端进程是指在系统中正在运行的一个应用程序(注:iOS是单进程,Android可以实现多进程)
  3. 每个进程之间是独立的,每个进程均运行在其专用的切受保护的内存

1.3进程与线程的关系

  1. 地址空间:统一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  2. 资源拥有:同一进程内的线程共享本进程的资源内存、I/O、CPU等,但是进程之间的资源是独立的。
  3. 相互影响:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃后整个进程就死掉了,所以多进程要比多线程健壮
  4. 资源占用:进程切换时资源消耗大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
  5. 执行过程:每个独立的进程有一个程序的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须存在应用程序中(进程),有应用程序提供多个线程执行控制(多线程开发)。
  6. 线程是处理器调度的基本单位,但进程不是,线程也是进程执行的基本单位

1.4线程与队列的关系

多线程中的队列有:串行队列,并发队列,全局队列,主队列。
队可以理解为一种数据结构,以某种方式,等待线程去执行。

1.5 iOS线程与Runloop的关系

  1. 线程与Runloop是一一对应的,一个Runloop对应一个核心线程,为什么说是核心线程,因为Runloop是可以嵌套的,但是核心只有一个,他们的对应关系保存在一个全局字典里
  2. Runloop是用来管理线程的,线程执行完任务时会进入休眠状态,有任务进来时会被唤醒,开始执行任务。所以说Runloop是事件驱动的。
  3. Runloop在第一次获取时被创建,线程结束时被销毁。主线程的Runloop在程序启动的时候就会被创建。
  4. 子线程的Runloop是懒加载的,只有在使用的时候才被创建。
    注:在子线程使用NSTimer时要注意确保子线程的Runloop已经创建,否则NSTimer不会生效。

2.多线程

2.1 多线程的定义

多线程是一个进程中并发多个线程同时执行各自的任务。就是由单核CPU通过时间片不断的切换执行程序。

分时操作系统会把CPU的时间划分为长短基本相同的时间区间(时间片),在一个时间片内,CPU只能处理一个线程中的一个任务,对于一个单核CPU来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”。

2.2多线程的优缺点

1.优点
    *减少应用程序的堵塞,增加程序的执行效率
    *适当提高CPU和内存的利用率
    *线程上的任务执行完成后,线程自动销毁(部分API可实现)
3.缺点
    *线程的开启需要占用一定的内存空间,默认是512KB/线程
    *线程开启的越多内存占用越大,会降低程序的性能
    *线程越多CPU在调用线程上的开销就越大
    *程序设计更加复杂,需要考虑线程间通信,多线程的数据共享等问题

2.3 多线程的生命周期

线程的生命周期.jpg
可能造成阻塞的条件:
锁(lock),循环引用,sleep()函数,循环执行

2.4 可调度线程池

什么是线程池:

提供一组线程资源用来复用线程资源的一个池子

线程池中常用参数释义:

  • corePoolSize 线程池的基本大小(核心线程池大小)
  • maximumPool 线程池最大大小
  • keepAliveTime 线程池中超过corePoolSize数目的空闲线程的最大存活时间
  • unit keepAliveTime参数的时间单位
  • workQueue 任务阻塞队列
  • threadFactory 新建线程的工厂
  • handler 当提交任务数超过maximumPoolSizeworkQueue之和时,任务会交给RejectedExecutionHandler来处理

线程池调度原理:

线程池调度原理.jpg

饱和策略:

  1. AboutPolicy 直接抛出RejectedExecutionExeception异常,阻止系统的正常运行
  2. CallerRunsPolicy 将任务回退到调用者
  3. DisOldestPolicy 丢掉等待最久的任务
  4. DisCardPolicy 直接丢弃任务

注: 以上四种拒绝策略均实现的RejectedExencutionHandler

注:iOS开发中不会直接接触到线程池,这是因为GCD已经包含了线程池的管理,我们常用的就是GCD(NSOperation底层也是GCD),所以只需要通过GCD获取线程来执行任务即可。

继续探索多线程更底层的知识可以尝试搜索Java Posix

2.6 iOS的几种多线程技术方案

iOS多线程API.jpg
  1. pthread:即POSIX Thread,是线程的POSIX标准,是一套通用的多线程API,可以在Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。
  2. NSThread:苹果封装的面向对象的线程类,可以直接操作线程,比起GCDNSThread效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。
  3. GCD:全称Grand Central Dispatch,由C语言实现,是苹果为多核的并行运算提出的解决方案,GCD会自动利用更多的CPU内核,自动管理线程的声明周期,程序员只需要告诉GCD需要执行的任务,无需编写任何管理线程的代码。GCD也是iOS使用频率最高的多线程技术。
  4. NSOperation:基于GCD封装的面向对象的多线程类,相较于GCD提供了很多方便的API,使用频率较高。

3. 线程间的通讯

3.1 几种线程间的通讯方式

通过上面的介绍我们对线程有了基本的了解,那么线程之间是如何通讯的呢?其实我们都知道的就是在子线程获取数据,然后切换到主线程进行刷新UI,其实这只是表象,并不是真正的线程通讯方式,如果在面试中这样回答基本就是回家等消息了。那么线程之间倒地是如何通讯的呢?在苹果官方文档中给我们列出了线程间通讯的几种方式:

线程间通讯的几种方式.jpg

线程之间有许多通信方式,每种方式都有其优缺点。配置线程本地存储列出了您可以在OS x中使用的最常见的通信机制(除了消息队列和Cocoa分布式对象,这些技术在iOS中也可用)。这个表中的技术是按照复杂度递增的顺序列出的。其中后两种只能在OS X中使用。

  • Direct messaging:这个就是我们常用的selector系列,例如-performSelector:
  • Global variables, shared memory, and objects: 直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。
  • Conditions:条件锁,一种特殊的锁,我们可以使用它来控制线程何时执行特定部分的代码。
  • Run loop sources: 自定义Runloop来设置用于在线程上接收应用程序特定消息的循环源,因为它们是事件驱动的,所以运行循环源会在无事可做时将线程自动休眠,从而提高线程的效率。
  • Ports and sockets: 通过端口和套接字来实现线程间通讯。

4. iOS中使用线程间通讯示例(使用NSPort)

此处的例子是通过一个PortPerson对象跟ViewController进行发消息的实例。

代码中首先将VCself.myPort添加到主线程的Runloop中,然后起新线程让PersonVC发送消息,VC在收到消息后也像Person发送一条消息,在此过程中并没有切换线程的代码,通过打印线程可以看到VC的操作是在主线程中完成的,Person是在子线程(7)中完成的,通过这些代码就完成了线程间的通讯。

PortPerson 代码:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PortPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end

NS_ASSUME_NONNULL_END

#import "PortPerson.h"

@interface PortPerson()<NSMachPortDelegate>

@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;

@end

@implementation PortPerson

- (void)personLaunchThreadWithPort:(NSPort *)port {
    
    @autoreleasepool {
        NSLog(@"Person Thread %@", [NSThread currentThread]);
        //1. 保存主线程传入的port
        self.vcPort = port;
        //2. 设置子线程名字
        [[NSThread currentThread] setName:@"PortPersonThread"];
        //3. 开启runloop
        [[NSRunLoop currentRunLoop] run];
        //4. 创建自己port
        self.myPort = [NSMachPort port];
        //5. 设置port的代理回调对象
        self.myPort.delegate = self;
        //6. 完成向主线程port发送消息
        [self sendPortMessage];
    }
    
}

- (void)sendPortMessage {
 
    NSData *data1 = [@"这是一条port信息" dataUsingEncoding:NSUTF8StringEncoding];
//    NSData *data2 = [@"data2" dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
    // 发送消息到VC的主线程
    // 第一个参数:发送时间。
    // msgid 消息标识。
    // components,发送消息附带参数。
    // reserved:为头部预留的字节数
    [self.vcPort sendBeforeDate:[NSDate date]
                          msgid:10086
                     components:array
                           from:self.myPort
                       reserved:0];
}

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{
    
    NSLog(@"person:handlePortMessage  == %@",[NSThread currentThread]);


    NSLog(@"从VC 传过来一些信息:");
    NSLog(@"components == %@",[(id)message valueForKey:@"components"]);
    NSLog(@"receivePort == %@",[(id)message valueForKey:@"receivePort"]);
    NSLog(@"sendPort == %@",[(id)message valueForKey:@"sendPort"]);
    NSLog(@"msgid == %@",[(id)message valueForKey:@"msgid"]);
}

@end

PortViewController 代码:

#import "PortViewController.h"
#import <objc/runtime.h>
#import "PortPerson.h"

@interface PortViewController ()<NSMachPortDelegate>

@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) PortPerson *person;

@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    // 创建主线程的port,子线程通过此端口发送消息给主线程
    self.myPort = [NSMachPort port];
    // 设置port的代理回调对象
    self.myPort.delegate = self;
    // 把port加入runloop,接收port消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[PortPerson alloc] init];
    
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
}

- (void)handlePortMessage:(NSPortMessage *)message {
    
    NSLog(@"VC Thread --- %@", [NSThread currentThread]);
    
    NSLog(@"从 person 传过来一些信息");
    
    NSArray *messageArr = [(id)message valueForKey:@"components"];
    
    NSString * dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
    
    NSLog(@"通过port传过来一些信息:%@", dataStr);

    NSPort *destinPort = [(id)message valueForKey:@"remotePort"];
    
    if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
        NSLog(@"传过来的数据有误");
        return;
    }
    
    NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
    
    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
    
    // 非常重要,如果你想在Person的port接收信息,必须加入到当前主线程的runloop
    [[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
    
    NSLog(@"VC == %@",[NSThread currentThread]);
    
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);
}

@end

打印结果:

打印结果.jpg

注意!!!

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

推荐阅读更多精彩内容

  • 多线程掌握:GCD + NSOperation + SDWebImage + 单例模式 基本概念 进程:在系统中正...
    Arthur澪阅读 150评论 0 0
  • 什么是进程? 进程是指系统中正在运行的一个程序,每个进程间是独立的,每个进程均运行在其专用且受保护的内存空间内. ...
    星辰流转轮回阅读 221评论 0 0
  • 系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...
    林安530阅读 371评论 0 0
  • 学习线程之前我们先温习下 进程: 线程: 多线程的原理:同一时间,cpu只能处理一条线程,只有一条线程在工作,多线...
    Jey阅读 185评论 0 0
  • 多线程,一直是解决内存暴增方法的重点,图片、视频、大量数据的的下载现在总结一下 基本概念 进程:应用程序的执行实例...
    艳晓阅读 274评论 0 0