美文网首页
iOS底层原理-多线程

iOS底层原理-多线程

作者: 我是一只攻城狮_ifYou | 来源:发表于2018-07-25 14:46 被阅读428次

    多线程相关知识:

    同步线程:dispatch中的sync函数,即是在当前线程做事情

    异步函数:dispatch中的async函数,即在另外一条线程做事情

    并发队列:允许多个任务同时执行
    可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    并发功能只有在异步函数(dispatch_async)下才有效

    串行队列:让任务一个接一个地执行(执行完一个再执行下一个)

    注:通过CF开头的函数创建出来的变量,需要手动调用CFRealease去释放,但GCD的是不用的

    同步异步:能否开启新线程 (决定了是在哪个线程执行)

    • 同步:在当前线程中执行任务.不具备开启新线程的能力
    • 异步:在新的线程中执行任务,具备开启新线程的能力
    • 同步函数:立马在当前线程执行任务,执行完毕后才能继续往下执行,即同步函数内的任务不执行完,该函数就会卡住,不会继续往下执行
    • 异步函数:不要求立马在当前线程执行任务,会等上一个任务执行完再执行

    并发和串行:任务的执行方式

    • 并发:多个任务并发(同时)执行
    • 串行:一个任务执行完成后,再执行下一个任务

    主队列是一种特殊的串行队列
    只要是放到主队列的任务,都是在主线程执行

    Snip20180611_5.png

    死锁:

    队列的特点:FIFO(First In First Out)先进先出

    产生死锁的两个情况:

    • 当同步函数内的队列是主队列时,会产生死锁
    • 使用sync函数往当前串行队列中添加任务,就会产生死锁

    队列组:

    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"任务1 ----%@",[NSThread currentThread]);
        };
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"任务2 ----%@",[NSThread currentThread]);
        };
    });
    
    //等前面的任务都执行完,会自动执行这个任务
    dispatch_group_notify(group, queue, ^{
        
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i<5; i++) {
                NSLog(@"任务3 ----%@",[NSThread currentThread]);
            };
        });
    });
    
    

    多线程的安全隐患:

    Q:一块资源可能会被多个线程共享,容易造成数据错乱和数据异常的问题
    A:解决方法:使用线程同步技术(同步:协同步调,按预定的先后顺序次序进行)
     常用的线程同步技术:加锁

    • OSSpinLock(自旋锁):等待锁的线程会处于忙等状态,一直占用着CPU资源(high-level lock)
    //需要导入头文件<libkern/OSAtomic.h>
    
    _lock = OS_SPINLOCK_INIT;//初始化
    /*
     //也可以这么初始化锁
     static OSSpinLock lock;
     
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
     lock = 0;//OS_SPINLOCK_INIT的值就是0
     });
     
     */
    
    OSSpinLockLock(&_lock);//加锁
    //需加锁代码
    OSSpinLockUnlock(&_lock);//解锁
    

    注意点:

    • 所有线程应该共用一把锁,不然还是会存在问题,每次都创建一把新锁
      若有好几个方法同一时间只能使用一个方法,需要共用一把锁
    • 原理:类似写了一个while循环
    • 目前已经不再安全,有可能出现优先级反转的问题
      若有线程1和线程2,假设线程1的优先级大于线程2,线程2先进入代码,发现锁未加锁,故加锁执行代码,线程1后进入,发现该锁已经被加锁,故忙等,但由于线程1优先级大于线程2,CPU分配更多时间执行线程1的代码,可能导致线程2的代码没有时间执行,导致无法解锁会进入一个类似死锁的状态,目前苹果已经不推荐使用

    也可以使用static静态初始化锁,使自旋锁唯一,故锁不一定需要使用属性的方式
    无法在static变量初始化时动态调用函数,只能取个值,因为static是静态初始化,右边的值在编译就需要确定,若需要动态调用函数,需要再次使用once函数,在once代码里赋值

    当多条线程需要同时修改同一个值时,基本需要加锁,读取则不需要

    • os_unfair_lock(low-level lock)

    底层调用看,为互斥锁,等待os_unfair_lock锁的线程会处于休眠,而并非忙等,等不到锁就休眠(ios10以上使用)

    //需要导入头文件<os/lock.h>
    
    _lock = OS_UNFAIR_LOCK_INIT;//初始化
    
    os_unfair_lock_lock(&_lock);//加锁
    //需加锁代码
    os_unfair_lock_unlock(&_lock);//解锁
    
    • pthread_mutex(low-level lock)

    互斥锁,等待锁的线程会处于休眠状态,不需要使用时,要手动销毁

    注意点:结构体静态初始化只允许定义的同时进行赋值,不允许后续使用set方法赋值

    struct Date {
        int year;
        int month;
    }
    
    struct Date date = {2011,10}//这样是可以的
    
    struct Date date;
    date = {2011,10}//这样是不可以的
    
    //就是因为这样,解释了为什么不能定义一个pthread_mutex,再通过self的点语法方式对其进行静态初始化的原因
    
    
    //需要导入头文件<pthread.h>
    
    //静态初始化
    //self.lock = PTHREAD_MUTEX_INITIALIZER;
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    
    pthread_mutex_init(&_lock, &attr);
    
    //锁和条件可以在对象销毁时销毁(declloc)
    //属性可以立马销毁
    pthread_mutexattr_destroy(&attr);
    
    
    /*
     pthread_mutex_init(mutex, NULL);
     //初始化时,可以将&attr传NULL,即默认为PTHREAD_MUTEX_DEFAULT
     
     */
    

    PTHREAD_MUTEX_DEFAULT属性替换为PTHREAD_MUTEX_RECUESIVE,将锁变为递归锁,解决递归可能导致死锁的问题

    递归锁的本质:允许同一个线程对一把锁进行重复加锁

    //条件锁
    
    - (void)define{
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
        
        pthread_cond_init(&_con, NULL);//创建一个条件
        
        pthread_mutex_init(&_lock, &attr);
        
        pthread_mutexattr_destroy(&attr);
        
    }
    
    - (void)using1{
        pthread_mutex_lock(&_lock);
        
        //需要执行的代码
        pthread_cond_wait(&_con, &_lock);//会休眠,将锁放开,等待信号量到来,再次加锁执行下面代码
        
        pthread_mutex_unlock(&_lock);
    }
    
    - (void)using2{
        pthread_cond_signal(&_con);//告知对应的条件锁,该锁已经放开,可以继续使用
    }
    

    通过创建条件锁后,pthread_mutex能够办到线程等待的效果(多线程的依赖问题)

    tip:

    • 在汇编中敲入si,就是汇编指令级别的一行,遇到函数调用会进入函数内部
    • 汇编中敲入c,就是直接到断点的位置
    • NSLock(low-level lock)

    是pthread_mutex的普通锁的封装

    遵守NSLocking协议会执行下面两个函数:

    • - (void)lock;
    • - (void)unlock;

    其常用的API有2个:

    • - (BOOL)tryLock; 会进行尝试加锁,若能加锁,则加锁执行后续代码,不能加锁,则返回NO,继续执行后续代码(即相当于没有锁)
    • - (BOOL)lockBeforeDate:(NSDate *)limit;会判断在limit时间到来之前,能加锁成功,则加锁,执行后续代码,若等到limit时间到来时,还没加锁成功,则执行后续代码,返回加锁失败
    self.lock = [[NSLock alloc] init];
    
    [self.lock lock];
    //执行的代码
    [self.lock unlock];
    
    • NSRecursiveLock(low-level lock)

    是pthread_mutex的递归锁的封装,用法与NSLock一致

    • NSCondition(low-level lock)

    是对pthread_mutex和cond的封装,即加锁解锁条件都能使用

    其常用的API有4个:

    • - (void)wait;
    • - (BOOL)waitUntilDate:(NSDate *)limit;
    • - (void)signal;
    • - (void)broadcast;
    self.condition = [[NSCondition alloc] init];
    
    [self.condition lock];
    //需要执行的代码
    [self.condition wait];
    
    [self.condition unlock];
    
    
    [self.condition signal];//给NSCondition发送信号
    [self.condition broadcast];//给NSCondition发送广播
    
    • NSConditionLock(low-level lock)

    是对NSCondition的进一步封装,可以设置具体的条件值

    • - (instancetype)initWithCondition:(NSInteger)condition; 会初始化条件值,若直接使用init方法,则默认初始条件值为0
    • - (void)lockWhenCondition:(NSInteger)condition; 当条件值为condition时加锁
    • - (void)unlockWithCondition:(NSInteger)condition; 该方法会解锁,并将条件值修改为condition
    self.lock = [[NSConditionLock alloc]initWithCondition:1];//初始化条件值为1;
    
    [self.lock lockWhenCondition:1];//当条件值为1时加锁
    
    //要做的事
    
    [self.lock unlockWithCondition:2];//该方法会解锁,并将条件值修改为2
    
    

    注:若调用lock方法是不管条件值,即无论条件值为多少,lock方法都能进;

    • GCD串行队列

    直接使用gcd的串行队列也可以实现线程同步的

    • dispatch_semaphore

    semaphore:信号量

    • wait函数:如果信号量的值>0,就让信号量的值减1,继续往下执行代码
          如果信号量的值<=0,就会休眠等待,知道信号量的值变成>0,然后就让信号量的值减1,继续往下执行代码
    • signal函数:让信号量的值+1
    
    self.lock = dispatch_semaphore_create(5);//信号量的初始值,可以用来控制线程并发访问的最大数量,最大并发数量在这里就是5
    
    dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
    
    //这里的代码最多只有最大并发数条线程能同时执行,即最多只有5条线程能同时执行
    
    dispatch_semaphore_signal(self.lock);
    
    
    • @synchronized

    是对mutex递归锁的封装,底层实现是根据传入的对象,在hash表中寻找对应的锁

    @synchronized([self class]) { // objc_sync_enter
        //要做的事
    }// objc_sync_exit
    
    //()中可以传入任何对象为锁对象,当()中的锁对象一致时,表示共用一把锁
    

    以上的锁的性能从高到低排序:

    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • GCD串行队列
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

    推荐使用:dispatch_semaphore和pthread_mutex,是相对来说性能最高的

    当要实现一个方法一把锁时,可以通过static静态初始化变量,再通过dispatch_once方法,令该方法使用唯一一把锁(记得写在方法内部),也可以抽成宏定义,若多个地方需共用一把锁,只能把锁抽出来一起使用

    两个问题:

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

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

    属性的原子性与非原子性:

    • atomic:给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作,也就是说,保证setter和getter内部是线程同步,即在setter和getter内部中做加锁解锁的操作

    原子性操作:即保证多行代码能够按顺序执行完成,就如当做一个整体,同一行代码

    atomic并不能保证使用属性的过程是线程安全的,即只能保证setter和getter内部是线程安全的

    但atomic耗费性能,因为属性的setter和getter是调用很频繁的,但真正需要加锁操作时,再去进行加锁操作即可

    文件I/O操作:

    多读单写(读写安全):
    1.读取文件是同一时间允许多条线程同时进行读的操作
    2.只允许单条线程进行写的操作
    3.不允许既有读的操作又有写的操作 ,常用于文件等数据的读写操作

    • pthread_rwlock:等待锁的线程会进入休眠
    //需要导入#import <pthread.h>
    
    pthread_rwlock_init(&_lock, NULL);//初始化锁
    
    pthread_rwlock_rdlock(&_lock);//读锁加锁
    
    pthread_rwlock_wrlock(&_lock);//写锁加锁
    
    pthread_rwlock_unlock(&_lock);//解锁
    
    
    • dispatch_barrier_async

    执行到barrier里面的任务时,会自动出现一个栅栏,执行任务,其他任务时无法再进行读取操作的

    //这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的,如果传入的是一个串行或者是一个全局的并发队列,那这个函数等同于dispatch_async函数的效果
    self.queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    
    - (void)read {
        dispatch_async(self.queue, ^{
            //读操作
        });
    }
    
    - (void)write {
        dispatch_barrier_async(self.queue, ^{
            //写操作
        });
    }
    

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

    相关文章

      网友评论

          本文标题:iOS底层原理-多线程

          本文链接:https://www.haomeiwen.com/subject/blvpmftx.html