一、前言

前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronizedNSLockdispatch_semaphoreNSConditionpthread_mutexOSSpinLock。后来网上查了一下,发现他们的实现机制各不相同,性能也各不一样。下面我们先分别介绍每个加锁方式的使用,在使用一个案例来对他们进行性能对比。

二、非线程安全

举例说明:两个火车票销售窗口 共同销售车站总共的50张车票。看代码你最明白。

/**
 * 非线程安全
 * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"------开始放票了---%@",[NSThread currentThread]);  // 打印当前线程
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(非线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
        
        
    }
}

输出结果:

------开始放票了---<NSThread: 0x600003b98b00>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
......
剩余票数:7 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:5 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:6 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
剩余票数:1 窗口:<NSThread: 0x600003bcc080>{number = 3, name = (null)}
所有火车票均已售完
剩余票数:0 窗口:<NSThread: 0x600003bc6980>{number = 5, name = (null)}
所有火车票均已售完

可以看到在不考虑线程安全,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。防止两条线程同时对此任务进行编辑,每次只能有一条线程执行此任务。所以就用到了线程加锁

三、介绍与使用

2.1、@synchronized互斥锁

/**
 * 线程安全:使用 @synchronized  加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"------开始放票了---%@",[NSThread currentThread]);  // 打印当前线程
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        @synchronized(self) {
            if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
                self.ticketSurplusCount--;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
                [NSThread sleepForTimeInterval:0.2];
            } else { //如果已卖完,关闭售票窗口
                NSLog(@"所有火车票均已售完");
                break;
            }
        }
    }
}

输出结果:

------开始放票了---<NSThread: 0x60000018a0c0>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:47 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:46 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
剩余票数:45 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:3 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x6000001deec0>{number = 4, name = (null)}
剩余票数:0 窗口:<NSThread: 0x6000001d4d40>{number = 5, name = (null)}
所有火车票均已售完
所有火车票均已售完

@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。@synchronized 还有一个好处就是不用担心忘记解锁了。

2.2、dispatch_semaphore

/**
 * 线程安全:使用 semaphore  加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"------开始放票了---%@",[NSThread currentThread]);  // 打印当前线程
    
    semaphoreLock = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

输出结果:

------开始放票了---<NSThread: 0x600002c792c0>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
剩余票数:1 窗口:<NSThread: 0x600002c28d00>{number = 4, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600002c15100>{number = 6, name = (null)}
所有火车票均已售完
所有火车票均已售完

dispatch_semaphore 是 GCD 用来同步的一种方式,dispatch_semaphore_create是创建信号量,dispatch_semaphore_wait是等待信号,dispatch_semaphore_signal是发送信号。

详细请看 iOS多线程:GCD (三)

2.3、NSLock对象锁

/**
 * 线程安全:使用 NSLock  加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"------开始放票了---%@",[NSThread currentThread]);  // 打印当前线程
    

    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.gorpeln.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.gorpeln.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        // 相当于加锁
        [_lock lock];
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
        [_lock unlock];
    }
}

输出结果:

------开始放票了---<NSThread: 0x600000608b80>{number = 1, name = main}
剩余票数:49 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:48 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:47 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
......
剩余票数:4 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x60000066dd40>{number = 6, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600000651c80>{number = 4, name = (null)}
所有火车票均已售完
所有火车票均已售完

NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lockunlock方法外,NSLock还提供了tryLocklockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

2.4、NSRecursiveLock递归锁

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。我们先来看一个示例:

NSLock *lock = [[NSLock alloc] init];
 
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
    static void (^RecursiveMethod)(int);
    RecursiveMethod = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };
 
    RecursiveMethod(5);
});

这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息:

输出结果:

value = 5
*** -[NSLock lock]: deadlock ( '(null)')   *** Break on _NSLockError() to debug.

在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

所以,对上面的代码进行一下改造,

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

这样,程序就能正常运行了,其输出如下所示:

value = 5
value = 4
value = 3
value = 2
value = 1

2.5、NSConditionLock条件锁

//初始化锁时,指定一个默认的条件
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
NSMutableArray *products = [NSMutableArray array];
    
NSInteger HAS_DATA = 1; //条件一: 有数据
NSInteger NO_DATA = 0;  //条件二: 没有数据
    
//生产者,加锁与解锁的过程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (1) {
        [lock lockWhenCondition:NO_DATA];//1. 当满足 【没有数据的条件时】进行加锁
        [products addObject:[[NSObject alloc] init]];//2. 生产者生成数据
        NSLog(@"produce a product,总量:%zi",products.count);
        [lock unlockWithCondition:HAS_DATA];//3. 解锁,并设置新的条件,已经有数据了
        sleep(1);
    }
    
});

//消费者,加锁与解锁的过程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (1) {
        NSLog(@"wait for product");
        [lock lockWhenCondition:HAS_DATA];//1. 当满足 【有数据的条件时】进行加锁
        [products removeObjectAtIndex:0];//2. 消费者消费数据
        NSLog(@"custome a product");
        [lock unlockWithCondition:NO_DATA];//3. 解锁,并设置新的条件,没有数据了
    }
    
});

输出结果:

wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
......

当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:

在线程1中的加锁使用了lock,所以是不需要条件的,所以顺利的就锁住了,但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock也跟其它的锁一样,是需要lock与unlock对应的,只是lock,lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的,当然这是与你的需求相关的。

2.6、NSCondition

NSCondition *condition = [[NSCondition alloc] init];

NSMutableArray *products = [NSMutableArray array];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (1) {
        [condition lock];
        if ([products count] == 0) {
            NSLog(@"wait for product");
            [condition wait];
        }
        [products removeObjectAtIndex:0];
        NSLog(@"custome a product");
        [condition unlock];
    }

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (1) {
        [condition lock];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product,总量:%zi",products.count);
        [condition signal];
        [condition unlock];
        sleep(1);
    }

});

输出结果:

wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
produce a product,总量:1
custome a product
wait for product
......

一种最基本的条件锁。手动控制线程wait和signal。
[condition lock];一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问
[condition unlock];与lock 同时使用
[condition wait];让当前线程处于等待状态
[condition signal];CPU发信号告诉线程不用在等待,可以继续执行

不同点:
NSCondition条件量,需要一个外部共享变量,来探测条件是否满足
NSConditionLock条件锁, 不需要,条件锁自带一个探测条件,是否满足

2.7、pthread_mutex

C 语言下多线程加互斥锁的方式,那来段 C 风格的示例代码,需要 #import <pthread.h>

__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&theLock);
        NSLog(@"需要线程同步的操作1 开始");
        sleep(3);
        NSLog(@"需要线程同步的操作1 结束");
        pthread_mutex_unlock(&theLock);
    
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        pthread_mutex_lock(&theLock);
        NSLog(@"需要线程同步的操作2");
        pthread_mutex_unlock(&theLock);
    
});

输出结果:

需要线程同步的操作1 开始
需要线程同步的操作1 结束
需要线程同步的操作2

1:pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr); 初始化锁变量mutex。attr为锁属性,NULL值为默认属性。
2:pthread_mutex_lock(pthread_mutex_t* mutex);加锁
3:pthread_mutex_tylock(pthread_mutex_t* mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
4:pthread_mutex_unlock(pthread_mutex_t* mutex);释放锁
5:pthread_mutex_destroy(pthread_mutex_t* *mutex);使用完后释放

2.8、pthread_mutex(recursive)

__block pthread_mutex_t theLock;
    
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&theLock, &attr);
pthread_mutexattr_destroy(&attr);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveMethod)(int);
    
    RecursiveMethod = ^(int value) {
        
        pthread_mutex_lock(&theLock);
        if (value > 0) {
            
            NSLog(@"value = %d", value);
            sleep(1);
            RecursiveMethod(value - 1);
        }
        pthread_mutex_unlock(&theLock);
    };
    
    RecursiveMethod(5);
});

输出结果:

value = 5
value = 4
value = 3
value = 2
value = 1

这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。

如果使用pthread_mutex_init(&theLock, NULL);初始化锁的话,上面的代码会出现死锁现象。如果使用递归锁的形式,则没有问题。

2.9、OSSpinLock

//#import <libkern/OSAtomic.h>

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"需要线程同步的操作1 开始");
    sleep(3);
    NSLog(@"需要线程同步的操作1 结束");
    OSSpinLockUnlock(&theLock);
    
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    sleep(1);
    NSLog(@"需要线程同步的操作2");
    OSSpinLockUnlock(&theLock);
    
});

输出结果:

需要线程同步的操作1 开始
需要线程同步的操作1 结束
需要线程同步的操作2

OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。

四、性能对比

20200626143738115