多线程(二)、线程安全

703 阅读5分钟

数据安全隐患

  • 资源共享
    • 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 比如多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

如以下案例,会造成数据安全问题:

#import <Foundation/Foundation.h>

@interface MJBaseDemo : NSObject

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

#pragma mark - 暴露给子类去使用
- (void)__saveMoney;
- (void)__drawMoney;
- (void)__saleTicket;
@end
#import "MJBaseDemo.h"

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

@implementation MJBaseDemo

/**
 存钱、取钱演示
 */
- (void)moneyTest
{
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __drawMoney];
        }
    });
}

/**
 存钱
 */
- (void)__saveMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 取钱
 */
- (void)__drawMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 卖1张票
 */
- (void)__saleTicket
{
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}

/**
 卖票演示
 */
- (void)ticketTest
{
    self.ticketsCount = 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];
        }
    });
}

@end

调用

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJBaseDemo *demo = [[OSSpinLockDemo2 alloc] init];
    [demo ticketTest];
    [demo moneyTest];

}

以上卖票问题和存取钱案例,会造成数据安全问题

解决方案: 线程同步技术

常见线程同步方案

OSSpinLock

  • OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源(相当于 while(已上锁);)

  • 目前已经不再安全,可能会出现优先级反转问题。

    • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
    • 需要导入头文件 #import<libkern/OSAtomic.h>

代码如下,OSSpinkLockDemo继承MJBaseDemo

 #import "OSSpinLockDemo.h"
 #import <libkern/OSAtomic.h>

@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock moneyLock;
@property (assign, nonatomic) OSSpinLock ticketLock;
@end

@implementation OSSpinLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_SPINLOCK_INIT;
        self.ticketLock = OS_SPINLOCK_INIT;
    }
    return self;
}

- (void)__drawMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __drawMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saveMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __saveMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saleTicket
{
    OSSpinLockLock(&_ticketLock);
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&_ticketLock);
}

@end
  • 每个线程访问的锁必须是同一把锁。线程1进来访问这把锁,发现没有加锁,然后加锁-->处理业务-->解锁,这时线程2进来访问这把锁,发现已加锁,就处于忙等状态。这时就达到了目的。这里的锁是属性。如果锁是局部变量,每次进来都初始化一下,线程1进来锁是未锁状态,线程2进来,此时锁已经不是之前的锁了,也是未锁状态,就达不到目的。

  • 线程阻塞有两种方案,1.线程休眠 2.忙等,相当于相当于 while(已上锁);一直占用CPU资源。

  • 自旋锁目前已不再安全。比如有三个线程,线程1、线程2、线程3,系统会进行线程调度,分出很小的时间片,先执行线程1,再执行线程2...这样就能达到“微观串行,宏观并发”。优先级较高的线程,会分出比较多的时间片去执行。如果此时,线程2先进来,加锁。同时,线程1进来处于忙等状态,因为线程1优先级高,可能CPU一直会分出时间片给线程1,这时线程1处于忙等状态,线程2没有CPU资源也无法解锁。所以,可能会造成死锁。线程和进程都会有这种时间片轮转调度算法。

  • 自旋锁已经不安全了,不推荐使用。但是效率还是比较高的,因为忙等,不会让线程休眠。如果线程休眠再唤醒也是消耗性能的。

  • 还可以用另一个API,尝试加锁。加锁成功,就处理业务,然后解锁,尝试加锁失败,就处于忙等。

//    if (OSSpinLockTry(&_lock)) {
//        int oldTicketsCount = self.ticketsCount;
//        sleep(.2);
//        oldTicketsCount--;
//        self.ticketsCount = oldTicketsCount;
//        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
//
//        OSSpinLockUnlock(&_lock);
//    }
  • 这两个锁也可以不用属性,而是用static 全局变量和static局部变量。只要只初始化一次,使每次访问的锁是同一把锁就可以。
#import "OSSpinLockDemo2.h"
#import <libkern/OSAtomic.h>

@implementation OSSpinLockDemo2

static OSSpinLock moneyLock_;
+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        moneyLock_ = 0;
    });
}

- (void)__drawMoney
{
    OSSpinLockLock(&moneyLock_);
    
    [super __drawMoney];
    
    OSSpinLockUnlock(&moneyLock_);
}

- (void)__saveMoney
{
    OSSpinLockLock(&moneyLock_);
    
    [super __saveMoney];
    
    OSSpinLockUnlock(&moneyLock_);
}

- (void)__saleTicket
{
//    static NSString *str = nil;
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//        str = [NSString stringWithFormat:@"123"];
//    });
    
    static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
    
    OSSpinLockLock(&ticketLock);
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&ticketLock);
}

@end

static NSString *str = [NSString stringWithFormat:@"123"]; 会报错。static静态初始化,右值必须是直接值,编译阶段就确定它的值,函数调用是运行阶段才知道它的值。上面可以改造成这样:

//    static NSString *str = nil;
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//        str = [NSString stringWithFormat:@"123"];
//    });

os_unfair_lock

  • os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持
  • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
  • 需要导入头文件 #import <os/lock.h>

demo:

OSUnfairLockDemo继承MJBaseDemo

#import "OSUnfairLockDemo.h"
#import <os/lock.h>

@interface OSUnfairLockDemo()
// Low-level lock
// ll lock
// lll
// Low-level lock的特点等不到锁就休眠
@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation OSUnfairLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_UNFAIR_LOCK_INIT;
        self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

- (void)__saleTicket
{
    os_unfair_lock_lock(&_ticketLock);
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

- (void)__saveMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __saveMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__drawMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __drawMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

@end

pthread_mutex

pthread开头的都是跨平台的

  • mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
  • 需要导入头文件 #import <pthread.h>

DEMO:

#import "MutexDemo.h"
#import <pthread.h>

@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t ticketMutex;
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
@end

@implementation MutexDemo

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    
//    // 初始化属性
//    pthread_mutexattr_t attr;
//    pthread_mutexattr_init(&attr);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//    // 初始化锁
//    pthread_mutex_init(mutex, &attr);
//    // 销毁属性
//    pthread_mutexattr_destroy(&attr);
    
    // 初始化锁
    pthread_mutex_init(mutex, NULL);

}

- (instancetype)init
{
    if (self = [super init]) {
        
//        self.moneyMutex = PTHREAD_MUTEX_INITIALIZER;  //会报错 结构体可以静态初始化,self.moneyMutex是运行的时候调setter方法
        
//        pthread_mutex_t mutex;
//        mutex = PTHREAD_MUTEX_INITIALIZER; //会报错
        
        //         pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //这样是可以的
        
        [self __initMutex:&_ticketMutex];
        [self __initMutex:&_moneyMutex];
    }
    return self;
}

// 死锁:永远拿不到锁
- (void)__saleTicket
{
    pthread_mutex_lock(&_ticketMutex);
    
    [super __saleTicket];
    
    pthread_mutex_unlock(&_ticketMutex);
}

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

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

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

@end
  • 结构体变量赋值是不能大括号赋值的,初始化时可以用大括号就行初始化。
//        pthread_mutex_t mutex;
//        mutex = PTHREAD_MUTEX_INITIALIZER; //会报错

所以

//        self.moneyMutex = PTHREAD_MUTEX_INITIALIZER;  //会报错 结构体可以静态初始化,self.moneyMutex是运行的时候调setter方法
        //         pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //这样是可以的
  • pthread_mutex初始化:
//    // 初始化属性
//    pthread_mutexattr_t attr;
//    pthread_mutexattr_init(&attr);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//    // 初始化锁
//    pthread_mutex_init(mutex, &attr);
//    // 销毁属性
//    pthread_mutexattr_destroy(&attr);

如上面,锁的类型是默认时,初始化可以简化成:pthread_mutex_init(mutex, NULL);

  • 注意在 dealloc里销毁。

pthread_mutex 递归锁

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

调otherTest时会出现死锁

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJBaseDemo *demo = [[NSConditionDemo alloc] init];
//    [demo ticketTest];
//    [demo moneyTest];
    [demo otherTest];
}

这时就需要把pthread_mutex的锁的类型换成递归锁

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 递归锁:允许同一个线程对一把锁进行重复加锁
    
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
}

可以对同一线程,重复加锁。不同线程不能重复加锁。

/**
 线程1:otherTest(+-)
        otherTest(+-)
         otherTest(+-)
 
 线程2:otherTest(等待)
 */

OSSpinkLock 和 pthread_mutex 汇编分析

Xcode --> Debug --> Debug Workflow -- Always show Disassembly 显示汇编代码后,这时想要单步,不能用Xcode上的单步按钮,而是用LLDB命令。 step简写s是单步,但是是单步代码的,会一下过掉好几行汇编指令,stepinstruction简写stepi或者si是单步一条汇编的。nexti也是过掉一条汇编,但是遇到方法不会进入方法,直接过了方法。

改造下卖票代码,10个线程同时卖票

- (void)ticketTest
{
    self.ticketsCount = 15;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
    }
 }
 

OSSpinkLock

- (void)__saleTicket
{
    OSSpinLockLock(&_ticketLock);
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&_ticketLock);
}

断点断在 OSSpinLockLock(&_ticketLock);,第一次断在这时,是第一个线程,然后放开。第二次断在这儿时,进入显示汇编模式,然后 si,...

到callq(调方法),进入OSSpinLockLock,si进入,继续几个si...,然后进入_OSSpinLockLockSlow,继续si...

会一直在 0x7fff518c58d1 和 0x7fff518c58e6中间,jne j是jump跳入,ne是条件判断,很明显这部分代码是 while循环。所以OSSpinkLock是自旋锁,会一直占用CPU资源。

pthread_mutex

同理一直si,会进入_pthread_mutex_firstfit_lock_wait

然后N个si后进入 __psynch_mutexwait,

最后看到 syscall指令,这是系统调用,这之后就没反应了,线程休眠了。

所以 pthread_mutex是互斥锁。

os_unfair_lock

最后也是,syscall指令后,没反应了。线程休眠了。所以os_unfair_lock不是自旋锁。点进去看,头文件注释:

 * @abstract
 * Low-level lock that allows waiters to block efficiently on contention.

Low-level 简称 LL Lock或LLL,低级锁。Low-level lock的特点等不到锁就休眠。自旋锁OSSpinkLock是高级锁。