多线程学习(二)

多线程的安全隐患

资源共享
一块资源可能被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

两个经典的例子

存钱取钱

在这里插入图片描述
卖票
在这里插入图片描述

在这里插入图片描述

根据以上例子,我们写出以下对应验证程序:

#import "ViewController.h"

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (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(0.3);小数不起作用
    [NSThread sleepForTimeInterval:0.5];
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50,还剩%d元--%@", self.money, [NSThread currentThread]);
}

- (void)drawMoney
{
    int oldMoney = self.money;
    //sleep(0.3);
    [NSThread sleepForTimeInterval:0.5];
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20,还剩%d元--%@", self.money, [NSThread currentThread]);
}

- (void)saleTicket
{
    sleep(1);
    self.ticketsCount--;
    NSLog(@"还剩%d张票--%@", self.ticketsCount, [NSThread currentThread]);
}

- (void)saleTickets
{
    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];
        }
    });
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self saleTickets];
    [self moneyTest];
}

@end

注意📢
休眠的时候,sleep(1.2);不能使用小数,因为其定义:
unsigned int sleep(unsigned int) __DARWIN_ALIAS_C(sleep);里面接收的是int,所以,写小数最后也转换为整数,并且,如果值小于1没效果,相当于0

在这里插入图片描述

通过打印可以看出,第二步就有问题,因为之前存了50,剩余150,现在取20应该剩余130,而结果却是150

线程同步

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

在这里插入图片描述


iOS中的线程同步方案

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

在这里插入图片描述

OSSpinLock

spin:旋转;
lock:锁。
OSSpinLock叫做”自旋锁“。等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。
OSSpinLock使用方法:

导入头文件
#import <libkern/OSAtomic.h>

将OSSpinLock全局拥有
@property (assign, nonatomic) OSSpinLock lock;

//初始化锁
self.lock = OS_SPINLOCK_INIT;

//加锁
OSSpinLockLock(&_lock);

//尝试加锁
if (OSSpinLockTry(&_lock)) {
  //做任务
  OSSpinLockUnlock(&_lock);
}

//解锁
OSSpinLockUnlock(&_lock);

OSSpinLock目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
不建议使用


os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始支持
从底层调用来看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

os_unfair_lock的使用方法:

导入头文件
#import <os/lock.h>

//初始化锁
self.moneyLock = OS_UNFAIR_LOCK_INIT;

//加锁
os_unfair_lock_lock(&_moneyLock);
        
//解锁
os_unfair_lock_unlock(&_moneyLock);

pthread_mutex

mutex:互斥。等待锁的线程会处于休眠状态

pthread_mutex的使用方法:

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    //静态初始化锁
//        pthread_mutex_t moneyLock = PTHREAD_MUTEX_INITIALIZER;

    pthread_mutexattr_t attr;
    //初始化属性
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setprotocol(&attr, PTHREAD_MUTEX_NORMAL);
    
    //初始化锁
    pthread_mutex_init(mutex, &attr);
    //销毁属性
    pthread_mutexattr_destroy(&attr);
}

其中,pthread_mutexattr_setprotocol的第二个参数的代表含义:
#define PTHREAD_MUTEX_NORMAL		0//普通锁
#define PTHREAD_MUTEX_ERRORCHECK	1//错误锁,一般用不到
#define PTHREAD_MUTEX_RECURSIVE		2//递归锁
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL//默认锁=普通锁常

//加锁
pthread_mutex_lock(&_moneyLock);

//解锁
pthread_mutex_unlock(&_moneyLock);


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

需要注意的是:
在这里插入图片描述不能这样赋值,这是因为:
#define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}
PTHREAD_MUTEX_INITIALIZER是一个结构体,结构体只能在创建的时候初始化,不能在后面赋值。
也就是pthread_mutex_t moneyLock = PTHREAD_MUTEX_INITIALIZER;是可以这样写的。

当调用递归函数,而递归函数里面有锁的情况下,我们需要使用PTHREAD_MUTEX_RECURSIVE递归锁即可满足要求。
递归锁,允许同一个线程,对同一把锁进行重复加锁。

pthread_mutex-条件
在这里插入图片描述


NSLock和NSRecursiveLock

NSLock其实是对mutex的普通锁的一种封装
NSLock的使用方法:

//锁成为属性
@property (strong, nonatomic) NSLock *moneyLock;

//初始化锁
self.moneyLock = [[NSLock alloc] init];

//加锁
[self.moneyLock lock];

//解锁
[self.moneyLock unlock];

//尝试加锁
- (BOOL)tryLock;

//在limit时间前,一直等待休眠,一旦在这个时间前加锁成功,则返回YES;超过limit时间则加锁失败返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
	

NSRecursiveLock是对mutex中的递归锁的封装。
Recursive:循环递归

NSCondition是对mutex和条件(conditon)的封装。
NSCondition可以实现:
一个数组,可以添加元素,也可以删除元素。两个方法同时执行,有可能是先删后加。而删的时候里面元素个数为0,可以做到删的时候,如果元素个数为0,则等待,然后执行加元素,等加完元素后,单个通知或者多个通知删除操作,再执行删元素操作。

具体源码可以参考GUNSteps。

NSConditonLock是对NSCondition的进一步封装


汇编下查看自旋锁 与 互斥锁

汇编下的lldb指令:

si = stepi = step instruction = 下一步 指令,可以做到汇编一条一条的过
nexti也可以让汇编一条一条的过
区别是:
nexti遇到函数会过函数,到函数下面的汇编
si遇到函数会进函数,到函数里面的汇编

自旋锁
在这里插入图片描述
自旋锁,一直重复执行5到13行的汇编,是一个while循环,循环着做事情,等待着锁的解开。

互斥锁
在这里插入图片描述互斥锁,16看出,是psynch_mutexwait,是等待休眠。等着锁的解开。

一般来说,高级锁是循环,低级锁是休眠
也就是说,高级锁是自旋锁,低级锁是互斥锁。


线程同步: 即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。

利用串行队列dispatch_queue_t实现线程同步

#import “SerialQueueDemo.h”


@interface SerialQueueDemo()
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation SerialQueueDemo
- (instancetype)init
{
    if (self = [super init]) {
        self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
        self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)__saveMoney
{
    //加锁
    dispatch_sync(self.moneyQueue, ^{
        [super __saveMoney];
    });
}

- (void)__drawMoney
{
    //加锁
    dispatch_sync(self.moneyQueue, ^{
        [super __drawMoney];
    });
}

- (void)__saleTicket
{
    //加锁
    dispatch_sync(self.ticketQueue, ^{
        [super __saleTicket];
    });
}

dispatch_semaphore 信号量

semaphore 信号量的意思

信号量的初始值,可以控制线程并发访问的最大数量。
信号量的初始值为1,代表同时只允许一条线程访问资源,保证线程同步。

在这里插入图片描述

过程很简单,就是在wait处,根据value的值,做-1操作。
如果value=5,100个线程任务,那么5条线程进去后信号量的值为0,再进去一条线程,信号量的值为-1,则等待。
前面进去的线程任务执行完毕后,到signal,信号量值+1,若信号量的值<=0,则通知wait等待的线程去执行任务。
最大接收5个。从而保证线程的最大并发数量。

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end

@implementation SemaphoreDemo
- (instancetype)init
{
    if (self = [super init]) {
        self.semaphore = dispatch_semaphore_create(5);
    }
    return self;
}
- (void)otherTest
{
    for (int i = 0; i<20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}

- (void)test
{
    //DISPATCH_TIME_FOREVER,永远等待
    //DISPATCH_TIME_NOW 不等
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    dispatch_semaphore_signal(self.semaphore);
}

synchronized同步

synchronized是对mutex递归锁的封装

- (void)__saveMoney
{
    @synchronized (self) {
        [super __saveMoney];
    }
}

- (void)__drawMoney
{
    @synchronized (self) {
        [super __drawMoney];
    }
}

- (void)__saleTicket
{
    @synchronized (self) {
        [super __saleTicket];
    }
}

iOS线程同步方案性能比较

性能从高到低排序

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

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张

什么情况使用互斥锁比较划算

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

atomic

nonatomic和atomic
atomic:原子性

给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作,也就是说,保证setter和getter内部是线程同步的。

@property (assign, atomic) int age;

- (void)setAge:(int)age
{
    //加锁操作
    _age = age;
    //解锁操作
}

- (int)age
{
    //加锁操作
    return _age;
    //解锁操作
}

atomic并不能保证使用属性的过程是线程安全的
atomic加锁解锁太消耗性能,一般不使用


iOS中的读写安全方案

IO操作,也就是文件操作。

如何实现以下场景:
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许读和写的操作同时执行

上面的场景,就是典型的“多读单写”
经常应用于文件等数据的读写操作
iOS实现以上场景的方案有:

  • pthread_rwlock:读写锁
  • dispatch_barrier_async:异步栅栏调用
pthread_rwlock

等待锁的线程会进入休眠

pthread_rwlock使用方法:
在这里插入图片描述

举个例子:

#import <pthread.h>

@interface ViewController ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化s锁
    pthread_rwlock_init(&_lock, NULL);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        
        dispatch_async(queue, ^{
            [self write];
        });
    }
}

- (void)read
{
    pthread_rwlock_rdlock(&_lock);
    sleep(0.3);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    //销毁锁
    pthread_rwlock_destroy(&_lock);
}
dispatch_barrier_async

dispatch_barrier_async注意事项:
dispatch_barrier_async这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的
如果传入的是一个串行或者是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

dispatch_barrier_async使用方法:
在这里插入图片描述

举个例子:

@interface ViewController ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        [self read];
        [self read];
        [self read];
        [self write];
        [self write];
        [self write];
    }
}

- (void)read
{
    dispatch_async(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

- (void)write
{
    dispatch_barrier_async(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

问:NSMutableArray,在子线程操作,会有什么问题?

NSMutableArray可以做更删改查操作
如果多个子线程同时对NSMutableArray做删除操作,同时做添加操作,就会有问题
问题产生的现象其实跟买票卖票是一样的
只不过,一个是买卖票,一个是数组操作
这样的话,其实是需要加锁操作的

还是拿存钱取钱做例子,在存钱、取钱过程中,都加上NSLock的锁:

存取都加锁

使用上面介绍的NSLock,对存、取都进行加锁
打印结果:
在这里插入图片描述
执行结果没问题,数据是正确的,只是存和取不能同时进行

对于NSMutableArray,更删改都是需要对线程进行操作的,需要加锁,而是否需要加锁呢?

还是用存取钱做例子,增加一个操作,查看有多少钱的操作

- (void)checkMoney
{
    [NSThread sleepForTimeInterval:0.5];
    NSLog(@"查看余额,还剩%d元--%@", self.money, [NSThread currentThread]);
}

在这里插入图片描述
打印结果可知,中间出错了

还是切换成对数组的操作吧:


///测试MutableArray在子线程的操作
- (void)testMutableArrayInSubThread
{
    [self.dataSourceArray removeLastObject];
    
    dispatch_queue_t queue = dispatch_queue_create(@"myQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self insertArray];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self deleteArray];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self checkArray];
        }
    });
}

//插入数据
- (void)insertArray
{
    [self.moneyLock lock];
    [NSThread sleepForTimeInterval:0.5];
    [self.dataSourceArray addObject:@"2"];
    NSLog(@"插入数据后数组个数:%ld", self.dataSourceArray.count);
    [self.moneyLock unlock];
}

//删除数据
- (void)deleteArray
{
    [self.moneyLock lock];
    [NSThread sleepForTimeInterval:0.5];
    [self.dataSourceArray removeLastObject];
    NSLog(@"删除数据后数组个数:%ld", self.dataSourceArray.count);
    [self.moneyLock unlock];
}

//查看数据
- (void)checkArray
{
    [NSThread sleepForTimeInterval:0.5];
    NSLog(@"查看数据后数组个数:%ld", self.dataSourceArray.count);
}

运行结果:
在这里插入图片描述
说明是有问题的,所以,读,也必须加锁

当然,读可以多读,写是单写,因此,可以使用多读单写功能对NSMutableArray进行操作

扩展

数组移除的时候,会将对象置空操作吗?
比如:数组里面存了5个对象person,中间第3个person被移除,那么,此时数组里面存的是什么?

首先,数组里面存放的是地址,是对象存在堆上面的地址
移除元素后,要不将内容置空,要不不置空
根据资料查询可知:

  • 移除单个对象操作不会将元素置空
  • 移除所有元素[array removeAllObject];才会将元素置空

iOS NSMutableArray 底层分析


死锁

什么是死锁?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

死锁产生的4个必要条件?
  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。(资源的互斥性)
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。(时机:发生阻塞时)
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。(时机:资源在未使用完之前)
  • 循环等待条件:在发生死锁时,必然存在一个进程–资源的环形链。(时机:发生死锁时)

循环互斥、请求和保持、不可剥夺
方便记忆:互请不循环

解除死锁:

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

  • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
  • 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

更多学习关于死锁:
死锁面试题(什么是死锁,产生死锁的原因及必要条件)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值