多线程学习(二)
多线程的安全隐患
资源共享
一块资源可能被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
两个经典的例子
存钱取钱
卖票
死锁
什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
死锁产生的4个必要条件?
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
更多学习关于死锁:
死锁面试题(什么是死锁,产生死锁的原因及必要条件)
#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);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50,还剩%d元--%@", self.money, [NSThread currentThread]);
}
- (void)drawMoney
{
int oldMoney = self.money;
sleep(0.3);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20,还剩%d元--%@", self.money, [NSThread currentThread]);
}
- (void)saleTicket
{
sleep(1.2);
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
多线程安全隐患的解决方案
使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是: 加锁
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);
OSSpinkLock目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着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__);
});
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了