本篇文章将介绍iOS开发中的线程模块,涉及实现方式、多线程、串并行、同步异步、线程间通信、以及线程安全
iOS应用中,每一个应用都至少有一个主队列(MainQueue)和其对应的主线程(MainThread),并存在一个让应用存活的主RunLoop中。一个应用的存活与简单操作,其实一个线程就够了。但是,实际中我们开发的应用并不简单,涉及很多和复杂的业务需求。针对于大量的复杂的业务需求,那多线程的存在就至关重要了。
多线程的目的其实就是提高效率。
- 多线程实现方案
- 线程的多种形态
- 多线程使用场景
- 线程安全
1 多线程实现方案
iOS中,多线程的实现方法主要有以下四种方式:
1.1 pthread
pthread是C语言实现的,API是C语言的,它是跨平台的,由于是C语言实现,其性能较高。但是在开发时效率会低,因为其实现的线程生命周期需要开发者自己实现。
// 申明/定义
pthread_t thread1;
// 创建,run是方法
pthread_create(&thread1, NULL, run, NULL);
pthread_t thread2;
pthread_create(&thread2, NULL, run, NULL);
// c语言函数
void *run(void *param) {
NSLog(@"%@",[NSThread currentThread]);
return NULL;
}
1.2 NSThread
NSThrad是OC语言实现的,它是对pthread进行面向对象的封装,因其多了一层封装,他的性能是低于pthread的,但是其是面向对象的API,开发效率上会优于pthread。
// 创建线程---方式1
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"thread1"];
thread1.name = @"thread1";
// 启动线程
[thread1 start];
// 创建线程---方式2
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"thread2"];
1.3 GCD
CGD是C语言实现的,直接面向设备内核进行实现,充分利用设备的多核,旨在替代NSThread等线程技术,并且其生命周期自动管理,大大提高了开发者的开发效率。
// 创建串行
dispatch_queue_t queue = dispatch_queue_create("kirk", DISPATCH_QUEUE_SERIAL);
// 创建并行队列
dispatch_queue_t queue = dispatch_queue_create("kirk", DISPATCH_QUEUE_CONCURRENT);
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 获取全局队列,其是并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 队列的当前线程中添加同步任务,不会开辟新的线程,如果队列是当前队列,会造成现在阻塞卡死
dispatch_sync(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});
// 在队列的当前线程添加异步执行任务,如果队列queue不是当前队列会开辟新的线程
// 如果是当前队列,不会开启新的线程
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
}
});
1.4 NSOperation
NSOperation是OC语言实现的,基于GCD(底层是GCD)的面向对象的封装,比GCD多了一些更简单实用的功能,使用更加面向对象,其生命周期自动管理
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run1) object:nil];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---%@",[NSThread currentThread]);
}];
// 队列中添加任务
[queue addOperation:op1];
[queue addOperation:op2];
[op2 start];
2 线程的多种形态
这个模块将介绍多线程的执行形式——同步、异步执行,以及对其进行管理的队列——并行、串行队列。
2.1 线程:
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行或串行执行不同的任务。
2.2 队列:
其是先进先出的线性表。其只允许在后端进行插入操作,在前端进行删除操作。在多线程中,线程的管理依赖队列。每个线程队列中会有一个或多个线程处理任务。根据其执行的顺序可以分为串行队列和并行队列。
2.3 串行队列(Serial Dispatch Queue):
即队列中的任务是一个接着一个地执行,一个任务执行完毕后,再执行下一个任务。
串行队列中,即使里面的线程是异步执行的,也需要一个接一个的按顺序执行。
// 创建串行队列 --- gcd
dispatch_queue_t queue = dispatch_queue_create("kirk", DISPATCH_QUEUE_SERIAL);
// 主队列:
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建串行队列 ---NSOperation
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 创建队列
// 设置队列最大并发量为1,队列中添加的任务会在新的子线程中执行,并且依次执行
queue.maxConcurrentOperationCount = 1;
2.4 并行队列(Concurrent Dispatch Queue):
即队列中的任务是并发(同时)执行的,多个线程会同时开启执行任务。
// 创建并行队列 --- gcd
dispatch_queue_t queue = dispatch_queue_create("kirk", DISPATCH_QUEUE_CONCURRENT);
// 获取全局队列,这个队列是并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建并行队列 ---NSOperation
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 创建队列
queue.maxConcurrentOperationCount = 5; // 设置队列最大并发量为5
2.5 异步任务:
异步执行任务,具备开辟新线程的能力。如果在当前线程所在的队列中添加异步任务,不会开启新的线程,而非当前队列添加任务时会创建开辟新的线程。
// 添加异步任务,会不会创建新的线程取决于queue是不是并行队列,只有是并行队列才会开辟新的线程
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});
// 添加异步任务,queue添加的任务在子线程中执行,并且多个任务是同时执行的
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 5;
[queue addOperationWithBlock:^{
NSLog(@"1---%@",[NSThread currentThread]);
}];
// taget任务
NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOpereation1) object:nil];
[queue addOperation:operation1];
- (void)invocationOpereation1 {
for (int i = 0; i<10; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
}
}
// 打印信息是交替打印,1和2交替打印
2.6 同步任务:
这里的同步,不是指同时执行,而是在当前线程中执行,按照执行顺序依次执行。
// 添加同步任务,在当前线程执行
dispatch_sync(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});
// 添加异步任务,queue添加的任务是在子线程中执行,并且是串行,依次执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
[queue addOperationWithBlock:^{
NSLog(@"1---%@",[NSThread currentThread]);
}];
// taget任务
NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOpereation1) object:nil];
[queue addOperation:operation1];
- (void)invocationOpereation1 {
for (int i = 0; i<10; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
}
}
// 打印信息是依次打印,打印完1再2
2.7 队列组
对于队列和任务,可以将其添加到队列组中,队列组可控制其执行顺序。
// 创建队列组-----gcd
dispatch_group_t group = dispatch_group_create();
控制三个任务的执行顺序,任务一、二异步完成后再完成任务三
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
// 添加异步任务1
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});
// 添加异步任务2
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});
// 等前面的任务执行完毕后,会自动执行这个任务
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
2.8 线程栅栏:barrier
在一个队列中,前面的任务执行完才执行后面的任务。
dispatch_queue_t queue = dispatch_queue_create("ckck", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
sleep(1);
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
sleep(1);
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"3---%@",[NSThread currentThread]);
sleep(1);
}
});
//在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
dispatch_barrier_async(queue, ^{
NSLog(@"barrier---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"4---%@",[NSThread currentThread]);
sleep(1);
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"5---%@",[NSThread currentThread]);
sleep(1);
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
NSLog(@"6---%@",[NSThread currentThread]);
sleep(1);
}
});
// 123执行完(123是交替执行的),然后执行barrier,然后执行456(也是交替执行的)
2.9 其他:
只执行一次:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"111111111");
});
延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (ino64_t)(2 *NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
NSLog(@"11111");
});
关于多线程相关的这篇文章就讲这么多,基本上能满足我们日常开发中对于多线程的需求。当然,关于线程安全方面,接下来将介绍。
3 线程安全&锁
线程安全,简单来说,就是多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的。
比如,我的钱包money数值,如果在两个地方同时存取,之前我的money是500,然后a处要存入100,b处要取出50,且这两个几乎同时操作,a处看到的初始值是500,那么存入100后是600,存入是600,但是b在a存入前拿到了初始值时500,这时候取出50,那么取出后的值时450。这就出现了线程不安全。
为了保证线程安全,锁就出现了。锁的原理是,在其进行一次操作值的时候将其锁住,只能本次操作,将其他操作锁住外面,等此次操作结束后,解锁,依次处理下一次操作。
iOS中保护线程安全的方案锁有很多种,比如OSSpinLock、os_unfair_lock、pthread_mutex、dispatch_semaphore、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、@synchronized等,本文将阐述几种常用的,其他的用法都差不多就不再具体展开。
3.1 自旋锁
自旋锁,其实现原理是一个死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。因此自旋锁的劣势是一直占用着CPU资源,消耗性能。
3.1.1 OSSpinLock
目前已经不再安全,可能会出现优先级反转问题(p如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁)
需要导入头文件#import
// 初始化线程锁
self.moneyLock = OS_SPINLOCK_INIT;
// 取钱
- (void)drawMoney
{
// 加锁
OSSpinLockLock(&_moneyLock);
//drawMoney...
// 解锁
OSSpinLockUnlock(&_moneyLock);
}
// 存钱
- (void)saveMoney
{
// 加锁
OSSpinLockLock(&_moneyLock);
// saveMoney.....
// 解锁
OSSpinLockUnlock(&_moneyLock);
}
3.1.2 os_unfair_lock
用于取代不安全的OSSpinLock ,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import
// 初始化锁
self.moneyLock = OS_UNFAIR_LOCK_INIT;
// 存钱
- (void)saveMoney
{
// 加锁
os_unfair_lock_lock(&_moneyLock);
// saveMoney.....
// 解锁
os_unfair_lock_unlock(&_moneyLock);
}
// 取钱
- (void)drawMoney
{
// 加锁
os_unfair_lock_lock(&_moneyLock);
//drawMoney...
// 解锁
os_unfair_lock_unlock(&_moneyLock);
}
3.2 互斥锁
3.2.1 pthread_mutex
叫做”互斥锁”,等待锁的线程会处于休眠状态。
其有五个函数进行操作:
1> pthreadmutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t _attr)。初始化锁变量mutex。attr为锁属性,NULL值为默认属性。
2> pthread_mutex_lock(pthread_mutex_t *mutex);加锁
3> pthread_mutex_tylock(pthread_mutex_t *mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
4> pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁
5> pthread_mutex_destroy(pthread_mutex_t *mutex);使用完后释放
需要导入头文件#import
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
// 初始化锁:1、初始化锁的属性,2、初始化锁
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 存钱
- (void)__saveMoney
{
// 加锁
pthread_mutex_lock(&_moneyMutex);
// saveMoney.....
// 解锁
pthread_mutex_unlock(&_moneyMutex);
}
// 取钱
- (void)__drawMoney
{
// 加锁
pthread_mutex_lock(&_moneyMutex);
//drawMoney...
// 解锁
pthread_mutex_unlock(&_moneyMutex);
}
// 销毁锁
- (void)dealloc
{
pthread_mutex_destroy(&_moneyMutex);
}
3.2.2 NSLock
NSLock是对mutex普通锁的封装
self.moneyLock = [[NSLock alloc] init];
- (void)__saveMoney
{
[self.moneyLock lock];
// save
[self.moneyLock unlock];
}
- (void)__drawMoney
{
[self.moneyLock lock];
// draw
[self.moneyLock unlock];
}
3.3 递归锁
递归锁:允许同一个线程对一把锁进行重复加锁,递归锁的实现就是将互斥锁的属性进行设置
// 初始化锁:1、初始化锁的属性,2、初始化锁
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 递归锁标识
// 初始化锁
pthread_mutex_init(mutex, NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);
- (void)test
{
// 加锁
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
static int count = 0;
if (count < 10) {
count++;
[self otherTest];
}
// 解锁
pthread_mutex_unlock(&_mutex);
}
3.3.2 @synchronized
是对mutex递归锁的封装,@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
@synchronized (objc) {
// 对objc的操作
}
3.3.3 NSRecursiveLock
对mutex递归锁的封装,API跟NSLock基本一致
3.4 条件锁
可以添加条件
@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) {
// 等待
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);
}
3.4.1 NSCondition
NSCondition是对mutex和cond的封装
3.4.2 NSConditionLock
是对NSCondition的进一步封装,可以设置具体的条件值
3.5 信号量
3.5.1 dispatch_semaphore
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(5);
self.ticketSemaphore = dispatch_semaphore_create(1);
self.moneySemaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)__drawMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __drawMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saveMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __saveMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saleTicket
{
dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
[super __saleTicket];
dispatch_semaphore_signal(self.ticketSemaphore);
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
// 线程10、7、6、9、8
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
3.6 自旋锁、互斥锁比较
使用自旋锁:
1> 预计线程等待锁的时间很短
2> 加锁的代码(临界区)经常被调用,但竞争情况很少发生
3> CPU资源不紧张
4> 多核处理器
使用互斥锁:
1> 预计线程等待锁的时间较长
2> 预计线程等待锁的时间较长
3> 临界区有IO操作
4>临界区代码复杂或者循环量大,临界区竞争非常激烈
3.7 atomic
用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm
它并不能保证使用属性的过程是线程安全的
4 线程读写安全方案
线程读写安全的出现:多读单写,经常用于文件等数据的读写操作,iOS中的实现方案有
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写的操作,又有读的操作
其实现的方案有:
pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用
4.1 pthread_rwlock
其读数据锁和写数据锁是两个不同的函数,进行区分操作
// 初始化锁
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);
4.2 dispatch_barrier_async
这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
// 初始化队列
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
// 使用
for (int i = 0; i < 10; i++) {
dispatch_async(self.queue, ^{
[self read];
});
dispatch_async(self.queue, ^{
[self read];
});
dispatch_async(self.queue, ^{
[self read];
});
// 前面三个任务同步执行,执行完后再执行这个,这个执行完再到下一步
dispatch_barrier_async(self.queue, ^{
[self write];
});
}
- (void)read {
sleep(1);
NSLog(@"read");
}
- (void)write {
sleep(1);
NSLog(@"write");
}
以上。
不对之处,可评论探讨。