MST

星途 面试题库

面试题:Objective-C弱引用与强引用在多线程环境下的潜在问题及解决方案

在多线程编程的Objective-C项目里,弱引用和强引用可能会引发哪些潜在问题,比如数据竞争、野指针等。针对这些潜在问题,详细阐述你所知道的解决方案,包括但不限于使用锁机制、原子属性等,并分析每种方案的优缺点及适用场景。
37.7万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

弱引用和强引用可能引发的潜在问题

  1. 野指针
    • 强引用:当对象被强引用持有,在对象释放后,若没有将强引用指针置为 nil,则该指针会成为野指针。后续访问野指针会导致程序崩溃。例如,在多线程环境下,一个线程释放了对象,而另一个线程还持有指向该对象的强引用且未置 nil,就会出现这种情况。
    • 弱引用:本身不会导致野指针问题,因为当对象释放时,所有指向它的弱引用会自动被设置为 nil。但如果在对象即将释放前,某个线程通过弱引用访问对象并执行一些操作,而对象在另一个线程中马上被释放,可能会访问到已部分释放的对象,导致未定义行为。
  2. 数据竞争
    • 强引用:在多线程环境下,多个线程同时对一个对象的强引用计数进行操作(例如 retainrelease),可能会导致数据竞争。这会使得引用计数不准确,最终可能导致对象过早释放或过度保留,引发内存错误。
    • 弱引用:虽然弱引用的维护由系统自动管理,一般不会出现类似强引用计数的数据竞争问题。但当多个线程通过弱引用访问对象并修改其属性时,如果对象属性没有适当的保护机制,就会出现数据竞争问题。

解决方案及优缺点、适用场景

  1. 锁机制
    • 互斥锁(NSLockpthread_mutex
      • 解决方案:在访问和修改对象引用计数或对象属性时,使用互斥锁来保证同一时间只有一个线程能执行相关操作。例如,在 retainrelease 操作前后加锁:
NSLock *lock = [[NSLock alloc] init];
// 假设 obj 是一个对象
[lock lock];
[obj retain];
[lock unlock];
// 其他操作
[lock lock];
[obj release];
[lock unlock];
 - **优点**:实现简单,能有效避免数据竞争。
 - **缺点**:会带来性能开销,特别是在高并发场景下,频繁的加锁解锁操作会降低程序的执行效率。
 - **适用场景**:适用于并发访问频率不高,对性能要求不是极其苛刻的场景。
  • 读写锁(pthread_rwlock
    • 解决方案:对于读多写少的场景,使用读写锁。多个线程可以同时进行读操作,但写操作需要独占锁。例如,在读取对象属性时使用读锁,修改对象属性或引用计数时使用写锁:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
// 读操作
pthread_rwlock_rdlock(&rwlock);
// 读取对象属性
pthread_rwlock_unlock(&rwlock);
// 写操作
pthread_rwlock_wrlock(&rwlock);
// 修改对象属性或引用计数
pthread_rwlock_unlock(&rwlock);
 - **优点**:能提高读操作的并发性能,在读多写少的场景下比互斥锁性能更好。
 - **缺点**:实现相对复杂,且如果写操作频繁,可能会导致读线程长时间等待。
 - **适用场景**:适用于读操作远多于写操作的场景,如缓存数据的读取和偶尔更新。

2. 原子属性

  • 解决方案:在定义对象属性时,使用 atomic 关键字修饰。例如:
@property (nonatomic, atomic, strong) SomeObject *obj;
  • 优点:编译器会自动生成保证属性访问原子性的代码,能防止属性访问过程中的数据竞争。使用简单,不需要手动管理锁。
  • 缺点:性能开销较大,因为每次访问属性都需要进行原子操作。并且 atomic 只能保证属性的原子性,不能保证对象内部数据的一致性。例如,对象有多个相关联的属性,修改其中一个属性时,atomic 不能保证其他属性的同步修改。
  • 适用场景:适用于对简单属性的访问,对性能要求不是特别高,且只需要保证单个属性访问原子性的场景。
  1. 自动释放池(NSAutoreleasePool
    • 解决方案:在多线程环境下,合理使用自动释放池可以控制对象的释放时机,减少野指针的出现概率。例如,在一个长时间运行的线程中,定期创建自动释放池来释放不再使用的对象:
while (1) {
    @autoreleasepool {
        // 线程中的操作,创建的对象会在自动释放池销毁时释放
    }
}
  • 优点:能有效管理内存,减少内存峰值,在一定程度上避免因对象长时间未释放导致的野指针问题(因为对象及时释放了)。
  • 缺点:需要合理规划自动释放池的创建位置和生命周期,如果创建过于频繁可能会带来额外的性能开销。
  • 适用场景:适用于线程中会大量创建临时对象的场景,如在一个循环中不断创建对象的线程。
  1. GCD(Grand Central Dispatch)
    • 解决方案:使用 dispatch_queue 来串行化对对象的访问。例如,创建一个串行队列,所有对对象的操作都在这个队列中执行:
dispatch_queue_t queue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    // 对对象的操作,如 retain、release 或修改属性
});
  • 优点:使用方便,利用 GCD 的高效调度机制,能有效避免数据竞争。而且 GCD 支持多核并行,在多核设备上性能较好。
  • 缺点:如果使用不当,例如在队列中执行耗时操作,可能会影响主线程或其他队列的执行。
  • 适用场景:适用于各种多线程场景,特别是需要在后台线程执行任务并保证数据一致性的场景。