GCD 与线程安全使用

2,180 阅读15分钟

常见的多线程方案

类型简介线程生命周期
NSThread简单的线程使用, 直接操作线程对象自己管理
GCD替换 NSThread 的方案, 简单易用,底部是C语言实现系统管理
NSOperation对 GCD 的封装,更加的面向对象系统管理

队列

GCD 中有串行队列和并发队列两种类型。

  • 串行队列(Dispatch Serial Queue):任务 FIFO(先进先出),一次只能执行一个任务,也就是说执行完上一个任务才能执行下一个任务;

  • 并发队列(Dispatch Concurrent Queue):任务 FIFO,一次可能执行多个任务,但是任务的完成顺序不确定。

使用 DISPATCH_QUEUE_CONCURRENT 关键字, 创新自定义并发队列:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("queue-concurrent", DISPATCH_QUEUE_CONCURRENT);

使用 DISPATCH_QUEUE_SERIAL 关键字,创新自定义串行队列:

    dispatch_queue_t serailQueue = dispatch_queue_create("queue-serial", DISPATCH_QUEUE_SERIAL);

任务

任务的执行有同步执行和异步执行。

  • 同步执行(dispatch_sync):任务提交给队列后会卡在当前线程,等待任务执行完毕才往下走;

  • 异步执行 (dispatch_async):任务提交给队列后不等待返回结果,直接往下执行代码。

任务在队列中的执行情况

任务类型并发队列自定义串行队列主队列
同步执行串行执行任务;没有开启新线程串行执行任务;没有开启新线程串行执行任务;没有开启新线程
异步执行并发执行任务;有开启新线程串行执行任务;有开启新线程串行执行任务;没有开启新线程

由于主队列的任务都是在主线程执行的,而且它是一个串行队列,所以在执行任务的时候,不管是同步任务还是异步任务都不会开启新线程,直接在主线程执行。

而同步任务会在当前线程执行。也就是说如果队列里面有 任务A 正在执行,又刚刚好往这个任务里面添加了 同步任务B 那么 任务A 会先等这个 同步任务B 在当前线程执行完毕才往下走。

GCD 的一些使用

GCD 默认创建了一条串行主队列和多条不同优先级全局并发队列。

系统主队列的简单使用

获取主队列: dispatch_get_main_queue()

//  viewDidLoad 方法在主线程执行
- (void)viewDidLoad {
    [super viewDidLoad];

    // 系统默认的主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
        
    // 添加异步任务到队列
    // 主队列不开启其它线程执行异步任务,直接在主线程执行
    dispatch_async(queue, ^{
        // 输出: Async task, thread = <NSThread: 0x6000035f89c0>{number = 1, name = main}
        NSLog(@"Async task, thread = %@", [NSThread currentThread]);
    });
    
    // 添加同步任务到队列
    // 主队列添加同步任务,造成任务间的互相等待,导致死锁,崩溃
    /*
    dispatch_sync(queue, ^{
        NSLog(@"Sync task, thread = %@", [NSThread currentThread]);
    });*/
    
 }

在实际使用中,往往会在子线程执行完一些耗时操作以后,会在子线程中异步回到主队列: dispatch_async(main queue, ^{ ... }

系统全局并发队列的简单使用

获取系统默认的全局并发队列: dispatch_get_global_queue(...)

    // 系统默认的全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
    
    // 添加异步任务到队列
    // 由于是在主线程添加测试代码的,所以当前线程是主线程,不开启子线程执行这个同步任务
    dispatch_sync(queue, ^{
        // 输出: Sync task, thread = <NSThread: 0x6000036b0a00>{number = 1, name = main}
        NSLog(@"Sync task, thread = %@", [NSThread currentThread]);
    });

    // 添加同步任务到队列
    // 开启子线程异步执行任务
    dispatch_async(queue, ^{
        // 输出: Async task, thread = <NSThread: 0x60000225e0c0>{number = 7, name = (null)}
        NSLog(@"Async task, thread = %@", [NSThread currentThread]);
    });

系统全局并发队列优先级类型:

__QOS_ENUM(qos_class, unsigned int,
	QOS_CLASS_USER_INTERACTIVE
	QOS_CLASS_USER_INITIATED
	QOS_CLASS_DEFAULT
	QOS_CLASS_UTILITY
	QOS_CLASS_BACKGROUND
	QOS_CLASS_UNSPECIFIED
);

主线程死锁的一些说明

在上面的代码我们看到,如果当前线程是主线程,它正在执行 viewDidLoad 方法(可以看成是一个任务),之后又往主队列添加一个同步任务A,导致死锁。之所以会这样是因为主队列里面的同步任务会等待执行结果返回再执行其它任务,而主队列里面一次只能执行一个任务,此时正在执行的是 viewDidLoad 任务,导致 viewDidLoad 任务和 同步任务A互相等待,导致死锁(一个串行队列里面有两个同步任务互相等待)。

那么是不是就不能在主队列里面添加同步任务呢?当然不是,看下面一个例子:

    // 添加异步任务到全局并发队列
    // 开启子线程异步执行任务
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        NSLog(@"Async task, thread = %@", [NSThread currentThread]);
        
        // 添加同步任务到主队列
        // 从子线程回到主线程
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"Sync task, thread = %@", [NSThread currentThread]);
        });
    });

输出:

2021-01-24 21:56:54.547770+0800 Thread[96249:4339844] Async task, thread = <NSThread: 0x600000ca4040>{number = 5, name = (null)}
2021-01-24 21:56:54.557873+0800 Thread[96249:4339703] Sync task, thread = <NSThread: 0x600000ccc9c0>{number = 1, name = main}

这是因为一个并发队列里面的异步任务和主队列里面的同步任务不会互相等待造成死锁(两个队列里面的任务)。

dispatch_group (任务组)

一个需求:

需要异步并发多个耗时子任务,等所有子任务执行完毕以后再回到主线程刷新结果。

使用 GCD 的 dispatch_group(任务组)我们能够很容易实现上面的需求,示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
        
    // 创建任务组
    dispatch_group_t group = dispatch_group_create();
    
    // 创建两条不同的并发队列
    dispatch_queue_t queue1 = dispatch_queue_create("queue-concurrent-1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("queue-concurrent-2", DISPATCH_QUEUE_CONCURRENT);
    
    // 在 queue1 中添加异步任务,把任务标记给任务组
    dispatch_group_async(group, queue1, ^{
        NSLog(@"Asycn task 1, thread = %@", [NSThread currentThread]);
    });
    
    // 在 queue1 中添加异步任务,把任务标记给任务组
    dispatch_group_async(group, queue1, ^{
        NSLog(@"Asycn task 2, thread = %@", [NSThread currentThread]);
    });
    
    // 在 queue2 中添加异步任务,把任务标记给任务组
    dispatch_group_async(group, queue2, ^{
        NSLog(@"Asycn task 3, thread = %@", [NSThread currentThread]);
    });
    
    // 等所有队列里面标记的任务都完成后,通知任务组,回到主队列
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"All task done, thread = %@", [NSThread currentThread]);
    });
    
}

输出:

2021-01-24 22:12:09.958925+0800 Thread[97240:4372084] Asycn task 1, thread = <NSThread: 0x600002a452c0>{number = 3, name = (null)}
2021-01-24 22:12:09.958959+0800 Thread[97240:4372078] Asycn task 2, thread = <NSThread: 0x600002a0d640>{number = 4, name = (null)}
2021-01-24 22:12:09.958981+0800 Thread[97240:4372089] Asycn task 3, thread = <NSThread: 0x600002a0da40>{number = 5, name = (null)}
2021-01-24 22:12:09.970319+0800 Thread[97240:4371289] All task done, thread = <NSThread: 0x600002a40400>{number = 1, name = main}

可以看到任务执行开辟了3条子线程,等所有任务都异步执行完毕以后再回到主线程。

线程的安全使用

虽然在使用多线程的时候能够很好的利用设备的性能,但是当多个线程同时访问同一块资源的时候容易产生数据错乱问题,这就涉及对敏感资源的线程同步了。

线程同步方案经常使用直接加锁的方式实现,或者串行队列等(串行队列一次只能执行一个任务)。

查看非线程安全的例子

@interface BaseTest : NSObject

- (void)ticketTest;
- (void)moneyTest;

// 子类覆盖测试
- (void)sellTicket;
- (void)drawMoney;
- (void)saveMoney;

@end
#import "BaseTest.h"

@interface BaseTest()

@property (nonatomic, assign) NSInteger ticketCount;
@property (nonatomic, assign) NSInteger totalMoney;

@end

@implementation BaseTest

- (void)ticketTest {
    self.ticketCount = 20;
    
    dispatch_queue_t ticketQueue = dispatch_queue_create("ticket queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 20; i ++) {
        dispatch_async(ticketQueue, ^{
            [self sellTicket];
        });
    }
}

- (void)moneyTest {
    self.totalMoney = 1000;
    
    dispatch_queue_t moneyQueue = dispatch_queue_create("money queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 20; i ++) {
        dispatch_async(moneyQueue, ^{
            i % 2 == 0 ? [self drawMoney] : [self saveMoney];
        });
    }
}

// 买票
- (void)sellTicket {
    NSInteger old = self.ticketCount;
    sleep(arc4random()%20*0.1); // 睡眠,模拟耗时任务执行
    old --;
    self.ticketCount = old;
    NSLog(@"卖了一张票,剩余:%ld, 当前线程: %@", self.ticketCount, [NSThread currentThread]);
}

// 取钱
- (void)drawMoney {
    NSInteger old = self.totalMoney;
    sleep(arc4random()%20*0.1); // 睡眠,模拟耗时任务执行
    old -= 50;
    self.totalMoney = old;
    NSLog(@"取钱50,剩余 %ld, 当前线程: %@", self.totalMoney, [NSThread currentThread]);
}

// 存钱
- (void)saveMoney {
    NSInteger old = self.totalMoney;
    sleep(arc4random()%20*0.1); // 睡眠,模拟耗时任务执行
    old += 100;
    self.totalMoney = old;
    NSLog(@"存100,剩余 %ld, 当前线程: %@", self.totalMoney, [NSThread currentThread]);
}

@end

上面代码总共添加了两个测试例子,一个卖票例子,一个存取钱例子。

在卖票的例子中,初始共有 20 张票,卖票的操作是 for 循环 20 次,把任务异步提交给了一个自定义并发队列。理想卖完票的结束结果是 0,

存取钱的例子中,初始金额为 1000。每次存钱100,共存10次;每次取钱50,共取10次。具体的执行操作是把任务异步提交给自定义并发队列处理。 理想结束存取钱的操作结果是 1500

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    BaseTest *test = [[BaseTest alloc] init];
    
    // 测试的时候可以注释下面某个方法,单独查看输出
    [test ticketTest];
    [test moneyTest];
}

测试卖票输出:

... 
more
... 
more
...
2021-01-28 22:14:00.459163+0800 Thread[52454:5737609] 卖了一张票,剩余:16, 当前线程: <NSThread: 0x600001528000>{number = 12, name = (null)}
2021-01-28 22:14:00.459224+0800 Thread[52454:5737622] 卖了一张票,剩余:11, 当前线程: <NSThread: 0x600001514d00>{number = 13, name = (null)}
2021-01-28 22:14:00.459273+0800 Thread[52454:5737624] 卖了一张票,剩余:9, 当前线程: <NSThread: 0x60000153d6c0>{number = 14, name = (null)}

测试存取钱输出:

... 
more
... 
more
... 
2021-01-28 22:14:31.413352+0800 Thread[52494:5738835] 存100,剩余 1000, 当前线程: <NSThread: 0x6000006d3e40>{number = 16, name = (null)}
2021-01-28 22:14:31.413436+0800 Thread[52494:5738838] 存100,剩余 900, 当前线程: <NSThread: 0x600000695900>{number = 19, name = (null)}
2021-01-28 22:14:31.413444+0800 Thread[52494:5738839] 取钱50,剩余 750, 当前线程: <NSThread: 0x600000698240>{number = 18, name = (null)}

可以看到卖票和存取钱都开启了多条线程执行。卖票结束以后不等于0,存取钱操作结束以后也不等于1500。这是非线程安全操作。

之所以会这样,是因为卖票时写操作同一时刻有多条线程操作,导致票数的读取写入不一样,也就不能保证线程安全了(存取钱也是一样)。

对于卖票:如果能保证同一时刻票的卖出(修改票的数量,为写操作)只有一条线程执行,当其它线程也要来执行卖票操作的时候先在外面等待,等待之前的线程执行完卖票操作以后再执行自己的,这样就能保证线程安全了。

对于存取钱:保证存钱和取钱同一时刻只能进行一个,只有执行完上一次取钱才能执行下一次操作,或者只有执行完上一次的存钱才能执行下一次操作,这样也就能保证线程安全了。

下面实现一些线程安全的例子。

GCD API 的使用

dispatch_serial_queue

我们知道,串行队列是 FIFO类型,一次只能执行一个任务,下一个任务的执行需要等待前一个执行完毕才开始。那么可以把卖票的任务添加到串行队列里面,这样就能让多个线程提交的卖票任务一个接着一个执行。

创建一个子类 SerialQueueTest 继承上面的测试类 BaseTest:

代码:

@interface SerialQueueTest : BaseTest

@end

@interface SerialQueueTest()

@property (nonatomic, strong) dispatch_queue_t serialTicketQueue; // 卖票串行队列
@property (nonatomic, strong) dispatch_queue_t serialMoneyQueue;  // 存取钱串行队列

@end

@implementation SerialQueueTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化
        _serialTicketQueue = dispatch_queue_create("ticket serial queue", DISPATCH_QUEUE_SERIAL);
        _serialMoneyQueue = dispatch_queue_create("money serial queue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

// 重写父类的卖票,把卖票操作添加到串行队列
- (void)sellTicket {
    dispatch_sync(self.serialTicketQueue, ^{
        [super sellTicket];
    });
}

// 重写父类的取钱,把取钱操作添加到串行队列
- (void)drawMoney {
    dispatch_sync(self.serialMoneyQueue, ^{
        [super drawMoney];
    });
}

// 重写父类的存钱,把存钱操作添加到串行队列
- (void)saveMoney {
    dispatch_sync(self.serialMoneyQueue, ^{
        [super saveMoney];
    });
}

@end

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    SerialQueueTest *test = [[SerialQueueTest alloc] init];
    
    [test ticketTest];
    [test moneyTest];
}

测试卖票输出:

...
...
...
2021-01-28 22:12:41.360656+0800 Thread[52396:5735367] 卖了一张票,剩余:2, 当前线程: <NSThread: 0x600002925340>{number = 22, name = (null)}
2021-01-28 22:12:42.366275+0800 Thread[52396:5735368] 卖了一张票,剩余:1, 当前线程: <NSThread: 0x60000294e980>{number = 23, name = (null)}
2021-01-28 22:12:42.366823+0800 Thread[52396:5735369] 卖了一张票,剩余:0, 当前线程: <NSThread: 0x600002948a00>{number = 24, name = (null)}

测试存取钱输出:

...
...
...
2021-01-28 22:11:22.405259+0800 Thread[52333:5733877] 存100,剩余 1450, 当前线程: <NSThread: 0x60000218c340>{number = 22, name = (null)}
2021-01-28 22:11:23.406686+0800 Thread[52333:5733878] 取钱50,剩余 1400, 当前线程: <NSThread: 0x6000021c9680>{number = 23, name = (null)}
2021-01-28 22:11:24.407951+0800 Thread[52333:5733879] 存100,剩余 1500, 当前线程: <NSThread: 0x6000021e4840>{number = 24, name = (null)}

卖票结束,结果为0;存取钱结束,结果为 1500。完成线程安全操作。

dispatch_semaphore

API:

dispatch_semaphore_create 创新信号量,初始值为几,则可最多几条线程同时执行;

dispatch_semaphore_wait 执行后信号量值-1,如果结果 <0,则等待,当前线程休眠,等到信号量执行完的结果值 >=0 的时候往下执行;

dispatch_semaphore_signal 执行完后信号量+1,如果前面有线程的等待信号量 <0, 此时唤醒之前等待的线程,等待的线程如果满足执行条件,开始执行代码。

需要注意的是 dispatch_semaphore_wait 和 dispatch_semaphore_signal 需要配对使用。

创建一个子类 SemaphoreTest 继承测试类 BaseTest:

代码:

@interface SemaphoreTest : BaseTest

@end
@interface SemaphoreTest()

@property (nonatomic, strong) dispatch_semaphore_t ticketSemaphore; // 卖票串行队列
@property (nonatomic, strong) dispatch_semaphore_t moneySemaphore;  // 存取钱串行队列

@end

@implementation SemaphoreTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化
        _ticketSemaphore = dispatch_semaphore_create(1); // 初始一次只有一条线程可使用(卖票操作)
        _moneySemaphore = dispatch_semaphore_create(1); // 初始一次只有一条线程可使用(存取钱操作)
    }
    return self;
}

- (void)sellTicket {
    // 信号量-1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    [super sellTicket];
    // 信号量+1
    dispatch_semaphore_signal(_ticketSemaphore);
}

- (void)drawMoney {
    // 信号量-1
    dispatch_semaphore_wait(_moneySemaphore, DISPATCH_TIME_FOREVER);
    [super drawMoney];
    // 信号量+1
    dispatch_semaphore_signal(_moneySemaphore);
}

- (void)saveMoney {
    // 信号量-1
    dispatch_semaphore_wait(_moneySemaphore, DISPATCH_TIME_FOREVER);
    [super saveMoney];
    // 信号量+1
    dispatch_semaphore_signal(_moneySemaphore);
}

@end

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    SerialQueueTest *test = [[SerialQueueTest alloc] init];
    
    [test ticketTest];
    [test moneyTest];
}

测试卖票输出:

...
...
...
2021-01-28 23:12:11.711836+0800 Thread[54623:5777772] 卖了一张票,剩余:2, 当前线程: <NSThread: 0x600000fecd80>{number = 22, name = (null)}
2021-01-28 23:12:12.714759+0800 Thread[54623:5777773] 卖了一张票,剩余:1, 当前线程: <NSThread: 0x600000fe0100>{number = 23, name = (null)}
2021-01-28 23:12:13.719543+0800 Thread[54623:5777774] 卖了一张票,剩余:0, 当前线程: <NSThread: 0x600000fb5a80>{number = 24, name = (null)}

测试存取钱输出:

...
...
...
2021-01-28 23:12:41.229688+0800 Thread[54645:5778717] 存100,剩余 1450, 当前线程: <NSThread: 0x600002bf2500>{number = 22, name = (null)}
2021-01-28 23:12:41.230035+0800 Thread[54645:5778718] 取钱50,剩余 1400, 当前线程: <NSThread: 0x600002bbcc80>{number = 23, name = (null)}
2021-01-28 23:12:42.233434+0800 Thread[54645:5778719] 存100,剩余 1500, 当前线程: <NSThread: 0x600002be9480>{number = 24, name = (null)}

卖票结束,结果为0;存取钱结束,结果为 1500。完成线程安全操作。

C 语言 API 的使用

介绍锁的时候,需要了解一下什么是互斥锁,什么是自旋锁。

互斥锁:等待锁的时候,线程会休眠; 自旋锁:等待锁的时候,线程会忙等(可看成一直在做while循环),一直占用CPU资源。

pthread_mutex

pthread_mutex 一个API层面是C语言的互斥锁。它的主要使用接口有:

// 初始化
pthread_mutex_init

// 加锁
pthread_mutex_lock

// 尝试加锁
pthread_mutex_trylock

// 解锁
pthread_mutex_unlock

// 销毁
pthread_mutex_destroy

锁的属性类型:

/*
 * 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

创建一个子类 MutexTest 继承测试类 BaseTest:

@interface MutexTest : BaseTest

@end

#import <pthread.h>

@interface MutexTest()

@property (nonatomic, assign) pthread_mutex_t ticketMutex;
@property (nonatomic, assign) pthread_mutex_t moneyMutex;

@end

@implementation MutexTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化
        [self configureMutex:&_ticketMutex];
        [self configureMutex:&_moneyMutex];
    }
    return self;
}

- (void)configureMutex:(pthread_mutex_t *)mutex {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 普通类型
    // 初始化锁
    pthread_mutex_init(mutex,  &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
}

- (void)sellTicket {
    pthread_mutex_lock(&_ticketMutex);
    [super sellTicket];
    pthread_mutex_unlock(&_ticketMutex);
}

- (void)drawMoney {
    pthread_mutex_lock(&_moneyMutex);
    [super drawMoney];
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)saveMoney {
    pthread_mutex_lock(&_moneyMutex);
    [super saveMoney];
    pthread_mutex_unlock(&_moneyMutex);
}

// 销毁
- (void)dealloc {
    pthread_mutex_destroy(&_ticketMutex);
    pthread_mutex_destroy(&_moneyMutex);
}

@end

上面创建的是普通类型属性的互斥锁,查看测试结果可看出这样也是能实现线程安全的。

有一个问题就是,如果线程访问某个方法以后已经被自己加锁了,之后如果需要没解锁前再次递归调用加锁方法,那么由于方法已经被加锁了,此时加锁失败,线程休眠,也就永远不会释放之前加的锁了。解决这个问题的方式是把互斥锁的属性类型设置为递归类型属性 PTHREAD_MUTEX_RECURSIVE。那么也就能让同一个线程反复加锁了。

属性类型这样设置:

    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 递归类型

验证是否互斥锁:

前面说过,互斥锁会在线程等待解锁的时候跑去休眠。而且 pthread_mutex 就是一个互斥锁,那么怎么验证呢?

如下图所示:

以卖票的例子,总共开两条线程测试。 打上所示断点,放开第一个断点(此时第一条线程执行代码,加锁),第二条线程来了以后,调整断点信息到汇编格式,一路调试,最后可以看到第二条线程加锁失败以后,会调用一个 syscall 的系统底层方法,之后app断点信息就结束。所以可以看到第二条线程直接就不运行了(表明CPU直接就不分配资源给它了),此时线程就是休眠状态了。

(查看自旋锁类型也可以使用类似调试方法,打断点查看结果是否最终汇编代码会在一个 while 循环一直执行等待可以加锁的条件,如果是那样,那么这个锁就是一个自旋锁)

配合互斥锁使用的 pthread_cond 条件

pthread_cond 的一些API:

// 初始化条件
pthread_cond_init

// 等待条件
pthread_cond_wait

// 激活一个等待该条件的线程
pthread_cond_signal

// 激活所有等待该条件的线程
pthread_cond_broadcast

OC API 的一些锁

  • NSLock: NSLock 是对 pthread_mutex 普通锁的封装

  • NSRecursiveLock: NSRecursiveLock 是对 pthread_mutex 递归锁的封装,API 跟 NSLock 基本一致

  • NSCondition: NSCondition 是对 pthread_mutex 和 pthread_cond 的封装

  • NSConditionLock: NSConditionLock 是对 NSCondition的 进一步封装,可以设置具体的条件值

上面4种锁其实主要是对 pthread_mutex 互斥锁的一个封装,让它们能够以对象类型使用。

它们都实现了 NSLocking 协议,加锁和解锁。

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

下面是一个 NSLock 加锁的简单实现:

@interface LockTest : BaseTest

@end

@interface LockTest()

@property (nonatomic, strong) NSLock *ticketLock;
@property (nonatomic, strong) NSLock *moneyLock;

@end

@implementation LockTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化
        _ticketLock = [[NSLock alloc] init];
        _moneyLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)sellTicket {
    [self.ticketLock lock];
    [super sellTicket];
    [self.ticketLock unlock];
}

- (void)drawMoney {
    [self.moneyLock lock];
    [super drawMoney];
    [self.moneyLock unlock];
}

- (void)saveMoney {
    [self.moneyLock lock];
    [super saveMoney];
    [self.moneyLock unlock];
}

@end

运行加锁代码,查看测试结果,可以看到卖票和存取钱操作也是能实现线程安全的。

线程安全使用方案

在实际的使用过程其实使用 GCD 的串行队列和信号量就能很好的解决问题,如果想自己有更多的控制,则可以考虑直接使用 pthread_mutex 互斥锁。

补充

  • @synchronized 是对 pthread_mutex 递归锁的封装;

  • atomic 在对属性进行修饰的时候只是用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁。但是在使用过程中比如对数组的操作不一定线程安全。

使用 GCD 的 dispatch_barrier 实现线程的多读单写

多读单写问题:

  • 同一时间,只能有1个线程进行写的操作;
  • 同一时间,允许有多个线程进行读的操作;
  • 同一时间,不允许既有写的操作,又有读的操作。

下面是简单代码实现:

- (void)dispatchBarrierTaskTest {
    
    dispatch_queue_t queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        [self read];
    });
    dispatch_async(queue, ^{
        [self read];
    });
    dispatch_async(queue, ^{
        [self read];
    });
    dispatch_barrier_async(queue, ^{
        // 保证在之前添加队列的任务都执行完毕再执行这个任务
        // 之后添加到队列的任务要等这个栏栅任务结束以后才能开始执行
        [self write];
    });
    dispatch_async(queue, ^{
        [self read];
    });
    dispatch_async(queue, ^{
        [self read];
    });
}

- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write {
    sleep(1);
    NSLog(@"write");
}