iOS中的多线程开发技术第二篇(iOS中的自旋锁和互斥锁)

目录

前言

一、多线程资源共享的安全隐患

1. 资源共享的例子

1.详细解释

2.竞争条件的具体表现

二、线程同步技术

1.自旋锁(OSSpinLock)

1.OSSpinLock

1.概念

2.OSSpinLock的用法

1.初始化

2.加锁方法

3.解锁方法

3.示例代码

4.OSSpinLock的问题

1.什么是优先级反转

2.OSSpinLock导致优先级反转的原因

3.解决方法

2.os_unfair_lock

1.用法

1.加锁

2.加锁

3.解锁

2.互斥锁(pthread_mutex)

1.用法

1.初始化

2.加锁

3.解锁

2.互斥锁的类型

1. 普通锁

2. 递归锁

3. 错误检测锁

3.自旋锁条件

1.互斥条件

2.保持与等待条件

3.不剥夺条件

4.循环等待条件

3.NSLock

1.主要功能和方法

1. -lock:

2. -unlock:

3.-tryLock:

4. -lockBeforeDate:

5.-name和-setName:

2.使用场景

1.使用示例

2.优缺点

1.优点

2. 缺点


前言

    在上一篇博客中,我们了解到了GCD的用法。这篇博客主要介绍下多线程中的自旋锁和互斥锁。

一、多线程资源共享的安全隐患

        在 iOS 开发中,多线程编程可以显著提高应用的性能和响应能力。Grand Central Dispatch (GCD) 是一个用于多线程编程的强大工具。然而,在多线程环境下进行资源共享时,必须小心避免竞争条件和数据争用等问题。

        当多个线程同时访问和修改共享资源时,如果不进行适当的同步,就可能导致数据不一致的问题。这种情况称为“竞争条件”。

        下面我们看一下几个比较经典的多线程共享的例子。

1. 资源共享的例子

        假设我们有一个共享的整数变量 counter,两个线程将并发地对其进行递增操作。如果没有使用任何同步机制来保护 counter,这将导致竞争条件,结果可能是错误的。

        完整的代码如下:

#import "ViewController.h"

@interface ViewController ()

// 共享的计数器
@property (nonatomic, assign) int counter;

// 并发队列
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化计数器
    self.counter = 0;
    
    // 初始化并发队列
    self.concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    // 创建两个并发任务
    dispatch_async(self.concurrentQueue, ^{
        for (int i = 0; i < 10000; i++) {
            [self incrementCounter];
        }
    });
    
    dispatch_async(self.concurrentQueue, ^{
        for (int i = 0; i < 10000; i++) {
            [self incrementCounter];
        }
    });
}

// 不安全的计数器增加操作(没有使用任何锁)
- (void)incrementCounter {
    sleep(.01);
    self.counter += 1;
    NSLog(@"当前counter: %d", self.counter);
}



@end

1.详细解释

        在上述的demo中,counter 是一个共享资源,多个线程同时访问它。如果没有同步机制保护,线程竞争问题就会发生。

        在 viewDidLoad 方法中,通过 dispatch_async 启动了两个并发任务,这两个任务都会同时尝试增加 counter 的值。

         incrementCounter 方法在没有任何同步保护的情况下对 counter 进行增加操作。这就可能导致线程竞争问题,例如,两个线程可能会同时读取相同的 counter 值,然后都将其增加 1,最终导致 counter 只增加了一次,而不是两次。

        我们多运行几次,会发现每次控制台打印的结果都不一样。        

        图2.资源共享控制台输出

2.竞争条件的具体表现

        1.线程 A 读取 counter 的值(假设当前值为 100)。

        2.线程 B 读取 counter 的值(假设当前值为 100)。

        3.线程 A 将 counter 的值递增 1(结果为 101)并写回。

        4.线程 B 将 counter 的值递增 1(结果为 101)并写回。

        最终,尽管两个线程各自进行了递增操作,counter 的值却仅增加了 1,而不是期望的 2。 

        图3.多线程资源竞争

二、线程同步技术

        当多个线程同时访问和修改共享资源时,如果不进行适当的同步,就可能导致数据不一致的问题。这种情况称为“竞争条件”。为了解决这些问题,可以使用同步机制来确保线程安全。

        iOS中的线程同步有以下几种技术:

1.自旋锁(OSSpinLock)

1.OSSpinLock

1.概念

        自旋锁(OSSpinLock)是一种用于多线程编程的同步机制,与互斥锁(Mutex)类似,但有一些不同之处。自旋锁在等待锁的时候,不会使线程进入休眠状态,而是持续地循环检查锁是否可用。这种忙等待(busy-waiting)机制适用于锁定时间短的场景,因为在锁定时间较长时,自旋锁会消耗大量的 CPU 资源。

        在 iOS 中,自旋锁可以使用 os_unfair_lock 或 OSSpinLock 实现。需要注意的是,由于 OSSpinLock 存在优先级反转问题,苹果建议使用 os_unfair_lock 代替 OSSpinLock。

2.OSSpinLock的用法
1.初始化

        我们使用下面的方法初始化。

OSSpinLock lock = OS_SPINLOCK_INIT;

2.加锁方法

    //加锁

    OSSpinLockLock(&_lock);

3.解锁方法

        我们使用下面的代码进行解锁操作。

    //解锁

    OSSpinLockUnlock(&_lock);

3.示例代码

        还以上述计时器的demo为例,我们看看加了自旋锁之后的结果。我们在修改计数器之前先加锁,修改之后再把自旋锁解开,运行代码,我们发现每次运行都是我们期望的结果2000。

#import <libkern/OSAtomic.h>

@interface ViewController ()

@property (nonatomic) OSSpinLock spinLock;
@property (nonatomic, assign) int counter;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.spinLock = OS_SPINLOCK_INIT;
    self.counter = 0;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 创建两个并发任务
    dispatch_async(queue, ^{
        [self incrementCounterSafely];
    });
    
    dispatch_async(queue, ^{
        [self incrementCounterSafely];
    });
}

- (void)incrementCounterSafely {
    for (int i = 0; i < 10000; i++) {
        OSSpinLockLock(&_spinLock);
        self.counter += 1;
        OSSpinLockUnlock(&_spinLock);
    }
    
    NSLog(@"Final counter value: %d", self.counter);
}

@end        

        我们分析下这个自旋锁的作用:

        OSSpinLockLock(&_lock);: 进入 incrementCounter 方法时,首先尝试获取自旋锁。如果锁已经被其他线程持有,当前线程将进入忙等状态(自旋),直到锁被释放。

        sleep(.01);: 这个延时模拟了一个需要花费一定时间的操作,延长了线程持有锁的时间,从而增加了锁竞争的概率。

         self.counter += 1;: 在获取到锁之后,安全地对 counter 进行递增操作。        

        OSSpinLockUnlock(&_lock);: 完成 counter 的更新后,释放锁,使其他等待的线程可以继续执行。

4.OSSpinLock的问题

        OSSpinLock`是一种自旋锁,在等待锁时线程会不断尝试获取锁,而不会进入睡眠状态。这种忙等待的方式在某些情况下可能会造成严重的问题,特别是优先级反转问题。

1.什么是优先级反转

        优先级反转(Priority Inversion)是指在多线程环境中,低优先级线程持有某个锁,高优先级线程因为需要获取该锁而被阻塞,中优先级线程却继续执行的现象。具体步骤如下:

        1. 低优先级线程(线程 A)持有 OSSpinLock锁,并且正在执行某个临界区的代码。
        2. 高优先级线程(线程 B)需要获取相同的锁,但由于锁被线程 A 占用,线程 B 被阻塞,开始自旋等待。
        3. 此时,如果有一个中优先级线程线程 C)进入系统并开始执行,由于它的优先级比线程 A 高,但比线程 B 低,操作系统会调度线程 C 执行,而不是线程 A。
        4. 结果是:线程 A 无法执行并释放锁,线程 B 也无法获取锁并继续执行,而线程 C 占用了 CPU 时间。线程 B 的执行被延迟,尽管它的优先级比线程 C 高。

2.OSSpinLock导致优先级反转的原因

        OSSpinLock的设计并不考虑线程的优先级,当高优先级线程自旋等待低优先级线程释放锁时,CPU 的调度机制可能会使低优先级线程无法及时运行并释放锁。这种情况下,高优先级线程就会一直等待,导致优先级反转,最终影响系统的实时性和性能。

3.解决方法

        为了避免优先级反转,苹果在 iOS 10 和 macOS 10.12 中标记了OSSpinLock为不安全,并建议开发者使用 os_unfair_lock作为替代。

        os_unfair_lock:这是OSSpinLock的现代替代品,它能够避免优先级反转问题。os_unfair_lock在锁等待时会让出 CPU,允许其他线程执行。它还考虑了线程优先级,在竞争激烈的情况下具有更好的性能表现和公平性。

2.os_unfair_lock

        os_unfair_lock是iOS10之后为了替代OSSpinLock新增的API,它解决了OSSpinLock的优先级反转的问题。

1.用法

        首先我们需要导入头文件

#import <os/lock.h>。

1.加锁

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

2.加锁

 os_unfair_lock_lock(&_lock);

3.解锁

  os_unfair_lock_unlock(&_lock);

2.互斥锁(pthread_mutex)

        metex叫做互斥锁,等待锁的线程会处于休眠状态。

1.用法

1.初始化

        首先我们要导入头文件:

#import <pthread.h>

        我们使用下面的方法初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2.加锁

 pthread_mutex_lock(&_lock);

3.解锁

pthread_mutex_unlock(&_lock);

2.互斥锁的类型

1. 普通锁

        普通锁()是 pthread_mutex 的默认锁类型,它不允许同一个线程多次锁定。如果同一个线程尝试再次获取已锁定的普通锁,则会导致死锁。这个锁适用于不需要递归加锁的场景。

        以上面的计数器为例,我们看看如何使用普通锁。

#import <pthread.h>

@interface ViewController ()

@property (nonatomic, assign) int counter;
@property (nonatomic, assign) pthread_mutex_t lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化普通锁
    pthread_mutex_init(&_lock, NULL);
    
    // 创建两个并发线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self incrementCounter];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self incrementCounter];
    });
}

- (void)incrementCounter {
    pthread_mutex_lock(&_lock);
    
    for (int i = 0; i < 10000; i++) {
        self.counter += 1;
    }
    
    pthread_mutex_unlock(&_lock);
    NSLog(@"Final counter value: %d", self.counter);
}

- (void)dealloc {
    pthread_mutex_destroy(&_lock);
}

@end
2. 递归锁

        递归锁(PTHREAD_MUTEX_RECURSIVE)允许同一个线程多次获取同一把锁。它记录锁的获取次数,因此必须相应地解锁相同次数。这对于需要递归调用并且每次调用都需要锁定资源的场景非常有用。

#import <pthread.h>

@interface ViewController ()

@property (nonatomic, assign) int counter;
@property (nonatomic, assign) pthread_mutex_t recursiveLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化递归锁
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&_recursiveLock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    // 调用递归函数
    [self recursiveFunction:5];
}

- (void)recursiveFunction:(int)value {
    pthread_mutex_lock(&_recursiveLock);
    
    if (value > 0) {
        NSLog(@"Value: %d", value);
        [self recursiveFunction:value - 1];
    }
    
    pthread_mutex_unlock(&_recursiveLock);
}

- (void)dealloc {
    pthread_mutex_destroy(&_recursiveLock);
}

@end
3. 错误检测锁

        错误检测锁(PTHREAD_MUTEX_ERRORCHECK)也允许同一个线程再次尝试获取锁,但与递归锁不同的是,如果同一线程再次获取锁,会返回错误而不是死锁。这对于需要检测锁定错误的场景非常有用。

#import <pthread.h>

@interface ViewController ()

@property (nonatomic, assign) pthread_mutex_t errorcheckLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化错误检测锁
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
    pthread_mutex_init(&_errorcheckLock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    // 测试错误检测锁
    [self testErrorcheckLock];
}

- (void)testErrorcheckLock {
    int result = pthread_mutex_lock(&_errorcheckLock);
    if (result == 0) {
        NSLog(@"First lock acquired successfully.");
    }
    
    // 尝试再次加锁
    result = pthread_mutex_lock(&_errorcheckLock);
    if (result != 0) {
        NSLog(@"Error: Attempt to relock a lock held by the same thread.");
    }
    
    pthread_mutex_unlock(&_errorcheckLock);
}

- (void)dealloc {
    pthread_mutex_destroy(&_errorcheckLock);
}

@end

3.自旋锁条件

        用互斥锁时,需要满足以下四个条件才能确保互斥锁正确地工作。这些条件通常也被称为经典的同步问题中的必要条件。

1.互斥条件

        互斥条件要求在任何时刻,只能有一个线程能够占有互斥锁,其他试图占有该锁的线程必须等待。

pthread_mutex_lock(&mutex);   // 锁定
// 临界区代码
pthread_mutex_unlock(&mutex); // 解锁

        在上面的代码中,只有当一个线程成功获取了锁,才能进入临界区,其他线程会被阻塞,直到该线程释放锁。

2.保持与等待条件

        持有和等待条件是指,线程在已经持有至少一个资源(锁)的情况下,还可以请求其他资源,并等待这些资源被分配。

pthread_mutex_lock(&mutex1);  // 锁定第一个资源
pthread_mutex_lock(&mutex2);  // 请求并等待第二个资源
// 临界区代码
pthread_mutex_unlock(&mutex2); // 释放第二个资源
pthread_mutex_unlock(&mutex1); // 释放第一个资源

        这里线程先获取 mutex1,然后继续等待获取 mutex2。

3.不剥夺条件

        不剥夺条件是指,已经分配给线程的资源(锁)不能被强行夺走,除非线程主动释放它们。一旦线程成功获得锁,其他线程无法强制性地获取该锁。只有持有锁的线程主动释放锁后,其他线程才有机会获取锁。

pthread_mutex_lock(&mutex);
// 临界区代码
// 在此期间,其他线程不能强行获取该锁
pthread_mutex_unlock(&mutex);

4.循环等待条件

        循环等待条件是指,存在一个线程集合 {T1, T2, …, Tn},其中每个线程都在等待下一个线程持有的锁,且最后一个线程等待第一个线程持有的锁,从而形成一个等待循环。

// 线程1
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);

// 线程2
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);

        在这个例子中,线程1先获取 mutex1,然后等待 mutex2,而线程2先获取 mutex2,然后等待 mutex1,导致循环等待,从而可能引发死锁。

3.NSLock

        NSLock是对mutex普通锁的封装。

        NSLock是用于在多线程环境下确保对共享资源的互斥访问。它是基于 `NSObject` 的封装,使用起来更加简洁和方便。NSLock是一种不可递归的锁,即同一线程不能多次获取同一个锁,否则会导致死锁。

1.主要功能和方法

       NSLock提供了一些简单的方法来控制锁的获取和释放:

1. -lock:

        尝试获取锁。如果锁当前未被占用,调用线程会获得锁,并继续执行。如果锁已被其他线程持有,调用线程会阻塞,直到锁被释放。

[lock lock];

2. -unlock:

        释放锁。持有锁的线程在完成对共享资源的操作后,需要调用此方法来释放锁,以便其他线程可以获取锁。 

[lock unlock];

3.-tryLock:

        尝试获取锁,但与 -lock 方法不同的是,如果锁当前被占用,它不会阻塞线程,而是立即返回 NO。如果获取成功,返回 YES。

   if ([lock tryLock]) {
       // 获取锁成功
       [lock unlock];
   } else {
       // 锁被占用
   }

4. -lockBeforeDate:

        尝试在指定的时间之前获取锁。如果在指定时间内获取成功,返回 YES;如果超时返回 NO。

  if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]) {
       // 获取锁成功
       [lock unlock];
   } else {
       // 超时,未获取到锁
   }

5.-name和-setName:

        NSLock可以给锁指定一个名称,便于调试和日志记录。名称不会影响锁的行为,仅用于识别。

   [lock setName:@"MyLock"];
   NSLog(@"Lock name: %@", [lock name]);

2.使用场景

        NSLock适用于简单的线程同步场景,尤其是当你需要确保某一段代码在多线程环境下不会被并发执行时。比如:

         保护共享数据的访问。

        控制对临界区的访问,确保同一时间只有一个线程可以执行关键代码。

1.使用示例

        下面是一个简单的 `NSLock` 使用示例,展示如何在多线程环境下保护共享数据:

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, assign) int counter;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.lock = [[NSLock alloc] init];
    self.counter = 0;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 创建两个并发任务,模拟线程竞争
    dispatch_async(queue, ^{
        [self incrementCounter];
    });
    
    dispatch_async(queue, ^{
        [self incrementCounter];
    });
}

- (void)incrementCounter {
    for (int i = 0; i < 10000; i++) {
        [self.lock lock];  // 获取锁
        
        self.counter += 1;
        
        [self.lock unlock];  // 释放锁
    }
    NSLog(@"Final counter value: %d", self.counter);
}

@end
2.优缺点

       NSLock适合以下场景:
        1.简单的线程同步:如对共享数据的简单保护。
        2.临界区保护:确保某一段代码在同一时间只能被一个线程执行。

        NSLock的优缺点如下:

1.优点

        简单易用:NSLock提供了基础的锁机制,使用起来非常方便,适合简单的同步场景。
        轻量级:相比其他更复杂的锁(如递归锁、条件锁),NSLock更轻量,不涉及过多的资源开销。

2. 缺点

        不可递归:NSLock是不可递归的锁类型。如果同一个线程多次尝试获取同一个锁,会导致死锁。
        缺乏高级功能:相比其他同步机制(如NSCondition、NSRecursiveLock),NSLock不支持条件变量、递归锁等高级功能,功能较为基础。

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
自旋锁是一种基本的同步机制,用于保护共享数据的并发访问。在多线程环境自旋锁允许一个线程进入临界区,而其他线程必须等待,直到该线程释放自旋锁。与互斥锁不同,自旋锁使用忙等待的方式来获取锁,即线程不断地尝试获取锁,直到成功。 信号量是一种同步机制,可以限制对共享资源的访问。它允许多个线程同时访问共享资源,但是通过计数来控制同时访问的线程数。信号量的计数值代表可以同时访问的线程数,当计数值为0时,其他线程必须等待。通过P操作减少计数值,V操作增加计数值。 在iOS开发自旋锁和信号量都是常用的并发控制机制。自旋锁适用于临界区代码执行时间短且线程竞争激烈的情况,因为自旋锁避免了线程切换导致的性能损耗,但是会增加CPU的占用率。信号量适用于临界区代码执行时间长或者任务之间需要协调的情况,它可以控制线程的并发数,避免资源过度竞争。 在iOS自旋锁通常使用OSSpinLock来实现,它是一种非递归锁,适用于单个线程获取锁的情况。在iOS 10以后,苹果推荐使用os_unfair_lock替代OSSpinLock。信号量在iOS通过Dispatch Semaphore来实现,可以使用dispatch_semaphore_create和dispatch_semaphore_wait等函数创建和操作信号量。 综上所述,自旋锁和信号量是iOS开发常用的并发控制机制,它们分别适用于不同的情况。使用自旋锁可以提高效率,但增加CPU占用率,适用于临界区执行时间短且线程竞争激烈的情况。而信号量则可以控制线程的并发数,适用于临界区执行时间长或者需要任务协调的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫柱子哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值