多线程带来的安全隐患
当多个线程访问同一块资源时,容易引发数据错乱的数据安全问题。除了同步执行之外,我们可以通过锁来进行更细粒度的控制来解决多线程的安全问题。
锁的类型
互斥锁:等待锁释放时线程进入休眠状态,释放CPU资源,等待锁被释放时再被唤醒,适用于等待时间可能较长的情况。常见的互斥锁有信号量、NSLock、os_unfair_lock、@synchronized。
自旋锁:在等待锁释放时会一直占用CPU资源进行循环等待,适用于等待时间短的情况,避免了线程上下文切换的开销。常见的自旋锁:OSSpinLock,在IOS的弱引用表SideTable中有使用到。
iOS中如何使用多线程
- NSThread:创建和管理线程,只用其中几个简单的API,不用这个来创建线程,我们会使用NSOperation和GCD。
- pthread:一套多线程API、跨平台、可移植,使用难度大,几乎不用。
- GCD:旨在替代NSThread等线程技术充分利用CPU,在iOS中使用GCD管理并发任务是一种高效的方式。轻量级、高性能,在需要快速执行简单并发任务时更为便捷。
- NSOperation:提供了一套更加面向对象,易于管理和调试的并发编程解决方案,适合需要更细致控制和状态追踪的复杂应用开发。
GCD
dispatch_sync
同步执行,不论在哪种队列(并发、串行)都不会开启新线程,是串行执行任务。
dispatc_async
- 并发队列:开启新线程、并发执行
- 串行队列:开启新线程、串行执行
- 主队列:不会开启新线程,串行执行
同步异步的区别:同步会阻塞当前线程,直到任务执行完毕。异步不会阻塞当前线程,多个异步任务的执行顺序是不确定的。
问题一:同步并行是否等价于异步串行?
答案:不等同。同步执行是不会开启新线程的。异步串行并不一定是按顺序执行的,它是在不同线程中执行,只能说任务派发的时候是按照顺序派发,但是到不同线程中的执行开始时间是不可控制的。
static int num = 0;
static int num1 = 0;
for (int i = 0; i < 20; i++) {
dispatch_async(dispatch_queue_create("my_serial_queue", 0), ^{
num++;
NSLog(@"async thread: %@, num: %d", [NSThread currentThread], num);
});
}
for (int i = 0; i < 20; i++) {
dispatch_sync(dispatch_queue_create("my_concurrent_queue", 0), ^{
num1++;
NSLog(@"sync thread: %@, num: %d", [NSThread currentThread], num1);
});
}
运行结果:
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 1
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 2
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 3
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 4
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 5
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 6
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 7
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 8
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 9
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 10
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 11
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 12
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 13
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 14
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 15
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 16
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 17
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 18
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 19
sync thread: <NSThread: 0x600001733a00>{number = 7, name = (null)}, num: 20
async thread: <NSThread: 0x60000175c4c0>{number = 5, name = (null)}, num: 1
async thread: <NSThread: 0x6000017341c0>{number = 9, name = (null)}, num: 3
async thread: <NSThread: 0x60000175c4c0>{number = 5, name = (null)}, num: 9
async thread: <NSThread: 0x600001720940>{number = 11, name = (null)}, num: 7
async thread: <NSThread: 0x60000172fc40>{number = 3, name = (null)}, num: 6
async thread: <NSThread: 0x600001720840>{number = 8, name = (null)}, num: 2
async thread: <NSThread: 0x6000017705c0>{number = 12, name = (null)}, num: 9
async thread: <NSThread: 0x600001770540>{number = 10, name = (null)}, num: 4
async thread: <NSThread: 0x60000175c000>{number = 20, name = (null)}, num: 18
async thread: <NSThread: 0x6000017341c0>{number = 9, name = (null)}, num: 12
async thread: <NSThread: 0x600001708c80>{number = 17, name = (null)}, num: 15
async thread: <NSThread: 0x600001734240>{number = 16, name = (null)}, num: 14
async thread: <NSThread: 0x6000017706c0>{number = 21, name = (null)}, num: 20
async thread: <NSThread: 0x600001764040>{number = 13, name = (null)}, num: 10
async thread: <NSThread: 0x600001738300>{number = 18, name = (null)}, num: 16
async thread: <NSThread: 0x600001708c00>{number = 22, name = (null)}, num: 20
async thread: <NSThread: 0x600001722980>{number = 19, name = (null)}, num: 17
async thread: <NSThread: 0x60000175c2c0>{number = 14, name = (null)}, num: 12
async thread: <NSThread: 0x600001720800>{number = 4, name = (null)}, num: 5
async thread: <NSThread: 0x600001770640>{number = 15, name = (null)}, num: 13
demo中测试例子会发现异步串行的执行顺序不是预期中的顺序。
dispatch_barrier
异步执行两组操作,第二组一定会在第一组全部执行后执行,不适用于全局并发队列,并且需要保证执行的任务在同一个并发队列中。
// 创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("com.example.myqueue", DISPATCH_QUEUE_CONCURRENT);
// 提交一些并发任务
for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
// 模拟耗时操作
NSLog(@"Task %d is running", i);
sleep(1); // 休眠1秒
NSLog(@"Task %d is finished", i);
});
}
// 使用 dispatch_barrier 提交一个屏障任务
// 这个任务会在所有之前的并发任务完成后执行
dispatch_barrier_async(queue, ^{
// 这个任务会同步执行,此时不会有其他任务并发执行
NSLog(@"Barrier task is running");
// 执行一些需要同步的操作
NSLog(@"Barrier task is finished");
});
// 继续提交一些并发任务
for (int i = 5; i < 10; i++) {
dispatch_async(queue, ^{
// 模拟耗时操作
NSLog(@"Task %d is running", i);
usleep(100000); // 休眠100毫秒
NSLog(@"Task %d is finished", i);
});
}
dispatch_after
在指定时间后(大概时间)执行某个任务
dispatch_group_notify
分别执行多个耗时操作,当多个耗时任务执行完毕后触发dispatch_group_notify
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < 10; i++) {
dispatch_group_async(group, concurrent_queue, ^{
NSLog(@"dispatch group task %d 开始执行 %@", i, [NSThread currentThread]);
sleep(1);
NSLog(@"dispatch group task %d 完成执行 %@", i, [NSThread currentThread]);
});
}
dispatch_group_notify(group, concurrent_queue, ^{
NSLog(@"所有group任务执行完成");
});
dispatch_apply
快速迭代函数,按照指定的次数将指定的任务追加到指定的队列中,并等待全部任务执行结束。不会立即返回,是同步的调用。系统会根据实际情况分配和管理线程。
NSLog(@"dispatch_apply 开始");
// 不会立即返回,在执行完毕后才会返回,是同步的调用。
dispatch_apply(10, concurrent_queue, ^(size_t iteration) {
NSLog(@"dispatch_apply task %zu 开始执行 %@", iteration, [NSThread currentThread]);
sleep(1);
NSLog(@"dispatch_apply task %zu 完成执行 %@", iteration, [NSThread currentThread]);
});
NSLog(@"dispatch_apply 结束");
锁
dispatch_semaphore
通过一个计数器来控制访问资源的线程数量,当计数器大于0时,线程可以访问资源并将技数器减1,当计数器为0时,线程则等待直到计数器大于0.
通过信号量阻塞当前线程来达到不超过3个并发任务的效果。
dispatch_queue_t concurrent_queue = dispatch_queue_create("my_demo_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serial_queue = dispatch_queue_create("my_demo_queue_serial", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
for (NSInteger i = 0 ; i < 10; i++) {
dispatch_async(serial_queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(concurrent_queue, ^{
NSLog(@"当前线程%@,开始执行任务%d", [NSThread currentThread], (int)i);
sleep(1);
NSLog(@"当前线程%@,结束执行任务%d", [NSThread currentThread], (int)i);
dispatch_semaphore_signal(semaphore);
});
});
}
NSLog(@"主线程");
NSLock
NSLock、NSRecursiveLock、NSConditionLock:NSLock系列是对mutex锁的封装,遵守NSLocking协议,第一个是普通锁,第二个是递归锁,允许同一个线程多次加锁,不会死锁,第三个是条件锁。NSRecursiveLock虽然有递归性,但是不支持多线程的可递归。
NSLock *my_lock = [[NSLock alloc] init];
NSMutableArray *testArray = [NSMutableArray array];
for (NSInteger i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[my_lock lock];
[testArray addObject:[NSNumber numberWithInteger:i]];
[my_lock unlock];
});
}
@synchoronized
创建一个互斥锁,以保护代码块避免并发访问。也就是确保两个线程会连续地访问临界区的代码。它是对mutex递归锁的封装,需要加锁的是同一个对象才生效。适用于递归和多线程。自动根据传入对象创建一个与之相关的锁,在代码块开始的时候加锁,结束的时候自动解锁。
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 静态局部变量:在函数内部定义,只在首次调用函数时初始化一次,之后函数调用时其值会保留。作用域仍在定义它的函数内部。
static void (^myMethod)(int);
myMethod = ^(int value) {
// NSRecursiveLock虽然有递归性,但是不支持多线程的可递归,只运行一次就崩溃了
// @synchronized 符合递归和多线程特性的
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d", value);
myMethod(value - 1);
}
}
};
myMethod(10);
});
}
OSSpinLock
自旋锁,当获取锁之前一直处于 忙等阻塞状态。
os_unfair_lock
互斥锁,等待的线程会处于休眠,一个低等级的锁,atomic就是用了这个锁
pthread_mutex
互斥锁,等待锁的线程会处于休眠,可以避免死锁、性能更佳
NSOperation
- 可以使用addDependency指定任务之间的依赖
- 支持取消操作,支持设置任务的优先级
- 提供了丰富的状态追踪,包括isReady、isExcuting、isFinished等属性,使得追踪操作的生命周期变得容易。
- 可以被继承和封装,可以创建自定义操作类来封装特定任务的逻辑和数据,提高了代码的复用性和模块化。
通过自定义NSOperation的子类实现一个串行的请求发送,需要一个请求回来后再执行另外一个请求。
1.创建NSOperation的子类,通过信号量阻塞当前线程
// 封装一个能支持异步操作的 Operation 类
#import "PHXOperation.h"
@interface PHXOperation ()
@property (nonatomic, strong) NSURLSessionDataTask *task;
@end
@implementation PHXOperation
- (void)main {
// 这个方法需要阻塞,等到请求回来后才能继续往下走,才能达到控制请求回来后继续往下执行下一个operation的效果
@autoreleasepool {
if (self.isCancelled) return;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSLog(@"start %@ current thread:%@", self.number, [NSThread currentThread]);
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"completed %@%@", self.number, error);
dispatch_semaphore_signal(sema);
}];
[self.task resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
}
@end
2.创建一个NSOperationQueue,添加并发任务
- (void)handleRequest {
self.queue = [[NSOperationQueue alloc] init];
// self.queue.maxConcurrentOperationCount = 1;
self.array = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
[self createTaskWithI:@(i)];
}
}
3.创建并发任务并指定任务之间的优先级
- (void)createTaskWithI: (NSNumber *)i {
PHXOperation * task = [[PHXOperation alloc] init];
task.number = i;
[self.array addObject:task];
if (self.array.count > 1) {
NSInteger index = [self.array indexOfObject:task];
PHXOperation *preTask = self.array[index-1];
// 设置执行依赖
// 前一个任务执行后再执行下一个任务,可以通过设置依赖
[task addDependency:preTask];
// 设置优先级 优先级高的任务,调用的几率会更大。
// [task setQueuePriority:NSOperationQueuePriorityHigh];
}
if (self.array.count >= 5) {
[self.array removeAllObjects];
}
[self.queue addOperation:task];
// self.queue.maxConcurrentOperationCount = 1; // 设置并发度为1
}
执行结果如下:
start 0 current thread:<NSThread: 0x600001768280>{number = 4, name = (null)}
completed 0(null)
start 1 current thread:<NSThread: 0x600001768280>{number = 4, name = (null)}
completed 1(null)
start 2 current thread:<NSThread: 0x60000175c180>{number = 8, name = (null)}
completed 2(null)
start 3 current thread:<NSThread: 0x60000174df00>{number = 6, name = (null)}
completed 3(null)
start 4 current thread:<NSThread: 0x60000174df00>{number = 6, name = (null)}
completed 4(null)
NSBlockOperation:
通过[NSBlockOperation blockOperationWithBlock]并调用start可以在当前线程启动一个任务。
NSLog(@"当前线程:%@", [NSThread currentThread]);
// 在当前线程启动
NSBlockOperation *blockOp1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前Block1线程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3];
NSLog(@"当前Block1线程结束:%@", [NSThread currentThread]);
}];
// 开启一个新线程处理任务
[blockOp1 addExecutionBlock:^{
NSLog(@"当前Block1的附加线程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3];
NSLog(@"当前Block1的附加线程结束:%@", [NSThread currentThread]);
}];
[blockOp1 start];
从日志中可以看到 通过addExectionBlock方式可以开启一个新线程来处理任务。
当前线程:<_NSMainThread: 0x60000170c080>{number = 1, name = main}
当前Block1线程:<_NSMainThread: 0x60000170c080>{number = 1, name = main}
当前Block1的附加线程:<NSThread: 0x60000173a6c0>{number = 4, name = (null)}
当前Block1线程结束:<_NSMainThread: 0x60000170c080>{number = 1, name = main}
当前Block1的附加线程结束:<NSThread: 0x60000173a6c0>{number = 4, name = (null)}
直接调用start是在当前线程中执行,如果希望到其他线程执行,需要加入到队列中,让系统去调度执行
NSBlockOperation *blockOp2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"当前Block2线程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:5];
NSLog(@"当前Block2线程结束:%@", [NSThread currentThread]);
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOp2];
NSLog(@"blockOp2 start end");
blockOp2 start end
当前Block2线程:<NSThread: 0x60000175a000>{number = 6, name = (null)}
当前Block2线程结束:<NSThread: 0x60000175a000>{number = 6, name = (null)}
NSInvocationOperation:
假设已经有一个Engine类里面有个longrunningMethod异步任务
@implementation Engine
- (void)longRunningMethod
{
NSLog(@"Long running method is working:%@", [NSThread currentThread]);
sleep(3);
NSLog(@"long running method completed");
}
@end
可以通过NSInvocationOperation将这个任务放到异步线程中处理
Engine *obj = [[Engine alloc] init];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:obj selector:@selector(longRunningMethod) object:nil];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 直接调用是在当前线程,只有加入队列后由系统分配线程执行任务
// [op start];
// 加入队列后会开启线程处理任务,由系统决定是哪个线程处理任务
[queue addOperation:op];
[queue addOperationWithBlock:^{
NSLog(@"anthor invocation op: %@-%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
}];
NSLog(@"test:%@",[NSThread currentThread]);
执行结果如下:
test:<_NSMainThread: 0x60000170c000>{number = 1, name = main}
Long running method is working:<NSThread: 0x6000017689c0>{number = 6, name = (null)}
anthor invocation op: <NSOperationQueue: 0x109506db0>{name = 'NSOperationQueue 0x109506db0'}-<NSThread: 0x6000017684c0>{number = 5, name = (null)}
long running method completed
在iOS开发中如何避免常见的线程安全问题
使用原子操作
对于简单的读写操作,可以使用@property的修饰属性atomic来保证操作的原子性,防止读写过程中数据被其他线程干扰。
加锁机制
通过信号量、NSLock、@sychronized来保护共享资源的访问,确保同一时间只有一个线程可以访问资源。
Dispatch Barriers
在并发任务中,可以使用dispatch_barrier_async来确保某个任务在所有之前提交的任务完成后才开始执行,并且在它执行期间不会有其他任务开始(关键),适用于读多写少的场景。
避免全局变量
尽量减少全局变量的使用,特别是那些会被多个线程修改的变量,转而使用局部变量或者参数传递的方式。
Copy而非Strong引用
对于不可变数据类型(NSString、NSArray)在多个线程间传递采用copy属性而非strong,确保每个线程有自己的副本,避免无意间被修改影响其他线程。