线程安全是怎么产生的
每个线程都拥有它自己的执行堆栈,由内核调度独立的运行时间片。一个线程可 以和其他线程或其他进程通信,执行 I/O 操作,甚至执行任何你想要它完成的任务。 因为它们处于相同的进程空间,所以一个独立应用程序里面的所有线程共享相同的虚 拟内存空间,并且具有和进程相同的访问权限。线程编程的危害之一是在多个线程之间的资源争夺。如果多个线程在同一个时间 试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源。
同步工具
为了防止不同线程意外修改数据,你可以设计你的程序没有同步问题,或你也可 以使用同步工具。尽管完全避免出现同步问题相对更好一点,但是几乎总是无法实现。 以下个部分介绍了你可以使用的同步工具的基本类别。
1.原子操作(atomic
)
我们在声明一个变量的时候一般会使用nonatomic
,这个就是非原子操作;原子操作是atomic
。
简单的加减使用原子操作具有更高的性能优势。注意是加减,不是增删!!
也就是说仅仅对于getter
,setter
是线程安全的,两个线程都去对变量赋值是安全的。对于比如NSMutableArray
类型的增删操作不是线程安全的
2.内存屏障和 Volatile
变量
因为内存屏障和 volatile
变量降低了编译器可执行的优化,因此你应该谨慎使 用它们,只在有需要的地方时候,以确保正确性
3.锁Lock
锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section),这 些代码段在同一个时间只能允许被一个线程访问。比如,一个临界区可能会操作一个 特定的数据结构,或使用了每次只能一个客户端访问的资源
3.1 信号量 Dispatch Semaphore
我的上一篇文章中有讲过一次性搞懂 GCD的所有用法
使用
dispatch_semaphore_signal
加1dispatch_semaphore_wait
减1,为0时等待的设置方式来达到线程同步的目的和同步锁一样能够解决资源抢占的问题。
dispatch_semaphore_create(long value)
dispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema)
dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout)
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 等待ing");
dispatch_semaphore_wait(signal, overTime); //signal 值 -1
NSLog(@"线程1");
dispatch_semaphore_signal(signal); //signal 值 +1
NSLog(@"线程1 发送信号");
NSLog(@"--------------------------------------------------------");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 等待ing");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"线程2");
dispatch_semaphore_signal(signal);
NSLog(@"线程2 发送信号");
});
关于信号量,我们可以用停车来比喻:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值(signal
)就相当于剩余车位的数目,dispatch_semaphore_wait
函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)
),调用一次dispatch_semaphore_signal
,剩余的车位就增加一个;调用一次dispatch_semaphore_wait
剩余车位就减少一个;当剩余车位为 0 时,再来车(即调用dispatch_semaphore_wait
)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
3.2 POSIX
互斥锁
POSIX
互斥锁在程序里面很容易使用。
- 导入头文件
#import <pthread.h>
- 声明并
pthread_mutex_t mutex
, - 初始化
pthread_mutex_init(&mutex, NULL)
-
pthread_mutex_lock
和pthread_mutex_unlock
函数, 进行加锁解锁 *pthread_mutex_destroy
释放该锁的数据结构。
#import <pthread.h>
@interface MYPOSIXViewController ()
{
/** 声明pthread_mutex_t的结构 */
pthread_mutex_t mutex;
}
@end
@implementation MYPOSIXViewController
- (void)dealloc{
/** 释放该锁的数据结构 */
pthread_mutex_destroy(&mutex);
}
- (void)viewDidLoad {
[super viewDidLoad];
/** 初始化 */
pthread_mutex_init(&mutex, NULL);
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
/** 加锁 */
pthread_mutex_lock(&mutex);
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}
/** 解锁 */
pthread_mutex_unlock(&mutex);
}
3.3 NSLock
互斥锁
在 Cocoa 程序中 NSLock
中实现了一个简单的互斥锁。所有锁(包括 NSLock
)的 接口实际上都是通过NSLocking
协议定义的,它定义了 lock
和unlock
方法。你使用 这些方法来获取和释放该锁。
除了标准的锁行为,NSLock
类还增加了tryLock
和 lockBeforeDate:
方法。方法 tryLock
试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程。相反,它只 是返回 NO
。而 lockBeforeDate:
方法试图获取一个锁,但是如果锁没有在规定的时间 内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回 NO
)。
NSLock
头文件中只有如下方法,成员变量
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
3.4 NSCondition
条件锁
NSCondition
同样实现了NSLocking
协议,所以它和NSLock
一样,也有NSLocking
协议的lock
和unlock
方法,可以当做NSLock
来使用解决线程同步问题,用法完全一样。同时,NSCondition
提供更高级的用法。wait
和signal
,和条件信号量类似。
@interface TestViewController ()
/*
创建一个数组盛放生产的数据,创建一个线程锁
*/
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *products;
@end
@implementation TestViewController
#pragma mark - event reponse
/*
拖拽一个点击事件,创建两个线程
*/
- (IBAction)coditionTest:(id)sender {
NSLog(@"condiction 开始");
[NSThread detachNewThreadSelector:@selector(createConsumenr) toTarget:self withObject:nil];
[NSThread detachNewThreadSelector:@selector(createProducter) toTarget:self withObject:nil];
}
#pragma mark - provate methods
- (void)createConsumenr{
[self.condition lock];
while(self.products.count == 0){
NSLog(@"等待产品");
[_condition wait];
}
[self.products removeObject:0];
NSLog(@"消费产品");
[_condition unlock];
}
- (void)createProducter{
[self.condition lock];
[self.products addObject:[[NSObject alloc] init]];
NSLog(@"生产了一个产品");
[_condition signal];
[_condition unlock];
}
#pragma mark - getters and setters
- (NSMutableArray *)products {
if(_products == nil){
_products = [[NSMutableArray alloc] initWithCapacity:0];
}
return _products;
}
- (NSCondition *)condition {
if(_condition == nil){
_condition = [[NSCondition alloc] init];
}
return _condition;
}
@end
3.5 NSRecursiveLock
递归锁
递归锁可以被同一线程多次请求,而不会引起死锁,这主要是用在循环或递归操作中。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value){
[theLock lock];
if (value != 0){
--value;
MyRecursiveFunction(value);
}
MyRecursiveFunction(5);
3.6 @synchronized
写法最简单 @synchronized( xx )
, 也是性能消耗最高的锁
- (void)getIamgeName:(int)index{
NSString *imageName;
@synchronized(self) {
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
}
}
3.7 OSSpinLock
被公认为不安全,不做描述
线程安全总结
- 不可改变的对象一般是线程安全的。一旦你创建了它们,你可以把这些对象在线程间安全的传递。另一方面,可变对象通常不是线程安全的。为了在多线程应用 里面使用可变对象,应用必须适当的同步。
- 许多对象在多线程里面不安全的使用被视为是”线程不安全的”。只要同一时间 只有一个线程,那么许多这些对象可以被多个线程使用。这种被称为专门限制应 用程序的主线程的对象通常被这样调用。
- 应用的主线程负责处理事件。尽管ApplicationKit在其他线程被包含在事件路 径里面时还会继续工作,但操作可能会被打乱顺序。