Swift&Objective-CiOS 面试多线程

使用atomic一定是线程安全的吗?

2016-08-25  本文已影响2380人  IreneWu

线程安全

1.线程安全的概念

多条线程同时工作的情况下,通过运用线程锁,原子性等方法避免多条线程因为同时访问同一快内存造成的数据错误或冲突.

2.多线程数据为什么不安全

每条线程都有自己独立的栈空间. 但是他们公用了堆. 所以他们可能同时访问同一块内存空间. 因此造成数据冲突.

3.解决线程安全的方法

线程锁, 原子性.

补充

线程安全是相对的概念. 根据苹果的文档, 原子性并不能保证线程安全. 只是相对运用了原子性keyword 的属性来说是线程安全的. 对于类来说则不一定.

使用 atomic 一定是线程安全的么?

不是的。

nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全的,而atomic的操作是原子性的,但是并不意味着它是线程安全的,它会增加正确的几率,能够更好的避免线程的错误,但是它仍然是线程不安全的。

当使用nonatomic的时候,属性的setter,getter操作是非原子性的,所以当多个线程同时对某一属性读和写操作时,属性的最终结果是不能预测的。

当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:当线程A进行写操作,这时其他线程的读或者写操作会因为该操作而等待。当A线程的写操作结束后,B线程进行写操作,然后当A线程需要读操作时,却获得了在B线程中的值,这就破坏了线程安全,如果有线程C在A线程读操作前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。

也就是要注意:atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。如下列所示:

比如:@property(atomic,strong)NSMutableArray *arr;

如果一个线程循环的读数据,一个线程循环写数据,那么肯定会产生内存问题,因为这和setter、getter没有关系。如使用[self.arr objectAtIndex:index]就不是线程安全的。好的解决方案就是加锁。

据说,atomic要比nonatomic慢大约20倍

探讨一下Objective-C中几种不同方式实现的锁,在这之前我们先构建一个测试用的类,假想它是我们的一个共享资源,method1与method2是互斥的,代码如下:

@implementationTestObj

- (void)method1 

{

    NSLog(@"%@",NSStringFromSelector(_cmd));

}

- (void)method2

{

    NSLog(@"%@",NSStringFromSelector(_cmd));

}

@end

1.使用NSLock实现的锁

//主线程中

TestObj *obj = [[TestObj alloc] init];

NSLock *lock = [[NSLock alloc] init];

//线程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    [lock lock];

    [obj method1];

    sleep(10);

    [lock unlock];

});

//线程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    sleep(1);//以保证让线程2的代码后执行

    [lock lock];

    [obj method2];

    [lock unlock];

});

看到打印的结果了吗,你会看到线程1锁住之后,线程2会一直等待走到线程1将锁置为unlock后,才会执行method2方法。

NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lock和unlock方法外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

2.使用synchronized关键字构建的锁

当然在Objective-C中你还可以用@synchronized指令快速的实现锁:

//主线程中

TestObj *obj = [[TestObj alloc] init];

//线程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    @synchronized(obj){

        [obj method1];

        sleep(10);

    }

});

//线程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    sleep(1);

    @synchronized(obj){

        [obj method2];

    }

});

@synchronized指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(other),刚线程2就不会被阻塞,@synchronized指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

3.使用C语言的pthread_mutex_t实现的锁

//主线程中

TestObj *obj = [[TestObj alloc] init];

__block pthread_mutex_t mutex;

pthread_mutex_init(&mutex,NULL);

//线程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    pthread_mutex_lock(&mutex);

    [obj method1];

    sleep(5);

    pthread_mutex_unlock(&mutex);

});

//线程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    sleep(1);

    pthread_mutex_lock(&mutex);

    [obj method2];

    pthread_mutex_unlock(&mutex);

});

pthread_mutex_t定义在pthread.h,所以记得#include

4.使用GCD来实现的”锁”

以上代码构建多线程我们就已经用到了GCD的dispatch_async方法,其实在GCD中也已经提供了一种信号机制,使用它我们也可以来构建一把”锁”(从本质意义上讲,信号量与锁是有区别,具体差异参考信号量与互斥锁之间的区别):

//主线程中

TestObj *obj = [[TestObj alloc] init];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

//线程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    [obj method1];

    sleep(10);

    dispatch_semaphore_signal(semaphore);

});

//线程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    sleep(1);

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    [obj method2];

    dispatch_semaphore_signal(semaphore);

});

5.使用自旋锁OSSpinLock来实现的”锁”

//主线程中

TestObj *obj = [[TestObj alloc] init];

OSSpinLock spinlock = OS_SPINLOCK_INIT;

//线程1

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    OSSpinLockLock(&spinlock);

    [obj method1];

    sleep(10);

    OSSpinLockUnlock(&spinlock);

});

//线程2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

    sleep(1);//以保证让线程2的代码后执行

    OSSpinLockLock(&spinlock);

    [obj method2];

    OSSpinLockUnlock(&spinlock);

});

一些高级锁:详见Objective-C中不同方式实现锁(二)

1.NSRecursiveLock递归锁

2.NSConditionLock条件锁

3.NSDistributedLock分布式锁

总结:

耗时方面:

OSSpinlock耗时最少;

pthread_mutex其次。

NSLock/NSCondition/NSRecursiveLock 耗时接近,220ms上下居中。

NSConditionLock最差,我们常用synchronized倒数第二。

dispatch_barrier_async也许,性能并不像我们想象中的那么好.推测与线程同步调度开销有关。单独block耗时在1ms以下基本上可以忽略不计的。

1、@synchronized

内部会创建一个异常捕获的handler和其他内部使用的锁。所以会消耗大量的时间

2、NSLock 和 NSLock+IMP

两个时间非常接近。他们是pthread mutexes封装的,但是创建对象的时候需要额外的开销。

3、pthread_mutex

底层的API,性能比较高。

4、OSSpinLock

自旋锁几乎不进入内核,仅仅是重新加载自旋锁。

如果自旋锁被占用时间是几十,上百纳秒,性能还是挺高的。减少了代价较高的系统调用和一系列上下文言切换。

但是,该锁不是万能的;如果该锁抢占比较多的时候,不要使用该锁。会占用较多cpu,导致耗电较多。

这种情况下使用pthread_mutex虽然耗时多一点,但是,避免了电量过多的消耗。是不错的选择。

上一篇下一篇

猜你喜欢

热点阅读