美文网首页
iOS-多线程2-线程安全、OSSpinLock

iOS-多线程2-线程安全、OSSpinLock

作者: Imkata | 来源:发表于2019-12-17 17:28 被阅读0次

    一. 安全隐患

    利用多线程异步可以同时做不同的事情,效率更高,但是这样也会有安全隐患。

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

    两个比较经典的问题:

    存钱取钱.png 卖票.png

    下面用代码来验证卖票问题:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self ticketTest];
    }
    
    /**
     卖票演示
     */
    - (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];
            }
        });
    }
    
    /**
     卖1张票
     */
    - (void)saleTicket
    {
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);//为了凸显多线程的安全隐患
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
    }
    
    打印:
    还剩14张票 - <NSThread: 0x6000029c8480>{number = 5, name = (null)}
    还剩14张票 - <NSThread: 0x6000029ef3c0>{number = 3, name = (null)}
    还剩14张票 - <NSThread: 0x6000029cd600>{number = 4, name = (null)}
    还剩12张票 - <NSThread: 0x6000029cd600>{number = 4, name = (null)}
    还剩11张票 - <NSThread: 0x6000029ef3c0>{number = 3, name = (null)}
    还剩13张票 - <NSThread: 0x6000029c8480>{number = 5, name = (null)}
    还剩10张票 - <NSThread: 0x6000029cd600>{number = 4, name = (null)}
    还剩9张票 - <NSThread: 0x6000029ef3c0>{number = 3, name = (null)}
    还剩8张票 - <NSThread: 0x6000029c8480>{number = 5, name = (null)}
    还剩7张票 - <NSThread: 0x6000029cd600>{number = 4, name = (null)}
    还剩6张票 - <NSThread: 0x6000029ef3c0>{number = 3, name = (null)}
    还剩5张票 - <NSThread: 0x6000029cd600>{number = 4, name = (null)}
    还剩4张票 - <NSThread: 0x6000029c8480>{number = 5, name = (null)}
    还剩3张票 - <NSThread: 0x6000029ef3c0>{number = 3, name = (null)}
    还剩2张票 - <NSThread: 0x6000029c8480>{number = 5, name = (null)}
    
    可以发现卖票15次,一次卖1张,还剩2张,说明有问题。
    

    存钱取钱问题:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self moneyTest];
    }
    
    /**
     存钱、取钱演示
     */
    - (void)moneyTest
    {
        self.money = 100;
        //存500取200应该还剩400
        
        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];
            }
        });
    }
    
    /**
     存钱50块 存500
     */
    - (void)saveMoney
    {
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        
        NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    
    /**
     取钱20块 取200
     */
    - (void)drawMoney
    {
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    
    打印:
    ......
    存50,还剩380元 - <NSThread: 0x600000060140>{number = 3, name = (null)}
    取20,还剩360元 - <NSThread: 0x600000068380>{number = 4, name = (null)}
    取20,还剩340元 - <NSThread: 0x600000068380>{number = 4, name = (null)}
    存50,还剩410元 - <NSThread: 0x600000060140>{number = 3, name = (null)}
    存50,还剩460元 - <NSThread: 0x600000060140>{number = 3, name = (null)}
    
    存500取200,加上原来的100,应该还剩400,打印460有问题。
    

    分析造成安全隐患的原因:

    多线程安全隐患分析.png

    如图,按理说两个线程对17分别进行两次+1操作,应该是19,最后是18,就是因为这个安全隐患造成的。

    那么如何解决安全隐患呢?

    二. 解决方案

    使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)解决线程的安全隐患,常见的线程同步技术是:加锁

    加锁实现原理如下图,就不解释了:

    加锁解释.png

    三. 加锁方案

    OSSpinLock 自旋锁
    os_unfair_lock
    pthread_mutex
    dispatch_semaphore 信号量
    dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
    NSLock
    NSRecursiveLock
    NSCondition
    NSConditionLock
    @synchronized

    1. OSSpinLock

    OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。

    1. 目前已经不再安全,可能会出现优先级反转问题,也就是,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
    2. 需要导入头文件#import <libkern/OSAtomic.h>

    使用自旋锁解决上面问题:

    #import "ViewController.h"
    #import <libkern/OSAtomic.h>
    
    @interface ViewController ()
    @property (assign, nonatomic) int money;
    @property (assign, nonatomic) int ticketsCount;
    @property (assign, nonatomic) OSSpinLock lock;
    @property (assign, nonatomic) OSSpinLock lock1;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 初始化锁,要所有的线程都用同一把锁
        self.lock = OS_SPINLOCK_INIT;
        self.lock1 = OS_SPINLOCK_INIT;
        
        [self ticketTest];
        [self moneyTest];
    }
    
    /**
     存钱、取钱演示
     */
    - (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
    {
        // 加锁
        OSSpinLockLock(&_lock1);
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        
        NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        
        // 解锁
        OSSpinLockUnlock(&_lock1);
    }
    
    /**
     取钱
     */
    - (void)drawMoney
    {
        // 加锁
        OSSpinLockLock(&_lock1);
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        // 解锁
        OSSpinLockUnlock(&_lock1);
    }
    
    /**
     卖1张票
     */
    - (void)saleTicket
    {
        // 加锁
        OSSpinLockLock(&_lock);
    
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
        
        // 解锁
        OSSpinLockUnlock(&_lock);
    }
    
    /**
     卖票演示
     */
    - (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
    
    打印:
    ......
    还剩0张票 - <NSThread:  0x6000013c4040>{number = 3, name = (null)}
    ......
    取20,还剩400元 - <NSThread: 0x6000013c4040>{number = 3, name = (null)}
    
    说明加锁成功。
    
    1. OSSpinLockLock(&_lock)这行代码里面做的事情:加锁之前会判断这把锁有没有在加锁,如果在加锁就阻塞在OSSpinLockLock(&_lock)这一行,等这把锁解锁完,再加锁。
    2. 卖票只需要saleTicket方法里面加锁,存钱取钱需要在saveMoney和drawMoney里面都要加锁,而且要使用同一把锁。
    3. 自旋锁就是下面方式②,类似于写了个while循环,一直占用cpu资源。

    线程阻塞方式有:
    ① 让线程睡眠,不占用cpu资源
    ② while循环,一直占用cpu资源

    优先级反转问题:

    上面说了自旋锁有可能造成优先级反转问题,下面解释这个:
    首先,CPU给多个线程分配任务的时候采用的是时间片轮转调度算法(进程、线程),意思就是当thread1、thread2、thread3同时执行任务,如果thread1的优先级> thread2,那么给thread1分配时间的概率就大一些,CPU就是不断的给线程分配时间,线程拿到CPU分配的时间做事情,这样就感觉三个线程是同时做事情,这就叫时间片轮转调度算法。

    在上面的自旋锁代码中:

    - (void)drawMoney
    {
        // 加锁
        OSSpinLockLock(&_lock1);
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        // 解锁
        OSSpinLockUnlock(&_lock1);
    }
    

    如果线程2先来到上面代码,线程2发现没有加锁,就会加锁,加锁之后线程1又来了,线程1发现已经加锁了,就会处于忙等状态(相当于一直执行while),由于线程1优先级大于线程2,这时候CPU就会给线程1分配更多的时间,这时候大量的时间都分配给线程1执行while了,线程2就没有时间执行完它的代码,也就不会解锁了,这时候线程1就会一直在等,就产生类似死锁的感觉了,这样这把锁就有可能一直放不开。

    其实自旋锁效率是比较高的,因为它是类似while循环,没有休眠(线程从休眠到唤醒也是需要时间的),但是就因为自旋锁阻塞的方式是类似while循环,所以说这把锁已经不安全了,不推荐使用。

    补充:自旋锁加锁的另外一种方式

    // 尝试加锁,如果能加锁就加锁,不能加锁,返回加锁失败就继续往下走,这种方式不会阻塞线程。
    if (OSSpinLockTry(&_lock)) {
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
    
        OSSpinLockUnlock(&_lock);
    }
    

    2. 其他加锁方案

    上面只介绍了自旋锁,后面还有好几种锁,为了演示方便,在文末的Demo中对重复代码进行了抽取。
    其中OSSpinLockDemo是中规中矩的代码抽取,OSSpinLockDemo2中没有使用属性,使用了static变量:

    #import "OSSpinLockDemo2.h"
    #import <libkern/OSAtomic.h>
    
    @implementation OSSpinLockDemo2
    
    //只有当前文件可以访问
    static OSSpinLock moneyLock_;
    + (void)initialize
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //OS_SPINLOCK_INIT 就是 0
            moneyLock_ = 0;
        });
    }
    
    - (void)__drawMoney
    {
        OSSpinLockLock(&moneyLock_);
        
        [super __drawMoney];
        
        OSSpinLockUnlock(&moneyLock_);
    }
    
    - (void)__saveMoney
    {
        OSSpinLockLock(&moneyLock_);
        
        [super __saveMoney];
        
        OSSpinLockUnlock(&moneyLock_);
    }
    
    - (void)__saleTicket
    {
        //延长局部变量生命周期,至程序结束
        static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
        
        OSSpinLockLock(&ticketLock);
        
        [super __saleTicket];
        
        OSSpinLockUnlock(&ticketLock);
    }
    
    @end
    

    关于static:

    关于上面的代码有两个知识点:

    1. static的作用
      static修饰局部变量会延长局部变量生命周期,至程序结束
      static修饰全局变量,那么全局变量作用域只在本文件

    自己思考一下,为什么要用static修饰上面的全局变量和局部变量?

    1. static是静态初始化,右边只能放一个值

    比如上面的 static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
    点进去:

    #define OS_SPINLOCK_INIT    0
    

    发现OS_SPINLOCK_INIT就是0,所以可以这么写。

    再举个例子:

    static NSString *str = [NSString stringWithFormat:@"123"];
    

    上面代码会报错,因为被static修饰的变量在编译的时候就要知道值,而右边的函数只有在运行的时候才知道值。

    修改为如下代码就不报错了:

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

    加锁的条件:

    只有多条线程同时修改同一个变量的时候才需要加锁。

    比如如下代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        MJBaseDemo *demo = [[OSSpinLockDemo2 alloc] init];
    
        for (int i = 0; i < 10; i++) {
            [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
        }
    }
    
    - (int)test
    {
        int a = 10;
        int b = 20;
        
        //NSLog(@"%p", self.demo);
        
        int c = a + b;
        return c;
    }
    

    上面代码虽然同时调用了test方法,但是不用加锁,因为他们访问的都是局部变量,不是同一个变量,没必要加锁。

    就算把上面NSLog(@"%p", self.demo);注释打开,这里访问了同一个变量了,但是只是打印一下,没有修改它的值,也不用加锁。

    其他加锁方案请参考:其他加锁方案

    Demo地址:线程安全、自旋锁

    相关文章

      网友评论

          本文标题:iOS-多线程2-线程安全、OSSpinLock

          本文链接:https://www.haomeiwen.com/subject/pmpsnctx.html