停不下来的多线程

2016-02-23  本文已影响314人  钱嘘嘘
http://www.jianshu.com/p/8f01d3398a0c

GCD 扫盲篇

Grand Central Dispatch 基础教程Swift:Part 1 & Part --  连续&并发,同步&异步,并发&并行,队列

GCD 深入理解:第一部分 & 第二部分  --  同上 + 代码实践


知其然亦知其所以然--NSOperation并发编程  --  mark

并发编程之Operation Queue  --  mark

iOS 并发编程之 Operation Queues  --  思路清楚,文笔清晰

Advanced NSOperations--  condition,有点晕..


关于iOS多线程,你看我就够了  --  mark

深入理解dispatch_queue  --  线程池,串行,并发,同步,异步

为 GCD 队列绑定 NSObject 类型上下文数据-利用 __bridge_retained(transfer) 转移内存管理权  --  为队列绑定自定义的数据

同步(sync) & 异步(async):

这些术语用来描述当一个函数的控制权返回给调用者时已完成的工作的数量

<1> 同步函数只有在其命令的任务完成时才会返回值。

Tip:同理,同步任务一般会阻塞当前线程,然后把 Block 中的任务放到指定的队列中执行,只有等到 Block 中的任务完成后才会让当前线程继续往下运行。

<2> 异步函数则不会等待其命令的任务完成,即会立即返回值。所以,异步函数不会锁住当前线程。异步调用的副作用就是它们很难调试。当我们在调试器里中止代码运行,回溯并查看已经变得没有意义了。

Tip:同理,异步一般会开启新的线程。

串行(Serial) & 并发(Concurrent)

任务和任务之间的执行方式。 串行是任务A执行完了任务B才能执行,只能顺序执行。并发则是任务A和任务B可以同时执行。

Tip:同理,无论串行还是并发队列,任务启动顺序都是按照FIFO的,只是并发队列允许同一时间有多个任务都在执行.


GCD中:

连续/串行队列 & 并发队列

连续/串行队列

任务每次执行只一个,一个任务只有在其前面的任务执行完毕后才可开始运行。如图,你不会知道前一个任务结束到下一个任务开始时的时间间隔。只知道按顺序执行。

并发队列

并发队列中的任务以FIFO顺序开始执行。任务间可以以任何顺序结束,不会知道下一个任务开始的时间也不会知道一段时间内正在运行任务的数量。因为,这一切都是由GCD控制的。



并发(Concurrent) & 并行(Parallelism)

并发是程序的属性,而并行是计算机的属性。

只能够以并发的方式设计你的代码(并发编程),但不能保证代码被并行的执行。系统会判断在某一个时刻是否有可用的core(多核CPU核心)。<1>有,并行(parallelism)执行 <2> 没有,通过上下文切换(context switch)来分时并发(concurrency)执行.

上下文切换(context switch)

上下文切换是当你在一个进程中的多个不同线程间进行切换时的一种进程进行储存与恢复的状态。这种进程在写多任务App时相当常见,但这通常会产生额外的系统开销。

任务(block + NSOperation) 和 队列(dispatch_queue_t + NSOperationQueue)的方式

<1> NSOperation是基于GCD之上的更高一层封装, 拥有更多的API(suspend, resume, cancel等等).

<2> 用KVO可以方便的监测NSOperation的状态(isExecuted, isFinished, isCancelled).

NSOperation  &&  NSOperationQueue

(1)NSOperation -- 添加依赖关系、取消一个正在执行的 operation 、暂停和恢复operation queue 等;

<1> NSInvocationOperation(方法selector+参数obj+result) -- 根据上下文动态调用.invocation.selector

<2> NSBlockOperation(里的block是并发的) -- i. 与queue组合  ii. 使用依赖关系,KVO观察operation的状态变化等。

自定义:

<3> main()(isCancelled)

<4> cancel

Operation并发

Operation的isConcurrent = YES => 并发,我们需要让main在独立的线程中执行。自定义start(配置任务执行的线程或者一些其它的执行环境)main(实现与该 operation 相关联的任务的)方法时,一定要手动的调用一些KVO通知方法(isExecuting & isFinished),以便让对象的KVO机制可以正常运作。(NSOperationQueue是用KVO方式侦听NSOperation状态的改变,以判断这个任务当前是否已完成,完成的任务需要在队列中除去并释放)

设置Operation.completionBlock

实现原理是对Operation的isFinished字段进行KVO,isFinished == YES,执行completionBlock。

.threadPriority -- 0.0 ~ 1.0

--  Operation.threadPriority字段只有在Operation单独执行时有效,在Operation Queue中是无效的。

--  threadPriority仅仅影响了main执行时的线程优先级,其他的方法包括completionBlock都是以默认的优先级来执行的。如果自定义的话,也要注意在main执行前设置好threadPriority,执行完毕后要还原默认线程优先级。

因为,当我们将一个非并发的 operation 添加到 operation queue 后,operation queue 会自动为这个 operation创建一个线程。因此,只有当我们需要手动地执行一个 operation ,又想让它异步执行(s)时,我们才有必要去实现一个并发的 operation 。

(2)NSOperationQueue

之前add/removeDependency  --  是operation自身属性,可以跨queue

isReady(依赖) -> queuePriority(normal)

maxConcurrentOperationCount

cancelAllOperations

设置Operation.completionBlock

实现原理是对Operation的isFinnshed字段进行KVO,isFinnished == YES,执行completionBlock。

暂停和恢复 Operation Queue -- setSuspended:暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。并不能单独地暂停执行一个 operation ,除非直接cancel掉。

Tip:

run loop调用NSURLConnection的delegate

如果你是在子线程调用的, 或者把operation加到了非main queue, 会发现NSURLConnection delegate不调用了  -->  主线程会自动创建RunLoop来保证程序一直运行。子线程默认不创建NSRunLoop,子线程的任务返回,所以run loop负责做NSURLConnection的delegate调用。

note that:

<1> 第一个加入到Operation Queue中的Operation,无论它的优先级有多么低,总是会第一个执行。

<2> 当一个Operation被加入Queue中后,请不要对这个Operation再进行任何修改。因为一旦加入Queue,它随时就有可能会被执行,对它的任何修改都有可能导致它的运行状态不可控制。

GCD

主队列(main queue),全局调度队列(Global Dispatch Queues)

QOS_CLASS_USER_INTERACTIVE: user interactive类代表着为了提供良好的用户体验而需要被立即执行的任务。它经常用来刷新UI、处理一些要求低延迟的加载工作。在App运行的期间,这个类中的工作完成总量应该很小。

QOS_CLASS_USER_INITIATED:user initiated类代表着从UI端初始化并可异步运行的任务。它在用户等待及时反馈时和涉及继续运行用户交互的任务时被使用。

QOS_CLASS_UTILITY:utility类代表着长时间运行的任务,尤其是那种用户可见的进度条。它经常用来处理计算、I/O、网络通信、持续数据反馈及相似的任务。这个类被设计得具有高效率处理能力。

QOS_CLASS_BACKGROUND:background类代表着那些用户并不需要立即知晓的任务。它经常用来完成预处理、维护及一些不需要用户交互的、对完成时间并无太高要求的任务。

底层并发 API

dispatch_once 会使得测试变得非常困难(单例和测试不是很好配合)。

dispatch_after

大多数的情况下,你最好把代码放到正确的位置。如果代码放到-viewWillAppear太早,那么或许-viewDidAppear就是正确的地方。通过在自己代码中建立直接调用(类似-viewDidAppear)而不是依赖于dispatch_after,你会为自己省去很多麻烦。

如果你需要一些事情在某个特定的时刻运行,那么dispatch_after或许会是个好的选择。确保同时考虑了NSTimer,这个API虽然有点笨重,但是它允许你取消定时器的触发。

queue

self.isolationQueue= dispatch_queue_create([label UTF8String],0);   //@"%@.isolation.%p", [self class], self

self.workQueue= dispatch_queue_create([label UTF8String],0);       //@"%@.work.%p",   [self class], self ;

GCD 通过创建所谓的线程池来大致匹配 CPU 内核数量。

为一个类创建它自己的队列而不使用全局队列被普遍认为是一种好的风格。这种方式下,你可以设置队列的名字,这让调试变得轻松许多—— Xcode 可以让你在 Debug Navigator 中看到所有的队列名字。

你可以改变你队列转发到的队列——你可以设置自己队列的目标队列。以这种方式,你可以将不同队列链接在一起。你的Foo类有一个队列,该队列转发到Bar类的队列,Bar类的队列又转发到全局队列。

使用DISPATCH_QUEUE_PRIORITY_BACKGROUND队列时,你需要格外小心。除非你理解了throttled I/Obackground status as per set priority(2)的意义,否则不要使用它。


从copy和mutableCopy谈起--  结合深浅拷贝看

用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?--  集合非集合对象深浅拷贝

从函数接口可以看出,-setCount:forKey:需要一个NSString参数,用来传递给dispatch_async。函数调用者可以自由传递一个NSMutableString值并且能够在函数返回后修改它。因此我们必须对传入的字符串使用copy操作以确保函数能够正确地工作。如果传入的字符串是不可变的(也就是正常的NSString类型),调用copy基本上是个空操作。-- ★★★ 对NSString调用copy(即不可变对象copy)是浅拷贝 == 指针拷贝,返回自身 = 不可变对象。对NSMutableString调用copy(即可变对象copy)是深拷贝 == 内容拷贝,返回新的不可变对象。

(1)copy方法,返回copyWithZone返回的对象。mutableCopy方法,返mutableCopyWithZone返回的对象

<1> 针对不可变对象调用copy返回该对象本身,调用mutableCopy返回一个新的可变对象

<2> 针对可变对象调用copy返回一个新的不可变对象,调用mutableCopy返回另外一个新的可变对象

(2)非集合对象

[immutableObject copy] // 浅复制

[immutableObject mutableCopy] //深复制

[mutableObject copy] //深复制

[mutableObject mutableCopy] //深复制

总结:

<1>copy返回的是不可变对象,mutableCopy返回的是可变对象。

<2> 对于不可变对象copy 是浅拷贝(指针拷贝),返回对象本身,mutableCopy深拷贝(内容拷贝),返回一个新的可变对象

对于可变对象copy 是深拷贝(内容拷贝),返回一个新的不可变对象(我测试是可变对象?__NSCFString)mutableCopy深拷贝(内容拷贝),返回一个新的可变对象

ps:即便class=__NSCFString,但是不能append,会报'Attempt to mutate immutable object with appendString:',由此证明还是一个不可变对象。

<3> 不可变对象,推荐使用copy,可变对象,不能使用copy。

集合对象

此处的深拷贝(内容拷贝),仅仅是拷贝array 这个对象,array 集合内部的元素仍然是指针拷贝

[immutableObject copy] // 浅复制

[immutableObject mutableCopy] //单层深复制

[mutableObject copy] //单层深复制

[mutableObject mutableCopy] //单层深复制



单一资源的多读单写 -- dispatch_barrier_async

dispatch_barrier_async(privateConcurrentQueue, ^{

// 写入操作会确保队列前面的操作执行完毕才开始,并会阻塞队列中后来的操作.

});

dispatch_apply

dispatch_group()

dispatch_group_enter() + dispatch_group_leave()


为队列绑定自定义的数据

//设置context

void dispatch_set_context (dispatch_object_t object, void*context);

//获取context

void* dispatch_get_context (dispatch_object_t object);

这两个函数分别完成了将context“绑定”到特定GCD队列和从GCD队列获取对应context的任务。

为队列“set”任意类型的数据,并在合适的时候取出来用。

事件源

dispatch_source_t  --GCD 事件源是以极其资源高效的方式实现的。

监视进程 (DISPATCH_SOURCE_TYPE_PROC)--  dispatch_source_create && dispatch_source_set_event_handler - fid

监视文件 (DISPATCH_SOURCE_TYPE_VNODE)--  fd

定时器 (DISPATCH_SOURCE_TYPE_TIMER + dispatch_source_set_timer)

取消 dispatch_source_set_cancel_handler()

输入输出  --  很有意思网络套接字


GCD 和缓冲区  --  dispatch_data_t能做别的事情,而且更通用。

dispatch_data_t可以被 retained 和 released ,并且dispatch_data_t拥有它持有的对象。

dispatch_data_t c= dispatch_data_create_concat(a, b)  //只是简单地 retain 了 a 和 b。

dispatch_data_apply来遍历对象 c 持有的内存区域:

dispatch_data_create_subrange来创建一个不做任何拷贝操作的子区域。

读和写

调度 I/O 通道  --  dispatch_io_create()

有两种从根本上不同类型的通道:流和随机存取。

如果你想要为一个文件创建一个通道,你最好使用需要一个路径参数的dispatch_io_create_with_path,让GCD来打开。

dispatch_io_read,dispatch_io_write 和 dispatch_io_close。

只是读取或者写入一个文件,GCD 提供了两个方便的封装:dispatch_read和dispatch_write。

基准测试

GCD优化代码的灵巧小工具:uint64_t  dispatch_benchmark(size_t count,void(^block)(void));

测量给定的代码执行的平均的纳秒数,私有API。它只是在调试和性能分析上起作用。

头文件libkern/OSAtomic.h里的函数,专门用来底层多线程编程。

编写高性能代码或者正在实现无锁的无等待的算法工作时,这些函数会吸引你。

计数器  --  OSAtomicIncrement && OSAtomicDecrement以原子操作的方式去增加和减少一个整数值。

如果你要做的仅仅是增加一个全局计数器,那么无屏障版本的OSAtomicIncrement是很合适的,并且当没有锁竞争时,调用它们的代价很小。

OSAtomicCompareAndSwap能用来做无锁的惰性初始化

原子队列

OSAtomicEnqueue()和OSAtomicDequeue()可以让你以线程安全,无锁的方式实现一个LIFO队列(常见的就是栈)。

自旋锁

OSAtomic.h头文件定义了使用自旋锁的函数:OSSpinLock。当没有锁竞争时使用自旋锁代价很小。

我们使用了ARC的__weak来确保一旦MyTableViewCell所有的实例都不存在,amountAttributes会调用dealloc。因此在所有的实例中,我们可以持有字典的一个单独实例。

思考题:

1. 

只输出1 ,发生主线程锁死。

打印完第一句后,dispatch_sync 立即阻塞当前的主线程,然后把 Block 中的任务放到 main_queue 中, main_queue 中的任务会被取出来放到主线程中执行,但主线程这个时候已经被阻塞了,所以 Block 中的任务就不能完成,它不完成,dispatch_sync 就会一直阻塞主线程,这就是死锁现象。导致主线程一直卡死。

2. 

queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)

dispatch_async(queue, {

NSLog("sync之前 - %@", NSThread.currentThread())

dispatch_sync(queue, {

NSLog("sync - %@", NSThread.currentThread())

})

NSLog("sync之后 - %@", NSThread.currentThread())

})

队列是串行的。async把block放入queue中,执行,内部sync阻塞当前线程,也把block放入queue中,因为一次只能执行一个,要等待上一个任务执行完毕,所以阻塞。上一个任务是async放入的block,所以相互等待,死锁。

延迟方法:3

[self performSelector:@selector(run:)   withObject:@"abc"  afterDelay:3]; - 底层是创建一个NSTimer定时器,所以要有runloop

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)),queue, ^{ ... } );

[NSTimer scheduledTimerWithTimeInterval:3.0target:selfselector:@selector(run:) userInfo:@"abc"repeats:NO];

主线程:3

[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];

dispatch_async(dispatch_get_main_queue(), ^{ ... } );

[[NSOperationQueue mainQueue] addOperationWithBlock:^{ ... } ];

在某个线程上想要performSelector的代码被执行,需要run loop开启。

在自定义队列中被调度所有 block 最终都将被放入到系统的全局队列中和线程池中。

深入理解dispatch_queue  --  线程池,串行,并发,同步,异步

dispatch queue是一个工作队列,其背后是一个全局的线程池。特别是,提交到队列的任务会在后台线程异步执行。所有线程共享同一个后台线程池,这使得系统更有效率。

精心设计的功能:线程池的线程数量会根据待完成的任务数 和 系统CPU的使用率动态作调整。并发队列的目标队列和调度屏障功能。

dispatch queue的精髓:能串行、能并发、能同步、能异步以及共享同一个线程池。

源码:

MADispatchQueue,四个方法:

1.获取全局共享队列的方法(GCD有多个不同优先级的全局队列,出于简单考虑,我们在实现中保留一个)

2.串行并发队列的初始化函数。

3.异步分发调用

4.同步分发调用

线程池实现

线程池只做一件事:投递任务并运行。

队列实现

全局线程池可以使用block队列和智能产生的线程实现。使用一个共享全局线程池,就能构建一个能提供基本的串行/并发、同步/异步功能的dispatch queue。其内部工作过程。

上一篇下一篇

猜你喜欢

热点阅读