首先看一下问题引出,先看一些经典的问题.

多线程的隐患

首先我们利用多线程的话肯定是好处多多,因为我们可以同时去做一些事情,大大的提高了效率.像我们下载视频的时候就可以同时下载多个视频,这样是节省了很多时间,用户体验也会更好.但是用得时候也会存在一些安全隐患,比如同一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,这样会出现一些数据错乱和数据安全的问题.下面我们就看一些例子.


存钱取钱案例

比如我现在有1000元,同时有2个线程去处理,一个线程是取钱100元,一个线程是存钱100元,我制作了一张示意图如下:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

存钱取钱示意图

我们从上面的图应该很清楚,存100、取100的最终结果就是还是剩余1000元,但是看我们上面的示意图,最终结果要么是900要么是1100,这就与1000的结果对不上,所以很明显用多线程是会存在隐患,下面我们用代码演示一下上面的结果:

卖票案例

这个和上面的稍微有点差别,因为上面的是2个操作,而卖票呢,它是1个操作.同样的比如我现在有1000张票,同时有2个线程去处理卖票,一个线程是一个线程卖票100张,另一个线程也是卖票100张,同时操作的话,也会出现异常,我制作了一张示意图如下:

我们同样的也可以用代码演示一下效果:

上面的两个,大家可以试试.接下来针对上面的问题,我们就引出了今天的主角,线程同步技术

线程同步技术

解决方案:使用线程同步技术 (同步,就是协同步调,按照预定的先后次序进行运行),我们先来看下线程同步技术有哪些方案:线程同步方案最常见的技术就是:加锁.大概方案如下(大致这么多方案,当然还有其他的)

OSSpinLock (自旋锁)

os_unfair_lock (互斥锁)

pthread_mutex (互斥锁、递归锁)(里面3种类型,目前只说2种对我们有用的)

dispatch_queue (DISPATCH_QUEUE_SERIAL)

NSLock

NSRecursiveLock

NSCondition

NSConditionLock

@synchronized

以上的这些都是可以做到线程同步方案,我会一个一个介绍,并且介绍它们的优缺点,性能怎么样,我们怎么去选择等等.我们就以上面的例子讲解.

OSSpinLock (自旋锁)

OSSpinLock 叫做 "自旋锁", 等待锁的状态会处于忙等( busy-wait )状态,一直占用着CPU内存.头文件导入<libkern/OSAtomic.h>,而且这个锁是过期锁,iOS10以后就过期了,但是我们还是来看看,因为面试中可能会遇到,用法如下:

初始化锁:OSSpinLock spinLock = OS_SPINLOCK_INIT;

加锁: OSSpinLockLock(&_spinLock);

解锁: OSSpinLockUnlock(&_spinLock);

我们先看卖票:

我是加锁和解锁了,为什么上面的代码还有问题,有发现原因的吗?这是因为SpinLock是局部变量,所以我们进去都是初始化了一把新锁,这把锁并没有被使用过,是达不到加锁的目的.所以所有的线程都是用同一把锁才能达到加锁的目的.请看下面代码:

确实是剩下85张,没有问题.原理是这样,每次执行saleTicket都会进入 //加锁: OSSpinLockLock(&_SpinLock) 这个代码,第一次进来是正常给_SpinLock加锁,第二次进来的时候,发现_SpinLock已经被人加了锁,它会在这边等待,等待这把锁被解锁,解锁完了以后然后它再去加锁,就这样依次进行,就保证了里面的那段代码同时只有一个线程在处理.这就解决了线程同步的问题.

接下来我们就看存钱和取钱的问题.存钱、取钱是2个操作,我们是用同一把锁还是2把锁?思考了这个问题我们就知道怎么做了,因为存钱和取钱是不能同时进行,所以我们就用同一把锁即可(2把锁是有问题的大家可以自己试试),请看下面的代码,我们验证一下:

自旋锁"忙等"状态是怎么等呢?忙等就是一直忙碌,而且还在等待,类似这样while(锁还没有解开),就会一直执行,占用cpu,直到锁被放开.而且OSSpinLock是已经过期了,而且目前已经不再安全,可能会出现优先级反转的问题.

下面说一下线程的调度问题

其实你看上面的图,如果随着时间的推移,操作系统把时间给thread1一点时间,再给thread2一点时间,再给thread3一点时间,而且这个时间周期非常短,就这样一直非常快的切换,这样下来给我们的感觉就是同时执行.这就是实现多线程的一个方案.也就是多线程的原理,我们也可以说这是时间片轮转调度算法.调用进程或者线程都是用这套算法

还有个就是线程的优先级问题,比如thread1的优先级比较高,那么操作系统就会给thread1多一点时间去执行.其他的线程就少一点时间去执行.这样的话,我们使用自旋锁就会存在一个优先级反转的问题.比如thread1优先级非常高,thread2优先级很低.首先是thread2先进去加锁,thread1再进来就会等thread2解锁,由于thread1的优先级非常高,CPU就把大量的时间给了thread1,此时可能导致thread2没有时间执行解锁,thread1就会一直执行等待,有点死锁的感觉.

这样大家想一想:如果优先级高的不是忙等,而是休眠,休息就不会占用CPU,那不就是解决了这个问题.

os_unfair_lock (互斥锁)

os_unfair_lock用于取代不安全OSSpinLock,是从iOS10开始支持.

从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等(后面会证明一下)

它的用法和OSSpinLock非常像,需要倒入头文件<os/lock.h>,用法如下:

初始化锁:os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;

加锁: os_unfair_lock_lock(&_unfairLock);

解锁:os_unfair_lock_unlock(&_unfairLock);

下面我们就去看一下用法

存钱和取钱也是一样的道理,我们可以自己试试.

pthread_mutex (互斥锁)

像这种pthread开头的一般都是跨平台的Windows、linux等等都是可以用的,mutex叫做"互斥锁",等待锁的线程会处于休眠状态

其实用法都是差不多,我们先来看下怎么用,这个稍微代码多一点点

//初始化属性

pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

//初始化锁

pthread_mutex_init(&_mutex, &attr);(&attr也可以传NULL,这样的话,上面的都是默认的,上面可以都不用写)

//加锁

pthread_mutex_lock(&_mutex);

//解锁

pthread_mutex_unlock(&_mutex);

//销毁相关资源

pthread_mutexattr_destroy(&attr);

pthread_mutex_destroy(&_mutex);

其中PTHREAD_MUTEX_NORMAL是锁的类型,后面会细说,先传默认PTHREAD_MUTEX_NORMAL

先看运行结果:

没有问题,记得销毁哈,之前说的2个锁,没有提供销毁的方法,那我们就不写,如果提供了,我们还是写一下的好!

pthread_mutex (递归锁)

我们再看另一种情况,请看下面的代码:

上面这些代码会出现什么情况?死锁,会出现相互等待的情况,只会输出第一个NSLog,遇到这种情况我们怎么解决才好呢?2把不同的锁即可解决问题,就是otherMutexTest里面一把锁,otherMutexTest2里面另一把锁就可以解决了,这个我就不截图了,我们可以自己试试

再看下面另一种情况:出现递归怎么办?如下图

遇到上面的这种情况我们又怎么处理,如果就像截图那样的话,就会出现休眠等待.如果我们想执行下去,我们就是可以设置锁的类型来解决这个问题:一共3种类型如下

我们只要把锁的类型换成递归锁,立刻就能解决这个问题,我们加一个递归停止条件,不然会一直运行

还有一个注意的,这个允许重复加锁,一定是在同一个线程,如果是多个线程的话,就不行.递归锁:允许同一个线程对一把锁重复加锁.

自旋锁、互斥锁汇编分析

自旋锁:一直忙等,占用CPU内存,一直在执行代码;互斥锁:不等待,休眠.不执行代码.我们怎么去证明这个问题呢?我们可以从汇编实现上去证明这个问题,我们先看OSSpinLock自旋锁:

首先我们如果用这个上面的来调试的话,是看不出来什么效果的,因为这里面都是一大段一大段汇编代码执行的,我们需要一句一句的执行汇编指令.就需要敲si,s是step的意思,代码一行一行的执行,如果只用s的话,就是一行oc代码执行,一行oc对应可能一大段汇编,所以我们还需要加i,i是instruction的意思是一行一行汇编指令执行,简称si. 还有个是nexti,它也是一行一行汇编指令执行,只是nexti它是遇到函数就会一下执行过去.因为我们要看函数实现,所以我们用si.

我们再看一下,我代码是怎么写的:

我是创建了10个线程去执行卖票,而且在卖票中间sleep(100),这样是为了,第一条线程进去,我们不管,我们主要看第二条线程在这等待的时间,到底做了什么事.所以我们主要看第二条的汇编代码.sleep(100)是为了时间长点,方便我们能看出做什么事.如果时间太短,直接第二条线程就不等待,那我们就看不到效果,请看下面的结果

从上面的结果看,进入OSSpinLockLock函数,它会一直在81aef那里一直循环执行,这是外循环,我们所说的自旋锁就是这样,一直循环执行,占用CPU内存.一旦有人放开这把锁就会条件循环结束,不会再执行循环.

接下来我们看看互斥锁pthread_mutex

查找的方法和上面的一样,我就截图最关键的图即可,请看下面:

执行到最后,直接是callsys,调用系统的方法,是不是类似我之前说的runloop里面的休眠的方法,而且我们知道休眠是任何事情都不会做,不占用CPU内存,所以我们最后看到,我的模拟器立刻又弹出来了,说明确实是睡眠,不占用任何CPU内存.

os_unfair_lock_lock我们可以用上面的方法尝试,它的结果也是互斥锁.

由于锁内容较多,我会在接下来的博客继续介绍
接下来我会继续努力编写其他博客,您的支持就是我最大的动力!