原创Blog,转载请注明出处
http://blog.csdn.net/hello_hwc?viewmode=list
我的stackoverflow
前言:最近app中的日历小概率的在currentCalendar这个方法崩溃,看了下call
tree。研究了下,是线程安全问题。这里,就系统性的总结下线程安全这部分。
之后的博客绝大部分源码会用Swift来写了。
一些资料
iOS中的线程
什么是线程?
线程是操作系统进行调度的最小单位,它被包含在进程中,是进程运作的实际单位。在单核系统中,按照时间片轮转的方式实现线程的并发执行,在多核系统中,能够真正实现线程同时执行。
iOS中与多线程相关的常用的有NSThread,GCD,NSOperationQueue.还有一个不常用,但是理解很重要的NSRunloop。
什么是线程安全?
再看看WIKI中的定义
A piece of code is thread-safe if it only manipulates shared data structures in a manner that guarantees safe execution by multiple threads at the same time.
一段代码在多个线程上调用,并且共享一段内存空间。如果代码能够安全执行,就是线程安全的。
举个例子
NSMutableArray不是线程安全的,那么以下代码就会小概率崩溃
let@H_403_55@ queue1 = dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
let@H_403_55@ queue2 = dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
dispatch_async(queue1)@H_403_55@ { ()@H_403_55@ ->@H_403_55@ Void in@H_403_55@
for@H_403_55@ index in@H_403_55@ 1.@H_403_55@..500@H_403_55@{
self.emptyArray.addObject(NSNumber(integer@H_403_55@: index))
}
}
dispatch_async(queue2)@H_403_55@ { ()@H_403_55@ ->@H_403_55@ Void in@H_403_55@
for@H_403_55@ index in@H_403_55@ 500.@H_403_55@..1000@H_403_55@{
self.emptyArray.addObject(NSNumber(integer@H_403_55@: index))
}
}
运行的话,崩溃如图
错误Log
malloc: * error for object 0x7fb5804a9d40: pointer being freed was not allocated
* set a breakpoint in malloc_error_break to debug
注意,这种崩溃是小概率的,但是当用户量大的时候,就会知道小概率也是不可忽视的
这不会详细介绍NSMutableArray的背后实现原理,感兴趣的同学可以看看这篇文章
错误原因-抽象到malloc和free的问题
每次malloc会分配新的地址,然后free释放。有可能按照如图的方式malloc-malloc-free-free.这样同一个地址会free两次,也就crash了。类似的在ARC中release一个reference count为0的对象也会出错。
UIKit以及Fundation
事实上,大多数Cocoa提供的Api都不是线程安全的,尤其是与UI相关的UIKit,只能在主线程上操作。
那么,为什么不把Cocoa的API写成线程安全的呢?Apple那么多天才工程师,难道解决不了吗?
答案很明显:为了执行效率,大多数的时候不需要并行的执行一段代码,而加上锁,递归锁之类的东西,执行效率会降低很多。需要线程安全的时候,开发者自己维护就可以了。
通常,不可变对象是线程安全的,可变对象不是线程安全的
以下Fundation对象是线程安全的,
意味着,你可以在多个线程中访问一个对象(读写),而不需要加锁
NSArray@H_403_55@
NSAssertionHandler
NSAttributedString
NSCalendarDate
NSCharacterSet
NSConditionLock
NSConnection
NSData
NSDate@H_403_55@
NSDecimal functions
NSDecimalNumber
NSDecimalNumberHandler
NSDeserializer
NSDictionary@H_403_55@
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSException@H_403_55@
NSFileManager@H_403_55@ (in OS X v10.5@H_403_55@ and later)
NSHost
NSLock
NSLog@H_403_55@/NSLogv
NSMethodSignature
NSNotification@H_403_55@
NSNotificationCenter@H_403_55@
NSNumber@H_403_55@
NSObject@H_403_55@
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
NSProxy
NSRecursiveLock
NSSet
NSString@H_403_55@
NSThread@H_403_55@
NSTimer
NSTimeZone
NSUserDefaults@H_403_55@
NSValue
NSXMLParser
Object allocation and retain count functions
Zone and memory functions
以下对象不是线程安全的
NSArchiver
NSAutoreleasePool@H_403_55@
NSBundle@H_403_55@
NSCalendar@H_403_55@
NSCoder
NSCountedSet
NSDateFormatter
NSEnumerator
NSFileHandle
NSFormatter
NSHashTable functions
NSInvocation
NSJavaSetup functions
NSMapTable functions
NSMutableArray@H_403_55@
NSMutableAttributedString
NSMutableCharacterSet
NSMutableData
NSMutableDictionary@H_403_55@
NSMutableSet
NSMutableString@H_403_55@
NSNotificationQueue
NSNumberFormatter
NSPipe
NSPort
NSProcessInfo
NSRunLoop
NSScanner
NSSerializer
NSTask
NSUnarchiver
NSUndoManager
objc_sync_enter/objc_sync_exit
先看看文档,个人建议,遇到一个新的Api或者技术一定要先看看Apple的文档,别人的博客只是一个参考,很有可能别人博客的理解就是错的,当然也包括我的博客
objc_sync_enter
Begin synchronizing on ‘obj’. Allocates recursive pthread_mutex associated with ‘obj’ if needed.
这个和Objective C中的synchronized关键词类似。对某一个对象加互斥锁,保证线程一在访问对象的过程中,线程二以及其他线程不能访问。(简单来说互斥锁允许同一个线程lock一个对象两次,而不会死锁,更多可以看看StackOverFlow这个答案)
要成对的使用
objc_sync_enter(object@H_403_55@)
//Do something with object@H_403_55@
objc_sync_exit(object@H_403_55@)
Lock(锁)
先看看不加锁会出现什么情况,可以先看看Objc上关于计数器的例子,那个可能更简单更直接一点。
举个例子
一个很简单的Swift的Model 类
class@H_403_55@ Person{
var name@H_403_55@:String
var age:UInt32
init(name@H_403_55@:String,age:UInt32){
self.name@H_403_55@ = name@H_403_55@
self.age = age
}
func update(name@H_403_55@:String,delay@H_403_55@:UInt32,age:UInt32){
self.name@H_403_55@ = name@H_403_55@
self.age = age
}
}
这里,要明白那么,这个update在并行更新Self的时候,就会有问题了。画一张图来详细解释下
当我们在线程一上更新person为name:jack,age25,线程二上更新为另一个
于是,我们得到了一个混合的值,这个值是name lucy,age 25,这是一个完全没有任何意义的值。而且, 这种错误,很难调试
用代码来模拟这种情况
class ViewController: UIViewController {
var@H_403_55@ person =@H_403_55@ Person(name: "Leo"@H_403_55@,age: 23@H_403_55@)
override func viewDidLoad() {
super.@H_403_55@viewDidLoad()
let@H_403_55@ queue1 =@H_403_55@ dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
let@H_403_55@ queue2 =@H_403_55@ dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
dispatch_async(queue1) { () -> @H_403_55@Void@H_403_55@ in@H_403_55@
self@H_403_55@.@H_403_55@person.@H_403_55@update("jack"@H_403_55@,delay: 2@H_403_55@,age: 25@H_403_55@)
}
dispatch_async(queue2) { () -> @H_403_55@Void@H_403_55@ in@H_403_55@
self@H_403_55@.@H_403_55@person.@H_403_55@update("lucy"@H_403_55@,delay: 1@H_403_55@,age: 24@H_403_55@)
}
self@H_403_55@.@H_403_55@performSelector("logPerson"@H_403_55@,withObject: nil,afterDelay: 4@H_403_55@)
// Do any additional setup after loading the view,typically from a nib.@H_403_55@
}
func logPerson(){
NSLog("%@ %d"@H_403_55@,person.@H_403_55@name,person.@H_403_55@age)
}
}
可以看输出
lucy 25
使用互斥锁-NSLock
先看一下这个类的文档
An NSLock object is@H_403_55@ used to@H_403_55@ coordinate the@H_403_55@ operation of@H_403_55@ multiple threads of@H_403_55@ execution within the@H_403_55@ same application@H_403_55@. An NSLock object can be used to@H_403_55@ mediate access to@H_403_55@ an application@H_403_55@’s global@H_403_55@ data or@H_403_55@ to@H_403_55@ protect a critical section of@H_403_55@ code,allowing it@H_403_55@ to@H_403_55@ run@H_403_55@ atomically.
这个类继承自NSObject,实现了NSLocking协议
其中NSLocking协议,定义了两个方法,lock和unlock。用来加锁,解锁。
使用NSLock的时候,要注意unlock的调用要和lock在同一线程上
当我们调用互斥锁进行加锁以后
class ViewController: UIViewController {
let lock = NSLock()
var person = Person(name: "Leo"@H_403_55@,age: 23@H_403_55@)
override func viewDidLoad() {
super.viewDidLoad@H_403_55@()
let queue1 = dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
let queue2 = dispatch_queue_create("com.test.queue1"@H_403_55@,DISPATCH_QUEUE_SERIAL)
dispatch_async(queue1) { () -> Void in@H_403_55@
self.lock@H_403_55@.lock@H_403_55@()
self.person@H_403_55@.update@H_403_55@("queue1"@H_403_55@,age: 1@H_403_55@)
self.lock@H_403_55@.unlock@H_403_55@()
}
dispatch_async(queue2) { () -> Void in@H_403_55@
self.lock@H_403_55@.lock@H_403_55@()
self.person@H_403_55@.update@H_403_55@("queue2"@H_403_55@,age: 2@H_403_55@)
self.lock@H_403_55@.unlock@H_403_55@()
}
self.performSelector@H_403_55@("logPerson"@H_403_55@,afterDelay: 4@H_403_55@)
// Do any additional setup after loading the view,typically from a nib.
}
func logPerson(){
NSLog("%@ %d"@H_403_55@,person.name@H_403_55@,person.age@H_403_55@)
}
}
Lucy 24
死锁
dispatch_async(queue1) { () -> Void in@H_403_55@
self.lock@H_403_55@.lock@H_403_55@()
self.lock@H_403_55@.lock@H_403_55@()
self.person@H_403_55@.update@H_403_55@("Jack"@H_403_55@,age: 25@H_403_55@)
self.lock@H_403_55@.unlock@H_403_55@()
self.lock@H_403_55@.unlock@H_403_55@()
}
然后,你就会发现死锁了
2015-11-26 12:28:23.245 LeoThreadSafe[1365:131272] * -[NSLock lock]: deadlock ( ‘(null)’)
最后,住线程Log输出
2015-11-26@H_403_55@ 12:28@H_403_55@:27@H_403_55@.266@H_403_55@ LeoThreadSafe@H_403_55@[1365:131140]@H_403_55@ Leo@H_403_55@ 23
死锁的原因
线程1等待自己的结束,也就造成了一个死循环:自己不结束就一直等待,自己等待就没办法结束。同样,线程2因为要等待线程1的lock解锁才能开始,线程2直接就不能执行了,所以输出Leo 23
当然,死锁也会发生在线程之间
线程之前相互等待对方解锁,才能继续
递归锁
iOS中,除了NSLock之外,还提供了递归锁,递归锁解决了NSLock在同一个线程上不能多次加锁的问题。
相关类 - NSRecursiveLock
NSRecursiveLock defines a@H_403_55@ lock that may be acquired multiple times by@H_403_55@ the@H_403_55@ same thread without@H_403_55@ causing a@H_403_55@ deadlock,a@H_403_55@ situation where a@H_403_55@ thread is permanently blocked waiting for@H_403_55@ itself to@H_403_55@ relinquish a@H_403_55@ lock. While the@H_403_55@ locking thread has one@H_403_55@ or@H_403_55@ more locks,all other threads are prevented from@H_403_55@ accessing the@H_403_55@ code protected by@H_403_55@ the@H_403_55@ lock.
NSRecursiveLock定义了一种锁,这种锁能够在同一个线程上多次加锁,而不会引起死锁(一个线程永远的等待自己解锁)。当被锁住的线程有一个或者多个锁的时候,其他线程就不能访问被保护的代码。
把上文的例子换成递归锁,就能够正常输出了
let@H_403_55@ lock@H_403_55@ = NSRecursiveLock()
条件锁
Cocoa还提供了另外一种跟高级的锁-NSConditionLock,顾名思义,按照某种条件加锁。
class ViewController: UIViewController {
let lock = NSConditionLock(condition: 10@H_403_55@)
var person = Person(name: "Leo"@H_403_55@,DISPATCH_QUEUE_SERIAL)
dispatch_async(queue1) { () -> Void in@H_403_55@
self.lock@H_403_55@.lockWhenCondition@H_403_55@(10@H_403_55@)
self.person@H_403_55@.update@H_403_55@("Jack"@H_403_55@,age: 25@H_403_55@)
self.lock@H_403_55@.unlockWithCondition@H_403_55@(10@H_403_55@)
}
dispatch_async(queue2) { () -> Void in@H_403_55@
self.lock@H_403_55@.lockWhenCondition@H_403_55@(10@H_403_55@)
self.person@H_403_55@.update@H_403_55@("Lucy"@H_403_55@,age: 24@H_403_55@)
self.lock@H_403_55@.unlockWithCondition@H_403_55@(10@H_403_55@)
}
self.performSelector@H_403_55@("logPerson"@H_403_55@,person.age@H_403_55@)
}
}
可重入
简单来说,就是程序的执行过程中可能会发生中断,一个函数(代码)在中间中断后,再继续执行(
重入),可以正常再一次从头执行这个函数,而不要等待上一次执行完毕。
比如这个C函数
int function@H_403_55@()
{
mutex_lock();
// ...@H_403_55@
function@H_403_55@ body
// ...@H_403_55@
mutex_unlock();
}
如果在body处中断,则改线程一直加锁,不能重入。
关于Objective C
参考这篇文章
Atomic
一个非Atomic的属性在非ARC的时候像这样
- (void)@H_403_55@setUserName:(NSString@H_403_55@ *)@H_403_55@userName { if (userName@H_403_55@ != _@H_403_55@userName@H_403_55@)@H_403_55@ { [userName retain]; [_userName release]; _userName = userName; } }@H_403_55@
可以看到,如果在多线程同时set的情况下,可能会造成release两次。程序崩溃。
Property的Runtime对应的C代码为
static@H_403_55@ inline@H_403_55@ void@H_403_55@ reallySetProperty(id@H_403_55@ self@H_403_55@,SEL _cmd,id@H_403_55@ newValue,ptrdiff_t offset,bool@H_403_55@ atomic,bool@H_403_55@ copy,bool@H_403_55@ mutableCopy)
{
id@H_403_55@ oldValue;
id@H_403_55@ *slot = (id@H_403_55@*) ((char@H_403_55@*)self@H_403_55@ + offset);
if@H_403_55@ (copy) {
newValue = [newValue copyWithZone:NULL@H_403_55@];
} else@H_403_55@ if@H_403_55@ (mutableCopy) {
newValue = [newValue mutableCopyWithZone:NULL@H_403_55@];
} else@H_403_55@ {
if@H_403_55@ (*slot == newValue) return@H_403_55@;
newValue = objc_retain(newValue);
}
if@H_403_55@ (!atomic) {
oldValue = *slot;
*slot = newValue;
} else@H_403_55@ {
spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
_spin_lock(slotlock);
oldValue = *slot;
*slot = newValue;
_spin_unlock(slotlock);
}
objc_release(oldValue);
}
可以看到,如果是nonatomic的,
synchronized
可以看看这个StackOverflow问题
这个关键词会对访问的变量加互斥锁
- (NSString@H_403_55@ *)myString {
@synchronized@H_403_55@(self@H_403_55@) {
return@H_403_55@ [[myString retain] autorelease];
}
}
可以类似转变为(实际转变肯能要更复杂,可以这么理解)
- (NSString@H_403_55@ *)myString {
NSString@H_403_55@ *retval = nil@H_403_55@;
pthread_mutex_t *self_mutex = LOOK_UP_MUTEX(self@H_403_55@);
pthread_mutex_lock(self_mutex);
retval = [[myString retain] autorelease];
pthread_mutex_unlock(self_mutex);
return@H_403_55@ retval;
}