Swift线程安全详解-概念,三种锁,死锁,Atomic,synchronized

前端之家收集整理的这篇文章主要介绍了Swift线程安全详解-概念,三种锁,死锁,Atomic,synchronized前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

原创Blog,转载请注明出处
http://blog.csdn.net/hello_hwc?viewmode=list
我的stackoverflow


前言:最近app中的日历小概率的在currentCalendar这个方法崩溃,看了下call
tree。研究了下,是线程安全问题。这里,就系统性的总结下线程安全这部分。

之后的博客绝大部分源码会用Swift来写了。


一些资料

  1. objc.io上对应多线程这一章
  2. Apple thread safe

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,只能在主线程上操作。

后台更新UI是很多开发者容易犯的错误

那么,为什么不把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@)
    }

}

可重入

Wiki的定义

简单来说,就是程序的执行过程中可能会发生中断,一个函数代码)在中间中断后,再继续执行(
重入),可以正常再一次从头执行这个函数,而不要等待上一次执行完毕。

比如这个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;
}

猜你在找的Swift相关文章