iOS -- 浅谈多线程原理

进程与线程

如果把进程比作是一个电子厂,那么线程就是一条条的流水作业线。电子厂与电子厂之间相互独立,当前电子厂的作业流水线只能使用自己电子厂资源。

进程

  • 进程是指在系统中正在运行的一个应用程序,比如打开的Xcode
  • 每个进程之间是独立的,每个进程运行在专有的而且受保护的内存空间中。

线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
  • 进程想要执行任务,必须要有线程,进程至少要有一条线程用来执行任务。
  • 程序启动时会默认开启一条线程,这条线程被称为主线程或者UI线程。

进程与线程的关系

  1. 线程是进程的执行单元,进程的所有任务都在线程中执行,同一个进程内的线程共享进程资源。
  2. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  3. 资源拥有:同一进程内的线程共享本进程的资源如内存、I/Ocpu等,但是进程之间的 资源是独立的。
  4. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进 程都死掉。所以多进程要比多线程健壮。

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料: BAT 大厂最新面试题+答案合集(持续更新中) 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障

  1. 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  2. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  3. 线程是处理器调度的基本单位,但是进程不是。
  4. 线程没有地址空间,线程包含在进程地址空间中。

多线程

多线程原理

我们知道一个进程可以开启多个线程,进程的所有任务都在线程中执行,而一个线程中的任务是串行的,如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务,而在同一时刻,一个CPU只能处理一条线程(只有一个线程在执行任务),但CPU可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象。

多线程的优缺点

优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU,内存)
  • 线程上的任务执行完成后,线程会自动销毁

缺点

  • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1 MB,子线程都占用512 KB)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

那么提出一个疑问❓如果进程开启的线程非常非常多,会发生什么情况❓

答:CPU会在许多线程之间调度,CPU会累死,会消耗大量的CPU资源, 而且每条线程被调度执行的频次会降低(线程的执行效率也就降低)

主线程(UI线程)

一个iOS程序运行后,默认会开启一条线程,称为主线程UI线程.主线程主要用于显示刷新UI界面,处理UI事件。(最好不要将耗时任务放在主线程处理,耗时操作会卡住主线程,造成一种卡顿现象。)

线程的生命周期

image.png
  • 新建:实例化线程对象
  • 就绪:向线程对象发送start消息,线程对象并不会立即执行,线程对象被加入可调度线程池等待CPU调度。
  • 运行:CPU 负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
  • 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。当进入休眠时,会重新将线程加入就绪状态中。休眠的时间设置参数为:sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
  • 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行(或者在主线程中止线程对象)

关于线程的exitcancel

[NSThread exit]:一旦强行终止线程,后续的所有代码都不会执行

[thread cancel]:并不会直接取消正在执行的线程,只是给线程对象添加 isCancelled 标记

线程的优先级

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
复制代码

上述优先级从高到低,但是,线程执行的快慢,除了看线程的优先级,还需要查看执行任务资源的大小(即任务的复杂度)、以及 CPU调度情况。

线程池

image.png

线程安全

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好像售票系统,如果多人同时在售票,每个人的售票处理的速度不一样,那么就会造成余票的数量飘忽不定。 那么解决多线程安全问题有两种方法:互斥锁和自旋锁。

互斥锁和自旋锁

互斥锁(同步锁)@synchronized

@synchronized(锁对象) {
    // 需要锁定的代码
}
复制代码
  • 用于保护临界区,保证锁内的代码,同一时间,只有一条线程能够执行。
  • 判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用self作为锁对象,这样可以避免单独再创建一个锁对象。
  • 加了互斥锁的代码,当有新的线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。
  • 锁对象一定要保证所有的线程都能够访问。
  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。

自旋锁

自旋锁不同于互斥锁通过线程休眠来达到阻塞,自旋锁是线程在获取锁对象之前,一直处于忙等询问的阻塞状态。

加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。其中,属性修饰符atomic,本身就有一把自旋锁(atomic又称为原子锁)。

atomicnonatomic

atomic 原子属性,是默认属性,是线程安全的,保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以取值。使用其需要消耗大量的资源。

nonatomic 非原子属性,是非线程安全的,同一时间可以有很多线程读和写。相比atomic效率更高。

iOS开发的过程中,建议将所有属性都声明为nonatomic,开发过程中尽量避免多线程抢夺同一资源,将资源的业务逻辑交由服务端完成。

线程之间的通信

在苹果的文档Threading Programming Guide文档的Table 1-3 Communication mechanisms部分,有提到关于线程之间通信的方式。

截屏2021-06-18 下午2.16.46.png

简单用代码介绍一下常用的通信方式:

  1. 直接消息: 通过performSelector的一系列方法
//异步下载图像
[self performSelectorInBackground:@selector(downloadImageWithURL:) withObject:url];

- (void)downloadImageWithURL:(NSURL *)url {
    // 1\. 获取二进制数据
    NSData *data = [NSData dataWithContentsOfURL:url];

    // 2\. 将二进制数据转换成 image
    UIImage *image = [UIImage imageWithData:data];

    // 3\. 在主线程更新 UI
    // waitUntilDone: 是否等待 updateImage: 执行完成
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:YES];
    NSLog(@"完成");
}
复制代码
  1. 端口通信
ZhModel.h

@interface ZhModel : NSObject
- (void)modelLaunchThreadWithPort:(NSPort *)port;
@end

ZhModel.m

#import "ZhModel.h"
@interface ZhModel()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end

@implementation ZhModel
- (void)modelLaunchThreadWithPort:(NSPort *)port{

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

    NSData *data1 = [@"ZhModel" 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(@"model:handlePortMessage  == %@",[NSThread currentThread]);
    NSLog(@"从VC 传过来一些信息:");
    NSLog(@"components == %@",[message valueForKey:@"components"]);
    NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
    NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
    NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}
@end
复制代码
PortViewController.m

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

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) ZhModel *zhmodel;

@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //1\. 创建主线程的port
    // 子线程通过此端口发送消息给主线程
    self.myPort = [NSMachPort port];
    //2\. 设置port的代理回调对象
    self.myPort.delegate = self;
    //3\. 把port加入runloop,接收port消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];

    self.zhmodel = [[ZhModel alloc] init];
    [NSThread detachNewThreadSelector:@selector(modelLaunchThreadWithPort:)
                             toTarget:self.zhmodel
                           withObject:self.myPort];

}

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{

    NSLog(@"VC == %@",[NSThread currentThread]);
    NSLog(@"从person 传过来一些信息:");
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"传过来一些信息 :%@",dataStr);
    NSPort  *destinPort = [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(@"Thread == %@",[NSThread currentThread]);
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);
}
@end
复制代码
截屏2021-06-18 下午3.01.00.png

多线程的实现方式

多线程的四种实现方式分别是:pthreadNSThreadGCDNSOperation

image.png

下面通过代码来看一下这四种实现方式:

  1. pthread
/**
     pthread_create 创建线程
     参数:
     1\. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
     同时不需要 `*`
     2\. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
     3\. 线程要执行的`函数地址`
     void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
     (*): 函数名
     (void *): 参数类型,void *
     4\. 传递给第三个参数(函数)的`参数`

     返回值:int
     0          创建线程成功!成功只有一种可能
     非 0       创建线程失败的错误码,失败有多种可能!
 */

pthread_t threadId = NULL;
char *cString = "HelloWorld";
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
    NSLog(@"成功");
} else {
    NSLog(@"失败");
}

void *pthreadTest(void *para){
    // __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    return NULL;
}   
复制代码

2.NSThread

[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
复制代码
  1. GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self threadTest];
});
复制代码
  1. NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        [self threadTest];
}];

- (void)threadTest{
    NSLog(@"begin");
    NSLog(@"over");
}

未完待续......

作者:Henry_Jeannie
链接:https://juejin.cn/post/6975035560607875080
来源:掘金

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

推荐阅读更多精彩内容