• 多线程学习(二)


    多线程的安全隐患

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

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

    两个经典的例子

    存钱取钱

    在这里插入图片描述

    卖票

    在这里插入图片描述

    在这里插入图片描述

    死锁

    什么是死锁?

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

    死锁产生的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__);
        });
    }
    
  • 相关阅读:
    paip.突破 网站 手机 验证码 的 破解 总结
    paip.执行shell cmd 命令uapi java php python总结
    paip.数组以及集合的操作uapi java php python总结..
    paip.字符串操作uapi java php python总结..
    paip.uapi 获取网络url内容html 的方法java php ahk c++ python总结.
    paip.提升用户体验--提升java的热部署热更新能力
    paip.java 以及JavaScript (js) 的关系以及区别
    paip. java resin 远程 调试 java resin remote debug
    paip.解决问题Unable to access jarfile E: esin-4.0.22lib esin.jar
    paip.spring 获取bean getBean 没有beanid的情况下
  • 原文地址:https://www.cnblogs.com/r360/p/15927504.html
Copyright © 2020-2023  润新知