多线程相关知识:
同步线程:dispatch中的sync函数,即是在当前线程做事情
异步函数:dispatch中的async函数,即在另外一条线程做事情
并发队列:允许多个任务同时执行
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步函数(dispatch_async)下才有效
串行队列:让任务一个接一个地执行(执行完一个再执行下一个)
注:通过CF开头的函数创建出来的变量,需要手动调用CFRealease去释放,但GCD的是不用的
同步与异步:能否开启新线程 (决定了是在哪个线程执行)
- 同步:在当前线程中执行任务.不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 同步函数:立马在当前线程执行任务,执行完毕后才能继续往下执行,即同步函数内的任务不执行完,该函数就会卡住,不会继续往下执行
- 异步函数:不要求立马在当前线程执行任务,会等上一个任务执行完再执行
并发和串行:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完成后,再执行下一个任务
主队列是一种特殊的串行队列
只要是放到主队列的任务,都是在主线程执行
死锁:
队列的特点:FIFO(First In First Out)先进先出
产生死锁的两个情况:
- 当同步函数内的队列是主队列时,会产生死锁
- 使用sync函数往当前串行队列中添加任务,就会产生死锁
队列组:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("myQueue", 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]);
};
});
});
多线程的安全隐患:
Q:一块资源可能会被多个线程共享,容易造成数据错乱和数据异常的问题
A:解决方法:使用线程同步技术(同步:协同步调,按预定的先后顺序次序进行)
常用的线程同步技术:加锁
- OSSpinLock(自旋锁):等待锁的线程会处于忙等状态,一直占用着CPU资源(high-level lock)
//需要导入头文件
_lock = OS_SPINLOCK_INIT;//初始化
/*
//也可以这么初始化锁
static OSSpinLock lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = 0;//OS_SPINLOCK_INIT的值就是0
});
*/
OSSpinLockLock(&_lock);//加锁
//需加锁代码
OSSpinLockUnlock(&_lock);//解锁
注意点:
- 所有线程应该共用一把锁,不然还是会存在问题,每次都创建一把新锁
若有好几个方法同一时间只能使用一个方法,需要共用一把锁 - 原理:类似写了一个while循环
- 目前已经不再安全,有可能出现优先级反转的问题
若有线程1和线程2,假设线程1的优先级大于线程2,线程2先进入代码,发现锁未加锁,故加锁执行代码,线程1后进入,发现该锁已经被加锁,故忙等,但由于线程1优先级大于线程2,CPU分配更多时间执行线程1的代码,可能导致线程2的代码没有时间执行,导致无法解锁会进入一个类似死锁的状态,目前苹果已经不推荐使用
也可以使用static静态初始化锁,使自旋锁唯一,故锁不一定需要使用属性的方式
无法在static变量初始化时动态调用函数,只能取个值,因为static是静态初始化,右边的值在编译就需要确定,若需要动态调用函数,需要再次使用once函数,在once代码里赋值
当多条线程需要同时修改同一个值时,基本需要加锁,读取则不需要
- os_unfair_lock(low-level lock)
底层调用看,为互斥锁,等待os_unfair_lock锁的线程会处于休眠,而并非忙等,等不到锁就休眠(ios10以上使用)
//需要导入头文件
_lock = OS_UNFAIR_LOCK_INIT;//初始化
os_unfair_lock_lock(&_lock);//加锁
//需加锁代码
os_unfair_lock_unlock(&_lock);//解锁
- pthread_mutex(low-level lock)
互斥锁,等待锁的线程会处于休眠状态,不需要使用时,要手动销毁
注意点:结构体静态初始化只允许定义的同时进行赋值,不允许后续使用set方法赋值
struct Date {
int year;
int month;
}
struct Date date = {2011,10}//这样是可以的
struct Date date;
date = {2011,10}//这样是不可以的
//就是因为这样,解释了为什么不能定义一个pthread_mutex,再通过self的点语法方式对其进行静态初始化的原因
//需要导入头文件
//静态初始化
//self.lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_mutex_init(&_lock, &attr);
//锁和条件可以在对象销毁时销毁(declloc)
//属性可以立马销毁
pthread_mutexattr_destroy(&attr);
/*
pthread_mutex_init(mutex, NULL);
//初始化时,可以将&attr传NULL,即默认为PTHREAD_MUTEX_DEFAULT
*/
将PTHREAD_MUTEX_DEFAULT
属性替换为PTHREAD_MUTEX_RECUESIVE
,将锁变为递归锁,解决递归可能导致死锁的问题
递归锁的本质:允许同一个线程对一把锁进行重复加锁
//条件锁
- (void)define{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_cond_init(&_con, NULL);//创建一个条件
pthread_mutex_init(&_lock, &attr);
pthread_mutexattr_destroy(&attr);
}
- (void)using1{
pthread_mutex_lock(&_lock);
//需要执行的代码
pthread_cond_wait(&_con, &_lock);//会休眠,将锁放开,等待信号量到来,再次加锁执行下面代码
pthread_mutex_unlock(&_lock);
}
- (void)using2{
pthread_cond_signal(&_con);//告知对应的条件锁,该锁已经放开,可以继续使用
}
通过创建条件锁后,pthread_mutex能够办到线程等待的效果(多线程的依赖问题)
tip:
- 在汇编中敲入si,就是汇编指令级别的一行,遇到函数调用会进入函数内部
- 汇编中敲入c,就是直接到断点的位置
- NSLock(low-level lock)
是pthread_mutex的普通锁的封装
遵守NSLocking协议会执行下面两个函数:
-
- (void)lock
; -
- (void)unlock
;
其常用的API有2个:
-
- (BOOL)tryLock
; 会进行尝试加锁,若能加锁,则加锁执行后续代码,不能加锁,则返回NO,继续执行后续代码(即相当于没有锁) -
- (BOOL)lockBeforeDate:(NSDate *)limit
;会判断在limit时间到来之前,能加锁成功,则加锁,执行后续代码,若等到limit时间到来时,还没加锁成功,则执行后续代码,返回加锁失败
self.lock = [[NSLock alloc] init];
[self.lock lock];
//执行的代码
[self.lock unlock];
- NSRecursiveLock(low-level lock)
是pthread_mutex的递归锁的封装,用法与NSLock一致
- NSCondition(low-level lock)
是对pthread_mutex和cond的封装,即加锁解锁条件都能使用
其常用的API有4个:
-
- (void)wait
; -
- (BOOL)waitUntilDate:(NSDate *)limit
; -
- (void)signal
; -
- (void)broadcast
;
self.condition = [[NSCondition alloc] init];
[self.condition lock];
//需要执行的代码
[self.condition wait];
[self.condition unlock];
[self.condition signal];//给NSCondition发送信号
[self.condition broadcast];//给NSCondition发送广播
- NSConditionLock(low-level lock)
是对NSCondition的进一步封装,可以设置具体的条件值
-
- (instancetype)initWithCondition:(NSInteger)condition
; 会初始化条件值,若直接使用init方法,则默认初始条件值为0 -
- (void)lockWhenCondition:(NSInteger)condition
; 当条件值为condition时加锁 -
- (void)unlockWithCondition:(NSInteger)condition
; 该方法会解锁,并将条件值修改为condition
self.lock = [[NSConditionLock alloc]initWithCondition:1];//初始化条件值为1;
[self.lock lockWhenCondition:1];//当条件值为1时加锁
//要做的事
[self.lock unlockWithCondition:2];//该方法会解锁,并将条件值修改为2
注:若调用lock方法是不管条件值,即无论条件值为多少,lock方法都能进;
- GCD串行队列
直接使用gcd的串行队列也可以实现线程同步的
- dispatch_semaphore
semaphore:信号量
- wait函数:如果信号量的值>0,就让信号量的值减1,继续往下执行代码
如果信号量的值<=0,就会休眠等待,知道信号量的值变成>0,然后就让信号量的值减1,继续往下执行代码 - signal函数:让信号量的值+1
self.lock = dispatch_semaphore_create(5);//信号量的初始值,可以用来控制线程并发访问的最大数量,最大并发数量在这里就是5
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
//这里的代码最多只有最大并发数条线程能同时执行,即最多只有5条线程能同时执行
dispatch_semaphore_signal(self.lock);
- @synchronized
是对mutex递归锁的封装,底层实现是根据传入的对象,在hash表中寻找对应的锁
@synchronized([self class]) { // objc_sync_enter
//要做的事
}// objc_sync_exit
//()中可以传入任何对象为锁对象,当()中的锁对象一致时,表示共用一把锁
以上的锁的性能从高到低排序:
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- GCD串行队列
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
推荐使用:dispatch_semaphore和pthread_mutex,是相对来说性能最高的
当要实现一个方法一把锁时,可以通过static静态初始化变量,再通过dispatch_once
方法,令该方法使用唯一一把锁(记得写在方法内部),也可以抽成宏定义,若多个地方需共用一把锁,只能把锁抽出来一起使用
两个问题:
什么情况使用自旋锁比较划算?
1.预计线程等待锁的时间很短
2.加锁的代码(临界区)经常被调用,但竞争情况很少发生
3.CPU资源不紧张
4.多核处理器什么情况使用互斥锁比较划算?
1.计线程等待锁的时间较长
2.单核处理器
3.临界区有IO操作
4.临界区代码复杂或者循环量大
5.临界区竞争非常激烈
属性的原子性与非原子性:
- atomic:给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作,也就是说,保证setter和getter内部是线程同步,即在setter和getter内部中做加锁解锁的操作
原子性操作:即保证多行代码能够按顺序执行完成,就如当做一个整体,同一行代码
atomic并不能保证使用属性的过程是线程安全的,即只能保证setter和getter内部是线程安全的
但atomic耗费性能,因为属性的setter和getter是调用很频繁的,但真正需要加锁操作时,再去进行加锁操作即可
文件I/O操作:
多读单写(读写安全):
1.读取文件是同一时间允许多条线程同时进行读的操作
2.只允许单条线程进行写的操作
3.不允许既有读的操作又有写的操作 ,常用于文件等数据的读写操作
- pthread_rwlock:等待锁的线程会进入休眠
//需要导入#import
pthread_rwlock_init(&_lock, NULL);//初始化锁
pthread_rwlock_rdlock(&_lock);//读锁加锁
pthread_rwlock_wrlock(&_lock);//写锁加锁
pthread_rwlock_unlock(&_lock);//解锁
- dispatch_barrier_async
执行到barrier里面的任务时,会自动出现一个栅栏,执行任务,其他任务时无法再进行读取操作的
//这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的,如果传入的是一个串行或者是一个全局的并发队列,那这个函数等同于dispatch_async函数的效果
self.queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
- (void)read {
dispatch_async(self.queue, ^{
//读操作
});
}
- (void)write {
dispatch_barrier_async(self.queue, ^{
//写操作
});
}
注意点:这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的,如果传入的是一个串行或者是一个全局的并发队列,那这个函数等同于dispatch_async函数的效果