iOS开发多线程知识梳理

4,295 阅读15分钟

线程和进程


几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

  • 进程(Process )

当一个程序进入内存运行后,即变成一个进程。进程是处于是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。一般而言,进程有如下特征:

  1. 独立性:有自己独立的资源,且拥有自己私有的地址空间。在没有经过进程本省的允许下,其他进程是不能直接访问其进程的地址空间的。
  2. 动态性:程序只是静态的指令集合,而进程是一个正在系统中活动的指令集合。进程有时间的概念,具有自己的生命周期和各种状态。
  3. 并发性:多个进程可以在单个处理器上并发执行,互相不会影响。
  • 线程(Thread)

线程也被称做轻量级进程,线程是进程的执行单元。就像进程在系统中一样,线程在进程中也是独立的,并发的执行流程。一个进程可以拥有多个线程,一个线程必须有一个父进程,但不再拥有系统资源,而是和父进程一起共享父进程的全部资源。多线程由于共享父进程的资源,所以编程更加方便,但是也需要小心线程不会影响到父进程中的其他线程。线程是独立运行的,它并不知道其他线程的存在。线程执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便林另外一个线程可以运行。

  • 多线程优点
  1. 进程间不可以共享内存,但线程之间共享内存十分容易。
  2. 系统创建进程需要为其重新分配系统资源,但是创建线程代价小得多,因此效率更高

为什么要用多线程编程


为了提高资源利用率来提升系统整体效率,实际往往是将耗时操作放在后台执行,避免阻塞主线程,在iOS中UI绘制和用户响应都是主线程。

NSThread


常用API

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //打印当前线程
    NSLog(@"开始:%@   优先级:%d", [NSThread currentThread], [NSThread currentThread].qualityOfService);
    
    //1.创建NSTread对象,必须调用start方法开始,并且只能传一个参数object
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
    //    NSThread *thread = [[NSThread alloc] initWithBlock:^{}];
    thread.name = @"testThread";
    thread.qualityOfService = NSQualityOfServiceUserInteractive;
    [thread start];
    
    //2.直接创建并启动线程
    //    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"test"];
    //    [NSThread detachNewThreadWithBlock:^{}];
    
    //3.隐式直接创建
//    [NSThread performSelectorInBackground:@selector(run:) withObject:nil];
    
    //    NSLog(@"结束:%@", [NSThread currentThread]);
}

- (void)run:(NSObject *)object {
    //阻塞休眠
    //    [NSThread sleepForTimeInterval:5];
    //中止当前线程
    //    [NSThread exit];
    NSLog(@"子线程运行:%@ %@  优先级:%d", [NSThread currentThread], object, [NSThread currentThread].qualityOfService);
}
  • 线程的状态

线程被启动后,并不是直接进入执行状态,也不是一直处于执行状态,由于线程并发,线程会反复在运行、就绪间切换。创建一个线程后,处于新建状态,系统为其分配内存,初始化成员变量;调用-(void)start;方法后,该线程处于就绪状态,系统为其创建方法调用栈和程序计数器,此时并没有运行,何时运行取决于系统调度。

  • 终止子线程

每个线程都有一定的优先级,优先级越高获得执行机会越多。目前通过qualityOfService属性来设置,原来的threadPriority由于语义不够清晰,已经被废弃了。

NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回的任务
NSQualityOfServiceDefault:默认优先级,当没有设置优先级的时候,线程默认优先级
NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回的任务
NSQualityOfServiceBackground:后台优先级,用于完全不紧急的任务
  • 缺点

使用NSThread进行多线程编程较复杂,需要自己控制多线程的同步、并发,还需要自己控制线程的终止销毁,稍有不留神容易出现错误,对开发者要求较高,一般较少使用。

NSOperation


iOS还提供了NSOperation与NSOperationQueue来实现多线程,是基于GCD更高一层的封装,完全面向对象。但是GCD更简单易用、代码可读性也更高。

NSOperationQueue:负责管理系统提交的多个NSOperation,底层维护了一个线程池。不同于GCD中的调度队列FIFO(先进先出)原则。NSOperationQueue对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

NSOperation: 代表一个多线程任务。

  • 为什么要使用NSOperation、NSOPerationQueue?
  1. 可以添加完成的代码块,在操作完成后执行。
  2. 添加操作之间的依赖关系,方便的控制执行顺序。
  3. 设定操作执行的优先级。
  4. 可以很方便的取消一个操作的执行。
  5. 使用KVO观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
  • 常用API
    NSOperationQueue *queue;
    //获取执行当前NSOperation的NSOperationQueue队列
    //    queue = [NSOperationQueue currentQueue];
    //获取主线程的NSOperationQueue队列
    //    queue = [NSOperationQueue mainQueue];
    //自定义队列
    queue = [[NSOperationQueue alloc] init];
    //队列名
    queue.name = @"testOperationQueue";
    //最大并发操作数(系统有限制,即使设置很大,也会自动调整)
    queue.maxConcurrentOperationCount = 10;
    //设置优先级
    queue.qualityOfService = NSQualityOfServiceDefault;
    
    //自定义NSOperation,如果SEL和Block为空,系统不会加入到指定队列
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"blockOperation");
    }];
    //添加依赖关系,invocationOperation执行完后才执行blockOperation
    [blockOperation addDependency:invocationOperation];
    //添加到队列中
    //    [queue addOperation:invocationOperation];
    [queue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:NO];
    //直接添加代码块任务
    [queue addOperationWithBlock:^{
        
    }];
    
    //打印所有的NSOperation
    for(int i=0; i<queue.operationCount; i++) {
        NSLog(@"队列%@的第%d个NSOperation:%@", queue.name, i, queue.operations[i]);
    }
    
    //终止所有NSOperation
    //    [queue cancelAllOperations];
    //执行完所有NSOperation才能解除阻塞当前线程
    //    [queue waitUntilAllOperationsAreFinished];

GCD(Grand Central Dispatch)

  • 基本概念
  1. 队列:队列负责开发者提交的任务,不过不同任务的执行时间不一样,先处理的任务不一定先完成。队列即可是串行的,也可是并行的,队列底层会维持一个线程池来处理任务,串行队列只需要维护一个线程即可,并行队列则需要维护多个线程。
  2. 任务:用户提交给队列的工作单元,这些任务将会提交给队列底层维护的线程池。
  3. 异步:可以在新的线程中执行任务,但不一定会开辟新的线程。dispatch函数会立即返回,然后Block在后台异步执行。
  4. 同步:在当前线程执行任务,不会开辟新的线程。必须等到Block函数执行完毕后,dispatch函数才会返回。

注:队列的串行和并行决定了任务以何种方式执行,执行的异步和同步决定了是否需要开辟新线程处理任务。

  • 特点
  1. GCD可用于多核的并行运算;
  2. GCD会自动利用更多的CPU内核(比如双核、四核);
  3. GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  4. 程序员只需要告诉GCD想要执行什么任务,不需要写任何线程管理代码;
  • 常用API
    /** 获取队列 */
    //获取指定优先级的全局并发队列(flag填0即可,仅预留的参数,使用其他值可能会返回null)
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建自定义并行队列
    dispatch_queue_t queue1 = dispatch_queue_create("testQueue1", DISPATCH_QUEUE_CONCURRENT);
    //获取系统主线程关联的串行队列
    dispatch_queue_t queue2 = dispatch_get_main_queue();
    //创建自定义串行队列
    dispatch_queue_t queue3 = dispatch_queue_create("testQueue3", DISPATCH_QUEUE_SERIAL);
    
    /** 提交任务 */
    //异步提交代码块到并发队列
    dispatch_async(queue, ^{
        
    });
    //同步提交代码块到自定义并发队列
    dispatch_sync(queue1, ^{
        
    });
    
    //异步提交代码块到串行队列,线程池将在指定时间执行代码块(实际是5秒后加入到队列中,实际并不一定会立马执行,一般精度要求下是没问题的)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)), queue2, ^{
        
    });
    
    //异步提交代码到自定义串行队列,同步函数,无论是在串行还是并行队列中执行,都要执行完才返回,所以要防止线程阻塞和死锁,time表示当前是第几次(如果提交给并发队列,会启动五个线程来执行)
    dispatch_apply(5, queue3, ^(size_t time) {
        
    });
    
    //实际是个long类型变量,用于判断该代码块是否被执行过
    static dispatch_once_t onceToken; 
    //主线程执行一次代码块
    dispatch_once(&onceToken, ^{
        
    });

    //等group执行完后,才能执行下一步
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    /** 组(用于需要等待多个任务全部执行完再进行下一步) */
    dispatch_group_t group = dispatch_group_create();
    
    //并发执行的代码块1
    dispatch_group_async(group, queue, ^{
        
    });
    
    //并发执行的代码块2
    dispatch_group_async(group, queue, ^{
        
    });
    
    //等待两个代码块执行完汇总
    dispatch_group_notify(group, queue, ^{
        
    });
    
    /** 栅栏(用于需要依次执行完多个线程组) */
    //并发队列异步执行代码块1,2
    dispatch_async(queue, ^{
        //代码块1
    });
    dispatch_async(queue, ^{
        //代码块2
    });
    //1,2执行完后才会执行3,4
    dispatch_barrier_async(queue, ^{
        
    });
    //并发队列异步执行代码块3,4
    dispatch_async(queue, ^{
        //代码块3
    });
    dispatch_async(queue, ^{
        //代码块4
    });

    /** 信号量(用于控制线程的等待和执行) */
    //创建信号量,value表示初始信号总量,支持多少个操作来执行
    dispatch_semaphore_t t = dispatch_semaphore_create(1);
    //发送一个信号,让信号总量+1
    dispatch_semaphore_signal(t);
    //使信号总量-1,如果总量为0,则会一直等待(阻塞所在线程),直到总量大于0则继续执行
    dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);
    
    /*1.可以将异步执行变为同步执行,如需要等待下载完后再直接返回数据(我们也可以通过block回调)*/
    //总信号量设置为0
    dispatch_semaphore_t t1 = dispatch_semaphore_create(0);
    //执行耗时代码
    void (^downloadTask)(void) = ^ {
        //下载图片
        ...
        ...
        //完成后发送信号量
        dispatch_semaphore_signal(t1);
    };
    downloadTask();
    //一直等到信号量计数为1才执行下一步,也就是等到图片下载完后
    dispatch_semaphore_wait(t1, DISPATCH_TIME_FOREVER);
    
    /*2.保证线程安全*/
    //设置信号量初始计数为1,保证只能有一个操作能进来
    dispatch_semaphore_t t2 = dispatch_semaphore_create(1);
    //相当于加锁,消耗使用计数,如果已经被一个线程使用,后续只能挂起等待信号量回复
    dispatch_semaphore_wait(t2, DISPATCH_TIME_FOREVER);
    //执行业务代码
    ...
    ...
    //解锁
    dispatch_semaphore_signal(t2);
    
    /*3.模拟NSOperationQueue的最大并发操作数*/
    //最大并发操作支持10
    dispatch_semaphore_t t3 = dispatch_semaphore_create(10);
    //剩余操作同上,其实就是类似于将NSOperationQueue的maxConcurrentOperationCount设置为10
  • 后台运行

在App程序进入后台时,我们应该尽量释放内存和保存用户数据或者状态信息。在默认情况下,应该仅在5秒钟处理这些工作,我们可以通过UIApplicationbeginBackgroundTaskWithExpirationHandler方法来申请延长处理时间,最多有十分钟。

- (void)applicationDidEnterBackground:(UIApplication *)application {
    //声明关闭后台任务代码块
    void (^endBackgroundTask)(UIBackgroundTaskIdentifier backgroudTask) = ^(UIBackgroundTaskIdentifier backgroudTask) {
        [[UIApplication sharedApplication] endBackgroundTask:backgroudTask];
        backgroudTask = UIBackgroundTaskInvalid;
    };
    
    //开启后台任务
    __block UIBackgroundTaskIdentifier backgroudTask;
    backgroudTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        //十分钟内仍然没有完成,系统处理终止句柄
        endBackgroundTask(backgroudTask);
    }];
    
    //执行相关代码
    
    //结束后台任务
    endBackgroundTask(backgroudTask);
}
  • 线程死锁
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
}

在主队列中增加同步代码块,就会造成死锁,由于同步是需要立即顺序执行的,上述代码中,Block中的方法需要在viewDidLoad结束后才能完成,但是viewDidLoad想要结束又必须先结束Block中的方法,所以相互永久等待,造成了死锁。

GCD会造成循环引用吗?

直接使用GCD的相关API一般是不会的,block结束后没有循环引用的条件,YYKit的issues下有个有去的讨论:dispatch_async的block里面需要_weak self吗?

  • 注意
  1. 同步执行会在当前线程执行任务,不具有开辟线程的能力或者说没有必要开辟新的线程。并且,同步执行必须等到Block函数执行完毕,dispatch函数才会返回,从而阻塞同一串行队列中外部方法的执行。
  2. 异步执行dispatch函数会直接返回,只有异步执行才有开辟新线程的必要,但是异步执行不一定会开辟新线程。
  3. 想要开辟新线程必须让任务在异步执行,想要开辟多个线程,只有让任务在并行队列中异步执行才可以。执行方式和队列类型多层组合在一定程度上能够实现对于代码执行顺序的调度。
  4. 同步+串行:未开辟新线程,串行执行任务;同步+并行:未开辟新线程,串行执行任务;异步+串行:新开辟一条线程,串行执行任务;异步+并行:开辟多条新线程,并行执行任务;在主线程中同步使用主队列执行任务,会造成死锁。

线程安全

线程安全主要是由于系统的线程调度具有一定的随机性造成的,由于是多并发,多个线程同时对一份数据进行读写,就可能在读取执行一般的时候另外一个线程去写入,导致数据异常。线程安全即保证线程同步

  • 线程安全的类的特征
  1. 该类的对象可以被多个线程安全访问。
  2. 每个线程调用对象的任意方法都会得到正确的结果。
  3. 每个线程调用对象的任意方法之后,该对象仍保持合理状态。
  • @synchronized是对mutex递归锁的封装

为了解决这个问题,Objective-C的多线程支持引入同步,使@synchronized修饰代码块,被修饰的代码块可简称为同步代码块,语法格式如下

@synchronized (obj) {
    //同步代码块
}

其中obj就是同步监视器,当一个线程执行同步前,必须先获得同步监视器的锁定,任何时刻只能有一个线程获得锁定,执行完成后,才会释放,如果此时有新的线程访问,那么新线程会进入休眠状态。通常推荐使用可能被并发访问的共享资源作为同步监视器。

iOS中的锁


1. OSSpinLock(自旋锁)

  • 等待锁的线程处于忙等(busy-wait)状态,一直占用着CPU资源;
  • 目前已经不再安全,可能会出现优先级翻转问题;
  • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁;
  • 需要导入头文件#import <libkern/OSatomic.h>
//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待加锁,返回true)
bool resule = OSSpinLockTry(&lock);
//加锁
OSSpinLock(&lock);
//解锁
OSSpinLockUnlock(&lock);

2. os_unfair_lock

  • 用于取代不安全的OSSpinLock,从iOS10开始支持;
  • 从底层调用看,等待os_unfair_locks锁的线程会处于休眠状态,并非忙等;
  • 需要导入头文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

3. pthread_mutex

互斥锁

  • mutex叫做“互斥锁”,等待的线程会处于休眠状态
  • 需要导入头文件#import <pthread.h>
//初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_NORMAL);
//初始化
pthread_mutex_t mutex;
pthread_mutex_init (&mutex,&attr);
//尝试加锁
pthread_mutex_trylock (&mutex);
//加锁
pthread_mutex_lock (&mutex);
//解锁
pthread_mutex_unlock (&mutex);
//销毁相关资源
pthread_mutexattr_unlock(&attr);
pthread_mutex_destroy(&mutex);

递归锁

  • 递归锁:允许同一个线程对一把锁进行重复加锁
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

条件

 // 初始化锁
pthread_mutex_t mutex;
//NULL代表使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
//等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&cond, &mutex);
//激活一个等待条件的线程
pthread_cond_signal(&cond);
//销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

4. NSLock、NSRecursiveLock

  • NSLock是对mutex普通锁的封装
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking>
{
- (BOOL)tryLock;
- (BOOl)lockBeforeDate:(NSDate *)limit;
}
@end

//初始化锁
NSLock *lock = [[NSLock alloc] init];
  • NSRecursiveLock也是对mutex递归所得封装,API跟NSLock基本一致。

5. NSCondition

  • NScondition 是对mutex和cond的封装
@interface NSCondition : NSObject <NSLocking>
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

6. NSConditionLock

  • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
@interface NSConditionLock : NSObject <NSLocking> {
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
}
@end

7. dispatch_semaphore

  • semaphore叫做信号量;
  • 信号量的初始值,可以用来控制线程并发访问的最大数量;
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
//信号量的初始值
int value = 1;
//初始化信号量
dispatch_semaphore semephore = dispatch_semaphore_creat(value);
//如果信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
//如果信号量的值>0, 就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//让信号量的值加1
dispatch_semaphore_signal(semaphore);

8. dispatch_queue

  • 直接使用GCD的串行队列,也是可以实现线程同步的
dispatch_queue_t queue = dispatch_queue_creat("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//任务
})