多线程是每个面试官必选择面试的问题,因为通过多线程可以连接到异步,同步,串行,并行的基本概念,可以根据对多线程的理解程度揣测面试者的水平。自己当初面试时,就遇到这样的问题,回答的模棱两可,自己也不是很理解,这次好好整理一下。
一.基本概念
1.进程-------进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
2.线程-------1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程,称为主线程)
一个进程(程序)的所有任务都在线程中执行。
进程和线程的对比
1.线程是CPU调用(执行任务)的最小单位。
2.进程是CPU分配资源的最小单位。
3.一个进程中至少要有一个线程。
4.同一个进程内的线程共享进程的资源。
3.多线程------1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务,多线程技术可以提高程序的执行效率。
4.主线程-------一个iOS程序运行之后,默认会开启一条线程,这个线程也叫做UI线程。
二.实现方案
1.NSThread的使用
(1).创建线程
// 方法一:创建线程,需要自己开启线程NSThread *thread = [[NSThread alloc]initWithTarget: self selector:@selector(run) object:nil]; // 开启线程[thread start]; // 方法二:创建线程后自动启动线程[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];// 方法三:隐式创建并启动线程[self performSelectorInBackground:@selector(run) withObject:nil];
(2).常用方法
//获取当前线程 NSThread *thread = [NSThread currentThread];//判断当前是否在多线程 [NSThread isMultiThreaded]//判断当前是否在主线程 [NSThread isMainThread]//让当前线程睡眠几秒 [NSThread sleepForTimeInterval:3]; //回到主线程 [self performSelectorOnMainThread:<#(SEL)#> withObject:<#(id)#> waitUntilDone:<#(BOOL)#>]
(3).NSThread线程的状态
启动线程 - (void)start; // 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态阻塞(暂停)线程 + (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti;// 进入阻塞状态强制停止线程 + (void)exit;// 进入死亡状态
(4).NSThread多线程安全问题
安全原因:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。那么当多个线程访问同一块资源时,其中有的线程改变访问对象,很容易引发数据错乱和数据安全问题。
解决办法:
互斥锁使用格式
@synchronized(锁对象) { // 需要锁定的代码 }
通过一个售票实例来看一下线程安全的重要性
- (void)viewDidLoad { [super viewDidLoad]; // 总票数为30 self.numTicket = 30; self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread01.name = @"售票员01"; self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread02.name = @"售票员02"; self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread03.name = @"售票员03"; // Do any additional setup after loading the view. } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.thread01 start]; [self.thread02 start]; [self.thread03 start]; }// 售票-(void)saleTicket { while (1) { // 锁定的时候,其他线程没有办法访问这段代码 @synchronized (self) { // 模拟售票时间,我们让线程休息0.05s [NSThread sleepForTimeInterval:0.05]; if (self.numTicket > 0) { self.numTicket -= 1; NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket); }else{ NSLog(@"票已经卖完了"); break; } } } }
当没有加互斥锁的时候我们看一下输出:
可以看出出现22张票为两次,出现数据混乱的现象。
加了互斥锁之后打印的数据输出:
总结:
互斥锁的使用前提:多条线程抢夺同一块资源时
注意:锁定1份代码只用1把锁,用多把锁是无效的
互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
(5).NSThread线程之间的通信
做线程间通信---------在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信,例如我们在子线程完成下载图片后,回到主线程刷新UI显示图片。
线程间通信的体现
1个线程传递数据给另1个线程。
在1个线程中执行完特定任务后,转到另1个线程继续执行任务。
线程间通信常用的方法
// 返回主线程- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;// 返回指定线程- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
2.GCD的使用
1.基本概念
GCD---------全称 Grand Central Dispatch,可以称之为大中央调度。实际上GCD是管理着一个线程池,如何创建线程,如何回收线程,以及分配多少个线程,这些都是GCD来控制的。在开发中,程序员是不用操作线程的相关事情,程序员只需要把应该做的操作放到相应的队列里面即可。
GCD中有多种队列,其中自定义的队列有两种:串行队列和并行队列。
串行队列---------队列中的任务只会顺序执行,执行完一个任务后,才会执行下一个任务。
并行队列----------可以一次执行多个任务。
GCD中有两种操作,分别是同步操作和异步操作。
1:同步操作:不会新开线程,任务创建后必须执行完才可以继续走下去。
2:异步操作:会开启新的线程,任务创建后可以先不执行,过会在执行。
2.串行,并行,同步,异步组合特点
(1).并行队列+异步执行(同一时刻可能会同时执行多个任务,开启多个线程,且每个任务的结束时间是不确定的)
//创建一个并行队列 dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_CONCURRENT); //使用异步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
打印结果:
(2).串行队列+异步执行(开启新的线程,所有的任务都在新的线程上顺序执行)
//创建一个串行队列 dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_SERIAL); //使用异步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
打印结果:
(3).主队列+异步执行(在主线程中顺序执行)
//获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); //使用同步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
打印结果:
(4)并行队列+同步执行(并行队列可以一次开始多个任务,但实际上仍旧是每个任务都在主线程上执行,且按顺序执行)
//创建一个并行队列 dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_CONCURRENT); //使用同步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
打印结果:
(5).串行队列+同步执行(不会新建线程,而且任务是一个一个的执行)
//创建一个串行队列 dispatch_queue_t queue = dispatch_queue_create("TestGCD", DISPATCH_QUEUE_SERIAL); //使用同步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
打印结果:
(6).主队列+同步执行(死锁)
//获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); //使用同步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务3---%@", [NSThread currentThread]); });
死锁原因:
主队列中本身是有一个任务A的(主任务),且该任务A还没有执行完。在执行任务A的过程中,又插入了新的同步任务1。我们知道,串行队列中,必须先执行完一个任务后,才能继续执行另一个任务。此时的情况时:
若想继续执行任务A,需要先把任务1执行完,若想继续执行任务1,需要先把任务A执行完,因此造成了阻塞。
3.GCD 常用函数
(1).延迟
//参数1 DISPATCH_TIME_NOW 表示 现在 NSEC_PER_SEC 秒 NSEC_PER_MSEC 毫秒 即是多长时间加入队列去执行block //参数2 (int64_t)(2 * NSEC_PER_SEC) 表示的是多长时间去执行 //参数3 dispatch_get_main_queue() 想要加入哪个队列去执行 可以自定义dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ NSLog(@"3秒后执行这个方法"); });
(2).只执行一次
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //只执行一次代码 常用单例 });
(3).创建主线程(串行)
dispatch_async(dispatch_get_main_queue(), ^{ //刷新界面代码 });
(4).创建异步线程(并行)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //比较耗时的代码放这里 });
(5).dispatch_barrier_async的使用(dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行)
dispatch_queue_t queue = dispatch_queue_create("create_asy_queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"dispatch_async1"); }); dispatch_async(queue, ^{ NSLog(@"dispatch_async2"); }); dispatch_barrier_async(queue, ^{ NSLog(@"dispatch_barrier_async"); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"刷新界面"); }); }); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"dispatch_async3"); });
打印结果:
4.GCD在项目中的应用
(1).上文中的延迟和单例
(2).在开发中,遇到N个网络请求都完成后更新UI的需求,这时使用dispatch_group_async
/** 于是可以建立一个分组,让多个任务形成一个组,下面的代码在组中多个任务都执行完毕之后再执行后续的任务: **/ dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); /** 创建一个group组 **/ dispatch_group_t group = dispatch_group_create(); /** 将block任务添加到queue队列,并被group组管理 **/ dispatch_group_async(group, queue, ^{ NSLog(@"任务1"); }); dispatch_group_async(group, queue, ^{ NSLog(@"任务2"); }); dispatch_group_notify(group,dispatch_get_main_queue(),^{ NSLog(@"更新UI"); });
(3).GCD的使用可以让程序在后台较长久的运行。
在没有使用GCD时,当app被按home键退出后,app仅有最多5秒钟的时候做一些保存或清理资源的工作。但是在使用GCD后,app最多有10分钟的时间在后台长久运行。这个时间可以用来做清理本地缓存,发送统计数据等工作。
// AppDelegate.h文件 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundUpdateTask; // AppDelegate.m文件 - (void)applicationDidEnterBackground:(UIApplication *)application { [self beingBackgroundUpdateTask]; // 在这里加上你需要长久运行的代码 [self endBackgroundUpdateTask]; } - (void)beingBackgroundUpdateTask { self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endBackgroundUpdateTask]; }]; } - (void)endBackgroundUpdateTask { [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }
3. NSOperation
NSOperation是一个抽象的基类,表示一个独立的计算单元,可以为子类提供有用且线程安全的建立状态,优先级,依赖和取消等操作。系统已经给我们封装了NSBlockOperation和NSInvocationOperation这两个实体类。
1.怎么使用NSOperation
(1).使用NSInvocationOperation
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil]; // 调用start方法执行操作operation操作 [operation start];
(2).NSBlockOperation
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task0---%@", [NSThread currentThread]); }]; [operation start];
打印结果:
根据打印的结果我们会发现,直接调用start方法时,系统并不会开辟一个新的线程去执行任务,任务会在当前线程同步执行。
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"task0---%@", [NSThread currentThread]); }]; [operation addExecutionBlock:^{ NSLog(@"task1----%@", [NSThread currentThread]); }]; [operation addExecutionBlock:^{ NSLog(@"task2----%@", [NSThread currentThread]); }]; // 开始必须在添加其他操作之后 [operation start];
打印结果:
根据打印结果:task0的是执行在主线程中的(因为是在主线程中调用start方法),但task1和task2都是在自己的新线程中执行.也就是说:当NSBlockOperation封装的操作数大于1的时候,就会执行异步操作。
(3).自定义NSOperation
创建一个类继承自NSOperation
重写main方法,自动释放池
初始化该操作的时候直接调用alloc及init即可
取消操作(取消正在执行的操作)
以下载图片为例如何自定义NSOperation:
@interface DTDownloaderOperation : NSOperation //要下载图片的地址@property (nonatomic, copy) NSString *urlString; //执行完成后,回调的block@property (nonatomic, copy) void (^finishedBlock)(UIImage *img); + (instancetype)downloaderOperationWithURLString:(NSString *)urlString finishedBlock:(void(^)(UIImage *img))finishedBlock; @end @implementation DTDownloaderOperation + (instancetype)downloaderOperationWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *))finishedBlock { DTDownloaderOperation *op = [[DTDownloaderOperation alloc] init]; op.urlString = urlString; op.finishedBlock = finishedBlock; return op; } - (void)main { @autoreleasepool { //模拟网络延时 [NSThread sleepForTimeInterval:2.0]; //判断是否被取消 取消正在执行的操作 if (self.isCancelled) { return; } NSLog(@"下载图片 %@ %@",self.urlString,[NSThread currentThread]); //假设图片下载完成 //回到主线程更新UI if (self.finishedBlock) { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.finishedBlock(self.urlString); }]; } } }
-------------------------------------ViewController----------------------------------
//全局队列@property (nonatomic, strong) NSOperationQueue *queue; /懒加载 - (NSOperationQueue *)queue { if (_queue == nil) { _queue = [[NSOperationQueue alloc] init]; _queue.maxConcurrentOperationCount = 2;//最大并发数,为1 时就变成了串行执行任务 } return _queue; } - (void)viewDidLoad { [super viewDidLoad]; for (int i = 0; i<20; i++) { DTDownloaderOperation *op = [DTDownloaderOperation downloaderOperationWithURLString:@"abc.png" finishedBlock:^(UIImage *img) { //图片下载完成更新UI NSLog(@"更新UI %d %@",i,[NSThread currentThread]); }]; [self.queue addOperation:op]; } } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { //设置所有操作的canceled属性为yes [self.queue cancelAllOperations]; NSLog(@"取消"); }
总结:NSOperation的start方法默认是同步执行任务,这样的使用并不多见,只有将NSOperation与NSOperationQueue进行结合,才会发挥出这种多线程技术的最大功效.当NSOperation被添加到NSOperationQueue中后,就会全自动地执行异步操作.
三.多线程优缺点
优点
1.适当的提高执行效率
2.提高资源利用率(cpu 内存)缺点
开启大量线程,会降低程序性能
线程越多,CPU在线程之间的调度越大
程序设计更复杂,线程之间的通信,线程之间的数据共享
虽然我们在实际开发中多线程用到的很少,那是因为系统和第三方都已经封装好了,我们直接拿来使用,作为程序员,普通水平和大神的区别在哪里? 我感觉不在做项目的能力,项目做多了,都有经验了,而在于对实现原理,底层研究的能力,大神深知写每句代码的原因,自己也可以封装出更好的。
本篇独发金蝶云社区
推荐阅读