iOS 多线程

进程与线程

进程:一个正在执行的程序实例,是 CPU 调度的基本单位。CUP 通过时间片轮转等调度算法在多个进程之间快速切换,制造多个进程并发的假象,从而实现多任务。但当遇到 I/O 操作阻塞时就会放弃进程时间片。这对性能有很大的影响,因为进程上下文切换开销很大。所以就有了线程来优化这种劣势。

线程:现在操作系统的中的轻量进程,是 CPU 调度的基本单位。而进程作为线程的容器,是资源管理的单位。线程的优势是当是某个线程 I/O 操作阻塞时,还可以执行其他线程从而最大化利用进程的时间片。

因为每个进程都有独立且保护的内存空间,保存文件、子进程、即将发生的报警、信号处理程序、账号信息等。线程只拥有程序计数器 、寄存器、堆栈等少量资源,但与其他线程共享整个进程的内存空间,因此线程切换速度要比进程快 10 到 100 倍。

为什么使用多线程

使用多线程可充分利用现在的多核 CPU、减少 CPU 的等待时间、防止主线程阻塞等。除了性能上的提升,对批量业务,使用多线程也能使代码逻辑更加清晰。

NSThread

苹果封装的线程管理对象, 不过 NSThread 创建时不代表一个真正的线程被创建,只有我们调用 start 时才真正创建了线程。

创建
- (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;

使用该方法虽然是实例的创建,但确实不能创建一个线程,因为没有提供其他 api 来操作,没办法启动线程的 RunLoop。所以用这个函数创建线程是没有意思的。

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

可以使用这两种函数来创建线程的实例,这两个函数分别通过 selector 和 block 来管理线程后面执行任务,创建后使用 start 启动线程。

+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

也可以使用这两种函数创建线程实例,和上面的区别是上面创建后要手动调用 start 来启动线程,而这两种构造方法则不用手动启动,他会在创建完后自动调用 start 来启动线程。

@property (class, readonly, strong) NSThread *currentThread;
@property (class, readonly, strong) NSThread *mainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

当然,还可以使用这个两个函数获取当前线程和主线程。这也是 Apple 提供的获取线程对象的唯一两个函数。

启动
- (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)main API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

上面提到过启动线程使用 start 函数。main 函数是线程入口的主函数,子类重写后不用调用 super,Apple 建议不用去使用它。

派发任务

系统有提供分类的 API 对派发任务的函数。

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

向主线程派发任务,值得注意的是他的带的几个参数。
arg:执行 slector 携带的参数,没有可以为 nil;
wait:布尔值,是否阻塞当前线程等待选择器,设置为 YES 时,等到当前线程执行完后,执行主线程 selector,设置为 NO 时,阻塞当前线程,先执行主线程 selector。如果当前为主线程,则立即执行 selector。
array:selector 的执行模式RunLoop model,要注意改值设置为 nil 或者空数组时,该方法返回时不执行指定的选择器。

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

向指定线程派发任务。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

向后台模式的线程派发任务。

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

两个函数分别设置优先级和获取优先级,优先级为 0.0-1.0 ,越大表示优先级越高,默认为0.5,但由于优先级由内核决定,也不保证实际一直是 0.5.

休眠
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

两个函数设置线程休眠,一个为休眠到某个时间,一个为休眠多长时间,时间段类型为 NSTimeInterval。

状态
@property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

布尔类型,分别线程是否正在进行、是否完成、是否取消。

退出
+ (void)exit;
- (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

一个类方法,一个实例方法;
exit:是使用currentThread类方法来访问当前线程。在退出线程之前,此方法将发送NSThreadWillExitNotification,并将线程退出到默认的通知中心。因为通知是同步发送的,所以确保NSThreadWillExitNotification的所有观察者在线程退出之前接收通知。
Apple 建议我们应该避免调用此方法,因为它没有给您的线程一个机会来清理它在执行期间分配的任何资源。

cancel:这个方法和 NSOperation 的 cancle 方法类似,正常取消线程,释放资源。

GCD

苹果自己封装的一套管理现成的 API,分串行队列和并发队列,任务派发分异步任务和同步任务,两者可组合搭配。队列原则是先进先出,所以不管是串行队列还是并发队列都是按顺序执行的。串行队列需要在前一个任务执行完后执行下一个任务而并发队列可以允许多个任务同时进行,虽然他们开始的时间是一致的,但结束的时间却不确定。

队列和派发任务搭配结果:

同步派发 异步派发
串行队列 当前线程串行执行,阻塞当前线程 新建单个线程串行执行,不阻塞当钱线程
并发队列 当前线程并发执行,阻塞当前线程 新建多个线程并发执行,不阻塞当前线程
主队列(串行队列) 主线程串行执行,阻塞当前线程 主线程串行执行,不阻塞当前线程

这里要说一点,就是串行队列搭配同步派发任务后,在添加任务后,该任务被添加在队列的末尾,要执行这个任务就得前面任务等待执行完,而刚派发的这个同步任务要阻塞线程直到自己执行完,然后造成死锁。

全局并发队列

dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags);

全局串行主队列

dispatch_queue_t
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);
}

自定义队列

dispatch_queue_t
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

label 队列标签,以便调试时标注,一般为反dns,attr 设置不同,可以创建串行队列或者并发队列。下面分别对应不同参数

  • DISPATCH_QUEUE_SERIAL:创建的队列为串行队列;
  • DISPATCH_QUEUE_CONCURRENT:创建的队列为并发队列。

异步派发任务

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

同步派发任务

void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

NSOperation、NSOperationQueue

NSOperation 也是一个任务面向对象的封装,不过他是一个抽象类,在使用它的使用应该是用他的子类 NSBlockOperation 和 NSInvocationOperation ,这两个类分别以 Blok 和 Invocation 方式来管理任务。然后搭配 NSOperationQueue 面向对象队列的封装来使用,更好管理多线程。

NSBlockOperation

NSBlockOperation 通过 Block 来管理任务,可以使用 init 创建,然后通过 addExecutionBlock 来添加多个任务,也可以使用 blockOperationWithBlock 创建且添加任务,然后调用 start 开始执行。

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

- (void)addExecutionBlock:(void (^)(void))block;
NSInvocationOperation

NSInvocationOperation 通过 Invocation 管理任务,通过 taeget ,selector 形式创建, 然后 start 开始执行。

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
NSOperationQueue

NSOperationQueue 队列的面向对象封装,可以和 NSOperation 子类更好的管理线程。

默认的最大并发数,由当前系统决定

static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;

添加队列管理,参数 wait 为布尔类型,YES 时同步执行,阻塞当前线程,NO 时异步执行,不阻塞当前线程。

- (void)addOperation:(NSOperation *)op;
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
- (void)addOperationWithBlock:(void (^)(void))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

获取并发队列数,1 时代表此时队列为串行队列

@property NSInteger maxConcurrentOperationCount;

getter 方法为获取队列状态,setter 方法为设置队列状态(暂停 or 恢复)

@property (getter=isSuspended) BOOL suspended;

取消所有队列

- (void)cancelAllOperations;

阻塞当前线程直到所有任务完成,同步执行

- (void)waitUntilAllOperationsAreFinished;

获取主队列、当前队列

@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
@property (class, readonly, strong) NSOperationQueue *mainQueue API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里 转载请注明出处 h...
    WWWWDotPNG阅读 4,589评论 2 27
  • 前言 GCD是iOS开发中十分重要的多线程方案之一,通过对大神文章的学习,此篇文章为代码的练习和一部分自己学习的总...
    炒河粉儿阅读 398评论 0 0
  • OC中的多线程 OC中多线程根据封装程度可以分为三个层次:NSThread、GCD和NSOperation,另外由...
    爱笑的猫mi阅读 15,480评论 2 34
  • 概览 进程与线程的概念 多线程的由来 并行与并发 多线程的实现 串行与并行 线程的几种状态 串行队列与并发队列区别...
    a_只羊阅读 222评论 0 0
  • 什么是进程? 进程是指在系统中正在运行的一个应用程序。 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存...
    珍此良辰阅读 1,253评论 1 5