iOS底层(十三)-多线程

810 阅读7分钟

一、线程与进程

线程的定义:

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

进程的定义:

  • 进程是指在系统中正在运行的一个程序。
  • 每个进程之间都是独立的, 每个进程均运行在其专用的且受保护的内存。

线程和进程的关系和区别:

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

二、线程的意义

先来看一段代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self threadTest];
}

- (void)threadTest{
    for (NSInteger i = 0; i < 10000000; i++) {
        NSLog(@"%d", i);
    }
}

当我们在开发中要进行某个数据的遍历的时候,把它放到主线程去做事,运行起来后肯定就会发现主界面无法进行任何操作。 这是因为这段代码的运行消耗时间较长,堵塞住了主线程的向下运行。 为了解决这种问题, 所以就需要使用多线程这个概念。

我们通过NSThread、GDC、NSOperation等来进行多线程的实现。 例如:

//1: NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
// 2: GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self threadTest];
});
//3: NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
    [self threadTest];
}];

多线程的优点就在于:

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

同样也会有一些缺点:

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

三、多线程的原理

多线程对于我们来说貌似就是多条线程处理任务。多线程这个命名主要来源于单核CPU, 同一时间内CPU只能处理一个线程,但是多个线程怎么处理呢? 这就有一个叫做 时间片 的概念, CPU在多个线程中来回切换执行任务。这个时间片小到我们根本就察觉不到,所以能给我们造成一种多个线程都在同时运行的错觉。

真正意义上的多线程来源于多核CPU, 多核CPU主要就是为了来加快执行的效率

3.1、多线程的生命周期

例如NSThread,当我们去创建一个线程之后, 我们下一步就是通过start来启动它。当我执行了start后, 并不代表着这个线程开始执行,而是这个线程进入一个Runnable就绪状态,它告诉计算机这个线程已经准备好了可以运行了。此时就会等待CPU的资源调度来执行这个线程。当程序执行完毕就会强制退出,销毁线程。

当线程开始运行,有某种堵塞导致线程无法进行的情况(例如加锁), 导致线程会重新进入Runnable就绪状态, 等待调起。

3.2、线程池

当系统需要调用线程的时候,它就会来到一个地方,就是线程池。

首先会判断一下线程池的状态,也就是防止线程池装满了。线程池没有满就去判断工作队列是否满,工作队列满了就去看看有没有线程闲置状态, 都是工作状态就会进入饱和策略。

饱和策略分为四种策略:

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

这四种策略均实现的RejectedExecutionHandler接口。

四、线程安全

先来看一份代码:

// 1. 开启一条售票线程
NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
t1.name = @"售票 A";
[t1 start];

// 2. 再开启一条售票线程
NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
t2.name = @"售票 B";
[t2 start];

加入有两个售票窗口同时售票,那么就会产生一个问题,总票数量是不变的,很有可能当A正在出售的时候B已经售出最后一张票。就会导致A访问到一个错误的剩余票数。

为了避免这种问题的产生,就需要引入一个锁。 对于实现方法:

- (void)saleTickets {
    while(YES) {
        @synchroized(self) {
            //售票操作
        }
    }
}

这样我们就可以简单的为共享的资源加上一把锁,当A在进行数据操作的时候,B正好过来执行这个操作,就会被锁拒之门外,只有A执行完毕后,B才能进行数据的操作。

后续会详情分析一下锁。

五、线程通讯

我们一般的线程通讯就是线程一向线程二传值发送消息等。这里演示 一下基于端的线程通讯,就是NSPort。

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) Person *person;
@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Port线程通讯";
    self.view.backgroundColor = [UIColor whiteColor];

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

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{
    
    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];
    
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);
}


- (void)getAllProperties:(id)somebody{
    
    u_int count = 0;
    objc_property_t *properties = class_copyPropertyList([somebody class], &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(properties[i]);
         NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

/*
#pragma mark - Navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

}
*/

@end



@interface Person()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end

@implementation Person


- (void)personLaunchThreadWithPort:(NSPort *)port{
    
    NSLog(@"VC 响应了Person里面");
    @autoreleasepool {
        //1. 保存主线程传入的port
        self.vcPort = port;
        //2. 设置子线程名字
        [[NSThread currentThread] setName:@"PersonThread"];
        //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 = [@"data1" 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(@"从VC 传过来一些信息:");
    //通过message的KVC取数据
}
@end