多线程相关知识

多线程带来的安全隐患

当多个线程访问同一块资源时,容易引发数据错乱的数据安全问题。除了同步执行之外,我们可以通过锁来进行更细粒度的控制来解决多线程的安全问题。

锁的类型

互斥锁:等待锁释放时线程进入休眠状态,释放CPU资源,等待锁被释放时再被唤醒,适用于等待时间可能较长的情况。常见的互斥锁有信号量、NSLock、os_unfair_lock、@synchronized。

自旋锁:在等待锁释放时会一直占用CPU资源进行循环等待,适用于等待时间短的情况,避免了线程上下文切换的开销。常见的自旋锁:OSSpinLock,在IOS的弱引用表SideTable中有使用到。

iOS中如何使用多线程

  1. NSThread:创建和管理线程,只用其中几个简单的API,不用这个来创建线程,我们会使用NSOperation和GCD。
  2. pthread:一套多线程API、跨平台、可移植,使用难度大,几乎不用。
  3. GCD:旨在替代NSThread等线程技术充分利用CPU,在iOS中使用GCD管理并发任务是一种高效的方式。轻量级、高性能,在需要快速执行简单并发任务时更为便捷。
  4. 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

  1. 可以使用addDependency指定任务之间的依赖
  2. 支持取消操作,支持设置任务的优先级
  3. 提供了丰富的状态追踪,包括isReady、isExcuting、isFinished等属性,使得追踪操作的生命周期变得容易。
  4. 可以被继承和封装,可以创建自定义操作类来封装特定任务的逻辑和数据,提高了代码的复用性和模块化。

通过自定义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,确保每个线程有自己的副本,避免无意间被修改影响其他线程。

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值