多线程学习(二)

多线程的安全隐患

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

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

两个经典的例子

存钱取钱

在这里插入图片描述

卖票

在这里插入图片描述

在这里插入图片描述

死锁

什么是死锁?

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

死锁产生的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__);
    });
}
posted @   任淏  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示