美文网首页
多线程使用要注意哪些事项

多线程使用要注意哪些事项

作者: _我和你一样 | 来源:发表于2019-01-03 21:36 被阅读0次

    使用多线程是每个程序员必须要掌握的。然而使用多线程的时候,如果不加注意就会产生很多比较难排查的bug。所以要对多线程有深入的理解才行。比如可变数组和可变字典是线程安全的吗?本身就是异步执行的任务,如何等待真正的结果返回之后才继续后面的事情?在线程中睡眠,睡醒之后当前类已经释放,self会为nil吗?等等,在多线程的实际使用过程中会有很多出现,因此需要把基础打牢,也要有更深的理解。

    首先从最基本的定义说起

    什么是进程?

    进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

    狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

    上面是百科的部分定义,简单来说,每个应用启动之后,都对应着一个进程。打开活动监视器即可看到,第一列就是进程名称,每一个程序对应一个进程,每个进程都有一个唯一标示PID,即进程ID。

    进程概念主要有两点:

    1. 进程是一个实体。每个进程都有自己的地址空间,一般情况下包括 文本区域text region、数据区域data region 和堆栈stack region。
    2. 进程是一个执行中的程序。程序是一个没有生命的实体,只有程序执行的时候,它才能成为一个活动的实体。我们称它为进程。

    除去进程的新建和终止,运行中的进程具有三种基本状态:

    运行中的进程状态
    • 就绪:进程已经获得除处理器外的所需资源,等待分配处理器资源。只要分配了处理器进程就可以执行。
    • 运行:进行占用处理器资源,出于此状态的运行数小于等于处理器数
    • 阻塞:由于进程等待某种条件,比如I/O操作或者进程同步,在条件满足之前无法继续执行

    什么是线程?

    线程是程序执行的最小单元。一个标准的线程由线程ID,当前指令指针,寄存器集合和堆栈组成。线程是进程中的一个实体。线程也具有就绪,阻塞和运行三种基本状态。

    通常一个进行中可以包含若干个线程,它们可以利用进程所拥有的全部资源。进程是分配资源的基本单位。而线程则是独立运行和调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。

    线程是进程的基本执行单元,进程的所有任务都是在线程中执行的。

    多线程的实现原理

    先说下任务执行有两种方式,串行和并行。

    串行:任务一个一个执行,所需时间是所有任务执行完成之和。

    并行:任务并发执行,所需时间是耗时最久的任务完成时间。

    任务的串行于行与线程并没有必然的联系。串行并非就只有一个线程,也可以有多个线程。并行必然有多个线程。

    对于单核操作系统,同一时间只有一个线程在执行。每个线程都分配有时间片进行执行,然后切换到其他线程。从宏观来看是并行的,从微观上来看,是串行的。

    对于多核操作系统,就真正实现了并行执行任务。在同一时间可以有多个线程在执行任务。

    多线程的场景使用场景:

    在iOS系统中,UIKit中的所有操作都是在主线程中执行的,包括用户的触摸事件等。如果在主线程执行耗时操作就会造成卡顿等现象,影响用户体验。常见的耗时操作有:

    • 网络请求

    • 图片加载

    • 文件处理

    • 数据存储

    • 多任务

    常见实现方式

    • pThread --c 语言
    • NSThread -- oc对象
    • GCD -- c语言
    • NSOpreation -- oc对象

    pThread 用的是一套c语言库,在iOS开发中使用的不多。

    使用时需要先导入头文件#import <pthread.h>

    使用时创建线程,并指定执行的方法即可

    - (IBAction)pThreadClicked:(UIButton *)sender {
        NSLog(@"主线程事件");
        pthread_t pthread;
        pthread_create(&pthread, NULL, run, NULL);
    }
    
    void *run(){
        NSLog(@"run方法执行");
        sleep(1);
        NSLog(@"执行结束");
        return NULL;
    }
    // 打印结果4540是进程id,后面的是线程id,打印结果可以看出run在子线程中执行。
    2019-01-03 17:22:26.260893+0800 ThreadTest[4540:1088694] 主线程事件
    2019-01-03 17:22:26.262011+0800 ThreadTest[4540:1089164] run方法执行
    2019-01-03 17:22:27.263254+0800 ThreadTest[4540:1089164] 执行结束
        
    

    因为phread不常用,所以也不做多做介绍,使用的时候时候看一下官方的api就可以了。

    NSThread 的三种创建方式

    // 1. 对象方式,可以获取到线程对象,需要手动执行start方法,当然也方便设置其他属性,比如优先级,比如线程名字等。

    - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    - (IBAction)NSTreadClick:(UIButton *)sender {
        NSLog(@"主线程");
        NSThread *thread = [[NSThread alloc]initWithBlock:^{
            sleep(1);
            NSLog(@"子线程");
        }];
        thread.name = @"thread1";
        thread.threadPriority = 1.0;
        [thread start];
    }
    // 打印结果
    2019-01-03 17:44:14.421668+0800 ThreadTest[4668:1122856] 主线程
    2019-01-03 17:44:15.423002+0800 ThreadTest[4668:1123018] 子线程
    

    // 2. 类方法 不能直接获取线程对象 不过可以在线程执行过程中使用类方法currentThread获取当前线程

    + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
    

    // 3. NSObject的分类方法,不能直接获取线程对象

    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    

    GCD

    以下面的代码为例

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           ... 
        });
    // async 异步
    // get_global_queue 并发
    // ^{       ...     } 执行的任务
    

    GCD的使用需要告诉gcd三个东西

    1. 同步 还是 异步:同步会阻塞当前线程,异步不会阻塞。
    2. 在哪个队列:队列分为串行和并行,即需要一个一个执行还是需要并发执行。
    3. 任务是什么: block中的东西即是要执行的任务。

    主队列:dispatch_get_main_queue() 串行队列

    全局队列: dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 并发队列

    举个异步执行任务回到主线程刷新的例子:

    - (IBAction)gcdClick:(UIButton *)sender {
        NSLog(@"用户点击事件在主线程");
        // 在自线程执行任务
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"start...");
            [NSThread sleepForTimeInterval:3];
            NSLog(@"end...");
            // 回到主线程 刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"回到主线程刷新UI");
            });
        });
    }
    // 打印 可以通过线程id区分是不是主线程,也可以打印当前线程
    2019-01-03 19:14:14.841561+0800 ThreadTest[4789:1195593] 用户点击事件在主线程
    2019-01-03 19:14:14.841752+0800 ThreadTest[4789:1195941] start...
    2019-01-03 19:14:17.845246+0800 ThreadTest[4789:1195941] end...
    2019-01-03 19:14:17.845676+0800 ThreadTest[4789:1195593] 回到主线程刷新UI
    

    获取全局队列时,第一个参数是设置优先级的,第二个是预留参数,暂时没有用。优先级越高,先执行的概率越大。

    进行串行执行,串行还是并发和同步异步没有关系,也就是和线程没有必然的联系,我们举个例子来说明这一点,同步因为没有开启线程必然是串行的,但异步如果指定了串行队列,也会一个一个执行。

    同步的串行:

        // 同步 -- 串行
        NSLog(@"当前主线程");
        dispatch_queue_t serial = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
        dispatch_sync(serial, ^{
            NSLog(@"sync-task1");
        });
        dispatch_sync(serial, ^{
            NSLog(@"sync-task2");
        });
        dispatch_sync(serial, ^{
            NSLog(@"sync-task3");
        });
    
    

    异步的串行:

        //异步 -- 串行
        dispatch_async(serial, ^{
            NSLog(@"async-task1");
        });
        dispatch_async(serial, ^{
            NSLog(@"async-task2");
        });
        dispatch_async(serial, ^{
            NSLog(@"async-task3");
        });
    

    上述两者的打印结果如下:

    2019-01-03 19:28:09.275753+0800 ThreadTest[4836:1220108] 当前主线程
    2019-01-03 19:28:09.276001+0800 ThreadTest[4836:1220108] sync-task1
    2019-01-03 19:28:09.276151+0800 ThreadTest[4836:1220108] sync-task2
    2019-01-03 19:28:09.276287+0800 ThreadTest[4836:1220108] sync-task3
    2019-01-03 19:28:09.276452+0800 ThreadTest[4836:1220151] async-task1
    2019-01-03 19:28:09.276656+0800 ThreadTest[4836:1220151] async-task2
    2019-01-03 19:28:09.277657+0800 ThreadTest[4836:1220151] async-task3
    

    上述结果说明:

    1. 同步时,没有开启线程。
    2. 异步时,开启了线程,因为是串行,所以只开了一个线程。
    3. 串行是为了保证任务的顺序执行,并行是为了保证任务的并发执行,串行没有创建新线程,并行会根据需要至少创建一个线程。

    关于同步&异步。串行&并行 可以列个象限图

    有四种组合:

    • 同步-串行:同步会阻塞当前线程,因此不会创建线程,也可以保证任务同步执行

    • 同步-并发:同步会阻塞当前线程,所以不会创建线程,所以无法实现并发,实际上还是串行。

    • 异步-串行:异步不会阻塞当前线程,会创建新线程,为保证串行,一般创建一个线程就够了。

    • 异步-并发:异步不会阻塞当前线程,会创建新线程,为保证并发,一般会创建多个线程。

    这里重点说明一下同步-并发 其实因为同步会阻塞线程所以不能并发

        // 同步-并发,实际无法实现并发,依然是串行执行
        NSLog(@"当前主线程");
        dispatch_queue_t async = dispatch_queue_create("com.test.async", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i < 8; i++) {
            dispatch_sync(async, ^{
                NSLog(@"sync_concurrent_task%d",i);
            });
        }
    // 打印结果 并没有创建新的线程,所以并不是并发执行的。
    2019-01-03 19:44:25.405840+0800 ThreadTest[4888:1241863] 当前主线程
    2019-01-03 19:44:25.406140+0800 ThreadTest[4888:1241863] sync_concurrent_task0
    2019-01-03 19:44:25.406288+0800 ThreadTest[4888:1241863] sync_concurrent_task1
    2019-01-03 19:44:25.406429+0800 ThreadTest[4888:1241863] sync_concurrent_task2
    2019-01-03 19:44:25.406534+0800 ThreadTest[4888:1241863] sync_concurrent_task3
    2019-01-03 19:44:25.406634+0800 ThreadTest[4888:1241863] sync_concurrent_task4
    2019-01-03 19:44:25.406756+0800 ThreadTest[4888:1241863] sync_concurrent_task5
    2019-01-03 19:44:25.407134+0800 ThreadTest[4888:1241863] sync_concurrent_task6
    2019-01-03 19:44:25.407561+0800 ThreadTest[4888:1241863] sync_concurrent_task7
    

    // 任务组 — 用来一组任务结束之后,再执行其他操作,组任务结束之后会调用notify方法

        NSLog(@"当前主线程");
    dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_async(group, group_queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"group task 1");
        });
        dispatch_group_async(group, group_queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"group task 2");
        });
        dispatch_group_async(group, group_queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"group task 3");
        });
        
        dispatch_group_notify(group, group_queue, ^{
            NSLog(@"all task done");
        });
    // 打印 启用了三个线程 任务等待完成之后执行。
    2019-01-03 19:55:20.669903+0800 ThreadTest[4924:1260901] 当前主线程
    2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260946] group task 2
    2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260947] group task 1
    2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260945] group task 3
    2019-01-03 19:55:22.674753+0800 ThreadTest[4924:1260946] all task done
    

    需要注意的是:以上每一个任务本身就是同步的,如果任务本身就是异步的,每个任务很快就会执行,可能获取不到我们想要的结果。比如我们模拟一个异步请求。

        // 任务组
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_group_async(group, group_queue, ^{
            [self requestOneInfo:^{
                NSLog(@"one info done");
            }];
        });
        
        dispatch_group_async(group, group_queue, ^{
            [self requestOtherInfo:^{
                NSLog(@"other info done");
            }];
        });
        
        dispatch_group_notify(group, group_queue, ^{
            NSLog(@"all task done");
        });
    // 打印结果 很明显不是我们想要的
    2019-01-03 20:07:51.231103+0800 ThreadTest[4950:1281306] 当前主线程
    2019-01-03 20:07:51.231406+0800 ThreadTest[4950:1281362] get OneInfo start
    2019-01-03 20:07:51.231426+0800 ThreadTest[4950:1281360] all task done
    2019-01-03 20:07:51.231414+0800 ThreadTest[4950:1281359] get OtherInfo start
    2019-01-03 20:07:53.234123+0800 ThreadTest[4950:1281359] get OtherInfo end
    2019-01-03 20:07:53.234130+0800 ThreadTest[4950:1281362] get OneInfo end
    2019-01-03 20:07:53.234489+0800 ThreadTest[4950:1281359] other info done
    2019-01-03 20:07:53.234491+0800 ThreadTest[4950:1281362] one info done
    

    以上结果很明显不是我们想要的,就是因为任务本身是异步执行的,任务组的任务很快就结束了,真正的任务并没有结束。这个时候,我们需要使用 enter 和 leave , enter 和 leave要成对出现。

    我们修改一下代码,让任务组可以执行预期的异步操作

        // 任务组
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_enter(group);
        dispatch_group_async(group, group_queue, ^{
            [self requestOneInfo:^{
                NSLog(@"one info done");
                dispatch_group_leave(group);
            }];
        });
        
        dispatch_group_enter(group);
        dispatch_group_async(group, group_queue, ^{
            [self requestOtherInfo:^{
                NSLog(@"other info done");
                dispatch_group_leave(group);
            }];
        });
        
        dispatch_group_notify(group, group_queue, ^{
            NSLog(@"all task done");
        });
    // 打印,符合预期
    2019-01-03 20:13:05.609618+0800 ThreadTest[4963:1290616] 当前主线程
    2019-01-03 20:13:05.609897+0800 ThreadTest[4963:1290656] get OneInfo start
    2019-01-03 20:13:05.609923+0800 ThreadTest[4963:1290654] get OtherInfo start
    2019-01-03 20:13:07.615273+0800 ThreadTest[4963:1290656] get OneInfo end
    2019-01-03 20:13:07.615284+0800 ThreadTest[4963:1290654] get OtherInfo end
    2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290654] other info done
    2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290656] one info done
    2019-01-03 20:13:07.615914+0800 ThreadTest[4963:1290654] all task done
    

    NSOpreation

    是GCD的一种封装,需要使用子类。

    任务队列:NSOpreationQueue 相当于一个线程池的概念,可以添加任务,设置最大并发数。

    任务有几种状态:ready,canceld,executing,finished,asynchronous

    任务可以很方便的添加依赖

    我们可以使用系统提供了两个子类创建NSOpreaion通过调用任务的start方法启动任务,会在当前的线程同步执行。

    如果我们需要异步执行,通过创建队列,把任务添加到队列即可。

        NSBlockOperation *op =  [NSBlockOperation blockOperationWithBlock:^{
          NSLog(@"任务执行了");
        }];
        [op start];// 在当前线程同步执行
        
       NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperation:op];// 异步执行
    

    如果任务需要设置依赖关系,调用任务的方法

    -(void*)addDependency:(NSOperation *)op;

    如果我们需要等待所有任务完成,调用队列的方法

    -(void*)waitUntilAllOperationsAreFinished;

    注意事项:

    线程之间共用进程所有资源,当多线程操作同一个变量的时候,可能会使得结果不正确。

    因此要特别注意线程安全的问题。

    通常保证线程安全有很多种方式

    • 使用线程锁
    • 使用串行队列
    • 使用线程安全的类
    • 使用信号量或runloop使异步看起来像同步在执行
    • 注意任务可能本身就是异步的

    相关文章

      网友评论

          本文标题:多线程使用要注意哪些事项

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