美文网首页
iOS 多线程(基础篇一)

iOS 多线程(基础篇一)

作者: 鱿鱼干煎辣椒 | 来源:发表于2020-08-19 18:00 被阅读0次

    一、基本概念

    了解线程之前,我们得先了解进程的概念。
    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 <pthread.h>
    @interface FourPageVC ()
    
    @end
    
    @implementation FourPageVC
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [self creatPthread];
    }
    
    -(void)creatPthread{
        NSLog(@"%@",[NSThread currentThread]);
        //    phtread创建线程
        //    01 导入头文件
        //  #import <pthread.h>
        
        //  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<UITouch *> *)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<UITouch *> *)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 多线程(基础篇一)

          本文链接:https://www.haomeiwen.com/subject/fqxorktx.html