OC-多线程

多线程

官方文档:线程编程指南
GCD源码:https://github.com/apple/swift-corelibs-libdispatch

iOS 中常见的多线程方案

iOS 中常将的多线程方案如下:


iOS多线程方案

GCD 多线程基本概念

  • 同步/异步: 是否有开启新线程的能力
  • 串行队列/并行队列: 是否具有并行执行任务的能力。主队列是一种特殊的串行队列
队列概念

Note:
在主线程执行同步串行任务,会卡死主线程。
原理: 在串行队列里面执行同步任务,就会产生死锁。

多线程安全问题与解决

多线程安全问题在于:多个线程同时访问并修改同一变量值,会造成最终值不正确。
例如:存取钱问题、售票问题

多线程同时修改资源导致异常

解决方案: 使用线程同步技术(就是协同步调,按照预定的先后次序进行)。常见的线程同步技术为:加锁

加锁同步多线程资源竞争

原则: 对于修改同一个变量值,需要用同一个锁。如果只是读取,则无需加锁

常用的锁(效率从高到底):

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock 自旋锁, 等待锁的线程会处于忙等状态(busy-wait),一直占用着CPU资源

目前已经不再安全,可能会出现线程优先级翻转问题。表现上也类似死锁:如果等待锁的线程优先级较高,它就会一直占用CPU资源,优先级低的线程就无法释放锁

已经在iOS10开始被废弃。需要引入头文件#import <libkern/OSAtomic.h>,使用如下:

#import <libkern/OSAtomic.h>


// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&lock);

    // 中间需要做的操作...
    
// 解锁
OSSpinLockUnlock(&lock); 

/////////////////////////////////////////////
iOS 10之后替代 os_unfair_lock 头文件<os/lock.h>

os_unfair_lock

os_unfair_lock 作为 OSSpinLock 的替代品,解决了优先级反转问题,能做到让等待的线程处于真正的休眠状态,其接口与OSSpinLock 相似。需导入头文文件<os/lock.h>

#import <os/lock.h> 

// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

// 加锁/尝试加锁
void os_unfair_lock_lock(os_unfair_lock_t lock);
bool os_unfair_lock_trylock(os_unfair_lock_t lock);

// 解锁
void os_unfair_lock_unlock(os_unfair_lock_t lock);

// 判断是否锁的拥有者是自己,
void os_unfair_lock_assert_owner(os_unfair_lock_t lock);
void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);

pthread_mutex

pthread_mutex 能做到让等待的线程处于休眠状态。需要引入头文件 <pthread.h>

互斥锁/递归锁/条件锁

// 普通互斥锁,属性传NULL
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
    // 中间需要的操作
pthread_mutex_unlock(&_mutex);

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

pthread_mutex_lock(&_mutex);
    // 中间需要的操作
pthread_mutex_unlock(&_mutex);

----条件锁--------------------------
当多线程执行任务有条件依赖的是可以用条件锁。
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    pthread_mutex_unlock(&_mutex);
}

// 线程2
// 往数组中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    sleep(1);
    
    [self.data addObject:@"Test"];
    
    // 信号 - 唤醒被该条件加的锁
    pthread_cond_signal(&_cond);
    // 广播
//    pthread_cond_broadcast(&_cond);
    pthread_mutex_unlock(&_mutex);
}

NSLock、NSRecursiveLock、NSCondition、NSConditionLock

这几个锁是基于 pthread_mutex 的 OC 封装。其使用更加简单、更加面向对象。

// NSLock - 封装自 pthread_mutex_lock 默认锁
self.lock = [[NSLock alloc] init]; 
[self.ticketLock lock];
    // 加锁代码
[self.ticketLock unlock];

// NSCondition -- 封装自 pthread_mutex_lock 默认条件锁
self.condition = [[NSCondition alloc] init];
[self.condition lock];
// 等待
[self.condition wait];
// 信号
[self.condition signal];    
// 广播
[self.condition broadcast];
[self.condition unlock];

// NSConditionLock -- 封装自 pthread_mutex_lock 条件锁,可加自定义条件
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
// 以下三段代码可以按顺序执行
[self.conditionLock lock];
NSLog(@"__one");
[self.conditionLock unlockWithCondition:2];

[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];

[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
[self.conditionLock unlock];

// 

dispatch_queue(DISPATCH_QUEUE_SERIAL)

使用串行队列也能解决多线程资源竞争问题,将线程加入到串行队列按顺序执行。

self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);

dispatch_sync(self.serialQueue, ^{
        // 处理变量赋值等核心功能代码
    });

dispatch_semaphore

semaphore 叫做“信号量”,用来控制线程的最大并发数量。

如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码。
如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码。

dispatch_semaphore_signal(); 给对对应的信号量 +1

semaphore 初始值为1时候,非常适合做线程同步

// 设置最大允许并发数 5
self.semaphore = dispatch_semaphore_create(5);

- (void)test
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    // 相关代码
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

@synchronized

@synchronized 是对 mutex 递归锁的封装。可以参考 runtime 源码 objc_sync源码。

// 参数即要设置为锁的值,就是一个指针
@synchronized([self class]) {
        [super __drawMoney];
    }

锁的使用小技巧: 宏

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

--------------
SemaphoreBegin;
// .....    
SemaphoreEnd;

atomic 原子操作

写属性的时候常用 atomic、nonatomic

给属性加上 atomic 修饰,可以保证属性 setter 和 getter 方法都是原子性操作,也就是保证 setter 和 getter 内部都是线程同步的。这里可以参考 runtime 源码 objc-accessors。本质上也是加锁,源码如下

// 获取属性对象
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) { // 对象本质为结构体,根据属性的在结构体内的 offset 获取。如果offset == 0 即获取结构体首地址,即 isa 地址
        return object_getClass(self);
    }

    // Retain release world
    // 根据 offset 获取结构体内 属性指针
    id *slot = (id*) ((char*)self + offset);
    // 如果是非原子属性,就直接返回属性指针
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

atomic 给 setter/getter 内部加锁保证了属性存取的安全,但是不能保证属性取出之后的操作安全。

因为存取方法使用过于频繁,所以 atomic 显得过于消耗性能。

iOS 中的读写安全方案

IO 操作 -> 文件读写操作 -> 【多度,单写】

实际操作条件:

  1. 同一时间,只能有一个线程进行写操作
  2. 同一时间,允许有多个线程进行读操作
  3. 同一时间,不允许既有写操作,又有读操作

方案如下:

  1. pthread_rwlock
  2. dispatch_barrier_sync

pthread_rwlock

pthread_rwlock 也是互斥锁,等待锁的进程会进入休眠。使用如下

// 创建读写锁属性
pthread_rwlockattr_t rwAttr;
pthread_rwlockattr_init(&rwAttr);
    
// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, &rwAttr); // pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);

dispatch_barrier_sync

  • 这个函数闯入的并发队列,必须是自己通过dispatch_queue_create创建的
  • 如果传入的是一个串行或者全局并发队列,那就相当于调用dispatch_async函数
// 创建队列
dispatch_queue_t  _Nonnull queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
    
// 读 - 异步线程,可以多线程同时访问
dispatch_barrier_async(queue, ^{
    
});
    
// 写 - 同步任务,只有一个线程可以写
dispatch_barrier_sync(queue, ^{
    
});

面试题

  • 你理解的多线程?
线程是应用程序内部实现多个执行路径的相对轻量的方法。

系统->并行执行进程->进程执行一个或者多个线程。 
这些线程可以同时或者几乎同时的方式执行不同的任务。
系统本身实际上管理这些执行的线程,安排它们在可用的内核上运行,并根据需要中断它们,将执行时间分配给其他线程。

多线程有点:
1. 可以提高程序的感知响应能力,
2. 可以提高应用程序在多核系统上的实时性能

缺点:
1. 增加代码复杂性,它们可以访问同样的资源,多个线程需协同合作,防止破坏程序的状态信息
2. 线程间的资源竞争问题,需要线程同步的技术来额外处理
  • 以下代码执行情况如何?正确执行/奔溃?why?
- (void)interview01
{
    // 会产生死锁!卡死主线程
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2");
    });
    
    NSLog(@"执行任务3");
    
    // dispatch_sync立马在当前线程同步执行任务
}

- (void)interview02
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
    });
    
    NSLog(@"执行任务3");
    
    // dispatch_async不要求立马在当前线程同步执行任务
}

- (void)interview03
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");
        
        dispatch_sync(queue, ^{ // 1
            NSLog(@"执行任务3");
        });
    
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}

- (void)interview04
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");
        
        dispatch_sync(queue2, ^{ // 1
            NSLog(@"执行任务3");
        });
        
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}

- (void)interview05
{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    NSLog(@"执行任务1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{ // 0
        NSLog(@"执行任务2");
        
        dispatch_sync(queue, ^{ // 1
            NSLog(@"执行任务3");
        });
        
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}
  • 下面代码打印什么?为什么?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self test2];
}

- (void)test
{
    NSLog(@"2");
}

- (void)test2
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
}

// 打印 1、3
// 原因: performSelector:withObject:afterDelay 这个方法本质上是给Runloop 添加定时器。而子线程虽然已经创建了 runloop 但是并没有运行,所以不会打印,处理方式就是运行子线程的 runloop,让子线程保活

// 以下代码同理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");

        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];

    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
  • 如何实现如首页多个网络请求,最后一个请求基于前面的网络请求的情况
// 使用 dispatch_group 的方式。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 添加异步任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });
    
    // 等前面的任务执行完毕后,会自动执行这个任务
//    dispatch_group_notify(group, queue, ^{
//        dispatch_async(dispatch_get_main_queue(), ^{
//            for (int i = 0; i < 5; i++) {
//                NSLog(@"任务3-%@", [NSThread currentThread]);
//            }
//        });
//    });
    
//    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任务3-%@", [NSThread currentThread]);
//        }
//    });
    
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4-%@", [NSThread currentThread]);
        }
    });  
}
  • iOS 的多线程有几种方案,你更倾向于哪一种?
pthread
NSThread
GCD  ---> 更倾向
NSOperation
  • 你在项目中用过 GCD 吗?
用过
如: 
dispatch_semaphore -> 信号量
dispatch_barrier
dispatch_queue
dispatch_group
dispatch_sync & dispatch_async
...
  • GCD 的队列类型
串行队列 & 并行队列
  • 说一下 OperationQueue 和 GCD 的区别,以及各自优势?
GCD:
    基于C语言的API,旨在替代 NSThread 的线程技术,可以高效利用设备多核。

OperationQueue:
    底层封装自 GCD,增加了很多使用功能,更加面向对象。
  • 线程安全处理的手段有哪些?
1. 加锁
2. 使用 GCD 串行队列
3. 使用 GCD 信号量
  • OC 你了解的锁有哪些?在你的回答基础上进行二次提问
    • 1.自旋锁和互斥锁的对比
    • 2.使用以上锁需要注意哪些?
    • 3.使用 C/OC/C++,任选其一,实现自旋或互斥?口述即可
了解的锁:
OSSpinLock、os_unfair_lock、pthread_mutex、NSLock、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized

自旋锁适合的场景
1. 预计线程等待锁的时间很短
2. 加锁的代码(临界区)经常被调用,但竞争情况不是很激烈
3. CPU 资源不是很紧张
4. 多核处理器

互斥锁比较适合的场景
1. 预计线程等待的时间较长
2. 单核处理器(减少CPU占用)
3. 临界区有 IO 操作(IO 操作本身占CPU)
4. 临界区代码复杂或者循环量很大
5. 临界区竞争激烈
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345