【OC多线程】数据竞争问题与线程同步方案

目录
一、数据竞争问题
二、线程同步方案
  1、加锁
   1.1 自旋锁——OSSpinLock
   1.2 os_unfair_lock
   1.3 互斥锁——pthread_mutex普通锁、NSLock
   1.4 递归锁——pthread_mutex递归锁、NSRecursiveLock@synchronized
  2、设置线程的最大并发数为1
   2.1 用GCD信号量设置
   2.2 把多个线程放入GCD串行队列,让它们一个一个串行执行
  3、线程同步方案性能对比


一、数据竞争问题


多个线程并发执行,同一时间访问同一块数据(如同一个变量、对象、文件等)、且其中至少有一个线程是在对数据进行写操作(所有的线程都是读取的话就没事),就会出现数据竞争,从而导致数据错乱。

因为多个线程是并发执行的,我们无法确定它们到底是在哪一时刻访问这块数据,比如线程1正在修改这块数据的值,修改完之前,线程2又来修改这块数据的值了(即出现了数据竞争),那这块数据的值最终到底是被谁修改的我们无法确定,而且这块数据的值很有可能跟我们直观上以为的不一样(即导致了数据错乱)。

我们举“卖票”的例子来演示一下数据竞争导致数据错乱的问题。

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount; // 总票数

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 假设就剩10张票了
    self.totalTicketCount = 10;
    
    // 假设有10个窗口在卖票 --> 即多个线程
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self saleTicket];
        });
    }
}

- (void)saleTicket {
    
    // 卖一张票
    self.totalTicketCount--;
    
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
}

@end


// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 5, name = (null)}
剩余票数:7,{number = 7, name = (null)}
剩余票数:6,{number = 8, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 7, name = (null)}
剩余票数:1,{number = 8, name = (null)}
剩余票数:4,{number = 5, name = (null)}
剩余票数:5,{number = 4, name = (null)}
剩余票数:0,{number = 3, name = (null)}

我们直观上以为十条数据应该是“9、8、7、6、5、4、3、2、1、0”这样倒序排下来的,但从第五条数据起就全错了,这就是因为出现了数据竞争而导致了数据错乱。


二、线程同步方案


如何解决数据竞争呢?既然数据竞争是因为“多个线程并发执行,同一时间访问同一块数据”才出现的,而我们肯定还是要让“多个线程并发执行”(你当然可以直接不使用多线程技术,但那不就降低任务的执行效率了嘛),所以就只能从后半句下手。

同一时间只让多个线程中的一个线程访问这块数据,一个线程访问完再让下一个线程访问不就解决了嘛,这其实就是我们常说的线程同步,即线程一个接一个地挨个执行,而常用的线程同步方案有两种:加锁和设置线程的最大并发数为1。

1、加锁

加锁是指多个线程使用同一把锁,对访问数据的关键代码进行加锁。这样每个线程在访问数据之前都会看看锁是否锁上了,如果锁上了,线程就阻塞在锁这里等待(正是这个阻塞才达到了线程同步的效果),直到锁解开后才会继续执行,如果没锁上,就加锁并访问数据。不能每个线程都使用一把新锁,也最好不要无脑地对很多无关代码进行加锁。iOS常用的锁有:

1.1 自旋锁——OSSpinLock
  • 自旋锁是指“如果线程发现锁已经锁上了,就阻塞在锁这里等待”的这个“等待”是忙等(busy-wait),即线程会一直占用CPU资源来判断锁是否解开,相当于在这里写了个while循环。

  • 所以如果加锁代码是轻量级的数据访问(比如int数据简单的+1/-1操作,系统引用计数表那里不就使用的是自旋锁嘛),完全可以使用自旋锁,因为这种情况下互斥锁反复地休眠和唤醒反而更耗性能,还不如让线程忙等一小会儿呢。

  • 自旋锁在iOS10之后就被废弃了,因为它可能出现优先级反转问题,不是那么安全。比如低优先级的线程访问数据时加了锁,高优先级的线程第二次访问数据时CPU就会优先分配资源让它忙等,这样CPU就没有机会分配资源给低优先级的线程去访问数据和解锁了,这就进而导致高优先级的线程一直处于忙等,这就造成了死锁。苹果推荐使用os_unfair_lock替代它。

#import "ViewController.h"
#import  // 导入头文件

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) OSSpinLock spinLock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建锁
    self.spinLock = OS_SPINLOCK_INIT;
    
    self.totalTicketCount = 10;
    
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self saleTicket];
        });
    }
}

- (void)saleTicket {
    
    // 加锁
    OSSpinLockLock(&_spinLock);
    
    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
    
    // 解锁
    OSSpinLockUnlock(&_spinLock);
}

@end


// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 6, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 6, name = (null)}
剩余票数:4,{number = 6, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 7, name = (null)}
剩余票数:0,{number = 4, name = (null)}
1.2 os_unfair_lock
  • 苹果推荐使用os_unfair_lock替代自旋锁,不过要iOS10之后才能用。
  • os_unfair_lock其实也是一个互斥锁,因为它等待上一个线程解锁时也是休眠,而不是忙等(busy-wait)。
#import "ViewController.h"
#import  // 导入头文件

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) os_unfair_lock unfairLock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建锁
    self.unfairLock = OS_UNFAIR_LOCK_INIT;
    
    self.totalTicketCount = 10;
    
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self saleTicket];
        });
    }
}

- (void)saleTicket {
    
    // 加锁
    os_unfair_lock_lock(&_unfairLock);
    
    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
    
    // 解锁
    os_unfair_lock_unlock(&_unfairLock);
}

@end


// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 3, name = (null)}
剩余票数:7,{number = 5, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 4, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 9, name = (null)}
剩余票数:1,{number = 10, name = (null)}
剩余票数:0,{number = 11, name = (null)}
1.3 互斥锁——pthread_mutex普通锁、NSLock
  • 互斥锁是指“如果线程发现锁已经锁上了,就阻塞在锁这里等待”的这个“等待”是休眠,而不是忙等(busy-wait),即线程会进入休眠状态,而不会一直占用CPU资源来判断锁是否解开,锁解开后系统会唤醒该线程。

  • 所以如果加锁代码的任务量较大,使用互斥锁就比较划算,因为这种情况下自旋锁的忙等会很耗性能。

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

  • pthread_mutex普通锁
#import "ViewController.h"
#import  // 导入头文件

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) pthread_mutex_t mutexLock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // pthread_mutex普通锁
    // 创建锁
    pthread_mutex_init(&_mutexLock, &attr);
    
    self.totalTicketCount = 10;
    
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self saleTicket];
        });
    }
}

- (void)saleTicket {
    
    // 加锁
    pthread_mutex_lock(&_mutexLock);
    
    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
    
    // 解锁
    pthread_mutex_unlock(&_mutexLock);
}

- (void)dealloc {
    
    // 销毁锁
    pthread_mutex_destroy(&_mutexLock);
}

@end



// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 6, name = (null)}
剩余票数:7,{number = 7, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 3, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 8, name = (null)}
剩余票数:0,{number = 4, name = (null)}
  • NSLock
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, strong) NSLock *lock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建锁
    self.lock = [[NSLock alloc] init];
    
    self.totalTicketCount = 10;
    
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self saleTicket];
        });
    }
}

- (void)saleTicket {
    
    // 加锁
    [self.lock lock];
    
    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
    
    // 解锁
    [self.lock unlock];
}

@end


// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 5, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 6, name = (null)}
剩余票数:4,{number = 7, name = (null)}
剩余票数:3,{number = 8, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 4, name = (null)}
剩余票数:0,{number = 3, name = (null)}
1.4 递归锁——pthread_mutex递归锁、NSRecursiveLock@synchronized
  • 递归锁是指允许同一个线程对同一把锁重复加锁,但其它的线程看到上锁时该等还是得等,它可以加锁存在递归调用的代码,当然也可以加锁没有递归调用的代码。所有的递归锁都是互斥锁,它只是互斥锁分支出来可以加锁递归调用的锁,我们可以理解为递归锁继承于互斥锁。
  • NSRecursiveLock@synchronized都是对pthread_mutex递归锁的封装。
  • pthread_mutex递归锁
#import "ViewController.h"
#import  // 导入头文件

@interface ViewController ()

@property (nonatomic, assign) pthread_mutex_t mutexLock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // pthread_mutex递归锁
    // 创建锁
    pthread_mutex_init(&_mutexLock, &attr);
        
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self test];
        });
    }
}

- (void)test {
    
    // 加锁
    pthread_mutex_lock(&_mutexLock);
    
    NSLog(@"%s,%@", __func__, [NSThread currentThread]);
    static int count = 0;
    if (count == 0) {
        
        count ++;
        [self test]; // 递归调用
    }
    
    // 解锁
    pthread_mutex_unlock(&_mutexLock);
}

- (void)dealloc {
    
    // 销毁锁
    pthread_mutex_destroy(&_mutexLock);
}

@end


// 控制台打印:共打印11次,前2次是第一条线程进入时递归调用打印的,后面的线程都不会再递归调用
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 3, name = (null)}
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 4, name = (null)}
-[ViewController test],{number = 8, name = (null)}
-[ViewController test],{number = 9, name = (null)}
-[ViewController test],{number = 10, name = (null)}
-[ViewController test],{number = 11, name = (null)}
-[ViewController test],{number = 12, name = (null)}
-[ViewController test],{number = 13, name = (null)}
  • NSRecursiveLock
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSRecursiveLock *recursiveLock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建锁
    self.recursiveLock = [[NSRecursiveLock alloc] init];
        
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self test];
        });
    }
}

- (void)test {
    
    // 加锁
    [self.recursiveLock lock];
    
    NSLog(@"%s,%@", __func__, [NSThread currentThread]);
    static int count = 0;
    if (count == 0) {
        
        count ++;
        [self test]; // 递归调用
    }
    
    // 解锁
    [self.recursiveLock unlock];
}

@end


// 控制台打印:共打印11次,前2次是第一条线程进入时递归调用打印的,后面的线程都不会再递归调用
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 6, name = (null)}
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 3, name = (null)}
-[ViewController test],{number = 4, name = (null)}
-[ViewController test],{number = 8, name = (null)}
-[ViewController test],{number = 9, name = (null)}
-[ViewController test],{number = 10, name = (null)}
-[ViewController test],{number = 11, name = (null)}
-[ViewController test],{number = 12, name = (null)}
  • @synchronized
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.totalTicketCount = 10;

    for (int i = 0; i < 10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self saleTicket];
        });
    }
}

- (void)saleTicket {

    // 加锁、解锁
    @synchronized (self) {
        
        self.totalTicketCount--;
        NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
    }
}

@end


// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 8, name = (null)}
剩余票数:4,{number = 9, name = (null)}
剩余票数:3,{number = 10, name = (null)}
剩余票数:2,{number = 11, name = (null)}
剩余票数:1,{number = 12, name = (null)}
剩余票数:0,{number = 13, name = (null)}

2、设置线程的最大并发数为1

2.1 用GCD信号量设置
  • 虽然有多个线程,但是我们设置线程的最大并发数为1,不就实现线程同步了嘛。
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (strong, nonatomic) dispatch_semaphore_t semaphore;// 信号量

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建信号量,并设置信号量的初始值为1,即设置线程的最大并发数为1
    self.semaphore = dispatch_semaphore_create(1);

    self.totalTicketCount = 10;

    for (int i = 0; i < 10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self saleTicket];
        });
    }
}

- (void)saleTicket {

    // 每个线程进来执行到dispatch_semaphore_wait函数时,函数内部有如下操作:
    // 如果发现信号量的值 > 0,就让信号量的值 - 1,并让这条线程往下执行代码
    // 如果发现信号量的值 <= 0,就会让线程阻塞在这里休眠等待,直到信号量的值 > 0,才再次让信号量的值 - 1,并唤醒一条休眠的线程进去执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);

    // 每个线程执行完有效代码后,执行到dispatch_semaphore_signal函数时,函数内部有如下操作:
    // 会让信号量的值 + 1,代表这个线程执行完任务了,此时上面就可以唤醒一个休眠的线程进来执行代码了
    dispatch_semaphore_signal(self.semaphore);
}

@end


// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 3, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 9, name = (null)}
剩余票数:2,{number = 10, name = (null)}
剩余票数:1,{number = 11, name = (null)}
剩余票数:0,{number = 12, name = (null)}
2.2 把多个线程放入GCD串行队列,让它们一个一个串行执行
  • 虽然有多个线程,但是我们把多个线程放入串行队列,让它们串行执行,不就实现线程同步了嘛。

注意不要把任务同步和线程同步搞混了:

  • 直接把所有任务放进串行队列,GCD就只会开辟一个线程来执行这些任务,只有一个线程,根本就不存在什么线程同步不同步,这是任务同步而不是线程同步。
  • 还是把所有任务放进并发队列,GCD会开辟多个线程来执行这些任务,这才存在线程同步,我们再把这多个线程放进串行队列里让线程串行执行,这才是线程同步。
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, strong) dispatch_queue_t queue; // 队列

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建串行队列
    self.queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    
    self.totalTicketCount = 10;
    
    // 这才是线程同步
    for (int i = 0; i < 10; i++) {

        // 这里是for循环,所以10个线程的开辟是有先后顺序的
        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            // 把每个子线程的任务都放到串行队列里,这样这些子线程就是挨个执行的了
            // 注意:这里得用dispatch_sync添加,用dispatch_async添加的话那就是在子线程里又开辟子线程了,又因为是串行队列,所以就只会开辟一个子线程,那10个任务就会在这一个子线程上串行执行,这就相当等于是任务同步了
            // 假设线程1先开辟好,执行这个任务,就会卡住线程1,往self.queue添加售票任务、执行售票任务
            // 如果线程1在卡住或者往self.queue添加售票任务、执行售票任务时,线程2开辟好进来了,就会卡住线程2,往self.queue添加售票任务、执行售票任务,但是必须得等线程1的售票任务完成才能执行,因为人家是先添进串行队列里的
            dispatch_sync(self.queue, ^{
                [self saleTicket];
            });
        });
    }
}

- (void)saleTicket {
    
    self.totalTicketCount--;
    NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
}

@end


// 控制台打印:
剩余票数:9,{number = 3, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 5, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 9, name = (null)}
剩余票数:2,{number = 10, name = (null)}
剩余票数:1,{number = 11, name = (null)}
剩余票数:0,{number = 12, name = (null)}

3、线程同步方案性能对比

性能从高到低依次为:os_unfair_lock > 自旋锁 > GCD信号量(推荐使用) > 互斥锁(推荐使用) > GCD串行队列 > 递归锁。

  • os_unfair_lock,性能最高,不过要iOS10之后才能用

自旋锁

  • OSSpinLock,性能也很高,但是苹果已经不推荐使用了,用os_unfair_lock
  • 设置线程的最大并发数为1——GCD信号量(推荐使用)

互斥锁

  • pthread_mutex普通锁(推荐使用)
  • NSLock
  • 把多个线程放入串行队列——GCD串行队列

递归锁

  • pthread_mutex递归锁
  • NSRecursiveLock
  • @synchronized

你可能感兴趣的:(【OC多线程】数据竞争问题与线程同步方案)