四.并发编程

四.并发编程

  • 创建和管理线程
  • 多线程优化技术(GCD)
  • 操作和队列

4.1 线程

线程时运行时执行的一组指令序列,每个进程至少包含一个线程。在iOS中,进程启动时的主要线程称作主线程。所有UI元素都需要在主线程中创建和管理。

4.2 线程开销

每个线程都有一定的开销,从而影响到应用的性能。线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。

4.2.1 内核数据结构

每个线程大约消耗1KB的内核内存空间。这块内存用于存储于线程有关的数据结构和属性。这块内存时联动内存(联动内存(Wired Memory)中的内容必须保留在内存中而不能被移动到磁盘或其他外部存储中,联动内存的容量取决于当前使用的应用软件)。

4.2.2 栈空间

主线程的栈空间大小为1M,而且无法修改。所有的二级线程默认分配512KB的栈空间。 在线程启动前,栈空间的大小可以被改变。栈空间的最小值是16KB,而且其数值必须是4KB的倍数。

//修改栈空间
+(NSThread *)createThreadWithTarget:(id)target selector:(SEL)selector object:(id)argument stackSize:(NSUInteger)size{
	if ((size % 4096) != 0 ){
		return nil;
	}
	
	NSThread *t = [[NSThread alloc]initWithTarget:target selector:selector object:argument];
	t.stackSize = size;
	
	return t;
}

4.3 GCD

GCD API:https://developer.apple.com/documentation/dispatch?language=objc GCD提供的功能列表:

  • 任务或分发队列,允许主线程中执行,并行执行和串行执行。
  • 分发组,实现对一组任务执行情况的跟踪,而与这些任务所基于的队列无关
  • 信号量
  • 屏障,允许在并行分发队列中创建同步的点
  • 分发对象和管理源,实现更为底层的管理和监控
  • 异步I/O,使用文件描述或管道

GCD同样解决了线程的创建和管理,它帮助我们跟踪应用中的线程的总数,且不会造成任何泄露。

大多数情况下,应用单独使用GCD就可以很好的工作,但仍有特定的情况需要考虑使用NSThread或NSOperationQueue。当应用中有多个长耗时的任务需要并行执行时,最好,对线程的创建过程加以口控制。如果代码执行的时间过长,很有可能达到线程的限制64个,即GCD的线程池上限

4.4 操作与队列

NSOperation封装了一个任务以及和任务相关的数据和代码,而NSOperationQueue以先入先出的顺序控制了一个或多个这类任务的执行。 NSOperation和NSOperation都提供了控制线程个数的能力。可用maxConcurrentOperation属性控制队列的个数,也可以控制每个队列的线程个数。 NSThread,NSOperation和GCD API的快速比较:

GCD

  • 抽象程度最高。
  • 两种队列开箱即用:main和global。
  • 可以创建更多的队列(使用dispatch_queue_create)。
  • 可以请求独占访问(使用dispatch_barrier_sync和dispatch_barrier_async)。
  • 基于线程管理。
  • 硬性限制创建64个线程。

NSOperation

  • 无默认队列。
  • 应用管理自己创建的队列。
  • 队列是优先级队列。
  • 操作可以有不同的优先级(使用queuePriority属性)。
  • 使用cancel消息可以取消操作。注意,cancel仅仅是个标记。如果操作已经开始执行,则可能会继续执行下去。
  • 可以等待某个操作执行完毕(使用waitUnitilFinished消息)。

NSThread

  • 低级别构造,最大化控制。
  • 应用创建并管理线程。
  • 应用创建并管理线程池。
  • 应用启动线程。
  • 线程可以拥有优先级,操作西永会根据优先级调度他们的执行。
  • 无直接API用于等待线程完成。需要使用互斥量(如NSLock)和自定义代码。

NSOperation是多核安全的。可以放心地分享队列,从不同的线程中提交任务,而无需担心损坏队列

4.5 线程安全的代码

4.5.1 原子属性

@property(atomic) NSString *atomic;//原子属性
@property(nonatomic) NSString *Nonatomic;//非原子属性

因为原子属性存在开销,所以过度使用它们并不明智。如果能保证某个属性在任何时刻都不会被多个线程访问,最好还是标记为nonatomic。

4.5.2 同步块

原子属性并不能保证代码一定是线程安全的。 所有相关的状态都应该再同一个事物中批量更新。 使用@synchronized指令可以创建一个信号量,并进入临界区,临界区在任何时刻都只能被一个线程执行。

-(void)updateUser:(HPUser *)user properties:(NSDictionary *)properties{
	@synchronized(user){//取得针对user对象的锁。一切相关的修改都会被一同处理,而不会发生竞争状态。
		NSString *fn = [properties objectForKey:@"firstName"];
		if (fn != nil){
			user.firstName = fn;
		}
		NSString *ln = [properties objectForKey:@"lastName"];
		if (ln != nil){
			user.lastName = ln;
		}
	}
}

过渡使用@synchronized指令会拖慢应用的运行速度,因为任何时间都只有一个线程在临界区内执行。 通过uesr对象获取锁。因此updateUser: properties:方法可以从多个线程被调用,不同的用户用不同的线程。当user对象不同时,该方法仍然能够高并发地执行。 获取锁的对象是良好定义的临界区的关键。作为经验法则,可以选择状态会被访问和修改的对象作为信号量的引用

4.5.3 锁

锁是进入临界区的基础构件。atomic属性和@synchronized块是为了实现便捷实用的高级别抽象。

  • NSLock
@interface ThreadSafeClass : NSObject
{
	NSLock *lock;
}
@end

@implementation ThreadSafeClass
-(instancetype)init{
	if (self = [super init]){
		self -> lock = [NSLock new];
	}
	return self;
}
-(void)safeMethod{
	[self -> lock lock];//获取锁,进入临界区
	
	//线程安全代码,在临界区,任意时刻最多只允许一个线程执行
	
	[self -> lock unlock];//释放锁标记着临界区的结束。其他线程现在能够获取锁了。
}
@end
  • NSRecursiveLock NSRecursiveLock允许在被解锁前锁定多次。如果解锁的次数与锁定的次数相匹配,则认为锁被释放,其他线程可以获取锁。
@interface ThreadSafeClass : NSObject
{
	NSRecursiveLock *rLock;
}
@end

@implementation ThreadSafeClass
-(instancetype)init{
	if (self = [super init]){
		
		self -> rLock = [NSRecursiveLock new];
	}
	return self;
}

-(void)safeMethod{
	[self -> rLock lock];//safeMethod获取锁
	
	[self safeMethod2];//调用safeMethod2方法
	
	[self -> rLock unlock];//safeMethod释放了锁。因为每个锁定操作都有一个相应的解锁操作与之匹配,所以锁现在被释放,并可以被其他线程所获取。
}

-(void)safeMethod2{
	[self -> rLock lock];//safeMethod2从已经获取到的锁再次获取了锁
	
	//线程安全的代码
	
	[self -> rLock unlock];//safeMethod2释放了锁
}
  • NSCondition

    NSCondition可以原子性地释放锁,从而使得其他等待的线程可以获取锁,而初始的线程继续等待。

  @implementation Producer
  //1生产者的初始化器需要用于协调配合的NSCondition对象和用于存放产品的collector
-(instancetype)initWithCondition:(NSCondition *) condition collector:(NSMutableArray *)collector{
	if (self = [super init]){
		self.condition = condition;
		self.collector = collector;
		self.shouldProduce = NO;
		self.item = nil;
	}
	return self;
}

-(void)produce{
	self.shouldProduce = YES;
	while (self.shouldProduce) {//2.生产者会在shouldProduce为YES时进行生产。其他线程需要将设置为NO以停止生产者的生产
		[self.condition lock];
		if (self.collector.count > 0 ){
			[self.condition wait];//4.如果collector中有未消费的产品,则等待,这会阻塞当前线程的执行直到condition被通知(signal)为止
		}
//		[self.collector addObject:[self nextItem]];//5.
		[self.condition signal];//6.通知其他等待的线程。这里是产品完成生产的标志,并将产品将入到了collector中,可供消费
		[self.condition unlock];
	}
}

@implementation Consumer
-(instancetype)initWithCondition:(NSCondition *) condition collector:(NSMutableArray *)collector{//8.
	if (self = [super init]){
		self.condition = condition;
		self.collector = collector;
		self.shouldConsume = NO;
		self.item = nil;
	}
	return self;
}

-(void)consume{
	self.shouldConsume = YES;
	while (self.shouldConsume) {//9.shouldConsume为YES,消费者进行消费。其他线程可以将其设置为NO来停止消费者的消费
		[self.condition lock];
		if (self.collector.count == 0){
			[self.condition wait];//11如果collector没有产品则等待
		}
	}
	id item = [self.collector objectAtIndex:0];
	NSLog(@"%@",item);
	//处理产品
	[self.collector removeObjectAtIndex:0];//12消费collector的中的下一个产品。确保已从collector中移除它。
	[self.condition signal];//13通知其他等待的线程。这里标识一个产品被消费并从collector中移除了
	[self.condition unlock];
}

@implementation Coordinator
-(void)start{
	NSMutableArray *prprline = [NSMutableArray array];
	NSCondition *condition = [NSCondition new];//Coordinator类为生产者和消费者准备好了输入数据
	Producer *p = [Producer new];
	p = [p initWithCondition:condition collector:prprline];
	Consumer *c = [Consumer new];
	c = [c initWithCondition:condition collector:prprline];//16
	NSThread *t = [NSThread new];
	//在不同的线程中开启生产和消费任务
	[[t initWithTarget:self selector:@selector(startProducer) object:p]start];
	[[t initWithTarget:self selector:@selector(startCollector) object:c]start];
	
	p.shouldProduce = NO;
	c.shouldConsume = NO;//18.一旦完成,分别设置生产者和消费者停止生产和消费
	
	[condition broadcast];//19.因为生产者和消费者线程可能会等待,所以broadcast本质上会通知所有等待中的线程。不同的是,signal方法只会影响一个等待的线程。
	
}

4.5.4 将读写锁应用于并发读写

避免并发写入的最佳实践: 如果有多个线程视图读取一个属性,同步的代码在同一时刻只允许单个线程进行访问。因此,使用atomic属性会拖慢应用的性能。这可能是个严重的瓶颈,尤其是当某个状态需要在多个线程间共享,且需要被多个线程访问时。cookie或登录后的访问令牌就是这样的例子。它可以周期性的变化,但会被所有访问服务器的网络请求所调用。 GCD屏障允许在并行分发队列上创建一个同步的点。当遇到屏障时,GCD会延迟执行提交的代码块,直到队列中所有在屏障之前提交的代码块都执行完毕。随后,通过屏障提交的代码块会单独地执行。

输入图片说明

代码实现步骤: (1)创建一个并行队列 (2)使用dispatch_sync执行所有的读操作 (3)在相同的队列上使用dispatch_barrier_sync执行所有的写操作

@interface HPCache : NSObject
+(HPCache *)shareInstance;
-(id)objectForKey:(id)key;
-(void)setObject:(id)object forKey:(id)key;
@end

@interface HPCache()
@property(nonatomic,readonly) NSMutableDictionary *cacheObjects;
@property(nonatomic,readonly) dispatch_queue_t queue;
@end
@implementation HPCache

-(instancetype) init{
	if (self = [super init]){
		_cacheObjects = [NSMutableDictionary dictionary];
		_queue = dispatch_queue_create(kCacheQueueName, DISPATCH_QUEUE_CONCURRENT);//1.创建自定义的DISPATCH_QUEUE_CONCURRENT队列
	}
	return self;
}

+(HPCache *)shareInstance{
	static HPCache *instance = nil;
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		instance = [[HPCache alloc]init];
	});
	return instance;
}

-(id)objectForKey:(id)key{
	__block id rv = nil;
	
	dispatch_sync(self.queue, ^{//2.将dispatch_sync用于不修改状态的操作
		rv = [self.cacheObjects objectForKey:key];
	});
	
	return rv;
}

-(void)setObject:(id)object forKey:(id<NSCopying>)key{
	dispatch_barrier_sync(self.queue, ^{//3.将dispatch_barrier_sync用于可能修改状态的操作
		[self.cacheObjects setObject:object forKey:key];
	});
}
@end

4.5.5 使用不可变实体

遵循的最佳实践:

  • 使用不可变实体。
  • 通过更新子系统提供支持。
  • 允许观察者接收有关数据变化的通知。
@interface HPUser : NSObject 
@property (nonatomic,copy) NSString *userId;
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,copy) NSString *lastName;
@property (nonatomic,copy) NSString *gender;
@property (nonatomic,copy) NSDate *dateOfBirth;
@property (nonatomic,strong) NSArray *albums;
//+(instancetype) userWithBlock:(void(^)(HPUserBuilder *)) block;
@end

@class HPPhoto;

@interface HPAlbum
@property (nonatomic , copy) NSString *albumId;
@property (nonatomic , strong) HPUser *owner;
@property (nonatomic , copy) NSString *name;
@property (nonatomic , copy) NSString *descripiton;
@property (nonatomic , copy) NSDate *creationTime;
@property (nonatomic , copy) HPPhoto *coverPhoto;
@end

@interface HPPhoto
@property (nonatomic , copy) NSString *photoId;
@property (nonatomic , strong) HPAlbum *album;
@property (nonatomic , strong) HPUser *user;
@property (nonatomic , copy) NSString *caption;
@property (nonatomic , strong) NSURL *url;
@property (nonatomic , assign) CGSize size;
@end

@interface HPUserBuilder : NSObject  //生成器
@property (nonatomic , copy) NSString *userId;
@property (nonatomic , copy) NSString *firstName;
@property (nonatomic , copy) NSString *lastName;
@property (nonatomic , copy) NSString *gender;
@property (nonatomic , copy) NSDate *dateOfBirth;
@property (nonatomic , copy) NSArray *albums;

-(HPUser *)build;
@end


@interface HPUser ()//模型的私有扩展--自定义的初始化设置
-(instancetype) initWithBuilder:(HPUserBuilder *)builder;
@end


@implementation HPUserBuilder
-(HPUser *)build{
	return [[HPUser alloc]initWithBuilder:self];
}
@end

@implementation HPUser
-(instancetype) initWithBuilder:(HPUserBuilder *)builder{//模型自定义初始化器的实现
	if (self = [super init]){
		self.userId = builder.userId;
		self.firstName = builder.firstName;
		self.lastName = builder.lastName;
		self.gender = builder.gender;
		self.dateOfBirth = builder.dateOfBirth;
		self.albums = [NSArray arrayWithArray:_albums];
	}
	return self;
}
+(instancetype) userWithBlock:(void(^)(HPUserBuilder *)) block{//6
	HPUserBuilder *builder = [HPUserBuilder new];
	block(builder);
	return [builder build];
}

//生成器创建对象的使用示例
-(HPUser *)createUser{
	HPUser *rv = [HPUser userWithBlock:^(HPUserBuilder *builder){
		builder.userId = @"id001";
		builder.firstName = @"Zou";
		builder.lastName = @"Jie";
		builder.gender = @"F";
		
		NSCalendar *cal = [NSCalendar currentCalendar];
		NSDateComponents *components = [[NSDateComponents alloc]init];
		[components setYear:2017];
		[components setMonth:10];
		[components setDay:1];
		builder.dateOfBirth = [cal dateFromComponents:components];
		
		builder.albums = [NSArray array];
	}];
  return rv;
}
@end

4.5.6 状态观察者与通知

响应式编程是基于异步数据流的编程方式。流是廉价且无处不在的,一切都可以是流:变量,用户输入,属性,缓存,数据结构,等等。 ReactiveCocoa库(https://github.com/ReactiveCocoa/ReactiveObjC)实现了Objective-c中进行响应式编程。它不仅可以实现对任意状态的观察,还提供了高级的分类扩展,以便同步更新UI元素或响应视图的交互。 Cocoapods集成(http://www.jianshu.com/p/7c786eee1705)后,引入ReactiveCocoa头文件ReactiveObjC.h。

@interface HPUserService ()
@property (nonatomic , strong) NSMutableDictionary *userCache;
@end
@implementation HPUserService
-(RACSignal *)signalForUserWithId:(NSString *)userid{//1
	@weakify(self);
	return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {//2.
		@strongify(self);
		HPUser *userFromCache = [self.userCache objectForKey:userid];
		if (userFromCache){
			[subscriber sendNext:userFromCache];
			[subscriber sendCompleted];
		}else{
			
		}
		return nil;
	}];
}

-(RACSignal *)signalForUpateUser:(HPUser *)user{//更新HPUser对象
//	@weakify(self);
	return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {//4.
		//更新服务器
//		@strongify(self);
		[NSNotificationCenter.defaultCenter postNotificationName:@"userUpdated" object:nil];
		return nil;
	}];
}

-(RACSignal *)signalForUserUpdates:(id)object{
	return [[NSNotificationCenter.defaultCenter rac_addObserverForName:@"userUpdateed" object:object]
			flattenMap:^__kindof RACSignal * _Nullable(NSNotification * _Nullable value) {
		return value.object;
	}];//rac_addObserverForName 订阅userUpdated通知
}

4.5.7 异步优于同步

在实际场景中使用dispatch_sync

//场景A
	dispatch_sync(queue, ^{
		dispatch_sync(queue, ^{
			NSLog(@"nested sync call");
		})
	});

//场景B
-(void) methodA1{
	dispatch_sync(queue1, ^{
//		[objB methodB];
	});
}
-(void)methodA2{
	dispatch_sync(queue1, ^{
		NSLog(@"indirect nested dispatch_sync");
	});
}
-(void)methodB{
	//	[objA methodA2];
}

场景A演示了一个假设的场景,在这个场景中使用分发队列调用了一个嵌套的dispatch_sync。这会导致死锁。嵌套的dispatch_sync不能分发到队列中,因为当前线程已经在队列中且不会释放锁。 场景B演示了更相似的场景。类A有两个使用了相同队列的方法(methodA1和methodA2)。前一个方法对某个对象调用了methodB方法,后续会反调回来。最终结果还是死锁。 要想实现线程安全,不死锁且易于维护的代码,使用异步风格。使用Promise是最好的方式。ReactiveCocoa为Objective-C引入了FRP风格。dispatch_async不受这一行为影响。

PromiseKit: githubhttps://github.com/mxcl/PromiseKit

处理地狱回调:http://www.jianshu.com/p/f060cfd52f17, http://www.cocoachina.com/swift/20160719/17085.html

猜你喜欢

转载自my.oschina.net/u/2319073/blog/1559315