IOS面试题——OC中多线程实现与线程安全(10)

一 面试题汇总

  1. iOS多线程方案有哪些?如何选择?有什么区别?
  2. 串行队列,并行队列的区别?全局队列和主队列呢?
  3. 同步任务和异步任务的区别?
  4. 使用sync函数往当前串行队列中添加任务会发生什么现象?
  5. 异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3怎么实现?
  6. Group,dispatch_barrier_asyncdispatch_semaphore分别用来做什么?
  7. 多线程安全问题有哪些?如何解决
  8. 自旋锁和互斥锁的区别?递归锁,条件锁是什么?
  9. atomic,noatomic的区别?
  10. iOS读写安全方案有哪些?读写锁pthread_rwlock,栅栏函数
  11. dispatch_barrier_async 如果传入的是一个串行或是一个全局的并发队列会发生什么现象?

二 面试题解答(仅供参考)

2.1 iOS多线程方案有哪些?如何选择?有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在iOS开发中,有多种多线程方案可供选择。以下是其中一些主要的方案:

1-Grand Central Dispatch (GCD):

GCD 是苹果提供的一套用于管理应用程序中的并发任务执行的技术。
它使用队列(dispatch queue)来管理任务的执行,包括串行队列和并发队列。
GCD 提供了一种简单易用的方式来利用多核处理器,并提高应用程序的性能。
优点:易于使用,自动管理线程池和任务调度,适合绝大多数情况。
缺点:不够灵活,无法取消已经提交的任务。

2-Operation Queue:

Operation Queue 是建立在 GCD 之上的抽象,
它提供了对操作(Operation)和操作队列(Operation Queue)的高级控制。
操作可以是任何继承自 NSOperation 的对象,它们可以按照依赖关系来执行。
优点:提供了更高级别的任务控制,包括依赖关系、优先级和取消操作等。
缺点:相对于 GCD 略显复杂,对于简单的并发任务可能过于繁琐。

3-pthread:

POSIX 线程(pthread)是一种用于多线程编程的标准。在iOS中,你可以直接使用 pthread 库来创建和管理线程。
优点:更接近底层,提供了更细粒度的控制。
缺点:相比 GCD 和 Operation Queue 更为复杂,需要手动管理线程的创建、销毁和同步。

2.2 串行队列,并行队列的区别?全局队列和主队列呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在 Grand Central Dispatch (GCD) 中,有四种主要的队列类型:串行队列、并行队列、全局队列和主队列。

1-串行队列 (Serial Queue):

串行队列中的任务按照先进先出的顺序依次执行,每次只有一个任务在执行,后一个任务等待前一个任务执行完毕后才开始执行。
串行队列通常用于确保任务按照特定的顺序执行,或者避免并发访问共享资源。

2-并行队列 (Concurrent Queue):

并行队列中的任务可以同时执行,系统会根据可用的处理器核心数量来决定同时执行的任务数量,但任务的执行顺序是不确定的。
并行队列通常用于同时执行独立的、不需要特定顺序的任务,以提高性能。

3-全局队列 (Global Queue):

全局队列是由系统提供的并行队列,有四个优先级:高、默认、低、后台。你可以使用系统提供的函数将任务添加到全局队列中,并指定优先级。
全局队列适用于那些不需要自定义队列的情况,只需要简单地将任务添加到队列中执行。

4-主队列 (Main Queue):

主队列是一个特殊的串行队列,用于在主线程上执行任务。主队列通常用于更新 UI、处理用户交互等与界面相关的任务。
将任务提交到主队列中确保这些任务在主线程上按顺序执行,避免了并发访问 UI 的问题。

2.3 同步任务和异步任务的区别?

1
2
3
4
5
6
7
8
9
10
11
12
同步任务和异步任务是指在多线程编程中,任务执行的方式不同的两种情况。

1-同步任务 (Synchronous Task):

在同步任务中,任务会阻塞当前线程的执行,直到任务完成才会继续执行后续的代码。
调用同步任务的方法会等待任务执行完成后返回结果,这意味着在执行同步任务的线程中,任务的执行是顺序的,不会并发执行。

2-异步任务 (Asynchronous Task):

在异步任务中,任务会在后台执行,而不会阻塞当前线程的执行。
调用异步任务的方法会立即返回,不等待任务执行完成。
异步任务的执行通常会创建新的线程或利用线程池来执行,从而实现并发执行多个任务。

2.4 使用sync函数往当前串行队列中添加任务会发生什么现象?

1
2
3
4
5
6
7
8
9
10
11
12
当你使用 sync 函数往当前串行队列中添加任务时,会发生下面的情况:

1-任务添加行为:任务会被添加到当前串行队列的队列中,并且会立即执行。
由于是同步添加,调用 sync 函数的线程会等待任务执行完成后再继续执行后续的代码。

2-死锁风险:由于 sync 是同步添加任务的操作,如果当前串行队列是当前线程所在的串行队列,
那么当你尝试往该队列中添加任务时,会发生死锁。
因为调用 sync 的线程会等待任务执行完成,而任务需要等待调用 sync 的线程释放当前队列才能执行,两者相互等待导致死锁。

总之,使用 sync 函数往当前串行队列中添加任务会导致任务立即执行
,但需要注意死锁的风险,尤其是在当前线程已经在该串行队列中执行任务时。
因此,一般不建议在当前串行队列中使用 sync 添加任务,以避免死锁问题

2.5 异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3怎么实现?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建一个并行队列
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

// 异步并发执行任务1和任务2
concurrentQueue.async {
// 任务1
print("Task 1 started")
// 模拟耗时操作
Thread.sleep(forTimeInterval: 2)
print("Task 1 finished")
}

concurrentQueue.async {
// 任务2
print("Task 2 started")
// 模拟耗时操作
Thread.sleep(forTimeInterval: 3)
print("Task 2 finished")
}

// 回到主线程执行任务3
DispatchQueue.main.async {
// 任务3
print("Task 3 started")
// 模拟耗时操作
Thread.sleep(forTimeInterval: 1)
print("Task 3 finished")
}

2.6 Group,dispatch_barrier_asyncdispatch_semaphore分别用来做什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在 GCD 中,Group、dispatch_barrier_async 和 dispatch_semaphore 分别用于不同的目的。

1-Dispatch Group:

Dispatch Group 用于管理一组任务的执行,并在所有任务完成后执行额外的代码。
你可以使用 Dispatch Group 来异步执行多个任务,并在它们全部完成后执行一些操作,比如通知用户或者更新 UI。
你可以使用 dispatch_group_async 将任务添加到 Dispatch Group 中,然后使用 dispatch_group_notify 来指定当所有任务完成时执行的代码块。

2-dispatch_barrier_async:

dispatch_barrier_async 用于在并行队列中插入一个栅栏,保证在它之前添加的任务都执行完成后,再执行它之后添加的任务。
这个函数主要用于同步访问共享资源,比如读写文件或者数据库。
当你需要在并行队列中执行一系列任务,但是其中某些任务必须按顺序执行或者需要互斥访问共享资源时,
可以使用 dispatch_barrier_async。

3-dispatch_semaphore:

dispatch_semaphore 是一个计数信号量,用于控制同时访问资源的最大线程数。
它可以实现简单的线程同步和互斥,也可以用于等待异步操作完成。
你可以使用 dispatch_semaphore_wait 来等待信号量减少到指定值,使用 dispatch_semaphore_signal 来增加信号量。
简单总结一下它们的用途:

Dispatch Group 用于管理一组任务的执行并在所有任务完成后执行额外的代码。
1-dispatch_barrier_async 用于在并行队列中创建一个栅栏,控制任务的执行顺序或者同步访问共享资源。
2-dispatch_semaphore 用于实现简单的线程同步和互斥,以及控制同时访问资源的最大线程数。

2.7 多线程安全问题有哪些?如何解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
多线程安全问题是指在多线程环境下,由于多个线程同时访问共享资源而可能引发的问题。常见的多线程安全问题包括:

1-竞态条件(Race Condition):多个线程同时竞争对共享资源进行读写操作,导致结果依赖于线程执行的顺序或者时间。

2-数据竞争(Data Race):多个线程同时访问共享的可变数据,其中至少一个线程修改了数据,而其他线程可能读取到了不一致或者未定义的值。

3-死锁(Deadlock):多个线程因为互相等待对方释放资源而导致无法继续执行的情况。

4-活锁(Livelock):多个线程因为相互响应而无法继续执行的情况,类似于死锁但是线程会一直重试。

为了解决多线程安全问题,可以采取以下方法:

1-互斥锁(Mutex):使用互斥锁来保护共享资源,确保在任何时刻只有一个线程可以访问资源。常见的互斥锁有 NSLock、pthread_mutex 等。

2-信号量(Semaphore):使用信号量来控制同时访问资源的最大线程数,以及实现线程间的同步。可以使用 dispatch_semaphore 来实现信号量。

3-读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但是只有一个线程可以写入资源。
这可以提高并发性能。在 iOS 开发中可以使用 pthread_rwlock 或者 GCD 中的读写队列来实现。

4-串行队列(Serial Queue):将需要同步访问的代码放在串行队列中执行,确保这些任务按顺序执行,避免竞态条件。

5-原子操作(Atomic Operation):使用原子操作来保证对共享资源的读写是原子性的,不会被中断。
在 Objective-C 中,可以使用 @synchronized 块来实现原子操作。

6-避免死锁:设计良好的锁定顺序,避免出现循环等待的情况,以预防死锁的发生。

7-线程安全的数据结构:使用线程安全的数据结构,比如 NSArray 的 threadSafe、NSDictionary 的 threadSafe、NSCache 的 threadSafe 等,
它们内部已经处理了线程安全的问题。

通过合理地选择适当的同步机制和数据结构,以及遵循良好的编程实践,
可以有效地解决多线程安全问题,确保应用程序在多线程环境下的稳定性和可靠性。

2.8 自旋锁和互斥锁的区别?递归锁,条件锁是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
自旋锁(Spin Lock)和互斥锁(Mutex Lock)是多线程编程中常用的同步机制,它们都用于保护临界区,防止多个线程同时访问共享资源。

1-自旋锁(Spin Lock):

当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,该线程不会被阻塞,而是一直循环等待,不停地尝试获取锁,直到锁被释放。
自旋锁适用于保护临界区很小且短时间内能够释放的情况,因为自旋期间线程会一直占用 CPU 资源,
如果临界区很大或者锁被持有时间较长,则不适合使用自旋锁,因为会浪费大量的 CPU 时间。

2-互斥锁(Mutex Lock):

当一个线程尝试获取互斥锁时,如果锁已被其他线程占用,则该线程会被阻塞,直到锁被释放。
互斥锁适用于保护临界区较大或者锁被持有时间较长的情况,因为线程在等待锁期间不会占用 CPU 资源。
递归锁(Recursive Lock):

递归锁是一种特殊的互斥锁,允许同一线程多次获取同一把锁。每次成功获取锁后,
该锁的计数器会递增,当计数器为零时才释放锁。
递归锁在某些情况下可以简化代码实现,但需要注意避免死锁。
条件锁(Condition Lock):

条件锁是一种同步机制,用于线程间的等待和通知。它与互斥锁结合使用,
当某个条件不满足时,线程可以等待条件满足时再继续执行。
条件锁通常和条件变量一起使用,条件变量用于在等待和通知之间传递消息。
总结:

自旋锁和互斥锁都用于保护临界区,但自旋锁在获取失败时会循环等待,而互斥锁会阻塞等待。
递归锁允许同一线程多次获取同一把锁,条件锁用于线程间的等待和通知

2.9 atomic,noatomic的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在并发编程中,原子操作(Atomic Operation)和非原子操作(Non-Atomic Operation)之间有很大的区别。

1-原子操作(Atomic Operation):

原子操作是不可中断的单个操作,要么完全执行,要么完全不执行,没有中间状态。
原子操作通常由硬件或者特定的CPU指令来支持,保证了在多线程环境下对共享数据的操作是线程安全的,不会发生数据竞争或者数据不一致的情况。
常见的原子操作包括原子增减、原子赋值、原子比较交换等。

2-非原子操作(Non-Atomic Operation):

非原子操作是指可能被中断或者被其他线程干扰的操作,没有原子性保证。
在多线程环境下,如果多个线程同时对共享数据进行非原子操作,
可能会导致数据竞争、数据不一致等问题,需要额外的同步机制来保证线程安全。
例如,在多线程环境下对一个整数进行简单的加法操作,如果没有使用原子操作或者其他同步机制,可能会出现竞争条件,导致结果不确定。
总的来说,原子操作保证了对共享数据的操作是线程安全的,不需要额外的同步机制;而非原子操作则需要通过锁、互斥量等同步机制来保证线程安全。

2.10 iOS读写安全方案有哪些?读写锁pthread_rwlock,栅栏函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在iOS开发中,确保数据的读写安全是非常重要的。以下是一些常用的读写安全方案:

1-使用读写锁(pthread_rwlock):

pthread_rwlock 是 POSIX 线程库中的读写锁实现,在多线程环境下可以提供更细粒度的读写访问控制。
读写锁允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。
在iOS开发中,可以使用 pthread_rwlock 来保护共享数据的读写操作,以提高性能和并发度。

2-使用栅栏函数(dispatch_barrier_async):

GCD(Grand Central Dispatch)提供了栅栏函数 dispatch_barrier_async,
可以用于创建同步点,确保在栅栏之前的任务全部完成后才执行栅栏之后的任务。
栅栏函数适用于异步执行的任务,可以用于保护并发队列中的共享数据。
通过在读取和写入共享数据的任务之间插入栅栏,可以确保读写操作的顺序和一致性,避免竞态条件和数据不一致的问题。
除了上述方法,还有其他一些读写安全的方案,例如使用串行队列、信号量等同步机制,或者使用原子操作来操作共享数据。
在选择合适的方案时,需要根据具体的场景和需求来决定。

2.11 dispatch_barrier_async 如果传入的是一个串行或是一个全局的并发队列会发生什么现象?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果 dispatch_barrier_async 函数传入的是一个串行队列或者是一个全局的并发队列,其行为会有所不同:

1-传入串行队列:

如果传入的是一个串行队列,那么 dispatch_barrier_async 函数将会在该串行队列上创建一个同步点。
在这种情况下,栅栏函数会等待在该串行队列上排队的所有任务都执行完毕,
并且不允许新的任务插入到栅栏之后,直到栅栏之前的任务全部执行完毕。

2-传入全局的并发队列:

如果传入的是一个全局的并发队列(如 dispatch_get_global_queue 获取的队列),
栅栏函数的行为会有所不同。
全局并发队列是一个并发队列,即使使用栅栏函数也无法阻止其他任务在栅栏之后插入执行。
在这种情况下,栅栏函数仍然会等待在栅栏之前的任务执行完毕,但在栅栏之后可能会有其他任务同时执行。
总的来说,传入串行队列时,dispatch_barrier_async 函数会创建一个同步点,
保证栅栏之前的任务全部执行完毕后再执行栅栏之后的任务;
而传入全局的并发队列时,栅栏函数也会等待栅栏之前的任务执行完毕,但无法阻止其他任务插入执行。

三 参考

  • 简书—OC中多线程实现与线程安全