同步/异步以及多线程

来源:互联网 发布:北京润和软件 编辑:程序博客网 时间:2024/05/17 06:33

在介绍多线程编程之前先介绍几个大家特别容易混淆,也是非常重要的概念。

同步/异步

同步:多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。只存在一个线程也就是主线程。

异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。存在多条线程。


图1 同步示例图


图2异步示例图

并发/并行 

并行:指两个或多个时间在同一时刻发生。多核CUP同时开启多条线程供多个任务同时执行,互不干扰。

并发:指两个或多个事件在同一时间间隔内发生。可以在某条线程和其他线程之间反复多次进行上下文切换,看上去就好像一个CPU能够并且执行多个线程一样。其实是伪异步。如下并发图,在同一线程,任务A先执行了20%,然后A停止,任务B重新开始接管线程开始执行。


图3 并行示意图


图4 并发示意图

好了,简单谈了一下多线程可能涉及到的的概念,看官先消化一下,再接着往下看。

多线程是指在软件或硬件上实现多个线程并发执行的技术。通俗讲就是在同步或异步的情况下,开辟新线程,进行线程间的切换,以及对线程进行合理的调度,做到优化提升程序性能的目的。那么多线程编程会带给我们很多好处,同时也是一种容发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致[数据竞争],停止等待时间的线程会导致多个线程相互持续等待[死锁],使用太多线程会消耗大量内存。


图5 多线程编程的问题

尽管多线程编程极易发生各种问题,但是呢也应当使用多线程编程,这是为什么呢?因为使用多线程编程可以保证应用程序的相应性能。利大于弊,小心使用即可。

应用程序在启动时,在主线程中描绘用户界面,处理触摸屏幕的事件等。如果在该主线程中进行长事件的处理,比如一些多请求的情况,就会妨碍主线程的执行[阻塞]。这样会妨碍主线程中被称为RunLoop[在iOS和OS X的应用中]的主循环的执行,从而导致不能更新用户界面,应用程序的画面长时间停滞等问题。而使用了多线程编程,在执行长时间的处理仍可保证用户界面的相应性能。


图6 多线程编程的优点

那么我们iOS开发者,苹果给我们提供了哪些多线程技术呢?

一.NSThread      

我们先来介绍一下最古老但不怎么用的NSThread,下面是NSThread的所有API及注释,可参考了解,详细用法可以百度搜索详细用法,在这里不多做论述了哈~压轴的且看下文GCD。

+ (NSThread*)currentThread; //获取当前线程

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullableid)argument;//这个方法可以直接生成一个线程并启动它,而且无需为线程的清理负责

+ (BOOL)isMultiThreaded;// 判断是否为多线程

@property(readonly,retain) NSMutableDictionary *threadDictionary;

+ (void)sleepUntilDate:(NSDate*)date;//设置线程阻塞时间

+ (void)sleepForTimeInterval:(NSTimeInterval)ti;//设置线程阻塞时间

+ (void)exit;//退出

+ (double)threadPriority;//优先级

+ (BOOL)setThreadPriority:(double)p;//优先级

@property double threadPriority   NS_AVAILABLE(10_6,4_0);// To be deprecated; use qualityOfService below

@property NSQualityOfServicequalityOfService  NS_AVAILABLE(10_10,8_0);// read-only after the thread is started

+ (NSArray *)callStackReturnAddressesNS_AVAILABLE(10_5,2_0);// 线程函数地址

+ (NSArray *)callStackSymbols  NS_AVAILABLE(10_6,4_0);//查看方法被调用的callstack

@property (nullable,copy) NSString*name  NS_AVAILABLE(10_5,2_0);//线程名字

@property NSUInteger stackSize NS_AVAILABLE(10_5,2_0);//堆栈大小

@property (readonly) BOOL isMainThreadNS_AVAILABLE(10_5,2_0);//是否是主线程

+ (BOOL)isMainThread  NS_AVAILABLE(10_5,2_0);// reports whether current thread is main 是否为主线程

+ (NSThread*)mainThread  NS_AVAILABLE(10_5,2_0);//获取主线程

- (instancetype)init  NS_AVAILABLE(10_5,2_0)NS_DESIGNATED_INITIALIZER;//初始化

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullableid)argumentNS_AVAILABLE(10_5,2_0);//初始化并且具有参数

@property(readonly,getter=isExecuting) BOOL executing NS_AVAILABLE(10_5,2_0);//线程是否正在执行

@property(readonly,getter=isFinished) BOOL finished NS_AVAILABLE(10_5,2_0);//线程是否已经结束

@property(readonly,getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5,2_0);//线程是否已经取消

- (void)cancel NS_AVAILABLE(10_5,2_0);//线程取消

- (void)start NS_AVAILABLE(10_5,2_0);//线程启动

- (void)main NS_AVAILABLE(10_5,2_0);//线程启动

NSThread的优缺点:

优点:非常直观,简单快捷。

缺点:不能对线程进行更加详细的配置,所以一般开发中不推荐使用NSThread。

二.NSOperation

NSOperation是面向对象的多线程技术,不过底层还是GCD实现的,效率比GCD要低一些。NSOperation有两个子类NSInvocationOperation和NSBlockOperation并且我们还可以自定义子类集成NSOperation,配置一些自定义的方法。NSOperation要和NSOperationQueue一块使用,才能发挥威力。

本篇的标题已经说明了,本文旨在说多线程和GCD,对NSOperation不做详细论述,如果还想继续了解详细用法,请移步Yu大师的http://www.jianshu.com/p/a044cd145a3d。

- (void)start; //启动

- (void)main;//启动

@property(readonly,getter=isCancelled) BOOL cancelled;//是否已经取消

- (void)cancel;//取消

@property(readonly,getter=isExecuting) BOOL executing;//是否正在执行

@property(readonly,getter=isFinished) BOOL finished;//是否已经结束

@property(readonly,getter=isConcurrent) BOOL concurrent;// To be deprecated; use and override 'asynchronous' below//是否并发

@property(readonly,getter=isAsynchronous) BOO Lasynchronous NS_AVAILABLE(10_8,7_0);

@property(readonly,getter=isReady)BOOL ready;

- (void)addDependency:(NSOperation*)op;//添加依赖

- (void)removeDependency:(NSOperation*)op;//移除依赖

@property(readonly,copy)NSArray *dependencies;//获取所有依赖

typedefNS_ENUM(NSInteger, NSOperationQueuePriority) {//队列优先级

NSOperationQueuePriorityVeryLow = -8L,

NSOperationQueuePriorityLow = -4L,

NSOperationQueuePriorityNormal =0,

NSOperationQueuePriorityHigh =4,

NSOperationQueuePriorityVeryHigh =8

};

@property NSOperationQueuePriority queuePriority;//优先级属性

@property(nullable,copy) void(^completionBlock)(void)NS_AVAILABLE(10_6,4_0);//执行结束block

- (void)waitUntilFinished NS_AVAILABLE(10_6,4_0);

@property double threadPriority NS_DEPRECATED(10_6,10_10,4_0,8_0);

@property NSQualityOfService qualityOfServiceNS_AVAILABLE(10_10,8_0);

@property(nullable,copy) NSString *nameNS_AVAILABLE(10_10,8_0);

三.GCD

苹果官方对GCD是这样说明的:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。Dispatch Queue是执行处理的等待队列,我们可以通过dispatch_async等API,在block语法中记述想要执行的处理并将其追加到Dispatch Queue中,Dispatch Queue是按照追加的顺序 进行处理,先进先出FIFO。



图7 通过dispatch queue执行处理

用代码表示就是:

dispatch_async(queue,^{

//想执行的任务,这样执行就是在另一个新开辟的线程中

});

在执行处理时候是有两种Dispatch Queue,一种是Serial Dispatch Queue串行调度队列,这个是等待现在执行中的事件处理结束,另一种是Concurrent Dispatch Queue并发调度队列,这个是不等待现在执行中的事件处理结束。



图8 Serial Dispatch Queue

那我们来举个???? 来区分一下两种调度队列的区别:

有ABC三个任务等待执行,

dispatch_async(queue,A)

dispatch_async(queue,B)

dispatch_async(queue,C)

如果queue使用Serial Dispatch Queue,则同时执行的处理数只有一个,A执行结束才能执行B,B执行结束才能执行C,A->B->C顺序执行。

如果queue使用Concurrent Dispatch Queue,这样不用等待现在执行中的处理结束,可以并行执行多个处理,但并行执行的处理数取决于iOS和OS X的CPU核数以及CPU负荷等当前系统状态。所谓“并发执行”,就是使用多个线程同时执行多个处理。



图10 Serial开辟线程数量


图11 Concurrent开辟线程数量

现在各位看官应该可以明白Serial Dispatch Queue和Concurrent Dispatch Queue的大致区别了,那么我们怎样才能得到这些Dispatch Queue呢?一共分为两种。

第一种:通过GCD的API的dispatch_queue_create函数生成Dispatch Queue

dispatch_queue_create

生成Serial Dispatch Queue代码:

dispatch_queue_t  exampleSerialDispatchQueue = dispatch_queue_create("exampleSerialDispatchQueue.gcd.example.com",NULL)

我们先讲一下Serial Dispatch Queue生成个数的问题,上面也讲到了Serial Dispatch Queue只会生成一个线程,同一时间执行一个处理,如果想要实现Concurrent Dispatch Queue并发执行的效果,我们可以将多个任务放到多个Serial Dispatch Queue。


图12 多个Serial Dispatch Queue

但是如果有N个的任务,就需要创建N个线程,就会消耗大量内存,降低系统的响应性能。所以当需要遇到这种需求还是首先使用Concurrent Dispatch Queue。

接下来要讲解一下这个函数的组成,dispatch_queue_create的第一个参数指定了Dispatch Queue的名称,推荐使用name,虽然使用NULL也可以,但是为了后期调试维护还是给定一个name。第二个参数为NULL,则生成Serial Dispatch Queue。如果要生成Concurrent Dispatch Queue时,第二个参数要使用DISPATCH_QUEUE_CONCURRENT。

dispatch_queue_t  exampleConcurrentDispatchQueue = dispatch_queue_create("exampleSerialDispatchQueue.gcd.example.com",DISPATCH_QUEUE_CONCURRENT),

另外需要注意的点是:虽然有ARC编译器自动管理内存这一优秀技术,但生成的Dispatch Queue必须由程序员主动释放。

dispatch_release(exampleSerialDispatchQueue) 释放

dispatch_retain(exampleSerialDispatchQueue) 持有

第二种:直接使用系统提供的标准Dispatch Queue

Main Dispatch Queue和Global Dispatch Queue

Main Dispatch Queue是在主线程中执行的Dispatch Queue,也就是Serial Dispatch Queue。


图13 Main Dispatch Queue

Global Dispatch Queue不需要通过dispatch_queue_create函数来逐个生成Concurrent Dispatch Queue,只要获取Global Dispatch Queue就行了。Global Dispatch Queue的执行优先级有四种:

Global Dispatch Queue(High Priority) Global Dispatch Queue  优先级高

Global Dispatch Queue(Default Priority) Global Dispatch Queue  优先级默认

Global Dispatch Queue(Low Priority) Global Dispatch Queue  优先级低

Global Dispatch Queue(Backgroud Priority) Global Dispatch Queue  优先级后台

附上:Main Dispatch Queue的说明

Main Dispatch Queue  Serial Dispatch Queue   主线程执行

Main Dispatch Queue的获取方法:

dispatch_queue_t   mainDispatchQueue = dispath_get_main_queue();

Global Dispatch Queue的获取方法:

dispatch_queue_t globalDispatchQueueHigh = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0)  //高优先级

dispatch_queue_t globalDispatchQueueDefault = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)  //默认优先级

dispatch_queue_t globalDispatchQueueLow = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0)  //低优先级

dispatch_queue_t globalDispatchQueueBackgroud = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_GACKGROUND,0)  //高优先级

下面举出一个简单的示例代码:

//在默认优先级的Global Dispatch Queue中执行block

dispatch_async(dispatch_get_global_queue,0),^{

//并行想要执行的代码

//在Main Dispatch Queue中执行block

dispatch_async(dispatch_get_main_queue(),^{

//想要在主线程中执行的代码 如刷新UI

});

});

dispatch_after

此函数是可以实现一些延迟处理的功能,比如产品汪想要点击一个按钮,延迟两秒钟再执行操作这个场景。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_ROW, 3ull * NSEC_PER_SEC);

dispatch_after (time ,dispatch_get_main_queue(),^{

//等待三秒之后要执行的操作

});

"ull"是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”),如果NSEC_PER_SEC替换为NSEC_PER_MSEC则是以毫秒为单位进行计算。

DISPATCH_TIME_NOW表示现在的时间,到这里,需要注意一下,dispatch_after函数并不是在指定时间后执行处理,而是在指定时间追加处理到Dispatch Queue。

dispatch group

有时候我们会有这种需求,在刚进去一个页面需要发送两个请求,并且某种特定操作必须在两个请求都结束(成功或失败)的时候才会执行,最low的办法第二个请求嵌套在第一个请求结果后在发送,在第二个请求结束后再执行操作。还有就是只使用一个Serial Dispatch Queue,把想要执行的操作全部追加到这个Serial Dispatch Queue中并在最后追加某种特定操作,颇为复杂操作。但是呢,我们这里介绍更高级的办法使用dispatch group。

我们将ABC三个任务block追加到Global Dispatch Queue,ABC全部执行完,会执行dispatch_group_notify中的block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

dispatch_group_t  group = dispatch_group_creat();

dispatch_group_async(group,queue,^{执行任务A});

dispatch_group_async(group,queue,^{执行任务B});

dispatch_group_async(group,queue,^{执行任务C});

dispatch_group_notify(group,dispatch_get_main_queue(),^{执行最终的特定操作});

ABC的执行顺序不固定,因为是Global Dispatch Queue即Concurrent Dispatch Queue多个现场并行执行。

上面的dispatch_group_notify(group,dispatch_get_main_queue(),^{执行最终的特定操作});操作还可以更改为

dispatch_group_wait(group,DISPATCH_TIME_FOREVER);

dispatch_group_wait第二个参数指定为等待的时间(超时),属于dispatch_time_t类型,在这里使用DISPATCH_TIME_FOREVER,意味着永久等待。如果dispatch group的处理尚未结束,就会一直等待。

如果指定等待时间为1秒如下:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NESC_PER_SEC);

long result = dispatch_group_wait(group,time);

if(result == 0) { dispatch group的全部处理执行结束}

else { dispatch groupe的某一处理还在执行中};

但是呢上面这种dispatch_group的排列执行方式,是不会考虑block块内部的异步请求情况的,它只能保证把block内的非异步直观代码执行完,所以如果ABC三个任务中如果有执行异步的请求,那么在dispatch_group_notify最终任务执行中,那个异步请求不一定毁掉结束。

在这里给大家介绍针对这种问题另一个API。

dispatch_group_enter/dispatch_group_leave

dispatch_group_t group = disoatch_group_creat();

dispatch_group_enter(group);

dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{

//在这里执行异步请求A

并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);

});

dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{

//在这里执行异步请求B

并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);

});

dispatch_group_notify(group,dispatch_get_main_queue(),^{执行最终的特定操作});

上面这种做法当执行到dispatch_group_notify,一定是AB两个异步请求都加在结束了。dispatch_group_enter(group)和dispatch_group_leave(group);必须成对出现,编译器会强制识别当出现dispatch_group_leave全部结束才执行dispatch_group_notify,即使这种,档异步执行了AB两个请求,也不能保证请求执行结束的先后顺序,如果任务B的请求参数包含请求A的返回参数,那么只能用最Low的办法将请求B嵌套在A中执行,当然如果你有更好的办法欢迎提出来哈。

dispatch_apply

这个函数可以给定指定的次数将block追加到指定的Dispatch Queue中,并且等待全部结束处理执行。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

dispatch_apply(10,queue,^(size_t index){

NSLog(@“%zu”,idnex);

});

NSLog(@“done”);

执行结果:4,1,0,3,5,2,6,8,9,7,done

第一个参数是重复次数,第二个参数是追加对象的dispatch queue,第三个参数是追加的处理。dispatch_apply可以做遍历数组的操作,不必一个一个写for循环。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

dispatch_apply([array count],queue,^(size_t index)){

NSLog(@"%zu:%@",index,[array objectAtIndex:index]);

}

dispatch_suspend/dispatch_resume

如果有时候你希望不执行已经追加到dispatch queue中的一些处理,比如这些处理会对验算结果造成影响。在这种情况,只需要挂起dispatch_queue即可,需要时再恢复。

dispatch_suspend函数挂起指定的dispatch_queue:

dispatch_suspend(queue);

dispatch_suspend函数恢复指定的dispatch_queue:

dispatch_resume(queue);

函数对已经执行的处理没有影响,挂起后,追加到dispatch queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

dispatch_once

这个函数保证在应用程序执行中只执行一次指定处理的API。下面先举一个不用dispatch_once的常见的写法:一个对象A

if(!对象A){

对象A =[ [对象A  alloc] init];

}

使用dispatch_once函数为:

static dispatch_once_  onceToken;

dispatch_once( &onceToken,^{

对象A =[ [对象A  alloc] init];

});

通过dispatch_once创建的即使在多线程环境下执行也百分百安全。

另外关于GCD的API还有诸如:dispatch_set_target_queue,dispatch_barrier_async, disapatch_sync,dispatch Semaphore等 在这里不多做说明,不常用到用到的话可以查官方API。

有人觉得如果努力编写线程管理的代码,根本不需要GCD,但是呢,无论编程人员如何编写管理线程的代码,在性能方面也不可能胜过XNU内核级所实现的GCD。我们尽量多使用GCD或者使用了Cocoa框架GCD的NSOperationQueue类等API。

原创粉丝点击