【iOS】多线程 & 锁问题总结

前言

iOS 锁和多线程的总结

1. 你理解的多线程

多线程是同时执行多个线程(子任务)的能力,用于提高程序性能和响应性。它允许在一个程序中并发地处理多个任务。

  • 并发:在一个时间段多个线程同时进行,计算机通过切换不同的线程实现多线程任务。

优点

  1. 大大提高了程序的运行速度。
  2. 使用线程可以把占据时间较长的任务放到后面去处理,从而提升用户的体验。

缺点

  1. 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
  2. 更多的线程需要更多的内存空间。

2. atomic 和 nonatomic 的区别及其作用

  1. atomic原子操作:加锁,保证setter和getter存取方法的线程安全(仅仅对setter和getter方法加锁)。因为线程加锁,别的线程访问当前属性的时候会先执行完属性当前的操作。
  • 对同一对象的set和get的操作是顺序执行的。
  • 速度不快,因为要保证操作整体完成。
  • 线程安全,需要消耗大量系统资源为属性加锁。
  • 使用atomic并不能保证绝对的线程安全,因为atomic仅仅是对系统生成的的settergetter方法加锁, 对于绝对保证线程安全的操作,需要使用更高级的方式处理,NSSpinLock, @syncronized 锁保证线程安全。
  1. nanatomic非原子操作,不加锁,线程执行快,但是多个线程访问同一个属性可能产生crash。
  • 不是默认的
  • 速度更快,如果有两个线程访问同一个属性可能造成crash。
  • 非线程安全
  1. atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样
  • atomic系统自动生成的getter/setter方法会进行加锁操作。
  • nonatomic系统自动生成的getter/setter方法不会进行加锁操作。

⚠️:atomic修饰的属性,系统生成的 getter/setter 会保证 getset 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。

3. GCD的队列类型 - 三种队列类型

  1. The main queue(主线程串行队列)与主线程功能相同,提交到main queue的任务会在主线程中执行。
dispatch_get_main_queue() 来获取
  1. Global queue(全局并发队列) 全局并发队列由整个进程共享,有 高 中(默认是中) 低和后台四个优先级别。
dispatch_get_global_queue() 可以设置优先级
  1. Custom queue(自定义队列) 可以串行,也可以并发。
dispatch_queue_create()

4. GCD的死锁问题

线程死锁的四个必要条件

  • 互斥一个资源每次都只能被一个进程占用。
  • 占有且等待一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  • 不可抢占别人已经占有了某项资源,你不能因为自己需要资源而去抢占其他资源。
  • 循环等待存在一个进程链,使得每个进程都占有下一个进程所需要的至少一种资源。

概念:所谓死锁,通常是两个线程A和B都卡住了,A在等B,B在等A,互相等待到值死锁。

  1. .主线程串行队列同步执行任务,在主线程运行时,会产生死锁
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
    
    
    NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3

分析:

  • dispatch_sync表示是一个同步线程;
  • dispatch_get_main_queue表示运行在主线程中的主队列;
  • 任务2是同步线程的任务。
  • 任务3需要等待任务2结束之后再执行.

为什么造成死锁?

  1. 首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是主队列,是一个特殊的串行队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面

任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。

请添加图片描述

  1. 同步异步互相嵌套
// 同步 + 异步 互相嵌套产生死锁
- (void)sync_async {
    
    
    dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1"); // 任务1
    dispatch_async(queue, ^{
    
    
        NSLog(@"2"); // 任务2
        dispatch_sync(queue, ^{
    
    
            NSLog(@"3"); // 任务3
        });
        NSLog(@"4"); // 任务4
    });
    NSLog(@"5"); // 任务5
}

请添加图片描述
分析:首先通过自定义队列创建了dispatch_queue_create函数创建了一个DISPATCH_QUEUE_SERIAL的串行队列。

  1. 执行任务1.
  2. 遇到异步线程,将【任务2、同步线程、任务4】加入串行队列中。因为是异步线程,所以在主线程中的任务5不必等待异步线程中的所有任务完成;
  3. 因为任务5不必等待,所以2和5的输出顺序不能确定;
  4. 任务2执行完以后,遇到同步线程,这时,将任务3加入串行队列;
  5. 又因为任务4比任务3早加入串行队列,所以,任务3要等待任务4完成以后,才能执行。但是任务3所在的同步线程会阻塞,所以任务4必须等任务3执行完以后再执行。这就又陷入了无限的等待中,造成死锁。
    在这里插入图片描述
    主线程无限循环
- (void)async_loop {
    
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
    
        NSLog(@"1"); // 任务1
        dispatch_sync(dispatch_get_main_queue(), ^{
    
    
            NSLog(@"2"); // 任务2
        });
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
    while (1) {
    
    
    }
    NSLog(@"5"); // 任务5
    
    // a打印 4 1 / 1 4 顺序不定   
}

打印结果: 4 1 / 1 4 顺序不定

分析:

  • 先来看看都有哪些任务加入了Main Queue:【异步线程、任务4、死循环、任务5】。
  • 在加入到Global Queue异步线程中的任务有:【任务1、同步线程、任务3】。
  • 第一个就是异步线程,任务4不用等待,所以结果任务1和任务4顺序不一定。
  • 任务4完成后,程序进入死循环,Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,继续执行任务1后面的同步线程。
  • 同步线程中,将任务2加入到了主线程,并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。

最终,只能得到1和4顺序不定的结果。

5. 多线程之间的区别和联系

在这里插入图片描述

GCD和NSOperation

  • GCD的执行效率更高,执行的是由Block构成的任务,是一个轻量级的数据结构,写起来更加方便
  • GCD只支持FIFO队列,NSOperationQueue可以通过设置最大并发数、设置优先级、添加依赖关系来调整执行顺序
  • NSOperation可以跨越队列设置依赖关系,GCD仅仅能通过栅栏等方法才能控制执行顺序
  • NSOperation更加面向对象,支持KVO,也可以通过继承等关系添加子类。
  • 所以如果我们需要考虑异步操作之间的顺序行、依赖关系,比如多线程并发下载等等,就使用NSOperation

GCD 与 NSThread 的区别

  • NSThread 通过 @selector 指定要执行的方法,代码分散, 依靠的是NSObject的分类实现的线程之间的通讯,如果要开线程必须创建多个线程对象。经常只用的是[NSTread current] 查看当前的线程。
  • NSThread是一个控制线程执行的对象,它不如NSOperation抽象,通过它我们可以方便的得到一个线程,并控制它。但NSThread的线程之间的并发控制,是需要我们自己来控制的,可以通过NSCondition实现。
  • GCD 通过 block 指定要执行的代码,代码集中, 所有的代码写在一起的,让代码更加简单,易于阅读和维护,不需要管理线程的创建/销毁/复用的过程!程序员不用关心线程的生命周期

6. 进程和线程?

参考:进程和线程的概念、区别及进程线程间通信
1. 基本概念:

  • 进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程是进程的子任务,是CPU调度和分配的基本单位,用于保障程序执行的实时性,实现进程内部的并发。线程是操作系统可以识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器等,每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

2. 区别:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
  • 进程间不会相互影响 ;线程:一个线程挂掉将导致整个进程挂掉
  • 线程之间的通信更加方便,同一个进程下线程共享全局变量静态变量等数据。

3. 通信方式

进程间的通信方式
  1. 进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
  2. 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
线程间的通信方式
  1. 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
  2. 互斥量Synchronized/Lock采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
  3. 信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
  4. 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作进程间通信的方式:

6. iOS的线程安全手段如何保证

参考:iOS中有哪些技术可以保证线程安全?
问:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。此时,我们需要用线程锁来解决。

线程数据安全的方法:

  1. natomic原子操作:使用atomic多线程原子性控制,atomic的原理给setter加上锁,getter不会加锁。
  2. 使用GCD实现atomic操作:给某字段的setter方法和getter方法加上同步队列;
- (void)setCount:(NSInteger)newcount
{
    
    
    dispatch_sync(_synQueue, ^{
    
    
         count = newcount;
    });
}
- (NSInteger)count
{
    
    
     __block NSInteger localCount;
     dispatch_sync(_synQueue, ^{
    
    
          localCount = count;
     });
     return localCount;
}
  • 互斥锁能够有效的防止因多线程抢夺资源造成的数据安全问题,但是需要消耗大量的CPU资源。
  1. 互斥锁: 使用互斥锁可以确保同一时间只有一个线程访问共享资源。例如@synchronized创建互斥锁
@synchronized (self) {
    
    
    // 访问共享资源的代码
}

  1. 自旋锁:自旋锁(Spin Lock):自旋锁一种忙等待的锁,它会不断地尝试获取锁,直到成功为止。在Objective-C中,可以使用os_unfair_lock来创建自旋锁。
  2. 信号量(Semaphore):信量是一种数器,用于控制同时访问某个资源的线程数量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源的代码
dispatch_semaphore_signal(semaphore);

  1. 串行队列:串行队列(Serial Queue):使用串行队列可以确保任务按顺序执行,从而避多个线程同时访问共享资源。可以使用GCD(Grand Central Dispatch)来创建串行队列。
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    
    
    // 访问共享资源的代码
});

猜你喜欢

转载自blog.csdn.net/weixin_61639290/article/details/132011165