iOS 多线程(基础篇一)

一、基本概念

了解线程之前,我们得先了解进程的概念。
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、线程技术归类

image.png

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];
    
}

③ 线程的生命周期
从线程的创建到线程的释放。⚠️线程的释放:当线程内部的任务执行完毕,线程会自动释放。
④ 线程的几种状态


image.png
//如何控制线程状态
//启动线程
-(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;
}

你可能感兴趣的:(iOS 多线程(基础篇一))