iOS多线程-锁

398 阅读17分钟

1. 多线程安全隐患

在多线程开发时,如果遇到多个线程同时访问同一块资源,就会产生安全隐患的问题。多个线程访问同一个文件,同一个变量或者是同一个对象,最终会导致数据的错乱等问题

1.1 举例一:存取钱问题

假设我们可以同时进行存钱和取钱操作,存钱和取钱操作就会同时访问我们的账户。具体场景如下:

  • 假设银行卡的余额有500块钱
  • 此时我们进行取钱操作,读取银行卡的余额为500元,在取钱操作没有完成的同一时间,我们进行存钱操作,此时读取的银行卡余额为500元
  • 取出50元,500-50 =450元,此时更新账户余额为450元
  • 同时存入100元,500+100=600元,更新账户余额为600元
  • 最终,余额变为600元,正常的逻辑应该是500-50+100=550元,产生了安全隐患。

1.1.1 代码示例

@interface ViewController ()
@property (nonatomic, assign) int money;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self executeMoneyTask];
}

- (void)executeMoneyTask {
    
    self.money = 500;
    // 开启2条线程,同时进行存钱和取钱的操作
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self drawMoney];
        }
    });
}


/// 存钱操作
- (void)saveMoney {
    
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
}

/// 取钱操作
- (void)drawMoney {
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
}

打印结果:
取了50块,余额550, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额600,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额500, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额600,<NSThread: 0x600003609000>{number = 6, name = (null)}
存了100块,余额700,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额650, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额750,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额700, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额800,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额750, <NSThread: 0x60000360ba40>{number = 7, name = (null)}

1.1.2 打印结果分析

  • 上面代码分别在2个线程同时执行存钱和取钱的操作,取钱和存钱操作分别执行了5次。
  • 从打印结果分析,虽然最终的余额是750元,但是中间确实出现了余额显示不正确的问题。

1.2 举例二:卖票问题

假设线上去我们去购买郭德纲相声专场的票,一共开设2个售票窗口,同时进行卖票。具体场景如下:

  • 1号窗口读取票的剩余1000张,同时2号窗口读取票的剩余1000张
  • 1号窗口卖出1张剩余1000-1=999张,2号窗口卖出1张1000-1=999张
  • 随着时间的推移,数据就会非常的混乱了,结果你会发现卖出的票数远远大于剧场可以承载的观众人数。

1.2.1 代码示例

@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//    [self executeMoneyTask];
    
    [self executeTicketTask];
}


#pragma mark - 买票问题

- (void)executeTicketTask {
    
    self.tickets = 15;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
}

- (void)saleTicket {
    
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
}

打印结果:
还剩14张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩12张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩13张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩11张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩11张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩10张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩9张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩8张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩6张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩7张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩5张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩4张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩3张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩2张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩1张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}

1.2.2 打印结果分析

  • 开启3条线程,同时进行买票操作,正常情况下,打印结果剩余应该是0张票
  • 由于同时共享一块资源导致数据发生了错乱,这是非常危险的,接下来就研究一下如何解决以上两个问题。

2. 线程同步技术

上面出现的问题,在开发中会产生很严重的数据安全问题,通常解决方案就是线程同步技术。线程同步技术最常见的就是加锁。即在操作数据之前给线程加一把锁,数据操作结束后解锁。在有锁时候,其他线程是无法访问这块资源的,只要当这把锁被解开了,其他线程才可以进入来操作数据,其他线程进入之前,同样再加锁。这样一系列操作之后,就可以保证数据的安全了。

2.1 iOS中的线程同步方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispath_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

2.1.1 OSSpinLock

  • OSSpinLock叫做自旋锁,等待锁的线程会处于忙等状态,一直占用着CPU的资源
  • 目前已经被弃用,原因是不再安全,可能会出现优先级反转问题,即如果等待锁的线程优先级比较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁
  • 导入#import <libkern/OSAtomic.h>
  • 解决卖票问题
- (void)saleTicket {
  	// 初始化1次,OS_SPINLOCK_INIT 值就是0(进入头文件可以看到)
    static OSSpinLock lock = OS_SPINLOCK_INIT;
  	// 加锁操作
    OSSpinLockLock(&lock);
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
  	// 解锁操作
    OSSpinLockUnlock(&lock);
}

打印结果:
还剩14张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩13张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩12张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩11张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩10张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩9张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩8张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩7张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩6张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩5张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩4张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩3张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩2张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩1张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩0张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}

注意:

首先要保证锁只有1把,只有是同一把锁才能保证线程安全。

static OSSpinLock lock = OS_SPINLOCK_INIT;初始化一把锁,为什么可以使用static修饰,因为static修饰的变量是在编译时期确定值的,而OS_SPINLOCK_INIT本质就是一个常量值0。(具体可以进入头文件看一下这个宏定义)

  • 解决存取钱问题
/// 存钱操作
- (void)saveMoney {
    OSSpinLockLock(&_moneyLock);
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
    OSSpinLockUnlock(&_moneyLock);
}

/// 取钱操作
- (void)drawMoney {
    OSSpinLockLock(&_moneyLock);
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
    OSSpinLockUnlock(&_moneyLock);
}

打印结果:
存了100块,余额600,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额700,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额800,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额900,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额1000,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
取了50块,余额950, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额900, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额850, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额800, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额750, <NSThread: 0x600003dae880>{number = 5, name = (null)}
  • 存钱和取钱操作都需要加锁,而且必须是同一把锁

2.1.2 os_unfair_lock

因为OSSpinLock存在问题,所以在iOS10就已经被废弃了,在我们编写代码的时候,Xcode其实会提示我们使用os_unfair_lock进行替代。

os_unfair_lock 与 OSSpinLock不同之处在于,等待os_unfair_lock锁的线程会处于休眠状态。

  • 在使用os_unfair_lock需要导入头文件 import <os/lock.h>

使用的方式和OSSpinLock很类似,直接看代码

@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@property (nonatomic, assign) os_unfair_lock moneyLock;
@property (nonatomic, assign) os_unfair_lock ticketLock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  	// 初始化锁
    self.moneyLock = OS_UNFAIR_LOCK_INIT;
    self.ticketLock = OS_UNFAIR_LOCK_INIT;
    
    [self executeMoneyTask];
    [self executeTicketTask];
}

- (void)saleTicket {
    // 加锁操作
    os_unfair_lock_lock(&_ticketLock);
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
  	// 解锁操作
    os_unfair_lock_unlock(&_ticketLock);
}

/// 存钱操作
- (void)saveMoney {
  	// 加锁操作
    os_unfair_lock_lock(&_moneyLock);
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
  	// 解锁操作
    os_unfair_lock_unlock(&_moneyLock);
}

/// 取钱操作
- (void)drawMoney {
  	// 加锁操作
    os_unfair_lock_lock(&_moneyLock);
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
  	// 解锁操作
    os_unfair_lock_unlock(&_moneyLock);
}

2.1.3 pthread_mutex

  • 使用pthread_mutex需要导入头文件#import <pthread.h>
  • pthread_mutex作为互斥锁,等待锁的线程会处于休眠的状态,可以解决上面的卖票和取钱的问题

#import <pthread.h>

@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@property (nonatomic, assign) pthread_mutex_t moneyMutex;
@property (nonatomic, assign) pthread_mutex_t ticketMutex;
@end
  
- (void)viewDidLoad {
    [super viewDidLoad];
    // 执行存取钱任务
    [self executeMoneyTask];
  	// 执行卖票任务
    [self executeTicketTask];
    
    pthread_mutexattr_t attr;
    // 初始化锁属性
    pthread_mutexattr_init(&attr);
    // 设置锁属性类型,互斥锁
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    // 初始化锁
    pthread_mutex_init(&_moneyMutex, &attr);
    pthread_mutex_init(&_ticketMutex, &attr);
    // 销毁锁属性
    pthread_mutexattr_destroy(&attr);
}

// 卖票
- (void)saleTicket {
  	// 加锁操作
    pthread_mutex_lock(&_ticketMutex);
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
  	// 解锁操作
    pthread_mutex_unlock(&_ticketMutex);
}

/// 存钱操作
- (void)saveMoney {
    pthread_mutex_lock(&_moneyMutex);
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
    pthread_mutex_unlock(&_moneyMutex);
}

/// 取钱操作
- (void)drawMoney {
    pthread_mutex_lock(&_moneyMutex);
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
    pthread_mutex_unlock(&_moneyMutex);
}

// 销毁锁
- (void)dealloc {
    pthread_mutex_destroy(&_moneyMutex);
    pthread_mutex_destroy(&_ticketMutex);
}
  • 在上面的代码中,可以通过设置锁属性,来设置锁的类型
/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL		0 			// 互斥锁
#define PTHREAD_MUTEX_ERRORCHECK	1			// 错误检查(不用管)
#define PTHREAD_MUTEX_RECURSIVE		2			// 递归锁
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL
  • 设置PTHREAD_MUTEX_RECURSIVE锁属性,可以解决递归调用时,死锁的问题,递归锁进行多次加锁的操作
  • test1方法进行递归调用,如果使用互斥锁,打印只打印1次,因为第1次调用进行了加锁的操作,当递归调用test1方法,发现已经加锁了,所以不会再继续执行,需要等待解锁操作,所以导致了死锁。如何解决这个问题呢?
  • 使用递归锁,设置锁类型的时候,设置为PTHREAD_MUTEX_RECURSIVE即可。
- (void)viewDidLoad {
    [super viewDidLoad];
  
    pthread_mutexattr_t attr;
    // 初始化锁属性
    pthread_mutexattr_init(&attr);
    // 设置锁属性类型
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    pthread_mutexattr_destroy(&attr);
    [self test1];
}

- (void)test1 {
    pthread_mutex_lock(&_mutex);
    NSLog(@"test1");
    static int count = 0;
    if (count < 10) {
        count++;
        [self test1];
    }
    pthread_mutex_unlock(&_mutex);
}

2.1.4 pthread_mutex 条件的使用

  • 需求:

    1. 有2个线程,线程A对数组进行元素添加,线程B对数组中的元素进行删除。
    2. 如果数组中没有元素,就等待线程A添加元素后再进行删除操作
  • 需求分析:

    1. 删除操作和添加操作在2个线程执行,执行的顺序不确定
    2. 要访问同一份资源(可变数组),所以操作前后要进行加锁操作保证线程安全

根据上面的需求和分析,我们就可以运用到pthread中条件的使用

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *data;
@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, assign) pthread_cond_t conditon;
@end
  
- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutexattr_t attr;
    // 初始化锁属性
    pthread_mutexattr_init(&attr);
    // 设置锁属性类型
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 初始化条件
    pthread_cond_init(&_conditon, NULL);
    
    [self test1];
    
}

- (void)test1 {
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}

- (void)addObject {
    
    pthread_mutex_lock(&_mutex);
    NSLog(@"添加元素");
    [self.data addObject:@"test"];
    pthread_cond_signal(&_conditon);
    pthread_mutex_unlock(&_mutex);
}

- (void)removeObject {
    
    pthread_mutex_lock(&_mutex);
    NSLog(@"remove - begin");
    if (self.data.count == 0) {
        // 如果数组中没有任何元素,等待数组添加元素
        pthread_cond_wait(&_conditon, &_mutex);
    }
    NSLog(@"删除元素");
    [self.data removeLastObject];
    
    pthread_mutex_unlock(&_mutex);
}
  • 当删除操作先执行的时候,先加锁,如果数组中没有任何元素,此时使用pthread_cond_wait(&_conditon, &_mutex);进行等待操作,此时同时放开mutex锁。
  • 添加操作加锁,添加元素,发送信号,解锁操作
  • 删除操作线程接收到信号,进行加锁执行删除操作,删除结束后,解锁。

2.1.5 NSLock

  • NSLock是对mutex普通锁的封装,遵守了NSLocking协议,提供了lock和unlock方法进行加锁和解锁的操作
  • 下面是使用NSLock,对线程安全的处理
/// 存钱操作
- (void)saveMoney {
    [self.moneyLock lock];
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
    [self.moneyLock unlock];
}

/// 取钱操作
- (void)drawMoney {
    [self.moneyLock lock];
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
    [self.moneyLock unlock];
}


- (NSLock *)moneyLock {
    if (_moneyLock == nil) {
        _moneyLock = [[NSLock alloc] init];
    }
    return _moneyLock;
}

- (void)saleTicket {
    [self.ticketLock lock];
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
    [self.ticketLock unlock];
}

- (NSLock *)ticketLock {
    if (_ticketLock == nil) {
        _ticketLock = [[NSLock alloc] init];
    }
    return _ticketLock;
}

2.1.6 NSRecursiveLock

NSRecursiveLock是对mutex的封装,封装的是自旋锁。操作的方式与NSLock类似,有lock和unlock的操作。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self test1];
    
}

- (void)test1 {
    
    [self.lock lock];
    NSLog(@"test1");
    static int count = 0;
    if (count < 10) {
        count++;
        [self test1];
    }
    
    [self.lock unlock];
}

- (NSRecursiveLock *)lock {
    if (_lock == nil) {
        _lock = [[NSRecursiveLock alloc] init];
    }
    return _lock;
}

2.1.7 NSCondition

  • NSConditon 是对pthread_cond的封装,使条件操作变得更加面向对象
- (void)addObject {
    [self.condition lock];
    NSLog(@"添加元素");
    [self.data addObject:@"test"];
    [self.condition signal];
    [self.condition unlock];
}

- (void)removeObject {
    
    [self.condition lock];
    NSLog(@"remove - begin");
    if (self.data.count == 0) {
        // 如果数组中没有任何元素,等待数组添加元素
        [self.condition wait];
    }
    NSLog(@"删除元素");
    [self.data removeLastObject];
    [self.condition unlock];
}

2.1.8 NSConditonLock

  • 需求:分别在3个线程执行任务,要求任务1 任务2 任务3 依次执行。(相互依赖执行)
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化NSConditionLock对象
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    
    [self test1];
    
}

// 需求:任务1 任务2 任务3 依次按顺序执行
- (void)test1 {
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];
}

- (void)task1 {
    [self.conditionLock lockWhenCondition:1];
    NSLog(@"执行任务1 - %@", [NSThread currentThread]);
    [self.conditionLock unlockWithCondition:2];
}

- (void)task2 {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"执行任务2 - %@", [NSThread currentThread]);
    [self.conditionLock unlockWithCondition:3];
}

- (void)task3 {
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"执行任务3 - %@", [NSThread currentThread]);
    [self.conditionLock unlock];
}

打印结果:
执行任务1 - <NSThread: 0x600003c0b340>{number = 5, name = (null)}
执行任务2 - <NSThread: 0x600003c08900>{number = 6, name = (null)}
执行任务3 - <NSThread: 0x600003c09180>{number = 7, name = (null)}
  • 初始化NSConditonLock对象,并设置条件为1。
  • 当条件为1时,任务1才会执行,并加锁,当任务1执行完毕,解锁,将条件设置为2
  • 任务2想要执行,必须当条件为2时才可以,否则会一直等待,当发现条件为2,执行任务2,任务2执行结束,解锁将条件设置为3
  • 任务3发现条件为3,加锁操作,执行任务3,如果任务3之后没有其他任务需要依赖任务3的执行结果,那么就可以直接解锁。

注意:

  1. 在初始化NSConditonLock对象时,如果不传入条件[NSConditionLock alloc] init],默认的条件就是0。
  2. 如果加锁操作,不传入条件,那么不会根据条件来进行加锁操作,同理 ,如果解锁操作不设置任何条件,直接解锁。

2.1.9 使用串行队列,实现线程同步

  • 使用串行队列,同样可以实现线程的同步,只要保证不在同一时间读写同一块资源即可。
- (void)viewDidLoad {
    [super viewDidLoad];
		// 创建串行队列    
    self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
    [self executeMoneyTask];
    [self executeTicketTask];
}

/// 卖票
- (void)saleTicket {
    
    dispatch_sync(self.serialQueue, ^{
        
        int oldTickets = self.tickets;
        oldTickets -= 1;
        self.tickets = oldTickets;
        NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
        
    });
    
}

/// 存钱操作
- (void)saveMoney {
    dispatch_sync(self.serialQueue, ^{
        
        int oldMoney = self.money;
        oldMoney += 100;
        self.money = oldMoney;
        NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
        
    });
    
}

/// 取钱操作
- (void)drawMoney {
    dispatch_sync(self.serialQueue, ^{
        int oldMoney = self.money;
        oldMoney -= 50;
        self.money = oldMoney;
        NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
    });
}

2.1.10 semaphore信号量实现线程同步

  • semaphore可以控制线程的最大并发数
  • 假设我们需要创建20条线程去执行任务,设置semaphore的值为5,就可以控制最多只有5条线程在执行任务
  • dispatch_semaphore_wait函数会对semaphore的值进行判断
  • 如果semaphore的值 > 0,就让semaphore的值减1,继续执行下面的代码
  • 如果semaphore的值 <= 0,就会休眠等待,直到semaphore的值>0,继续让semaphore的值减1,继续执行下面的代码
  • dispatch_semaphore_signal会使semaphore的值加1
  • 下面的代码打印的效果就是,每隔2s进行5次打印。
- (void)viewDidLoad {
    [super viewDidLoad];
 		// 初始化信号量,并设置信号量的值为5
    self.semaphore = dispatch_semaphore_create(5);
    [self test];
    
}

- (void)test {
    // 创建20个线程
    for (int i = 0; i < 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(executeTask) object:nil] start];
    }
}

- (void)executeTask {
   
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    sleep(2);
    NSLog(@"%@", [NSThread currentThread]);
    dispatch_semaphore_signal(self.semaphore);
}
  • 上面的代码展示了使用semaphore可以控制最大并发数量,那么如何利用semaphore实现线程同步呢?
  • 其实只需要设置semaphore的初始值为1,就可以了,同时最多只有1条线程执行任务,通过设置semaphore的值加1和减1,进而控制同一时间只能有一个线程执行任务,最终实现线程同步。
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.moneySemaphore = dispatch_semaphore_create(1);
    self.ticketSemaphore = dispatch_semaphore_create(1);
    
    [self executeMoneyTask];
    [self executeTicketTask];
}

- (void)saleTicket {
    
    dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
    int oldTickets = self.tickets;
    oldTickets -= 1;
    self.tickets = oldTickets;
    NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
    dispatch_semaphore_signal(self.ticketSemaphore);
}

/// 存钱操作
- (void)saveMoney {
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    int oldMoney = self.money;
    oldMoney += 100;
    self.money = oldMoney;
    NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
    dispatch_semaphore_signal(self.moneySemaphore);
}

/// 取钱操作
- (void)drawMoney {
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    int oldMoney = self.money;
    oldMoney -= 50;
    self.money = oldMoney;
    NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);

    dispatch_semaphore_signal(self.moneySemaphore);

}

2.1.11 @synchronized

  • 使用@synchronized(obj)就可以实现加锁的操作
  • @synchronized会根据传入obj对象,来获取一把锁,只要保证obj对象是同一个对象,就可以保证是同一把锁,代码块的左侧大括号开始就进行加锁操作,直到操作结束,到右侧大括号解锁操作。
  • @synchronized性能很低,在开发中不推荐使用
- (void)saleTicket {
    
    @synchronized (self) {
        int oldTickets = self.tickets;
        oldTickets -= 1;
        self.tickets = oldTickets;
        NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
    }
}
/// 存钱操作
- (void)saveMoney {
    @synchronized (self) {
        int oldMoney = self.money;
        oldMoney += 100;
        self.money = oldMoney;
        NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
    }
}

/// 取钱操作
- (void)drawMoney {

    @synchronized (self) {
        int oldMoney = self.money;
        oldMoney -= 50;
        self.money = oldMoney;
        NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
    }
}

注意:

@synchronized() 传入参数,不一定非得是self,只要保证是同一个对象即可。