iOS -- 浅谈多线程原理

2021-06-22  本文已影响0人  iOS开发之家

进程与线程

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

进程

线程

进程与线程的关系

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

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

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

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

多线程

多线程原理

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

多线程的优缺点

优点

缺点

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

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

主线程(UI线程)

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

线程的生命周期

image.png

关于线程的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(锁对象) {
    // 需要锁定的代码
}
复制代码

自旋锁

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

加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。其中,属性修饰符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
来源:掘金

上一篇下一篇

猜你喜欢

热点阅读