多线程 线程安全

多线程的安全隐患

我们用多线程有很多好处,但是也存在安全隐患
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

例子多线程卖票的

- (void)saleTickets
{
    self.tickets = 100;
    dispatch_async(dispatch_get_global_queue(0, 0), ^
    {
        for (int i = 0; i < 5; i++)
        {
            [self sale];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^
    {
        for (int i = 0; i < 5; i++)
        {
            [self sale];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^
    {
        for (int i = 0; i < 5; i++)
        {
            [self sale];
        }
    });
}

- (void)sale
{
    int oldTickets = self.tickets;
    sleep(.2);
    oldTickets--;
    self.tickets = oldTickets;
    NSLog(@"还剩余%i张票", self.tickets);
}

产生的原因


隐患产生的原因

线程同步技术,就是线程访问同一个资源,需要按照特定的次序
解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁


加锁

11OSSpinLock01

自旋锁,所有的线程用同一把锁,才能达到加锁的目的等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁

需要导入头文件

import


@property (nonatomic, assign)  OSSpinLock *lock;
    self.lock = OS_SPINLOCK_INIT;
- (void)sale
{
    OSSpinLockLock(&_lock);
    int oldTickets = self.tickets;
    sleep(.2);
    oldTickets--;
    self.tickets = oldTickets;
    NSLog(@"还剩余%i", oldTickets);
    OSSpinLockUnlock(&_lock);
}

存钱和取钱也是共用一把锁
IOS10之后就过期了

12OSSpinLock02

让线程阻塞,1让线程睡觉,2忙等状态
尝试加锁


尝试加锁

14答疑问

用不用加锁 要看是不是多条线程同时访问同一个资源,也要看访问了是干嘛,如果没有修改的话是不需要的

15 os_unfair_lock

用于取代不安全的OSSpinLock ,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import
如果忘记解锁了,线程会一直在那里睡眠,其他的线程拿不到锁,这就是一个死锁。

 //初始化锁
       self.moneyLock = OS_UNFAIR_LOCK_INIT;
    //尝试加锁
   os_unfair_lock_trylock(&_ticketLock);
    //加锁
    os_unfair_lock_lock(&_ticketLock);
    //解锁
    os_unfair_lock_unlock(&_ticketLock);

16pthread_mutex01

pthread开头的一半都是通用的
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
需要导入头文#import

//创建一个锁的属性
    pthread_mutexattr_t attr;
    //初始化属性
    pthread_mutexattr_init(&attr);
    //设置属性类型
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    //出初始化锁
    pthread_mutex_t Lock;
    pthread_mutex_init(&lock, &attr);
//加锁
 pthread_mutex_lock(&_moneyLock);
    //解锁
    pthread_mutex_unlock(&_moneyLock);
//销毁相关资源
    pthread_mutex_destroy(&_ticketLock);
    pthread_mutexattr_destroy(&attr);

//锁的类型
/*
 * 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
    

17-pthread_mutex02-递归锁

pthread_mutex这种锁我们用完要记得手动销毁

实验一下在这个锁里面再加一个自己的锁,会出现什么情况?
死锁,
或者递归里面有这个锁,都会造成死锁
第一个情况,只需要换一把锁就可以解决
如果是递归,无法换锁,这种情况可以换成递归锁,只需要将这把锁的属性换成PTHREAD_MUTEX_RECURSIVE,他就可以重复加锁。
如果不痛的线程都来访问这个锁的疑问,递归锁是允许同一个线程,对一个锁重复加锁。 如果其他的线程来访问的时候已经加锁了,就不行再加锁,只能等当前线程解锁之后才可以加锁或者访问

//创建一个递归锁
/创建一个锁的属性
    pthread_mutexattr_t attr;
    //初始化属性
    pthread_mutexattr_init(&attr);
    //设置属性类型
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    //出初始化锁
    pthread_mutex_t Lock;
    pthread_mutex_init(&lock, &attr);

8-自旋锁、互斥锁汇编分析

实验,线程在等在锁的时候在做什么事情?
OSSpinLock
让线程sleep时间长一点,然后过掉第一个端点,查看
用lldb-step,他是执行一个oc的指令,可能是几行汇编代码,想要他执行一个汇编代码stepi->si简写,可以看到他一直在执行一个while循环,
pthread_mutex_lock 互斥锁
lldb-c可以快跳到下一个端点
通过上面的方法我们找到底层执行的一个函数mutexwait里面又个syscall,调用了系统级别的函数,执行之后,线程不做事情了,休眠了
os_unfair_lock
他也会执行systcall去休眠,其实他也是互斥锁
low_level lock低级锁,等不到锁就会休眠

19-pthread_mutex03-条件

pthread_mutex在我们创建锁的时候,传进来的type决定什么锁
条件
以前我们加锁的时候很简单,就是等待别的锁放开之后才可以加锁,条件也是另外一条件来放开这把锁.
假如 我们有两条线程同时对一个数组操作,一个添加对象,一个删除对象,我们想哟在删除的时候判断一个只有不是0的时候才可以删除。这样就可以加上一个条件,如果不符合条件,线程就是会休眠,并且放开,刚刚加的锁。如果其他的线程添加了元素可以发送信号来唤醒条件休眠的线程,
使用场景可以用来实现线程等待

20-NSLock、NSRecursiveLock、NSCondition

NSLock就是对mutex普通锁的封装


NSLock
   //初始化锁
        self.ticketLock = [[NSLock alloc] init];
        self.moneyLock = [[NSLock alloc] init];
//加锁
 [self.moneyLock lock];
//解锁
    [self.moneyLock unlock];

这种方式更加的面向对象,
可以使用打断点看汇编的方式,查看他底层调用的什么,也可以是gunstep开源的里面查看,对NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致

  • NSCondition
    NSCondition是对mutex和cond的封装


    NSCondition
  self.condition= [[NSCondition alloc] init];
        _arr = [NSMutableArray array];
//添加对象
//加锁
 [self.condition lock];
    [self.arr addObject:@"nihao"];
    NSLog(@"添加了元素");
//通知condition
    [self.condition signal];
//解锁
    [self.condition unlock];
//删除对象
//加锁
[self.condition lock];
    if (self.arr.count == 0)
    {
        //进入休眠
        [self.condition wait];
    }
    [self.arr removeLastObject];
    NSLog(@"删除了元素");
  //解锁
    [self.condition unlock];

21答疑

解答删除在添加的后面,多线程不确定谁先执行的时候,也是闲添加再删除,因为又个条件等待

22多线程遗留问题

[condition signal]是不会可以放外边更好,如果两个代码很近中间没有很多的操作,其实都可以,

runloop相关的问题
线程的任务一旦执行完毕,生命周期就结束了,这个线程就无法再使用了,虽然他还在内存中,但是他已经不是激活状态了,还是不能做事情了。可以用手动开启runloop来让线程存活,处于激活状态
为什么不用强指针,而使用runloop
这里说的线程的命,是保证线程的生命周期,保证线程处于激活状态,
dispatch_get_global_queue

//1
[self performSelector:<#(nonnull SEL2)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>];
//3

为什么打印1和3可以,而2不可以,因为1和3不需要runloop的支持的,2的方法本质是讲一个定时器添加到runloop上面,但是子线程的runloop默认是没有开启的,所以不能打印
主线程的runloop默认是打开的,所以主线的UI刷新,点击时间,performSelector,普通的代码平不是交给runloop的

23-NSconditionLock

利用conditionLock可以实现线程之间等待,

//创建一        
NSConditionLock *condition;
       self.condition= [[NSConditionLock alloc] init];//初始化的时候如果不指定condition的话,他默认就是0
        self.condition = [[NSConditionLock alloc] initWithCondition:1];

//加锁
- (void)addPerosn
{
    [self.condition lockWhenCondition:2];
    NSLog(@"2");
    [self.condition unlockWithCondition:3];
}

- (void)deletePerosn
{
    [self.condition lockWhenCondition:1];
    
    NSLog(@"1");

    [self.condition unlockWithCondition:2];
    
}

{
    [self.condition lockWhenCondition:3];
    
    NSLog(@"3");

    [self.condition unlock];
    
}

24-SerialQueue

直接使用GCD的串行队列,也是可以实现线程同步的
iOS中的线程同步方案

OSSpinLock
    os_unfair_lock
    pthread_mutex
    dispatch_semaphore
    dispatch_queue(DISPATCH_QUEUE_SERIAL)
    NSLock
    NSRecursiveLock
    NSCondition
    NSConditionLock
    @synchronized

线程同步的本质就是不能让多条线程同时占用一块资源,让他们安顺序来访问,利用串行队列

26semaphore01-最大并发数量

semaphore信号量,
信号量的初始值,可以用来控制线程并发访问的最大数量,比如卖票,希望有三个线程在卖票。
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

初始化
 self.semaphore = dispatch_semaphore_create(5);//最大并发数
- (void)saveMoney
{
    for (int i = 0; i < 20; i++)
    {
        //创建20条子线程
        [[[NSThread alloc] initWithTarget:self selector:@selector(drawMoney) object:nil] start];
        
    }
}
- (void)drawMoney
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"%@", [NSThread currentThread]);
    dispatch_semaphore_signal(self.semaphore);
}

26-semaphore02-线程同步

dispatch_semaphore_wait(),
做的事情若果信号量的值<=0,当第6个进来的时候,就让当前线程会进入休眠状态,等到信号量>0;执行就-1,然后往下执行后面的代码,
如果信号量>0,开始的时候是5,执行就-1,然后往下执行后面的代码
dispatch_semaphore_signal(),
这个就是让信号量+1;

用信号量保证线程同步,可以设置信号量=1;保证同时只能有一条线程访问
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

27-semaphore03-@synchronized

@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作,传进来的对象一样,找到的就是同一把锁

- (void)saveMoney
{
    @synchronized ([self class]) {
        [super saveMoney];
    }
}
- (void)drawMoney
{
    @synchronized ([self class]) {
        [super drawMoney];
    }
}

同步方案对比

性能从高到低排序
os_unfair_lock ios10才支持
OSSpinLock
dispatch_semaphore ios8支持
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock 对mutex的封装
NSCondition
pthread_mutex(recursive) //递归锁
NSRecursiveLock
NSConditionLock
@synchronized

使用技巧

static dispatch_semaphore_t semaphore ;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        semaphore = dispatch_semaphore_create(1);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    //
    dispatch_semaphore_signal(semaphore);

29-自旋锁、互斥锁对比

什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器

什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈

automaic

原子不可再分割,原子性操作就说说明这个操作不可以再分割的,他是一个整体,要执行就要全部执行完,其他线程才能执行。
原子性,一旦我们给属性automaic他就会对我们get和set加锁,都是原子性操作,也就是保证线程同步
atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm
它并不能保证使用属性的过程是线程安全的,

reallySetProperty方法里面查看
if (!atomic) {
       oldValue = *slot;
       *slot = newValue;
   } else {
       spinlock_t& slotlock = PropertyLocks[slot];
       slotlock.lock();
       oldValue = *slot;
       *slot = newValue;        
       slotlock.unlock();
   }

你可能感兴趣的:(多线程 线程安全)