多线程-GCD(1)(附带经典面试执行顺序求解)

一、进程和线程

1.1 什么是进程和线程

  • 进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程。程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

1.2 进程和线程的关系

  • 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

  •  所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的

  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

  • 根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位

1.3 线程调度

     一个 CPU 核心同一时刻只能执行一个线程。当线程数量超过 CPU 核心数量时,一个 CPU 核心往往就要处理多个线程,这个行为叫做线程调度。 就是 一个CPU 核心轮流让各个线程分别执行一段 时间,也就是说一个设备并发执行的线程数量是有限的。CPU在多个任务直接进行快速的切换,这 个时间间隔就是时间片。

二、多线程

2.1 线程的生命周期

2.2 多线程的优缺点

主线程是1M,子线程是512kb,开启一条线程大概也需要90微秒的时间。

2.2.1 优点:

  * 能适当提高程序的执行效率
  * 能适当提高资源的利用率(CPU,内存)
  * 线程上的任务执行完成后,线程会自动销毁

2.2.2 缺点

   * 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
   * 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
   * 线程越多,CPU 在调用线程上的开销就越大
   * 程序设计更加复杂,比如线程间的通信、多线程的数据共享

2.3 线程池

 2.4 通过一个代码简单的了解一下多线程:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@",[NSThread currentThread]);
    //输出设备能够支持线程的最大并发数量
    NSLog(@"%@",[NSProcessInfo processInfo].activeProcessorCount);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //主线程的number为1
    for (int i = 0; i < 200; i ++) {
        dispatch_async(dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL), ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
    }
}

我们从运行结果可以发现,主线程的number为1,多线程下不会一直开启新线程,也会从原来空闲的线程中拿已经有的线程。

三、GCD

全称是 Grand Central Dispatch,纯 C 语言,提供了非常多强大的函数。GCD 是苹果公司为多核的并行运算提出的解决方案。GCD 会自动利用更多的CPU内核(比如双核、四核)。GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

3.1 经典面试题

我们先了解一个概念:serial 串行 concurrent 并发

- (void)test1 {
    //每个打印都相当于是一个任务,任务执行的时间未知
    dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
//第一个执行的一定是1
//async是异步执行的 所以在异步执行函数内外的2和5 没有先后顺序
//sync是同步执行的 所以在同步执行函数内外的2和3 有先后顺序 先2后3
//async开启了线程,在新的线程里执行 所以内外顺序不一致
//sync没有开启线程 有先后顺序 所以234在同一个线程里 按照234执行
//1234(5不确定)
/这里需要注意创建队列是串行还是并行队列
- (void)test2 {
    //死锁
    //因为这个队列是FIFO的结构
    //async先执行,但是async需要等待sync先执行,这违反了FIFO的结构
    dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{//async可以解决这个问题
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
- (void)test6 {
    dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    dispatch_async(queue, ^{
        NSLog(@"6");
    });
}
//串行队列 2463
//代码的执行是从上往下,所以此时只需要看两个大的async即可(先不管包含的async),所以6在3的前面执行

3.2 同步异步和串行并发的搭配

同步+串行:可能死锁
同步+并发:不开启新线程,任务顺序执行
异步+串行:开启新线程,任务顺序执行
异步+并发:开启新线程,任务异步执行,没有顺序,与CPU调度有关

3.3 串行队列和并发队列的源码解析

- (void)test {
    //主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //全局并发队列
    dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
    //自己创建的串行队列
    dispatch_queue_t normalQueue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
}

我们找到关于dispatch_queue_create的api实现部分:

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
    return _dispatch_lane_create_with_target(label, attr,
            DISPATCH_TARGET_QUEUE_DEFAULT, true);
}
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
        dispatch_queue_t tq, bool legacy)
{
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);

我们可以看到是调用_dispatch_lane_create_with_target并添加2个默认参数实现的,找到对应实现。然后看到这个方法里面,把我们传入的DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE_CONCURRENT参数进行封装,封装成了dqai。我们可以大致看看封装的实现:

dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
    dispatch_queue_attr_info_t dqai = { };

    if (!dqa) return dqai;

#if DISPATCH_VARIANT_STATIC
    if (dqa == &_dispatch_queue_attr_concurrent) {
        dqai.dqai_concurrent = true;
        return dqai;
    }
#endif

    if (dqa < _dispatch_queue_attrs ||
            dqa >= &_dispatch_queue_attrs[DISPATCH_QUEUE_ATTR_COUNT]) {
#ifndef __APPLE__
        if (memcmp(dqa, &_dispatch_queue_attrs[0],
                sizeof(struct dispatch_queue_attr_s)) == 0) {
            dqa = (dispatch_queue_attr_t)&_dispatch_queue_attrs[0];
        } else
#endif // __APPLE__
        DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
    }

    size_t idx = (size_t)(dqa - _dispatch_queue_attrs);

    dqai.dqai_inactive = (idx % DISPATCH_QUEUE_ATTR_INACTIVE_COUNT);
    idx /= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT;

    dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT);
    idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;

    dqai.dqai_relpri = -(int)(idx % DISPATCH_QUEUE_ATTR_PRIO_COUNT);
    idx /= DISPATCH_QUEUE_ATTR_PRIO_COUNT;

这里我们可以看到dqai里面有个dqai_concurrent的属性,顾名思义是代表是否是并发,那么默认的就是串行。接下来我们继续看如何根据dqai创建队列:通过init方法初始化,第三个参数,如果是并发传入DISPATCH_QUEUE_WIDTH_MAX,如果是串行传入1

_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
static inline dispatch_queue_class_t
_dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf,
        uint16_t width, uint64_t initial_state_bits)
{
    uint64_t dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(width);
    dispatch_queue_t dq = dqu._dq;

    dispatch_assert((initial_state_bits & ~(DISPATCH_QUEUE_ROLE_MASK |
            DISPATCH_QUEUE_INACTIVE)) == 0);

    if (initial_state_bits & DISPATCH_QUEUE_INACTIVE) {
        dq->do_ref_cnt += 2; // rdar://8181908 see _dispatch_lane_resume
        if (dx_metatype(dq) == _DISPATCH_SOURCE_TYPE) {
            dq->do_ref_cnt++; // released when DSF_DELETED is set
        }
    }

    dq_state |= initial_state_bits;
    dq->do_next = DISPATCH_OBJECT_LISTLESS;
    dqf |= DQF_WIDTH(width);
    os_atomic_store2o(dq, dq_atomic_flags, dqf, relaxed);
    dq->dq_state = dq_state;
    dq->dq_serialnum =
            os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
    return dqu;
}

我们可以看到这里有一个DQF_WIDTH(width)的定义:

 

  • 串行队列 :它的DQF_WIDTH等于1,相当以它只有一条通道。所以队列中的任务要串行执行,也就 是一个一个的执行,必须等上一个任务执行完成之后才能开始下一个,而且一定是按照先进先出的 顺序执行的,比如串行队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行 a,并且等任务a完成之后,再执行b... 。
  • 并发队列:它的DQF_WIDTH大于1,相当于有多条通道。队列中的任务可以并发执行,也就任务可以同时执行,比如并发队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行 a,再执行b...,也是按照先进先出(FIFO, First-In-First-Out)的原则调用的,但是执行b的时候a不一定执行完成,而且a和b具体哪个先执行完成是不确定的。通道有很多,哪个任务先执行完得看 任务的复杂度,以及cpu的调度情况。
  • 队列的作用是用来存储任务。队列分类串行队列和并发队列。串行队列和并发队列都是 FIFO ,也就是先进先出的数据结构。

四 补充内容

线程和runloop的关系
1:runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
2:runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。
3:runloop在第一次获取时被创建,在线程结束时被销毁。
4:对于主线程来说,runloop在程序一启动就默认创建好了。
5:对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。
posted on 2022-06-03 10:57  suanningmeng98  阅读(239)  评论(0编辑  收藏  举报