线程安全:
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。
解决多线程安全问题的方法
-
方法一:互斥锁(同步锁)
@synchronized(锁对象) { // 需要锁定的代码 }
判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用self作为锁对象,这样可以避免单独再创建一个锁对象。
加了互斥做的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。
- 方法二:自旋锁
加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。
属性修饰atomic本身就有一把自旋锁。
nonatomic 非原子属性,同一时间可以有很多线程读和写
atomic 原子属性(线程安全),保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值),atomic 本身就有一把锁(自旋锁)
atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,不过效率更高,一般使用nonatomic
使用自旋锁时要注意:
-
由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。
-
持有自旋锁的线程在sleep之前应该释放自旋锁以便其他咸亨可以获得该自旋锁。内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起。
使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
1.建立锁所需要的资源
2.当线程被阻塞时所需要的资源
自旋锁和互斥锁
-
相同点:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。
-
不同点:
-
互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
-
自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
-
-
自旋锁的效率高于互斥锁。
iOS开发当中用到的锁:
-
NSLock
-
NSRecursiveLock
-
NSCondition
-
NSConditionLock
-
pthread_mutex
-
pthread_rwlock
-
POSIX Conditions
-
OSSpinLock
-
os_unfair_lock
-
dispatch_semaphore
-
@synchronized
信号量:
在多线程环境下用来确保代码不会被并发调用。在进入一段代码前,必须获得一个信号量,在结束代码前,必须释放该信号量,其他想要想要执行该代码的线程必须等待直到前者释放了该信号量。
互斥锁
一种用来防止多个线程同一时刻对共享资源进行访问的信号量,它的原子性确保了如果一个线程锁定了一个互斥量,将没有其他线程在同一时间可以锁定这个互斥量。它的唯一性确保了只有它解锁了这个互斥量,其他线程才可以对其进行锁定。当一个线程锁定一个资源的时候,其他对该资源进行访问的线程将会被挂起,直到该线程解锁了互斥量,其他线程才会被唤醒,进一步才能锁定该资源进行操作。
NSLock
NSLock实现了最基本的互斥锁,遵循了 NSLocking 协议,通过 lock 和 unlock 来进行锁定和解锁。其使用也非常简单
- (void)doSomething { [self.lock lock]; //TODO: do your stuff [
由于是互斥锁,当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。但是如果连续锁定两次,则会造成死锁问题。那如果想在递归中使用锁,那要怎么办呢,这就用到了 NSRecursiveLock 递归锁。
NSRecursiveLock
递归锁,顾名思义,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init]; void MyRecursiveFunction(int value) { [theLock lock]; if (value != 0) { --value; MyRecursiveFunction(value); } [theLock unlock]; } MyRecursiveFunction(5);
NSCondition
NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。
- (void)download { [self.condition lock]; //TODO: 下载文件代码 if (donloadFinish) { // 下载结束后,给另一个线程发送信号,唤起另一个处理程序 [self.condition signal]; [self.condition unlock]; } } - (void)doStuffWithDownloadPicture { [self.condition lock]; while (!donloadFinish) { [self.condition wait]; } //TODO: 处理图片代码 [self.condition unlock]; }
NSConditionLock
NSConditionLock 对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁。它和 NSCondition 很像,但实现方式是不同的。
当两个线程需要特定顺序执行的时候,例如生产者消费者模型,则可以使用 NSConditionLock 。当生产者执行执行的时候,消费者可以通过特定的条件获得锁,当生产者完成执行的时候,它将解锁该锁,然后把锁的条件设置成唤醒消费者线程的条件。锁定和解锁的调用可以随意组合,lock 和 unlockWithCondition: 配合使用 lockWhenCondition: 和 unlock 配合使用。
- (void)producer { while (YES) { [self.conditionLock lock]; NSLog(@"have something"); self.count++; [self.conditionLock unlockWithCondition:1]; } } - (void)consumer { while (YES) { [self.conditionLock lockWhenCondition:1]; NSLog(@"use something"); self.count--; [self.conditionLock unlockWithCondition:0]; } }
当生产者释放锁的时候,把条件设置成了1。这样消费者可以获得该锁,进而执行程序,如果消费者获得锁的条件和生产者释放锁时给定的条件不一致,则消费者永远无法获得锁,也不能执行程序。同样,如果消费者释放锁给定的条件和生产者获得锁给定的条件不一致的话,则生产者也无法获得锁,程序也不能执行。
pthread_mutex
POSIX 互斥锁是一种超级易用的互斥锁,使用的时候,只需要初始化一个 pthread_mutex_t 用 pthread_mutex_lock 来锁定 pthread_mutex_unlock 来解锁,当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。
pthread_mutex_init(&lock,NULL); pthread_mutex_lock(&lock); //do your stuff pthread_mutex_unlock(&lock); pthread_mutex_destroy(&lock);
pthread_rwlock
读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。
-
当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。
-
当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。
// 初始化 pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER // 读模式 pthread_rwlock_wrlock(&lock); // 写模式 pthread_rwlock_rdlock(&lock); // 读模式或者写模式的解锁 pthread_rwlock_unlock(&lock); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:1]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:2]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self writeBook:3]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self writeBook:4]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:5]; }); - (void)readBookWithTag:(NSInteger )tag { pthread_rwlock_rdlock(&rwLock); NSLog(@"start read ---- %ld",tag); self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"]; self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil]; NSLog(@"end read ---- %ld",tag); pthread_rwlock_unlock(&rwLock); } - (void)writeBook:(NSInteger)tag { pthread_rwlock_wrlock(&rwLock); NSLog(@"start wirte ---- %ld",tag); [self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil]; NSLog(@"end wirte ---- %ld",tag); pthread_rwlock_unlock(&rwLock); } start read ---- 1 start read ---- 2 end read ---- 1 end read ---- 2 start wirte ---- 3 end wirte ---- 3 start wirte ---- 4 end wirte ---- 4 start read ---- 5 end read ---- 5
POSIX Conditions
POSIX 条件锁需要互斥锁和条件两项来实现,虽然看起来没什么关系,但在运行时中,互斥锁将会与条件结合起来。线程将被一个互斥和条件结合的信号来唤醒。
首先初始化条件和互斥锁,当 ready_to_go 为 flase 的时候,进入循环,然后线程将会被挂起,直到另一个线程将 ready_to_go 设置为 true 的时候,并且发送信号的时候,该线程会才被唤醒。
pthread_mutex_t mutex; pthread_cond_t condition; Boolean ready_to_go = true; void MyCondInitFunction() { pthread_mutex_init(&mutex); pthread_cond_init(&condition, NULL); } void MyWaitOnConditionFunction() { // Lock the mutex. pthread_mutex_lock(&mutex); // If the predicate is already set, then the while loop is bypassed; // otherwise, the thread sleeps until the predicate is set. while(ready_to_go == false) { pthread_cond_wait(&condition, &mutex); } // Do work. (The mutex should stay locked.) // Reset the predicate and release the mutex. ready_to_go = false; pthread_mutex_unlock(&mutex); } void SignalThreadUsingCondition() { // At this point, there should be work for the other thread to do. pthread_mutex_lock(&mutex); ready_to_go = true; // Signal the other thread to begin work. pthread_cond_signal(&condition); pthread_mutex_unlock(&mutex); }
OSSpinLock
自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自选锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。
// 初始化 spinLock = OS_SPINLOCK_INIT; // 加锁 OSSpinLockLock(&spinLock); // 解锁 OSSpinLockUnlock(&spinLock);
os_unfair_lock
自旋锁已经不在安全,然后苹果又整出来个 os_unfair_lock_t
这个锁解决了优先级反转问题。
os_unfair_lock_t unfairLock; unfairLock = &(OS_UNFAIR_LOCK_INIT); os_unfair_lock_lock(unfairLock); os_unfair_lock_unlock(unfairLock);
dispatch_semaphore
信号量机制实现锁,等待信号,和发送信号,正如前边所说的看门人一样,当有多个线程进行访问的时候,只要有一个获得了信号,其他线程的就必须等待该信号释放。
- (void)semphone:(NSInteger)tag { dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); // do your stuff dispatch_semaphore_signal(semaphore); }
@synchronized
一个便捷的创建互斥锁的方式,它做了其他互斥锁所做的所有的事情。
- (void)myMethod:(id)anObj { @synchronized(anObj) { // Everything between the braces is protected by the @synchronized directive. } }
如果你在不同的线程中传过去的是一样的标识符,先获得锁的会锁定代码块,另一个线程将被阻塞,如果传递的是不同的标识符,则不会造成线程阻塞。
总结
应当针对不同的操作使用不同的锁,而不能一概而论那种锁的加锁解锁速度快。
-
当进行文件读写的时候,使用 pthread_rwlock 较好,文件读写通常会消耗大量资源,而使用互斥锁同时读文件的时候会阻塞其他读文件线程,而 pthread_rwlock 不会。
-
当性能要求较高时候,可以使用 pthread_mutex 或者 dispath_semaphore,由于 OSSpinLock 不能很好的保证线程安全,而在只有在 iOS10 中才有 os_unfair_lock ,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。
-
对于 NSLock 及其子类,速度来说 NSLock < NSCondition < NSRecursiveLock < NSConditionLock 。
参考:https://blog.csdn.net/deft_mkjing/article/details/79513500
iOS中的各种锁: http://www.cocoachina.com/ios/20161129/18216.html