本篇主要涉及多线程的基础知识,内容相对简单,为接下来的GCD、锁作好铺垫。html
进程
是指在系统中正在运行的一个应用程序。进程
之间是独立的,每一个进程
均运行在其专用的且受保护的内存补充:iOS系统是相对封闭的系统,App在各自的沙盒(sandbox)中运行,每一个App都只能读取iPhone上系统为该应用程序程序建立的文件夹AppData下的内容,不能随意跨越本身的沙盒去访问别的App沙盒中的内容。也就是说OS是单进程的,一个App就是一个进程。程序员
线程
是 进程
的基本执行单元,一个 进程
的全部任务都在 线程
中执行进程
要想执行任务,至少要有一条 线程
线程
,这条线程被称为主线程
或 UI线程
补充:对于iOS开发来讲,线程的底层实现是基于 POSIX threads API 的,也就是咱们常说的 pthreads
;编程
苹果不容许直接建立 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:缓存
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程建立一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,建立一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
复制代码
在同一时刻,一个CPU只能处理1条线程,但CPU能够在多条线程之间快速的切换,只要切换的足够快,就形成了多线程一同执行的假象。安全
新建
:实例化线程对象就绪
:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。运行
:CPU 负责调度可调度线程池中线程的执行。线程执行完成以前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。阻塞
:当知足某个预约条件时,可使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。死亡
:正常死亡,线程执行完毕。非正常死亡,当知足某个条件后,在线程内部停止执行/在主线程停止线程对象线程池是一种线程使用模式。 线程过多会带来调度开销,进而影响缓存局部性和总体性能。 而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。 这避免了在处理短期任务时建立与销毁线程的代价。bash
线程池的执行流程如图下:多线程
线程池大小
小于 核心线程池大小
,建立线程执行任务线程池大小
大于等于 核心线程池大小
,则判断线程池工做队列是否已满饱和策略
去处理。饱和策略:并发
AbortPolicy
直接抛出RejectedExecutionExeception 异常来阻止系统正常运行CallerRunsPolicy
将任务回退到调用者DisOldestPolicy
丢掉等待最久的任务‘DisCardPolicy
直接丢弃任务因此在并发的时候,同时能有多少个线程在运行是由线程池的线程缓存数量决定。GCD和NSOperation的线程池缓存数量都是64条app
线程编程的危害之一是在多个线程之间的资源争夺。若是多个线程在同一个时间试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源,并确保每一个线程都有在它操做的资源上面的独特设置。由于保持彻底独立的资源是不可行的,因此你可能必须使用锁,条件,原子操做和其余技术来同步资源的访问。异步
咱们看一下苹果官方给出的线程同步工具:
Atomic Operations是一种基于基本数据类型的同步形式,底层用汇编锁来控制变量的变化,保证数据的正确性,好处在于不会block互相竞争的线程,且相比锁耗时不多。
为了达到最佳性能,编译器一般会讲汇编级别的指令进行从新排序,从而保持处理器的指令管道尽量的满。做为优化的一部分,编译器可能会对内存访问的指令进行从新排序(在它认为不会影响数据的正确性的前提下),然而,这并不必定都是正确的,顺序的变化可能致使一些变量的值获得不正确的结果。
Memory Barriers是一种不会形成线程block的同步工具,它用于确保内存操做的正确顺序。Memory Barriers像一道屏障,迫使处理器在其前面完成必须的加载或者存储的操做。Memory Barriers常被用于确保一个线程中可被其余线程访问的内存操做按照预期的顺序执行。具体参考Memory Barriers。
在程序中应用Memory Barriers只须要在指定地方调用:
OSMemoryBarrier();
复制代码
Volatile Variables是另一种针对变量的同步工具。众所周知,CPU访问寄存器的速度比访问内存速度快不少,所以,CPU有时候会将一些变量放置到寄存器中,而不是每次都从内存中读取(例如for循环中的i值)从而优化代码,可是可能会致使错误。 例如,一个线程在CPUA中被处理,CPUA从内存获取变量F的值,此时,并无其余CPU用到变量F,因此CPUA将变量F存到寄存器中,方便下次使用,此时,另外一个线程在CPUB中被处理,CPUB从内存中获取变量F的值,改变该值后,更新内存中的F值。可是,因为CPUA每次都只会从寄存器中取F的值,而不会再次从内存中取,因此,CPUA处理后的结果就是不正确的。
对一个变量加上Volatile关键字能够迫使编译器每次都从新从内存中加载该变量,而不会从寄存器中加载。当一个变量的值可能随时会被一个外部源改变时,应该将该变量声明为Volatile。
Locks是一种最经常使用的同步工具。Locks能够对一段代码进行保护,保证同时只有一个线程在执行该段代码。
Conditions是一种特殊的lock,用于同步操做的顺序。与Mutex Lock不一样的是,一个等待Condition的线程保持block,直到另外一个线程显示对该Condition调用signal。
因为操做系统的缘由,Conditions可能会获得一些不正确的信号,为了不这类问题,能够在使用Conditions时,加入Predicate(断言)。Predicate是一种有效地判断是否让一个线程处理信号的方式。Conditions保持线程休眠,直到另外一个线程调用signal,并设置了Predicate。
cocoa应用能够用一种便利而同步的方式向线程传递消息,NSObjec对象声明了在线程上执行selector的方法,这些方法异步地传递消息,而系统确保会同步地在目标线程上执行这些selector,每一个请求都会在目标线程的runloop上排上队,并按收到的顺序进行执行。
线程间通讯的表现为:
先看下官方文档推荐的线程通讯方案:
直接消息传递
: 经过 performSelector
的一系列方法,能够实现由某一线程指定在另外的线程上执行任务。由于任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化全局变量、共享内存块和对象
: 在两个线程之间传递信息的另外一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,可是它们比直接消息传递更脆弱。必须使用锁或其余同步机制仔细保护共享变量,以确保代码的正确性。 不然可能会致使竞争情况,数据损坏或崩溃。条件执行
: 条件是一种同步工具,可用于控制线程什么时候执行代码的特定部分。您能够将条件视为关守,让线程仅在知足指定条件时运行。Runloop sources
: 一个自定义的 Runloop source 配置可让一个线程上收到特定的应用程序消息。因为 Runloop source 是事件驱动的,所以在无事可作时,线程会自动进入睡眠状态,从而提升了线程的效率Ports and sockets
:基于端口的通讯是在两个线程之间进行通讯的一种更为复杂的方法,但它也是一种很是可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其余进程和服务)进行通讯。为了提升效率,使用 Runloop source 来实现端口,所以当端口上没有数据等待时,线程将进入睡眠状态消息队列
: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,可是它们不如其余一些通讯技术高效Cocoa 分布式对象
: 分布式对象是一种 Cocoa 技术,可提供基于端口的通讯的高级实现。尽管能够将这种技术用于线程间通讯,可是强烈建议不要这样作,由于它会产生大量开销。分布式对象更适合与其余进程进行通讯,尽管在这些进程之间进行事务的开销也很高我的经常使用的通讯方案有:
NSThread这套方案是通过苹果封装后,而且彻底面向对象的。不过它的生命周期仍是须要咱们手动管理,因此实际上使用也比较少。
//数据请求完毕回调到主线程,更新UI资源信息 waitUntilDone 设置YES ,表明等待当前线程执行完毕
[self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
复制代码
//将当前的逻辑转到后台线程去执行
[self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
复制代码
//支持自定义线程通讯执行相应的操做
NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
[thread start];
//当咱们须要在特定的线程内去执行某一些数据的时候,咱们须要指定某一个线程操做
[self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
复制代码
//回到主线程更新UI操做
dispatch_async(dispatch_get_main_queue(), ^{
//数据执行完毕回调到主线程操做UI更新数据
});
复制代码
DISPATCH_QUEUE_PRIORITY_HIGH 全局队列高优先级
DISPATCH_QUEUE_PRIORITY_LOW 全局队列低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 全局队里后台执行队列
// 全局并发队列执行处理大量逻辑时使用
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
复制代码
//当咱们须要执行一些数据安全操做写入的时候,须要同步操做,后面全部的任务要等待当前线程的执行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//同步线程操做能够保证数据的安全完整性
});
复制代码
if ([[NSThread currentThread] isMainThread]) {
NSLog(@"## 我是主线程 能够更新UI ##");
} else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"### 我是在主队列执行的block ####");
}];
}
复制代码
本篇参照官方文档,学习了多线程的基础知识,下篇开始学习宏大的中央调度系统 - GCD。