一、基本概念
了解线程之前,我们得先了解进程的概念。
1、进程:是指在系统中正在运行的一个应用程序,是CPU分配资源和调度的单位。
重要条件:正在运行。
2、进程间的相互关系:每个进程之间是独立,互不干扰的关系。每个进程均运行在其专用且受保护的内存空间内。
3、查看进程示例
(1)MAC自带工具查看:活动监视器。
(2)终端命令查看:输入TOP命令,按“q”退出。
4、线程:是CUP调用(执行任务)的最小任务。
5、线程和进程的关系:
(1)1个进程要想执行任务, 必须要有线程(每个进程至少要有一条线程),一个进程中的所有任务都是在线程中进行的。
(2)一个程序可以对应多个进程,一个进程可以有多个线程,一个进程至少要有1个线程;
(3)同一个进程内的线程共享进程资源。
6、线程的串行执行方式:一个线程要执行多个任务,那么只能一个接一个的按顺序执行这些任务。也就是说,同一个时间内,一个线程只能执行一个任务。也可以说,线程是进程中的一条执行路径。
7、多线程:1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。(通俗理解:进程类比于车间,线程类比于车间工人,车间工人可以同时进行手上的工作)。可以提高程序的执行效率。
8、多线程的执行原理:同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发执行,其实是CPU在多线程中进行快速的调度。如果CPU调度线程的时间足够快,就造成了多条线程并发执行的假象。
⚠️开发过程中,并不是线程越多越好,如果线程过多,可能会导致大量的CPU资源消耗,反而降低了程序的执行效率。如果要运用到多线程技术,那么线程的数量控制在3~5条最佳。
9、总结多线程的优缺点:
(1)优点:
① 能适当提高程序的执行效率。
② 能适当提高资源利用率(CPU、内存利用率)。
(2)缺点:
① 创建线程是需要开销的,开销主要分成2个部分:
空间上的开销:内核的数据结构(大约1KB)、栈空间(子线程大约512KB、主线程大约1MB、也可以使用-setStackSize 设置,但必须是4K的倍数,最小单位为16K)。
时间上的开销:创建线程大约需要90ms的创建时间。
② 开启大量的线程,会降低程序的性能。
③ 线程越多,CPU在线程之间调度的开销就越大。
④ 程序上设计更加复杂:比如线程之间的通信、多线程的数据共享等。
二、主线程和子线程
1、主线程
(1)、主线程:一个iOS程序运行后,会默认开启一条线程,称为“主线程”或者“UI线程”。
(2)、主线程的作用:
① 显示或刷新UI界面。
② 处理UI事件(点击事件、滚动事件、拖拽事件等)。
(3)、主线程的使用注意点:
① 别将耗时的操作放入主线程中执行。如果将耗时操作放入主线程,会影响到程序的UI流畅度,严重影响用户的体验度。
② UI相关操作都必须放在主线程中执行。
2、子线程
(1)子线程:除去主线程外的线程,都是子线程(后台线程、非子线程)。
3、代码示例
通常情况下,执行任务都是在主线程中执行,除非人为创建线程或者其他特殊情况。
(1)、如何获得主线程
NSThread *mainThread = [NSThread mainThread];
NSLog(@"%@", mainThread);
(2)、如何获的当前线程(当前执行任务的/执行当前方法的)
NSThread *currentThread = [NSThread currentThread];
NSLog(@"%@", currentThread);
(3)如何判断线程是主线程
① 打印线程,看控制台输出的number ,如果number== 1,则是主线程,反之是子线程。
② 通过类方法判断
NSLog(@"%zd",[NSThread isMainThread]);//0:1 = 否:是
③ 通过对象方法判断
//判断给定线程是否是主线程
NSLog(@"%zd",[currentThread isMainThread]);//0:1 = 否:是
三、多线程的实现方法
1、线程技术归类
2、线程技术详解
(1)、pthread的使用(推荐掌握指数:✨✨✨✨)
#import "FourPageVC.h"
#import
@interface FourPageVC ()
@end
@implementation FourPageVC
- (void)viewDidLoad {
[super viewDidLoad];
[self creatPthread];
}
-(void)creatPthread{
NSLog(@"%@",[NSThread currentThread]);
// phtread创建线程
// 01 导入头文件
// #import
// 02 创建线程对象
pthread_t thead = nil;
// 03 创建线程 ,执行任务
// 参数解析
// 参数1: 线程对象 传地址
// 参数2: 线程属性(优先级)
// 参数3: 指向函数的指针
// 参数4: 传给第三个参数的(参数)
pthread_create(&thead, NULL, run, NULL);
}
//技巧:(* _Nonnull)改写成函数的名称,补全参数
void * _Nullable run(void * _Nullable str){
// NSLog(@"run ====%@",[NSThread currentThread]);
// 耗时操作
for (int i= 0; i<1000000; i++) {
NSLog(@"i == %d 线程 == %@",i,[NSThread currentThread]);
}
return NULL;
}
@end
(2)、NSThread的使用(推荐掌握指数:✨✨✨✨✨)
① NSThread创建线程方法
#pragma mark ------用NSThread创建线程方法
-(void)run{
NSLog(@"run====%@",[NSThread currentThread]);
}
-(void)creatThread1{
// 01 创建线程
// 参数解析:
// 参数1:目标对象
// 参数2:方法选择器,要执行的任务(方法)
// 参数3:调用函数 需要传递的参数
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
// 02 启动线程
[thread start];
}
-(void)creatThread2{
// 分离出一条子线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
-(void)creatThread3{
// 开启一条后台线程
[NSThread performSelectorInBackground:@selector(run) withObject:nil];
}
总结:如果需要对线程进行详细的设置(如名称等)则使用第一种创建方式,可以拿到具体线程对象;如果只是简单创建一条子线程,2、3两种方法任选均可。
② 线程属性的设置
-(void)creatThread0{
NSThread *threadA = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
NSThread *threadB = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
NSThread *threadC = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
// 给线程设置名字
threadA.name = @"threadA";
threadB.name = @"threadB";
threadC.name = @"threadC";
// 给线程设置优先级 范围是0.0~1.0 默认是0.5
// 优先级越高,被CPU调用的概率就越高
threadA.threadPriority = 1.0;
threadB.threadPriority = 0.1;
[threadA start];
[threadB start];
[threadC start];
}
③ 线程的生命周期
从线程的创建到线程的释放。⚠️线程的释放:当线程内部的任务执行完毕,线程会自动释放。
④ 线程的几种状态
//如何控制线程状态
//启动线程
-(void)start;
//进入就绪状态->运行状态,当线程任务执行完毕,自动进入死亡状态。
//阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//进入阻塞状态
//强制停止线程
+ (void)exit;
⑤ 控制线程状态
-(void)creatThread1{
// 01 创建线程对象 新建状态
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
// 02 启动线程 新建状态->就绪状态<->运行状态
[thread start];
}
-(void)run{
NSLog(@"start");
// 控制线程进入阻塞状态
// [NSThread sleepForTimeInterval:3.0];//阻塞3秒的时间
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
NSLog(@"end");
}
//强制退出线程
-(void)run1{
for (int i =0; i<100; i++) {
NSLog(@"i == %d thread == %@",i,[NSThread currentThread]);
if (i == 50) {
// 当执行到i == 50时,强制停止线程
[NSThread exit];
}
}
}
//线程死亡
三、线程安全问题
1、引起线程安全的原因及结果
① 原因:多个线程可能会访问同一块资源(如:同一个对象、同一个文件、同一个变量)。
②结果:数据安全和数据错乱。
2、线程安全的解决方法
在线程上加上“互斥锁”。锁定1份代码,只能用一把锁,用多把锁无效。
(1)互斥锁的使用前提:多条线程抢夺同一块资源。
(2)互斥锁的优点:能有效防止多条线程抢夺同一块资源引发的数据安全和数据错乱。
(3)互斥锁的缺点:需要消耗大量的CPU资源。
(4)拓展:线程同步(如互斥锁),线程异步(线程并行执行)。
3、代码示例
#import "FourPageVC.h"
@interface FourPageVC ()
#pragma mark -------------模拟售票过程
@property(nonatomic,strong)NSThread *threadA;//售票员1
@property(nonatomic,strong)NSThread *threadB;//售票员2
@property(nonatomic,strong)NSThread *threadC;//售票员3
@property(nonatomic,assign)NSInteger totalCount;//总共票数
@property(nonatomic,strong)NSObject *lock;//全局锁对象
@end
@implementation FourPageVC
- (void)viewDidLoad {
[super viewDidLoad];
// 设置总票数
self.totalCount = 100;
// 初始化全局锁对象
self.lock = [[NSObject alloc]init];
// 初始化售票员(新建线程对象)
self.threadA = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadB = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadC = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
// 设置线程名称
self.threadA.name = @"售票员A";
self.threadB.name = @"售票员B";
self.threadC.name = @"售票员C";
// 设置优先级,不设置优先级,线程执行顺序是随机的,不按ABC顺序执行
self.threadA.threadPriority = 1.0;
self.threadB.threadPriority = 0.5;
self.threadC.threadPriority = 0.1;
// 启动线程
[self.threadA start];
[self.threadB start];
[self.threadC start];
}
-(void)saleTicket{
// 售票
// 01 如果有余票,则卖出一张,否则提示用户没有票了
// 02 如果不加死循环 while 则一天只卖3张票
// 03 加上while 加上for循环执行次数相对够大时,会出现同一张票被多次卖出安全隐患问题
// 04 加上同步锁(互斥锁)解决一张票被多次卖出安全隐患问题
/**
token : 锁对象(要使用全局的对象)建议直接使用self
{}:要加锁的代码段
@synchronized (token) {
}
*/
while (1) {
NSLog(@"当前线程=== %@",[NSThread currentThread].name);
@synchronized (self) {
NSInteger count = self.totalCount;
if (count >0) {
self.totalCount = count-1;
for (int i =0; i<1000000; i++) {
}
NSLog(@"%@卖出去一张票,还剩%ld张票",[NSThread currentThread].name,(long)self.totalCount);
}else{
NSLog(@"%@发现票卖完了",[NSThread currentThread].name);
break;
}
}
}
}
@end
四、原子属性和非原子属性理解
1、原子属性(atomic)
atomic线程是安全的,原因是atomic内部会给setter方法加锁。需要消耗大量的资源。
2、非原子属性(nonatomic)
nonatomic线程是安全的,原因是atomic内部不会给setter方法加锁。nonatomic使用频率广的原因是性能好,同时多线程抢夺同一块资源情况出现不多。适合内存较小的移动设备。
五、多线程技术的应用
1、下载图片
① 普通下载
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"点击下载图片");
// 确定URL 地址 https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597834886768&di=5b858153bf0b21aa8588232e5316d71f&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fq_70%2Cc_zoom%2Cw_640%2Fimages%2F20180724%2F3140afc7fd954afa85620b4631357ab9.jpeg
NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597834886768&di=5b858153bf0b21aa8588232e5316d71f&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fq_70%2Cc_zoom%2Cw_640%2Fimages%2F20180724%2F3140afc7fd954afa85620b4631357ab9.jpeg"];
// 02 把图片的二进制数据下载到本地
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 03 把imageData转换为UIImage
UIImage *image = [UIImage imageWithData:imageData];
// 04 显示下载的图片
self.imageV.image = image;
}
//扩展知识 如何计算某一行代码的执行时间
-(void)timer1{
// OC方法
NSDate *start = [NSDate date];
NSDate *end = [NSDate date];
CGFloat time = [end timeIntervalSinceDate:start];
// C语言函数方法
// CFTimeInterval start = CFAbsoluteTimeGetCurrent();//获得当前时间(相对时间)
// CFTimeInterval end = CFAbsoluteTimeGetCurrent();//获得当前时间(相对时间)
//
// double time = end -start;
NSLog(@"执行时间=== %f",time);
}
② 线程通信下载
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"点击下载图片");
// 下载图片是个耗时操作,放在子线程操作
// 创建子线程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage) object:nil];
thread.name = @"downloadImage";
[thread start];
}
-(void)downloadImage{
// 确定URL 地址 https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597834886768&di=5b858153bf0b21aa8588232e5316d71f&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fq_70%2Cc_zoom%2Cw_640%2Fimages%2F20180724%2F3140afc7fd954afa85620b4631357ab9.jpeg
NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597834886768&di=5b858153bf0b21aa8588232e5316d71f&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fq_70%2Cc_zoom%2Cw_640%2Fimages%2F20180724%2F3140afc7fd954afa85620b4631357ab9.jpeg"];
// 02 把图片的二进制数据下载到本地
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 03 把imageData转换为image
UIImage *image = [UIImage imageWithData:imageData];
// 04 显示下载的图片 如果这段代码放在这里,会报错,原因是:UI操作被放在了子线程;解决方式:将UI操作切换为主线程
// 线程间通信:子线程切换为主线程
/**
方法释义:直接切换回主线程
参数1:方法选择器 回到主线程要做什么事(通过方法告知)
参数2:调用函数需要传递的参数
参数3:是否等待该方法执行完毕才继续往下执行
*/
// 第一种方法
// [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
/**
方法释义:切换回某个线程
参数1:方法选择器 回到主线程要做什么事(通过方法告知)
参数2:要切换的线程
参数3:调用函数需要传递的参数
参数3:是否等待该方法执行完毕才继续往下执行
*/
// 第二种方法
// [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
// 简便方法
[self.imageV performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image{
NSLog(@"显示图片currentThread == %@",[NSThread currentThread]);
self.imageV.image = image;
}