目录
前言
在
中,我们了解到了GCD的用法。这篇博客主要介绍下多线程中的自旋锁和互斥锁。上一篇博客
一、多线程资源共享的安全隐患
在 iOS 开发中,多线程编程可以显著提高应用的性能和响应能力。Grand Central Dispatch (GCD) 是一个用于多线程编程的强大工具。然而,在多线程环境下进行资源共享时,必须小心避免竞争条件和数据争用等问题。
当多个线程同时访问和修改共享资源时,如果不进行适当的同步,就可能导致数据不一致的问题。这种情况称为“竞争条件”。
下面我们看一下几个比较经典的多线程共享的例子。
1. 资源共享的例子
假设我们有一个共享的整数变量 counter,两个线程将并发地对其进行递增操作。如果没有使用任何同步机制来保护 counter,这将导致竞争条件,结果可能是错误的。
完整的代码如下:
#import "ViewController.h"
@interface ViewController ()
// 共享的计数器
@property (nonatomic, assign) int counter;
// 并发队列
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化计数器
self.counter = 0;
// 初始化并发队列
self.concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 创建两个并发任务
dispatch_async(self.concurrentQueue, ^{
for (int i = 0; i < 10000; i++) {
[self incrementCounter];
}
});
dispatch_async(self.concurrentQueue, ^{
for (int i = 0; i < 10000; i++) {
[self incrementCounter];
}
});
}
// 不安全的计数器增加操作(没有使用任何锁)
- (void)incrementCounter {
sleep(.01);
self.counter += 1;
NSLog(@"当前counter: %d", self.counter);
}
@end
1.详细解释
在上述的demo中,counter 是一个共享资源,多个线程同时访问它。如果没有同步机制保护,线程竞争问题就会发生。
在 viewDidLoad 方法中,通过 dispatch_async 启动了两个并发任务,这两个任务都会同时尝试增加 counter 的值。
incrementCounter 方法在没有任何同步保护的情况下对 counter 进行增加操作。这就可能导致线程竞争问题,例如,两个线程可能会同时读取相同的 counter 值,然后都将其增加 1,最终导致 counter 只增加了一次,而不是两次。
我们多运行几次,会发现每次控制台打印的结果都不一样。
图2.资源共享控制台输出
2.竞争条件的具体表现
1.线程 A 读取 counter 的值(假设当前值为 100)。
2.线程 B 读取 counter 的值(假设当前值为 100)。
3.线程 A 将 counter 的值递增 1(结果为 101)并写回。
4.线程 B 将 counter 的值递增 1(结果为 101)并写回。
最终,尽管两个线程各自进行了递增操作,counter 的值却仅增加了 1,而不是期望的 2。
图3.多线程资源竞争
二、线程同步技术
当多个线程同时访问和修改共享资源时,如果不进行适当的同步,就可能导致数据不一致的问题。这种情况称为“竞争条件”。为了解决这些问题,可以使用同步机制来确保线程安全。
iOS中的线程同步有以下几种技术:
1.自旋锁(OSSpinLock)
1.OSSpinLock
1.概念
自旋锁(OSSpinLock)是一种用于多线程编程的同步机制,与互斥锁(Mutex)类似,但有一些不同之处。自旋锁在等待锁的时候,不会使线程进入休眠状态,而是持续地循环检查锁是否可用。这种忙等待(busy-waiting)机制适用于锁定时间短的场景,因为在锁定时间较长时,自旋锁会消耗大量的 CPU 资源。
在 iOS 中,自旋锁可以使用 os_unfair_lock 或 OSSpinLock 实现。需要注意的是,由于 OSSpinLock 存在优先级反转问题,苹果建议使用 os_unfair_lock 代替 OSSpinLock。
2.OSSpinLock的用法
1.初始化
我们使用下面的方法初始化。
OSSpinLock lock = OS_SPINLOCK_INIT;
2.加锁方法
//加锁
OSSpinLockLock(&_lock);
3.解锁方法
我们使用下面的代码进行解锁操作。
//解锁
OSSpinLockUnlock(&_lock);
3.示例代码
还以上述计时器的demo为例,我们看看加了自旋锁之后的结果。我们在修改计数器之前先加锁,修改之后再把自旋锁解开,运行代码,我们发现每次运行都是我们期望的结果2000。
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property (nonatomic) OSSpinLock spinLock;
@property (nonatomic, assign) int counter;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.spinLock = OS_SPINLOCK_INIT;
self.counter = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建两个并发任务
dispatch_async(queue, ^{
[self incrementCounterSafely];
});
dispatch_async(queue, ^{
[self incrementCounterSafely];
});
}
- (void)incrementCounterSafely {
for (int i = 0; i < 10000; i++) {
OSSpinLockLock(&_spinLock);
self.counter += 1;
OSSpinLockUnlock(&_spinLock);
}
NSLog(@"Final counter value: %d", self.counter);
}
@end
我们分析下这个自旋锁的作用:
OSSpinLockLock(&_lock);: 进入 incrementCounter 方法时,首先尝试获取自旋锁。如果锁已经被其他线程持有,当前线程将进入忙等状态(自旋),直到锁被释放。
sleep(.01);: 这个延时模拟了一个需要花费一定时间的操作,延长了线程持有锁的时间,从而增加了锁竞争的概率。
self.counter += 1;: 在获取到锁之后,安全地对 counter 进行递增操作。
OSSpinLockUnlock(&_lock);: 完成 counter 的更新后,释放锁,使其他等待的线程可以继续执行。
4.OSSpinLock的问题
OSSpinLock`是一种自旋锁,在等待锁时线程会不断尝试获取锁,而不会进入睡眠状态。这种忙等待的方式在某些情况下可能会造成严重的问题,特别是优先级反转问题。
1.什么是优先级反转
优先级反转(Priority Inversion)是指在多线程环境中,低优先级线程持有某个锁,高优先级线程因为需要获取该锁而被阻塞,中优先级线程却继续执行的现象。具体步骤如下:
1. 低优先级线程(线程 A)持有 OSSpinLock锁,并且正在执行某个临界区的代码。
2. 高优先级线程(线程 B)需要获取相同的锁,但由于锁被线程 A 占用,线程 B 被阻塞,开始自旋等待。
3. 此时,如果有一个中优先级线程线程 C)进入系统并开始执行,由于它的优先级比线程 A 高,但比线程 B 低,操作系统会调度线程 C 执行,而不是线程 A。
4. 结果是:线程 A 无法执行并释放锁,线程 B 也无法获取锁并继续执行,而线程 C 占用了 CPU 时间。线程 B 的执行被延迟,尽管它的优先级比线程 C 高。
2.OSSpinLock导致优先级反转的原因
OSSpinLock的设计并不考虑线程的优先级,当高优先级线程自旋等待低优先级线程释放锁时,CPU 的调度机制可能会使低优先级线程无法及时运行并释放锁。这种情况下,高优先级线程就会一直等待,导致优先级反转,最终影响系统的实时性和性能。
3.解决方法
为了避免优先级反转,苹果在 iOS 10 和 macOS 10.12 中标记了OSSpinLock为不安全,并建议开发者使用 os_unfair_lock作为替代。
os_unfair_lock:这是OSSpinLock的现代替代品,它能够避免优先级反转问题。os_unfair_lock在锁等待时会让出 CPU,允许其他线程执行。它还考虑了线程优先级,在竞争激烈的情况下具有更好的性能表现和公平性。
2.os_unfair_lock
os_unfair_lock是iOS10之后为了替代OSSpinLock新增的API,它解决了OSSpinLock的优先级反转的问题。
1.用法
首先我们需要导入头文件
#import <os/lock.h>。
1.加锁
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
2.加锁
os_unfair_lock_lock(&_lock);
3.解锁
os_unfair_lock_unlock(&_lock);
2.互斥锁(pthread_mutex)
metex叫做互斥锁,等待锁的线程会处于休眠状态。
1.用法
1.初始化
首先我们要导入头文件:
#import <pthread.h>
我们使用下面的方法初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2.加锁
pthread_mutex_lock(&_lock);
3.解锁
pthread_mutex_unlock(&_lock);
2.互斥锁的类型
1. 普通锁
普通锁()是 pthread_mutex 的默认锁类型,它不允许同一个线程多次锁定。如果同一个线程尝试再次获取已锁定的普通锁,则会导致死锁。这个锁适用于不需要递归加锁的场景。
以上面的计数器为例,我们看看如何使用普通锁。
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) int counter;
@property (nonatomic, assign) pthread_mutex_t lock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化普通锁
pthread_mutex_init(&_lock, NULL);
// 创建两个并发线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self incrementCounter];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self incrementCounter];
});
}
- (void)incrementCounter {
pthread_mutex_lock(&_lock);
for (int i = 0; i < 10000; i++) {
self.counter += 1;
}
pthread_mutex_unlock(&_lock);
NSLog(@"Final counter value: %d", self.counter);
}
- (void)dealloc {
pthread_mutex_destroy(&_lock);
}
@end
2. 递归锁
递归锁(PTHREAD_MUTEX_RECURSIVE)允许同一个线程多次获取同一把锁。它记录锁的获取次数,因此必须相应地解锁相同次数。这对于需要递归调用并且每次调用都需要锁定资源的场景非常有用。
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) int counter;
@property (nonatomic, assign) pthread_mutex_t recursiveLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化递归锁
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_recursiveLock, &attr);
pthread_mutexattr_destroy(&attr);
// 调用递归函数
[self recursiveFunction:5];
}
- (void)recursiveFunction:(int)value {
pthread_mutex_lock(&_recursiveLock);
if (value > 0) {
NSLog(@"Value: %d", value);
[self recursiveFunction:value - 1];
}
pthread_mutex_unlock(&_recursiveLock);
}
- (void)dealloc {
pthread_mutex_destroy(&_recursiveLock);
}
@end
3. 错误检测锁
错误检测锁(PTHREAD_MUTEX_ERRORCHECK)也允许同一个线程再次尝试获取锁,但与递归锁不同的是,如果同一线程再次获取锁,会返回错误而不是死锁。这对于需要检测锁定错误的场景非常有用。
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) pthread_mutex_t errorcheckLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化错误检测锁
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&_errorcheckLock, &attr);
pthread_mutexattr_destroy(&attr);
// 测试错误检测锁
[self testErrorcheckLock];
}
- (void)testErrorcheckLock {
int result = pthread_mutex_lock(&_errorcheckLock);
if (result == 0) {
NSLog(@"First lock acquired successfully.");
}
// 尝试再次加锁
result = pthread_mutex_lock(&_errorcheckLock);
if (result != 0) {
NSLog(@"Error: Attempt to relock a lock held by the same thread.");
}
pthread_mutex_unlock(&_errorcheckLock);
}
- (void)dealloc {
pthread_mutex_destroy(&_errorcheckLock);
}
@end
3.自旋锁条件
用互斥锁时,需要满足以下四个条件才能确保互斥锁正确地工作。这些条件通常也被称为经典的同步问题中的必要条件。
1.互斥条件
互斥条件要求在任何时刻,只能有一个线程能够占有互斥锁,其他试图占有该锁的线程必须等待。
pthread_mutex_lock(&mutex); // 锁定
// 临界区代码
pthread_mutex_unlock(&mutex); // 解锁
在上面的代码中,只有当一个线程成功获取了锁,才能进入临界区,其他线程会被阻塞,直到该线程释放锁。
2.保持与等待条件
持有和等待条件是指,线程在已经持有至少一个资源(锁)的情况下,还可以请求其他资源,并等待这些资源被分配。
pthread_mutex_lock(&mutex1); // 锁定第一个资源
pthread_mutex_lock(&mutex2); // 请求并等待第二个资源
// 临界区代码
pthread_mutex_unlock(&mutex2); // 释放第二个资源
pthread_mutex_unlock(&mutex1); // 释放第一个资源
这里线程先获取 mutex1,然后继续等待获取 mutex2。
3.不剥夺条件
不剥夺条件是指,已经分配给线程的资源(锁)不能被强行夺走,除非线程主动释放它们。一旦线程成功获得锁,其他线程无法强制性地获取该锁。只有持有锁的线程主动释放锁后,其他线程才有机会获取锁。
pthread_mutex_lock(&mutex);
// 临界区代码
// 在此期间,其他线程不能强行获取该锁
pthread_mutex_unlock(&mutex);
4.循环等待条件
循环等待条件是指,存在一个线程集合 {T1, T2, …, Tn},其中每个线程都在等待下一个线程持有的锁,且最后一个线程等待第一个线程持有的锁,从而形成一个等待循环。
// 线程1
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);// 线程2
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
在这个例子中,线程1先获取 mutex1,然后等待 mutex2,而线程2先获取 mutex2,然后等待 mutex1,导致循环等待,从而可能引发死锁。
3.NSLock
NSLock是对mutex普通锁的封装。
NSLock是用于在多线程环境下确保对共享资源的互斥访问。它是基于 `NSObject` 的封装,使用起来更加简洁和方便。NSLock是一种不可递归的锁,即同一线程不能多次获取同一个锁,否则会导致死锁。
1.主要功能和方法
NSLock提供了一些简单的方法来控制锁的获取和释放:
1. -lock:
尝试获取锁。如果锁当前未被占用,调用线程会获得锁,并继续执行。如果锁已被其他线程持有,调用线程会阻塞,直到锁被释放。
[lock lock];
2. -unlock:
释放锁。持有锁的线程在完成对共享资源的操作后,需要调用此方法来释放锁,以便其他线程可以获取锁。
[lock unlock];
3.-tryLock:
尝试获取锁,但与 -lock 方法不同的是,如果锁当前被占用,它不会阻塞线程,而是立即返回 NO。如果获取成功,返回 YES。
if ([lock tryLock]) {
// 获取锁成功
[lock unlock];
} else {
// 锁被占用
}
4. -lockBeforeDate:
尝试在指定的时间之前获取锁。如果在指定时间内获取成功,返回 YES;如果超时返回 NO。
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]) {
// 获取锁成功
[lock unlock];
} else {
// 超时,未获取到锁
}
5.-name和-setName:
NSLock可以给锁指定一个名称,便于调试和日志记录。名称不会影响锁的行为,仅用于识别。
[lock setName:@"MyLock"];
NSLog(@"Lock name: %@", [lock name]);
2.使用场景
NSLock适用于简单的线程同步场景,尤其是当你需要确保某一段代码在多线程环境下不会被并发执行时。比如:
保护共享数据的访问。
控制对临界区的访问,确保同一时间只有一个线程可以执行关键代码。
1.使用示例
下面是一个简单的 `NSLock` 使用示例,展示如何在多线程环境下保护共享数据:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, assign) int counter;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [[NSLock alloc] init];
self.counter = 0;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建两个并发任务,模拟线程竞争
dispatch_async(queue, ^{
[self incrementCounter];
});
dispatch_async(queue, ^{
[self incrementCounter];
});
}
- (void)incrementCounter {
for (int i = 0; i < 10000; i++) {
[self.lock lock]; // 获取锁
self.counter += 1;
[self.lock unlock]; // 释放锁
}
NSLog(@"Final counter value: %d", self.counter);
}
@end
2.优缺点
NSLock适合以下场景:
1.简单的线程同步:如对共享数据的简单保护。
2.临界区保护:确保某一段代码在同一时间只能被一个线程执行。
NSLock的优缺点如下:
1.优点
简单易用:NSLock提供了基础的锁机制,使用起来非常方便,适合简单的同步场景。
轻量级:相比其他更复杂的锁(如递归锁、条件锁),NSLock更轻量,不涉及过多的资源开销。
2. 缺点
不可递归:NSLock是不可递归的锁类型。如果同一个线程多次尝试获取同一个锁,会导致死锁。
缺乏高级功能:相比其他同步机制(如NSCondition、NSRecursiveLock),NSLock不支持条件变量、递归锁等高级功能,功能较为基础。