关键词:
在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算。可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行。但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行。改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),及解决了线程阻塞又提高了运行效率。
- 多线程
- 简介
- iOS多线程
- NSThread
- 解决线程阻塞问题
- 多线程并发
- 线程状态
- 扩展-NSObject分类扩展
- NSOperation
- NSInvocationOperation
- NSBlockOperation
- 线程执行顺序
- GCD
- 串行队列
- 并发队列
- 其他任务执行方法
- 线程同步
- NSLock同步锁
- @synchronized代码块
- 扩展--使用GCD解决资源抢占问题
- 扩展--控制线程通信
- 总结
- 目 录
多线程
简介
当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其他UI操作),其处理能力并没有明显的变化。如今无论是移动操作系统还是PC、服务器都是多核处理器,于是“并行运算”就更多的被提及。一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。
下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。假设每个核心有两个线程,那么每个CPU中两个线程会交替执行,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。当然,不管是多核还是单核开发人员不用过多的担心,因为任务具体分配给几个CPU运算是由系统调度的,开发人员不用过多关心系统有几个CPU。开发人员需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。
iOS多线程
在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(新版iOS中,使用其他线程更新UI可能也能成功,但是不推荐)。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:
1.NSThread
2.NSOperation
3.GCD
三种方式是随着iOS的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且GCD也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。做过.Net开发的朋友不难发现其实这三种开发方式 刚好对应.Net中的多线程、线程池和异步调用,因此在文章中也会对比讲解。
NSThread
NSThread是轻量级的多线程开发,使用起来也并不复杂,但是使用NSThread需要自己管理线程生命周期。可以使用对象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到线程中并启动,也可以使用对象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 创建一个线程对象,然后调用start方法启动线程。
解决线程阻塞问题
在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。
// // NSThread实现多线程 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2014年 mxi All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController (){ UIImageView *_imageView; } @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame]; _imageView.contentMode=UIViewContentModeScaleAspectFit; [self.view addSubview:_imageView]; UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@"加载图片" forState:UIControlStateNormal]; //添加方法 [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } #pragma mark 将图片显示到界面 -(void)updateImage:(NSData *)imageData{ UIImage *image=[UIImage imageWithData:imageData]; _imageView.image=image; } #pragma mark 请求图片数据 -(NSData *)requestData{ NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"]; NSData *data=[NSData dataWithContentsOfURL:url]; return data; } #pragma mark 加载图片 -(void)loadImage{ //请求数据 NSData *data= [self requestData]; /*将数据显示到UI控件,注意只能在主线程中更新UI, 另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法, 它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法 Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装) waitUntilDone:是否线程任务完成执行 */ [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ //方法1:使用对象方法 //创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数 // NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; // //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行 // [thread start]; //方法2:使用类方法 [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil]; } @end
运行效果:
程序比较简单,但是需要注意执行步骤:当点击了“加载图片”按钮后启动一个新的线程,这个线程在演示中大概用了5s左右,在这5s内UI线程是不会阻塞的,用户可以进行其他操作,大约5s之后图片下载完成,此时调用UI线程将图片显示到界面中(这个过程瞬间完成)。另外前面也提到过,更新UI的时候使用UI线程,这里调用了NSObject的分类扩展方法,调用UI线程完成更新。
多个线程并发
上面这个演示并没有演示多个子线程操作之间的关系,现在不妨在界面中多加载几张图片,每个图片都来自远程请求。
大家应该注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument、- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法还是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能传一个参数,由于更新图片需要传递UIImageView的索引和图片数据,因此这里不妨定义一个类保存图片索引和图片数据以供后面使用。
KCImageData.h
// // KCImageData.h // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. All rights reserved. // #import <Foundation/Foundation.h> @interface KCImageData : NSObject #pragma mark 索引 @property (nonatomic,assign) int index; #pragma mark 图片数据 @property (nonatomic,strong) NSData *data; @end
接下来将创建多个UIImageView并创建多个线程用于往UIImageView中填充图片。
KCMainViewController.m
// // NSThread实现多线程 // MultiThread // // Created by mxi on 16-6-22. // Copyright (c) 2016年 mxj. All rights reserved. // #import "KCMainViewController.h" #import "KCImageData.h" #define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 @interface KCMainViewController (){ NSMutableArray *_imageViews; } @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //创建多个图片控件用于显示图片 _imageViews=[NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; // imageView.backgroundColor=[UIColor redColor]; [self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@"加载图片" forState:UIControlStateNormal]; //添加方法 [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } #pragma mark 将图片显示到界面 -(void)updateImage:(KCImageData *)imageData{ UIImage *image=[UIImage imageWithData:imageData.data]; UIImageView *imageView= _imageViews[imageData.index]; imageView.image=image; } #pragma mark 请求图片数据 -(NSData *)requestData:(int )index{ NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"]; NSData *data=[NSData dataWithContentsOfURL:url]; return data; } #pragma mark 加载图片 -(void)loadImage:(NSNumber *)index{ // NSLog(@"%i",i); //currentThread方法可以取得当前操作线程 NSLog(@"current thread:%@",[NSThread currentThread]); int i=[index integerValue]; // NSLog(@"%i",i);//未必按顺序输出 NSData *data= [self requestData:i]; KCImageData *imageData=[[KCImageData alloc]init]; imageData.index=i; imageData.data=data; [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ //创建多个线程用于填充图片 for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) { // [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]]; NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称 [thread start]; } } @end
通过NSThread的currentThread可以取得当前操作的线程,其中会记录线程名称name和编号number,需要注意主线程编号永远为1。多个线程虽然按顺序启动,但是实际执行未必按照顺序加载照片(loadImage:方法未必依次创建,可以通过在loadImage:中打印索引查看),因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。
从上面的运行效果大家不难发现,图片并未按顺序加载,原因有两个:第一,每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动);第二,每个线程执行时实际网络状况很可能不一致。当然网络问题无法改变,只能尽可能让网速更快,但是可以改变线程的优先级,让15个线程优先执行某个线程。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改:
-(void)loadImageWithMultiThread{ NSMutableArray *threads=[NSMutableArray array]; int count=ROW_COUNT*COLUMN_COUNT; //创建多个线程用于填充图片 for (int i=0; i<count; ++i) { // [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]]; NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称 if(i==(count-1)){ thread.threadPriority=1.0; }else{ thread.threadPriority=0.0; } [threads addObject:thread]; } for (int i=0; i<count; i++) { NSThread *thread=threads[i]; [thread start]; } }
线程状态
在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定线程。为了解决上面优先加载最后一张图片的问题,不妨让其他线程先休眠一会等待最后一个线程执行。修改图片加载方法如下即可:
-(NSData *)requestData:(int )index{ //对非最后一张图片加载线程休眠2秒 if (index!=(ROW_COUNT*COLUMN_COUNT-1)) { [NSThread sleepForTimeInterval:2.0]; } NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; return data; }在这里让其他线程休眠2秒,此时你就会看到最后一张图片总是第一个加载(除非网速特别差)。
线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。
假设在图片加载过程中点击停止按钮让没有完成的线程停止加载,可以改造程序如下:
// // NSThread实现多线程 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. All rights reserved. // #import "KCMainViewController.h" #import "KCImageData.h" #define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 @interface KCMainViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; NSMutableArray *_threads; } @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //创建多个图片空间用于显示图片 _imageViews=[NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; // imageView.backgroundColor=[UIColor redColor]; [self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } //加载按钮 UIButton *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect]; buttonStart.frame=CGRectMake(50, 500, 100, 25); [buttonStart setTitle:@"加载图片" forState:UIControlStateNormal]; [buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:buttonStart]; //停止按钮 UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect]; buttonStop.frame=CGRectMake(160, 500, 100, 25); [buttonStop setTitle:@"停止加载" forState:UIControlStateNormal]; [buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:buttonStop]; //创建图片链接 _imageNames=[NSMutableArray array]; [_imageNames addObject:@ for (int i=0; i<IMAGE_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]]; } } #pragma mark 将图片显示到界面 -(void)updateImage:(KCImageData *)imageData{ UIImage *image=[UIImage imageWithData:imageData.data]; UIImageView *imageView= _imageViews[imageData.index]; imageView.image=image; } #pragma mark 请求图片数据 -(NSData *)requestData:(int )index{ NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; return data; } #pragma mark 加载图片 -(void)loadImage:(NSNumber *)index{ int i=[index integerValue]; NSData *data= [self requestData:i]; NSThread *currentThread=[NSThread currentThread]; // 如果当前线程处于取消状态,则退出当前线程 if (currentThread.isCancelled) { NSLog(@"thread(%@) will be cancelled!",currentThread); [NSThread exit];//取消当前线程 } KCImageData *imageData=[[KCImageData alloc]init]; imageData.index=i; imageData.data=data; [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ int count=ROW_COUNT*COLUMN_COUNT; _threads=[NSMutableArray arrayWithCapacity:count]; //创建多个线程用于填充图片 for (int i=0; i<count; ++i) { NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称 [_threads addObject:thread]; } //循环启动线程 for (int i=0; i<count; ++i) { NSThread *thread= _threads[i]; [thread start]; } } #pragma mark 停止加载图片 -(void)stopLoadImage{ for (查看详情ios开发底层之多线程探索-19(代码片段)
...先级翻转(IOvccpu优先级提升)6.优先级的影响因素三.多线程下的问题1.资源抢夺的解决方法四.GCD简单介绍1.GCD简介2.串行队列与并行队列(数据结构FIFO)总结1.任务的执行 查看详情
ios开发底层之多线程探索-19(代码片段)
...先级翻转(IOvccpu优先级提升)6.优先级的影响因素三.多线程下的问题1.资源抢夺的解决方法四.GCD简单介绍1.GCD简介2.串行队列与并行队列(数据结构FIFO)总结1.任务的执行 查看详情
如何跳转到iOS开发中另一个UITab下的其他UINavigationController?
】如何跳转到iOS开发中另一个UITab下的其他UINavigationController?【英文标题】:HowtojumptootherUINavigationControllerunderanotherUITabiniOSdev?【发布时间】:2014-09-2901:47:07【问题描述】:我有三个标签,即Message、Contact和More。每个选项卡都是... 查看详情
多环境多需求并行下的代码测试覆盖率统计工具实现
马蜂窝技术原创内容,更多干货请关注公众号:mfwtech测试覆盖率常被用来衡量测试的充分性和完整性,也是测试有效性的一个度量。「敏捷开发」的大潮之下,如何在快速迭代的同时保证对被测代码的覆盖度和产品质量,是一... 查看详情
pysyft学习笔记四:minist数据集下的联邦学习(并行训练与非并行训练)(代码片段)
目录手写数字识别模型(非并行训练)概述图导入基本包创建客户机设置训练参数初始化数据集搭建神经网络模型定义训练与测试函数定义主函数训练效果手写数字识别模型(并行训练)概述图导入必要的包建立... 查看详情
生成器案例2-利用生成器实现单线程下的并行效果
#!usrinenvpython#-*-coding:utf-8 -*9importtimedefconsumer(name): #定义一个消费者模型&n 查看详情
并行计算开发了解
今天看了一些并行计算方面的资料,写一些收获吧,因为研究的并不深,也并不打算做专业解释,只是从一个程序员的角度,谈一些并行计算开发方面一些初步了解。并行计算方面的开发库,直到今天,搜起来有TBB,PPL,openMP,POP-cp... 查看详情
李洪强ios下的实际网络连接状态检测
iOS下的实际网络连接状态检测 序言 网络连接状态检测对于我们的iOSapp开发来说是一个非常通用的需求。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对绝大部分... 查看详情
生产环境下的配置和使用
2.1:RocketMQ各部分角色介绍:四个角色:Producer、Consumer、Broker和NameServer。如果一个Topic要发送和接收的数据量非常大,需要能支持增加并行处理的机器来提高处理速度,这时候一个Topic可以根据需求设置一个或多个MessageQueue。Topi... 查看详情
并行开发-paraller(代码片段)
并行开发的概念并行开发要做的事情就是将任务分摊给硬件线程去并行执行来达到负载和加速,传统的代码都是串行的,就一个主线程,当我们为了实现加速而开了很多工作线程,这些工作线程就是软件线程Parallel的使用Parallel类... 查看详情
pysyft学习笔记四:minist数据集下的联邦学习(并行训练与非并行训练)(代码片段)
...f0c;一种是P2P架构,不需要第三方。现在先实现C/S架构下的横向联邦学习模型。大概处理过程如下:1.数据预处理,得到data_loader2.建立虚拟机,分配数据集3.初始化模型4.将模型发送给虚拟机5.指导虚拟机训练6.回收... 查看详情
iOS环境下的可靠时差
】iOS环境下的可靠时差【英文标题】:ReliabletimedifferenceintheiOSenvironment【发布时间】:2011-10-0612:33:10【问题描述】:我正在开发一款随着时间的推移而有所改进的休闲游戏,就像TinyTower。但TinyTower的最大缺陷(恕我直言,它是一... 查看详情
异步/IO 和并行
】异步/IO和并行【英文标题】:Async/IOandParallelism【发布时间】:2018-09-1617:24:00【问题描述】:我正在使用aiohttp创建一个Async/IO网络服务器。但是,据我了解,Async/IO意味着服务器只能在一个处理核心上运行。另一方面,像uwsgi这... 查看详情
并行算法设计
...。并行算法的复杂性度量串行算法的复杂性度量最坏情况下的复杂度(Worst-CASEComplexity)期望复 查看详情
.net中并行开发优化(代码片段)
...程挑战:对大数组中的所有元素求和。现在可以通过使用并行性来轻松优化这一点,特别是对于具有数千或数百万个元素的巨大阵列,还有理由认为,并行处理时间应该与常规时间除以CPU核心数一样多。事实证明,这一壮举并不... 查看详情
具有多个并行版本的开发分支
】具有多个并行版本的开发分支【英文标题】:developmentbranchwithmultipleparallelreleases【发布时间】:2017-01-0221:13:49【问题描述】:在我们的SDLC中,我们通常同时处理接下来的两到三个版本。为此,我们使用分支,每个版本都有自... 查看详情
springboot开发案例之多任务并行+线程池处理
前言前几篇文章着重介绍了后端服务数据库和多线程并行处理优化,并示例了改造前后的伪代码逻辑。当然了,优化是无止境的,前人栽树后人乘凉。作为我们开发者来说,既然站在了巨人的肩膀上,就要写出更加优化的程序。... 查看详情