OC知识梳理:多线程

GCD

1. 队列和任务

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

  • 串行队列(Serial Dispatch Queue):
    • 每次只有一个任务被执行。让任务一个接着一个地执行。
    • 只开启一个线程,一个任务执行完毕后,再执行下一个任务。
  • 并发队列(Concurrent Dispatch Queue):
    • 可以让多个任务并发(同时)执行。
    • 可以开启多个线程,并且同时执行任务。

并发队列的并发功能只有在异步(dispatch_async)方法下才有效。
 

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:『同步执行』和『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

  • 同步执行(sync):
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async):
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力。

异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)。
 

区别 并发队列 串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 主线程调用:死锁卡住不执行;其他线程调用:没有开启新线程,串行执行任务
异步(async) 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务

死锁的原因:队列引起的循环等待。主队列并不特殊,任何一个串行队列嵌套执行同步任务都会造成死锁。

2. 队列的创建方法 / 获取方法

可以使用dispatch_queue_create方法来创建队列。该方法需要传入两个参数:

  • 第一个参数表示队列的唯一标识符,用于DEBUG,可为空。队列的名称推荐使用应用程序ID这种逆序全程域名。
  • 第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.hao.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.hao.testQueue", DISPATCH_QUEUE_CONCURRENT);
复制代码

对于串行队列,GCD 默认提供了『主队列(Main Dispatch Queue)』。

  • 所有放在主队列中的任务,都会放到主线程中执行。
  • 可使用dispatch_get_main_queue()方法获得主队列。
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
复制代码

注意:主队列其实并不特殊。主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。

对于并发队列,GCD 默认提供了『全局并发队列(Global Dispatch Queue)』。

  • 可以使用dispatch_get_global_queue方法来获取全局并发队列。
  • 需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
复制代码

3. 任务的创建方法

GCD提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});
复制代码

4. 栅栏方法:dispatch_barrier_async

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
 

dispatch_barrier_async方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
 

多读单写功能的实现

#import "UserCenter.h"

@interface UserCenter() {
    dispatch_queue_t queue;
}

@property (nonatomic, strong) NSMutableDictionary *dataDic;

@end

@implementation UserCenter

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 创建并发队列
        queue = dispatch_queue_create("concurrent_queues", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据容器
        self.dataDic = [NSMutableDictionary dictionary];
    }
    return self;
}

- (id)objectForKey:(NSString *)key {
    __block id obj;
    dispatch_sync(queue, ^{
        obj = [self.dataDic objectForKey:key];
    });
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key {
    dispatch_barrier_async(queue, ^{
        [self.dataDic setObject:obj forKey:key];
    });
}

@end
复制代码

在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。

5. GCD 队列组:dispatch_group

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到GCD的队列组。

  • 调用队列组的dispatch_group_async先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的dispatch_group_enterdispatch_group_leave组合来实现dispatch_group_async
  • 调用队列组的dispatch_group_notify回到指定线程执行任务。或者使用dispatch_group_wait回到当前线程继续向下执行(会阻塞当前线程)。
#import "GroupObject.h"

@interface GroupObject() {
    dispatch_queue_t queue;
}

@property (nonatomic, strong) NSMutableArray <NSURL *> *arrayURLs;

@end

@implementation GroupObject

- (instancetype)init
{
    self = [super init];
    if (self) {
        queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        self.arrayURLs = [NSMutableArray array];
    }
    return self;
}

- (void)handle {
    // 创建一个group
    dispatch_group_t group = dispatch_group_create();
    
    // 通过for循环,将任务异步分派到并发队列
    for (NSURL *url in self.arrayURLs) {
        dispatch_group_async(group, queue, ^{
            // 网络请求
            [NSThread sleepForTimeInterval:2];
            NSLog(@"Url is %@.", url);
        });
    }
    
    // 当group中的所有任务都执行完成后,回到主线程执行dispatch_group_notify中的任务
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"GroupEnd");
    });
}

@end
复制代码

NSOperation

1. NSOperation实现多线程有哪些优势和特点?

  • 添加任务依赖
  • 任务执行状态控制
  • 控制最大并发量(设为1则相当于串行)

2. NSOperation的任务都有那些执行状态?

  • isReady:当前任务是否处于就绪状态
  • isExecuting:当前任务是否处于正在执行中
  • isFinished:当前任务是否已执行完成
  • isCancelled:当前任务是否已取消

3. NSOperation的状态控制

  • 如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
  • 如果重写了start方法,自行控制任务状态。

系统是怎样移除一个isFinished=YES的NSOperation的?
通过KVO

NSThread

1. NSThread启动流程

  • 当我们创建一个NSThread之后,会调用它的start()方法来启动线程;
  • start()方法内部会创建一个pthread线程,然后指定pthread线程的启动函数;
  • 在pthread线程的启动函数当中会调用NSThread的main()函数;
  • main()函数中通过[target performSelector:selector]的形式来执行NSThread在创建时指定的目标对应的选择器;
  • 最后再调用线程关闭函数exit()来结束线程。

2. 如何实现一个常驻线程?

我们可以在NSThread对应的入口函数中添加一个事件循环,这个事件循环往往是添加到我们在创建NSThread时,所指定的选择器方法[target performSelector:selector]中,我们可以在选择器方法中维护一个RunLoop来实现常驻线程的目的。

1. 关于锁的一些概念

  • 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
  • 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
  • 互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
  • 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。
  • 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
  • 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

2. 时间片轮转调度算法

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在10-100毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
 

线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行I/O操作,或进入睡眠状态时,都会主动让出时间片。
 

但是主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要10微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。
 

自旋锁更适用于轻量级数据访问;而互斥锁的实现中,有时也会先进行一定次数的忙等(例如1000次),再进行线程睡眠,来保证效率。

3. pthread_mutex

pthread表示POSIX thread,定义了一组跨平台的线程相关的API,pthread_mutex表示互斥锁。互斥锁的实现原理不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
 

互斥锁的常见用法

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_lock(&mutex); // 申请锁
    // 临界区
pthread_mutex_unlock(&mutex); // 释放锁
复制代码

 

一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
 

然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex支持递归锁,也就是允许一个线程递归的申请锁,只要把attr的类型改成PTHREAD_MUTEX_RECURSIVE即可。

4. NSLock

NSLock是Objective-C以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了lock方法:

#define    MLOCK \
- (void) lock\
{\
  int err = pthread_mutex_lock(&_mutex);\
  // 错误处理 ……
}
复制代码

 

NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。
 

NSLock比pthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

5. dispatch_semaphore

类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,可通过。
 

dispatch_semaphore提供了三个方法:

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

 

dispatch_semaphore在实际开发中主要用于:

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

1) 使用信号量实现线程同步

- (void)semaphoreSync {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}
复制代码

2) 使用信号量实现线程安全

/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}
复制代码

6. 条件锁

1) NSCondition

NSCondition的底层是通过条件变量(condition variable)pthread_cond_t来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

void consumer () { // 消费者
    pthread_mutex_lock(&mutex);
    while (data == NULL) {
        pthread_cond_wait(&condition_variable_signal, &mutex); // 等待数据
    }
    // --- 有新的数据,以下代码负责处理 ↓↓↓↓↓↓
    // temp = data;
    // --- 有新的数据,以上代码负责处理 ↑↑↑↑↑↑
    pthread_mutex_unlock(&mutex);
}

void producer () {
    pthread_mutex_lock(&mutex);
    // 生产数据
    pthread_cond_signal(&condition_variable_signal); // 发出信号给消费者,告诉他们有了新的数据
    pthread_mutex_unlock(&mutex);
}
复制代码

2) NSConditionLock

NSConditionLock借助NSCondition来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock的内部持有一个NSCondition对象,以及_condition_value属性,在初始化时就会对这个属性进行赋值:

// 简化版代码
- (id)initWithCondition:(NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}
复制代码

 

它的lockWhenCondition方法其实就是消费者方法:

- (void)lockWhenCondition:(NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}
复制代码

 

对应的unlockWhenCondition方法则是生产者,使用了broadcast方法通知了所有的消费者:

- (void) unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}
复制代码

7. NSRecursiveLock

递归锁也是通过pthread_mutex_lock函数来实现,在函数内部会判断锁的类型,如果类型为PTHREAD_MUTEX_RECURSIVE,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

8. @synchronized

这其实是一个OC层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
 

我们知道@synchronized后面需要紧跟一个OC对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。

9. OSSpinLock(已废弃)

OSSpinLock是自旋锁,已被废弃,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量CPU时间,从而导致低优先级线程拿不到CPU时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
 

自旋锁的实现原理

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享