iOS-浅谈多线程

前言:本文简述多线程相关内容,如有错误请留言指正。

Q:多线程的几种方案?

pthread
一套通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大
C语言、程序员管理

NSThread
使用更加面向对象
简单易用,可直接操作线程对象
OC、程序员管理

GCD
旨在替代NSThread等线程技术
充分利用设备的多核
C语言、自动管理

NSOperation
基于GCD(底层是GCD)
比GCD多了一些更简单实用的功能
使用更加面向对象

多线程方案

第一部分 GCD

Q:GCD常用函数?

GCD中有2个用来执行任务的函数:同步函数、异步函数

同步函数
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_sync立马在当前线程同步执行任务
queue:队列
block:任务

异步函数
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

Q:GCD使用队列?

GCD的队列可以分为2大类型:并发队列、串行队列
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效

串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
主队列是特殊的串行队列

Q:简述同步、异步、并发、串行的作用?

同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力

并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务

Q:简述同步、异步、并发、串行相互搭配的结果?

队列各不同方式执行效果

Q:什么时候会产生死锁?

使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

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

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

- (void)test03{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
    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)test04{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    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)test05{
    // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
    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");
}

1.1 队列组

Q:队列组实现并发执行A、B任务,然后执行C任务?

// 创建队列组
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]);
//        }
//    });

也可以在子线程执行C、D任务

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]);
    }
});

第二部分:线程同步方案

Q:多线程存在哪些隐患?

  • 资源共享
    a)多个线程可能会访问同一块资源,多线程共享
    b)多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
多线程隐患分析图

Q:多线程安全隐患解决方案?

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

Q:线程同步的解决方案?

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

2.1 线程同步-OSSpinLock

Q:如何使用OSSpinLock对线程加锁?

OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
加锁要全局或者静态全局变量,不能每次调用方法都加锁,那是不同的锁

#import <libkern/OSAtomic.h>
@property (assign, nonatomic) OSSpinLock lock;

// 初始化锁
self.lock = OS_SPINLOCK_INIT;
//尝试加锁
bool result = OSSpinLockTry(&_lock);
// 加锁
OSSpinLockLock(&_lock1);
//需要加锁的内容
// 解锁
OSSpinLockUnlock(&_lock1);

Q:OSSpinLock为什么会被弃用?

  • 目前已经不再安全,可能会出现优先级反转问题
  • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁

2.2 线程同步-os_unfair_lock

  • os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
  • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
  • 需要导入头文件#import <os/lock.h>

Q:如何使用os_unfair_lock加锁?

注意:os_unfair_lock加锁解锁传入的是指针

#import <os/lock.h>
@property (assign, nonatomic) os_unfair_lock lock;
//初始化锁
self.lock = OS_UNFAIR_LOCK_INIT;

//尝试加锁
os_unfair_lock_trylock(&_lock);
//加锁
os_unfair_lock_lock(&_lock);
//解锁
os_unfair_lock_unlock(&_lock);

2.3 线程同步-pthread_mutex

  • mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
  • 需要导入头文件#import <pthread.h>

Q:如何使用pthread_mutex加锁?

//锁类型
#define PTHREAD_MUTEX_NORMAL        0//默认锁
#define PTHREAD_MUTEX_ERRORCHECK    1//错误检查锁
#define PTHREAD_MUTEX_RECURSIVE     2//递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL//默认锁

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

// 初始化锁-传空即可
pthread_mutex_init(mutex, NULL);

//加锁
pthread_mutex_lock(&_ticketMutex);
//加锁的其他操作
//解锁
pthread_mutex_unlock(&_ticketMutex);

//销毁锁
pthread_mutex_destory(&_lock)

Q:如何使用pthread_mutex-递归锁

当A函数内部调用B/A函数,A、B函数都加锁,就会导致死锁情况出现
使用递归锁可以解决,允许同一线程对一把锁进行重复加锁,解锁也是层级解锁

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

2.3.1 pthread_mutex-条件

多线程操作数组的增加和删除,需要控制删除前数组内存余值

//全局变量
@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;

//初始化
// 初始化属性
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_cond_init(&_cond, NULL);
self.data = [NSMutableArray array];

// 线程1-删除数组中的元素
- (void)__remove{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待条件->进入休眠->放开_mutex锁;被唤醒后->再对_mutex加锁
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    pthread_mutex_unlock(&_mutex);
}

// 线程2-往数组中添加元素
- (void)__add{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信号-激活一个等待条件的线程
    pthread_cond_signal(&_cond);
    // 广播-激活所有等待条件的线程
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

//销毁
- (void)dealloc{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}

2.4 线程同步-NSLock、NSRecursiveLock

  • NSLock是对mutex普通锁的封装
  • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

2.5 线程同步-NSCondition、NSConditionLock

NSCondition是对mutex和cond的封装

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (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;

NSConditionLock可以设置线程依赖

- (void)__one{
    [self.conditionLock lock];
    //[self.conditionLock unlockWithCondition:0];

    NSLog(@"__one");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two{
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:3];
}

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

2.6 线程同步-dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的
线程同步不一定是加锁,线程同步本质是不让多条线程占用一份资源

self.queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.queue, ^{
       //需要执行的代码
    });
dispatch_sync(self.queue, ^{
       //需要执行的代码
    });

2.7 线程同步-dispatch_semaphore

semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

@property (strong, nonatomic) dispatch_semaphore_t semaphore;
self.semaphore = dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
//需要同步的内容/代码
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

2.8 线程同步-@synchronized

  • @synchronized是对mutex递归锁的封装,源码查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作,不推荐使用性能较差
  • 底层使用哈希表结构,传进去对象作为key进行加锁和解锁
  • 是支持递归加锁
//使用self和[self class]都可以
@synchronized([self class]) { // objc_sync_enter
        //需要同步的内容/代码
 } 

2.9 iOS线程同步方案性能比较

性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

2.10 自旋锁、互斥锁比较

Q:什么情况使用自旋锁比较划算?

预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器

Q:什么情况使用互斥锁比较划算?

预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈

第三部分:

3.1 atomic

  • atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,自旋锁
  • 可以参考源码objc4的objc-accessors.mm
  • 它并不能保证使用属性的过程是线程安全的

3.2 读写安全方案

  • 多读单写
  • 只能有1个线程进行写的操作
  • 允许有多个线程进行读的操作
  • 不允许既有写的操作,又有读的操作

Q:“多读单写”,用于文件等数据的读写操作,iOS中的实现方案?

pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用

3.2.1 读写锁-pthread_rwlock

#import <pthread.h>

@interface ViewController ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // 初始化锁
    pthread_rwlock_init(&_lock, NULL);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self write];
        });
    }
}


- (void)read {
    pthread_rwlock_rdlock(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    pthread_rwlock_wrlock(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    pthread_rwlock_destroy(&_lock);
}

其他API

//写操作:尝试加锁
pthread_rwlock_trywrlock(&_lock);
//读操作:尝试加锁
pthread_rwlock_tryrdlock(&_lock);

3.2.2 dispatch_barrier_async

  • dispatch_barrier_async函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
  • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
//初始化队列
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

- (void)read {
    dispatch_async(self.queue, ^{
        sleep(1);
        NSLog(@"read");
    });
}
- (void)write
{
    dispatch_barrier_async(self.queue, ^{
        sleep(1);
        NSLog(@"write");
    });
}
补充
  • [self performSelector: withObject: afterDelay:]:选择线程执行方法,是往选择线程的RunLoop中添加的方法,必须当前线程的RunLoop启动才会执行
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容