比如你去银行存10K,在用ATM存的同时,又使用网银取了10K,如果线程同步做的不好的话, 虽然取10K元的操作会有记录,但是可能不会反映在余额上。如果要解决这样的问题,就需要用到线程同步,线程同步在开发过程中是非常常见的。下面我们以12306为例,尝试解决如何保证一张票不会被不同窗口,不同客户端重复购买。
首先我们来认识一下互斥锁的概念:
互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
以上是维基百科给出的解释,如需了解更多请 点击这里。
简单来讲,互斥锁是为了防止多个线程访问同一个对象、方法、变量、文件等引起的数据错误。
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕, 当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
互斥锁是同步锁的一种,与它同级的还有递归锁、条件锁、自旋锁。其余几种作者认识并不是很深入,先简单提一下。下面我们使用代码来了解互斥锁:
objc_sync_enter 和 objc_sync_exit
表达式如下:
|
|
其中的 self
代表一把锁,它的传入值是任意一个对象,如果多线程访问同一个资源,那么必须使用同一把锁才能锁住。因为必须使用同一把锁,开发中如果需要加锁,通常我们可以直接使用 self
即可。
下面我们来运用 objc_sync_enter
和 objc_sync_exit
。
首先创建一个变量,代表当前车票的数量:
|
|
接下来,我们使用 NSBlockOperation
实例并添加任务,当票数大于 0 时,票数就可以自减 1,直到票数等于 0,
|
|
我们知道,NSBlockOperation
本身是不具备开线程的能力的,NSBlockOperationQueue
才可以开线程,而 NSBlockOperationQueue
是否可以开线程,又取决于任务的数量,现在我们的任务数只有一个,如果要使用线程同步,必须是两个以上的线程,所以我们再创建一个 NSBlockOperation
。将两个任务添加到队列中。
|
|
打印日志如下:
|
|
可以看出我们开启了两条线程,使用互斥锁执行同一个任务,结果是两条线程交叉执行这个任务,结果基本符合预期,但是在票数上却有问题,那么造成这个问题的原因是什么呢?
因为这两个线程是并发执行,在票数等于 1 时,一个线程通过 while self.iTicketCount > 0
的判断,然后一个线程也进入了这个判断,前面的进程遇到 objc_sync_enter(self)
,进入加锁执行,输出了 0,然后解锁。另一个进程也进入加锁执行,此时了票数为 0,所以输出了 -1。
如果说因为条件判断通过,所以造成了结果的错误,那么我们将条件判断 while self.iTicketCount > 0
也加入锁的范围内,会怎样呢?
我们将代码改成这样:
|
|
打印日志如下:
|
|
可以看出,只有一条线程执行任务,那么可以说明,只会有一个线程执行完整个任务后,另一个进程才会执行,但此时它已经没有任务可做了。这样也不对,那么正确做法是怎样的呢?
|
|
我们在 objc_sync_enter(self)
再加入一条判断,if self.iTicketCount > 0
那么输出的结果就没有问题了,可以看到,加锁的位置和条件的判断在同步进程中是非常重要的,如果同步线程的加锁位置和判断条件没有做好,可能会出现这段代码还没有执行,另一个线程中已经执行了,因此造成数据存取的不一致,下面我们来了解一下使用 NSBlock
来做同步线程:
NSLock
首先来看下 NSLock
的表达式:
|
|
可以看出,NSLock
是一个类,包含两个实例方法 tryLock
和 lockBeforeDate
以及一个存储属性 name
,并且继承了 NSLocking
,我们再来看下 NSLocking
这个父类:
|
|
NSLocking
中有两个实例方法,分别是加锁和解锁,关于 NSLock
还有一些其他的属性和方法,大家可以在帮助文档中找到,在此就不一一介绍了。
下面我们使用 NSThread
和 NSBlock
来实现和上面同样的功能,首先创建两个成员属性:
|
|
|
|
下面我们将 myLock
进行实例化,创建两个线程并添加任务:
|
|
我们要执行的任务是:
|
|
做好条件控制,在需要同步线程的代码上下分别启用 myLock.lock()
和 myLock.unlock()
就可以了。