开发必备-多线程

477 阅读12分钟

blockGCDiOS知识中的重要部分,也是开发必备。之前讲过了block,这章讲述一下GCD。在讲GCD之前,先了解一些基本知识。

基础知识

进程、线程

  • 进程

1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。

2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app的运行。

3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。

  • 线程

1.程序执行流的最小单元,线程是进程中的一个实体。

2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程。

  • 两者的关系

1.线程是进程的执行单元,进程的所有任务都在线程中执行。

2.线程是CPU分配资源和调度的最小单位。

3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程。

4.同一个进程内的线程共享进程资源。

多进程、多线程

  • 多进程

打开mac的活动监视器,可以看到很多个进程同时运行。

  • 多线程

1.同一时间,CPU只能处理1条线程,只有1条线程在执行。在单个CPU下,多线程并发执行,其实是CPU快速地在多条线程之间调度。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。而在多CPU下,就是真正的并行。

2.在单CPU下,如果线程非常非常多,CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。

3.多线程的优点:

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

4.多线程的缺点

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

任务、队列

  • 任务

执行操作,线程中执行的那段代码。GCD中是放在block中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。

同步(Sync)

同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。

异步(Async)

线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务
  • 队列

这里的队列指执行任务的等待队列,即用来存放任务的队列。

队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

GCD中有两种队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

串行队列(Serial Dispatch Queue)

同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。
主队列是主线程上的一个串行队列,是系统自动为我们创建的。

并发队列(Concurrent Dispatch Queue)

同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。
并发队列的并发功能只有在异步(dispatch_async)函数下才有效。

iOS中的多线程

pthread

基于C语言,很多os上使用,移植性比较高,iOS中很少使用,主要是以下3种。

NSThread

经过苹果爸爸的封装,面向对象。轻量级别的多线程技术。

有3种方式。

//【通过初始化方式创建】
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(thread1) object:nil];
[thread1 start];//【需要自己启动,需要自己回收资源】
//【通过构造器方式创建并启动,不需要自己启动】
[NSThread detachNewThreadSelector:@selector(thread1) toTarget:self withObject:nil];
//【NSObject子类和对象,都可以调用该方法,开辟的子线程也是NSThread的一种方式】
[self performSelectorInBackground:@selector(thread1) withObject:nil];

一般使用NSThread+NSRunloop实现常驻线程

+ (NSThread *)shareThread {
    static NSThread *shareThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest2) object:nil];
        [shareThread setName:@"threadTest"];
        [shareThread start];
    });
    return shareThread;
}
+ (void)threadTest
{
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

GCD

苹果官方对GCD的说明:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中

自动管理线程生命周期,使用C语言,引入block,开发方便。

dispatch queue

执行处理的等待队列。

追加方式是FIFO。按照追加的顺序执行处理。

执行处理的时候,有2种。

串行队列(Serial Dispatch Queue):等待现在执行中处理结束。

并发队列(Concurrent Dispatch Queue):不等待现在执行中处理结束。

并行执行的处理数量取决于当前系统的状态.

系统对于一个Serial Dispatch Queue,只生成并使用一个线程。如果生成200个queue,则会生成200个线程。

Serial Dispatch Queue 比 Concurrent Dispatch Queue 能生成 更多的线程。

创建方式有2种。

//【通过GCD的API创建】
dispatch_queue_create

带有create的API,在不需要的时候,必须由开发者手动释放queue,通过dispatch_release函数。---MRC下

//【获取系统标准提供的】
dispatch_get_main_queue()
dispatch_get_global_queue()

不需要手动管理。

dispatch_after

想在指定事件后执行处理的情况。

并不是在指定时间后执行,而是在指定时间追加处理到queue中。

block的内容,最快在3s后执行,最慢是在主线程的下一次runloop中,3s+1/60后执行。

在需要严格时间的要求下会出现问题。大致的延迟处理是可以的。

dispatch_group

多个处理全部结束后执行结束处理。

dispatch_groupdispatch_group_notify

最后别忘记dispatch_release(group)。因为group是通过dispatch_group_create()创建的。--MRC

也可以使用dispatch_group_wait,仅仅等待全部处理执行结束。

{
    NSLog(@"主线程");
    dispatch_queue_t queue = dispatch_queue_create("gcd.group", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"start task 1");
        [NSThread sleepForTimeInterval:2];
        NSLog(@"end task 1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"start task 2");
        [NSThread sleepForTimeInterval:2];
        NSLog(@"end task 2");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"All over");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到主线程");
        });
    });
}

控制台打印如下。

主线程
start task 1
start task 2
end task 1
end task 2
All over
回到主线程

每个任务仅仅是休眠2s,比较简单。再来看一个例子。

{
    NSLog(@"主线程");
    dispatch_queue_t queue = dispatch_queue_create("gcd.group", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (NSInteger i = 0; i < 5; i++) {
                NSLog(@"%ld",i);
            }
        });
    });
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (NSInteger i = 11; i < 20; i++) {
                NSLog(@"%ld",i);
            }
        });
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"All over");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到主线程");
        });
    });
}

你会发现,控制台竟然没有等数字都打印完就输出了all over。这是为什么呢?

原因在于group_async中是个异步操作,执行时已经不在该queue中,group不持有这个操作。

所以,group_async中代码应该是个同步代码。

那,如果非要这种效果,如何实现呢?使用dispatch_group_enterdispatch_group_leave。一定要成对出现

{
    NSLog(@"主线程");
    dispatch_queue_t queue = dispatch_queue_create("gcd.group", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (NSInteger i = 0; i < 5; i++) {
                NSLog(@"%ld",i);
            }
            dispatch_group_leave(group);
        });
    });
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (NSInteger i = 11; i < 20; i++) {
                NSLog(@"%ld",i);
            }
            dispatch_group_leave(group);
        });
    }); 
    dispatch_group_notify(group, queue, ^{
        NSLog(@"All over");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到主线程");
        });
    });
}  

这个时候看控制台的打印,结果是正确的。

dispatch_barrier_async

这个原理跟group很相似,类似与栅栏,前面的所有执行都完毕后,再执行后面的操作。与dispatch_group_notify的区别在于,dispatch_barrier_async不可以使用系统定义的queue,只能使用自己通过create创建的queue

dispatch_once

保证只执行一次。

static dispatch_once_t pred;
dispatch_once(&pred, ^{
    //初始化操作
});

即使在多线程下,可以保证安全。如果是通过常用的代码,在多线程线可能会出现多次初始化。

dispatch_apply

dispatch_sync+group一起关联的api,按指定的次数,将block追加到指定的queue中。

Dispatch Semaphore

GCD 中的信号量是指Dispatch Semaphore,是持有计数的信号。

Dispatch Semaphore提供了三个函数:

1.dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量 2.dispatch_semaphore_signal:发送一个信号,让信号总量加1 3.dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

1、保持线程同步

{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block NSInteger number = 0;
    NSLog(@"before");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        number = 100;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}

before semaphore---end,number = 100 会 一直等待,直到 接收到 信号

2、保证线程安全,为线程加锁 在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁。

首先创建全局变量

{
   _semaphore = dispatch_semaphore_create(1); //注意到这里的初始化信号量是1。
    - (void)asyncTask
    {
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        count++;
        sleep(1);
        NSLog(@"执行任务:%zd",count);
        dispatch_semaphore_signal(_semaphore);
    }
}

异步并发调用asyncTask

{
    for (NSInteger i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self asyncTask];
        });
    }   
}

打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。

原因如下:

在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

NSOperation

GCD的一种封装,抽象的基类,需要使用子类。

两种方式创建:

  • 系统已经提供的,NSInvocationOptionation & NSBlockOperation
  • 自定义类,继承NSOperation

NSOperationQueue:队列,可以理解为 线程池。

addOperation

setMaxConcurrentOperationCount

状态: readycancelled(如果是正在执行的任务,则不会被取消,仍会执行)、executingfinishedasynchronous(判断任务是并发,还是非并发)

依赖:addDependency,任务之间的相互依赖

NSInvocationOptionation

- (void)nsOperationInv
{
    NSLog(@"主线程");
    NSInvocationOperation *invoOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [invoOp start];
}
- (void)run
{
    for (NSInteger i = 0; i < 5; i++) {
        NSLog(@"%@", @(i));
    }
}

查看控制台,发现在主线程 中执行,是因为 使用的是 start 方法,该方法会 阻塞当前 线程。可以将其 放在 dispatch_async 中。

NSBlockOperation

NSLog(@"主线程");
NSBlockOperation *blockInvoke = [NSBlockOperation blockOperationWithBlock:^{
    for (NSInteger i = 0; i < 5; i++) {
        NSLog(@"%@", @(i));
    }
}];
[blockInvoke start];

同样,也是阻塞当前线程

如果采用异步方式呢? 引入NSOperationQueue

一般是将 NSOperationQueue 和 NSInvocationOptionation、NSBlockOperation 一起使用。

主要修改点:将 operation 的启动方式进行修改,采用 [queue addOperation:] 的方式。

自定义Operation

继承自 NSOperation, 重写 main 或者 start 方法即可。

NSLog(@"主线程");
if (!_operationQueue) {
    _operationQueue = [[NSOperationQueue alloc] init];
}
CustomOperation *opA = [[CustomOperation alloc] initWithName:@"opA"];
CustomOperation *opB = [[CustomOperation alloc] initWithName:@"opB"];
[_operationQueue addOperation:opA];
[_operationQueue addOperation:opB];
NSLog(@"主线程-end");

异步并发,开启4个子线程。

如果设置了 queue 的setMaxConcurrentOperationCount的值,则 异步并发的线程数 = 该值,执行完 某个 任务,才会开启 下一项 任务。

如果添加 依赖 关系呢?

[opD addDependency:opA];
[opA addDependency:opC];
[opC addDependency:opB];

此时,执行结果为 B -> C -> A -> D。

在 重写 main 方法时,如果 方法内部 本身是个 异步 的过程,此时 会出现 结果错乱的场景,因为 queue 本身 线程 已经执行完毕,异步的任务 在另一个线程中。

如何解决呢?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [NSThread sleepForTimeInterval:1];
    if (self.cancelled) {
        return ;
    }
    NSLog(@"%@", _operationName);
    self.over = YES;
});
while (!self.over && !self.cancelled) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

通过 NSRunLoop 保证 线程 不会立即 被释放掉。

或者将block通过构造初始化的方式,传给operation,operation在start方法中,调用block,同时保留currentThread,block执行结束后,告知operation,各种置位nil。核心都是要保证线程不会被释放。

相互比较

GCD是面向底层的C语言的API,NSOperationQueue用GCD构建封装的,是GCD的高级抽象。

GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便。

GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数设置优先级添加依赖关系等调整执行顺序。

NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂。

NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)是否结束(isFinished)是否取消(isCanceld)

实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选

如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持。

不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销。

好文推荐iOS多线程:『GCD』详尽总结