iOS底层原理-多线程

多线程相关知识:

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

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

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

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

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

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

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

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

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

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

iOS底层原理-多线程_第1张图片
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)
//需要导入头文件

_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以上使用)

//需要导入头文件

_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的点语法方式对其进行静态初始化的原因

//需要导入头文件

//静态初始化
//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_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底层原理-多线程)