线程的风险
当运行在一个多线程环境中,你总是需要注意一些事情:你不能控制线程执行的顺序。例如,如果你有两个线程,线程1和线程2,CPU可能会在线程1上运行一段时间,然后又会切到线程2运行一段时间。问题是你不知道CPU何时切过去,也不知道会为一个线程分配多少时间。每个线程运行的时间都不是公平的。
为了演示线程不容易控制带来的风险,我会举一个例子。这个例子包括两个线程:线程1和线程2. 线程1打印奇数,线程2打印偶数。这些数的范围从1到20. 线程1先启动,然后线程2再启动。这个例子将会运行3次。Listing 6-5 显示了样例代码。
正如你看到的,我先调用打印奇数,然后再打印偶数。你可能会期待看到
先打印一些奇数
奇数和偶数平均打印,例如两个奇数和两个偶数
但是,这些猜测都是不正确的,你可以看表格 6-4, 显示了3次运行的结果。
你看下第2次,0先打印出来,而其他的都是先打印1。奇数和偶数打印也不是平均的;而且,也没什么迹象表明有多少奇数在偶数之前打印。
因此,在多线程环境中,你不能控制线程执行的顺序。多线程是一把双刃剑。开发者实现一个多线程应用需要注意下面的3个风险。
安全性:这个标准意味着在多线程环境下,输出要跟预期的一致。换句话说,程序可以运行在不同的顺序中多次,但是最终的输出必须是可预见的,正确的。“糟糕的事情不会发生”。
活跃性:这个和安全性不同。一种定义是“一些好的情况最终会发生”。例如,假设线程A必须等到线程B的结果,有时这些结果从来都不返回。因此,线程A从来不会计算最终结果。这个通常称为死锁。
性能:iPhone应用最重要的一个目标就是有一个比较好的性能和更灵敏的UI。因此,你的性能目标必须达到。活跃性只关注一些最终发生的事情;它并不关心多快获取到结果。
我将会在接下来的部分使用例子来涉及到每一个标准,这样你就可以理解什么样会导致一个不好的结果,你如何解决它,使得你的应用运行时有一个比较高的性能。
安全性
安全性要求程序运行在多线程环境中,产生一个正确的期待的结果,就想他运行在单线程环境中一样。我会讨论一个潜在的在多线程环境中经常会发生的一个问题,当两个或多个线程同时访问相同的数据。
图 6-5 描述了两个线程如何返回一个相同的item而导致应用崩溃。在图6-5中,线程1尝试把item push到当前栈中。然后,线程2和线程3想要把item取出来,然后检查确保这个item在这个栈中。但是,在两个线程检查之后,线程2先运行,然后获取item。Oops!像你看到的,线程3已经没有item可以获取了。这会导致你的应用崩溃。
你可以从ThreadSafety工程中获取到样例代码,但是Listing 6-6 显示了这个问题的代码注解。注意这个问题不会总是出现,但是如果你运行足够多的时间,它还是会发生的。代码使用了NSMutableArray变量存储,因此客户端代码能够添加和删除数据。
当你运行上面的代码一段时间,你会收到下面的信息:
它告诉你,你尝试在一个空的数组中删除对象,这是不应该发生的当你在删除之前已经检查了数组空的情况。你甚至打印出来看它是否是最后一个对象。
现在,如果你再一次查看图6-4,你应该理解为什么会崩溃 -- 因为第一个现场检查和打印出最后一个对象后,第一个线程已经停止,而第二个线程还在运行。
解决办法
对于这个问题我的解决办法是锁住这个方法直到线程执行完。锁是一种机制,它能够确保在一个时间内只有一个线程访问一个指定的代码块。想象一下,你现在在一个比赛中,需要直接和很多人竞争。你和你的竞争者被问了一个问题,而谁先响铃谁就能先回答问题。在第一个结束之后,另外一个人又可以摇铃了。当第一个人在回答问题时,这个铃是被锁住的。线程也是一样的。你可以创建一个锁,就像是你的铃一样:第一个得到锁的线程(类似摇铃)会阻塞其他所有的线程直到它结束。在第一个线程结束之后,锁就开了(类似于其他人可以摇铃了);其他线程能尝试获取锁,而这个过程可以重复下去。
锁机制的基本概念就是确保当一个线程在执行任务时,其他线程不能打断。例如,如果线程1获取到这个对象,然后打印出来,线程2必须等到线程1执行完才能获取和从数组中删除对象。
这个锁创建在一个对象上。如果线程1从对象A获取了锁,其他线程就不能再获取这个对象的锁了,这些线程必须等待线程1执行完后,然后把锁返回给对象A。
最简单的方式获取对象A的锁的就是使用@synchronized(objA),如下面Listing 6-7 的代码。
注意:在很多情况下,使用self作为锁,效果是一样的。你只需要确保你想要锁住的对象使用同一个对象锁即可。例如,你有两个存储变量,你可以考虑为每一个单独是有关一个锁。 |
图6-6 显示了@synchronized在线程中是如何工作的。
你需要同时同步push和pop data这两个方法,因为如果你只锁住其中的一个方法,当你pop检查时,依然会存在风险,还有存储器push了很多数据,而你不没有按照你想要的方式获取到对象。为了防止这些,你需要使用lockObj同时锁住他们,这样在一个时间段就只有一个方法在运行。
你的代码是安全的,但是依然还有两个更重要的多线程属性需要讨论。