多线程(上)

172 阅读7分钟

线程和进程的概念

线程的概念

线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
进程要想执行任务,必须得有线程,进程至少要有一条线程。
程序启动会默认开启一条线程,这条线程被称为主线程或UI线程。

进程的概念

进程是指在系统中正在运行的一个应用程序。
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。
通过“活动监视器”可以查看 Mac 系统中所开启的进程。

线程和进程的关系

  1. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
  2. 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
  3. 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
  4. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  5. 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
  6. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  7. 根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位。

时间片

CPU在多个任务直接进行快速的切换,这个时间间隔就是时间片

单核CPU同一时间,CPU 只能处理 1 个线程,换言之,同一时间只有 1 个线程在执行。而多线程同时执行本质是CPU快速的在多个线程之间的切换,CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。

但如果线程数非常多,CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源,每个线程被调度的次数会降低,线程的执行效率降低。

多线程的优缺点

优点

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

缺点

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

线程的生命周期

image.png 当一个线程被创建时,它并不会立即执行,而是除了一个Runnable的状态,当start时,通过cpu调度当前线程,才使线程真正执行。当线程执行完任务后,它就会自己消亡,当出现一些同步锁或Sleep时,线程就会被阻塞,当cpu去调度其他线程时,当前线程又会回到Runnable状态。

线程池

image.png

线程池饱和策略

  • AbortPolicy 直接抛出RejectedExecutionExeception异常来阻止系统正常运行
  • CallerRunsPolicy 将任务回退到调用者
  • DisOldestPolicy 丢掉等待最久的任务
  • DisCardPolicy 直接丢弃任务

这四种拒绝策略均实现的RejectedExecutionHandler接口

GCD

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

队列和线程

队列是先进先出的(FIFO),并且有串行队列、并行队列、全局并发队列、主队列。队列是用来存储任务的,通过cpu调度将队列中的任务放到线程中去执行。

源码

dispatch_get_main_queue()

我们先查看dispatch_get_main_queue函数的实现。

dispatch_queue_main_t dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

dispatch_get_main_queue调用了DISPATCH_GLOBAL_OBJECT,但是经过搜索发现GCD源码中有多个DISPATCH_GLOBAL_OBJECT宏定义,我们并不能单纯地通过查看源码来定位到代码的走向。

但是我们在通过NSLog("%@", dispatch_get_main_queue())时,打印了如下的内容。

image.png 我们尝试在源码中搜索com.apple.main-thread,最终发现了有这一段代码。

struct dispatch_queue_static_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
    .do_targetq = _dispatch_get_default_queue(true),
#endif
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) | DISPATCH_QUEUE_ROLE_BASE_ANON,
    .dq_label = "com.apple.main-thread",
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
    .dq_serialnum = 1,
};

这段代码定义了了一个dispatch_queue_static_s结构体类型的值_dispatch_main_q,而_dispatch_main_q刚刚出现在了dispatch_get_main_queue源码中作为DISPATCH_GLOBAL_OBJECT的参数。

接下来我们先查看dispatch_queue_create函数的实现。

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_queue_create里调用了_dispatch_lane_create_with_target这个函数,第一个参数就是队列的名称,第二个参数就是队列的类型,第三、四个参数苹果写死了。那继续查看这个函数的实现。

static dispatch_queue_t _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);

    //
    // Step 1: Normalize arguments (qos, overcommit, tq)
    //

    // 规范化一些参数,省略

    //
    // Step 2: Initialize the queue
    //

    if (legacy) {
        // if any of these attributes is specified, use non legacy classes
        if (dqai.dqai_inactive || dqai.dqai_autorelease_frequency) {
                legacy = false;
        }
    }

    const void *vtable;
    dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
    if (dqai.dqai_concurrent) {
            vtable = DISPATCH_VTABLE(queue_concurrent);
    } else {
            vtable = DISPATCH_VTABLE(queue_serial);
    }
    switch (dqai.dqai_autorelease_frequency) {
    case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
            dqf |= DQF_AUTORELEASE_NEVER;
            break;
    case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
            dqf |= DQF_AUTORELEASE_ALWAYS;
            break;
    }
    if (label) {
        const char *tmp = _dispatch_strdup_if_mutable(label);
        if (tmp != label) {
                dqf |= DQF_LABEL_NEEDS_FREE;
                label = tmp;
        }
    }

    dispatch_lane_t dq = _dispatch_object_alloc(vtable,
                    sizeof(struct dispatch_lane_s));
    _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                    DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                    (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

    dq->dq_label = label;
    dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
                    dqai.dqai_relpri);
    if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
        dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    }
    if (!dqai.dqai_inactive) {
        _dispatch_queue_priority_inherit_from_target(dq, tq);
        _dispatch_lane_inherit_wlh_from_target(dq, tq);
    }
    _dispatch_retain(tq);
    dq->do_targetq = tq;
    _dispatch_object_debug(dq, "%s", __func__);
    return _dispatch_trace_queue_create(dq)._dq;
}

这个函数的核心就是传入的第二个函数:队列的类型,根据传入的类型来创建不同队列,所以我们重点查看第二个参数。在函数实现的首行, dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa); 我们就看到了该函数把传入的第二个参数dqa封装成了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
// 省略不关注的代码
    return dqai;
}

typedef struct dispatch_queue_attr_info_s {
    dispatch_qos_t dqai_qos : 8;
    int      dqai_relpri : 8;
    uint16_t dqai_overcommit:2;
    uint16_t dqai_autorelease_frequency:2;
    uint16_t dqai_concurrent:1;
    uint16_t dqai_inactive:1;
} dispatch_queue_attr_info_t;

我们可以看到,里面先是初始化了一个dispatch_queue_attr_info_t结构体类型的变量dqai,但是并没有赋值,如果传入的参数dqa为空,则直接返回dqai,而dqai中的dqai_concurrent成员变量默认为0,所以如果不传第二个参数,默认返回的是串行队列。如果传入的参数是_dispatch_queue_attr_concurrent,则返回的是并行队列。

现在接下去看_dispatch_lane_create_with_target函数中Step 2的代码,其中有一个_dispatch_queue_init函数调用。

 _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                    DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                    (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

第三个参数为是否为concurrent,如果是值为DISPATCH_QUEUE_WIDTH_MAX,否则为1,那我们看一下这个宏的定义。

#define DISPATCH_QUEUE_WIDTH_FULL			0x1000ull
#define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)
#define DISPATCH_QUEUE_WIDTH_MAX  (DISPATCH_QUEUE_WIDTH_FULL - 2)

那么,如果是串行队列的话,第三个参数就为1,如果是并行队列,第三个参数就为8。那接下来就看看_dispatch_queue_init里面做了什么事情。

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

我们继续关注第三个参数width,那么理所当然就注意到这一行dqf |= DQF_WIDTH(width);。当串行队列时,这行代码就是dqf |= DQF_WIDTH(1);,这就与_dispatch_main_q .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),调用的函数和参数是一样的,这也说明了主队列也是一个串行队列。

面试题

dispatch_queue_t queue = dispatch_queue_create("queue_name", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

执行顺序为1-5-2-3-4。首先1肯定是先打印的,然后在queue中异步添加任务,然后再把5放到主线程的队列中。2和5的执行顺序不一定,要看cpu的调度情况。在2之后,又往queue中同步添加了3,因为是同步的,所以4一定会等3先执行完再执行。

dispatch_queue_t queue = dispatch_queue_create("queue_name", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

执行顺序为1-5-2然后死锁。在执行到3的时候,由于是串行队列,并且在queue中将3同步添加到queue,此时3在等4执行,4也在等3执行,造成了死锁。

dispatch_queue_t queue = dispatch_queue_create("queue_name", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"5");
});

执行的顺序为2-4-5-3。第一个dispatch_asyn先加到queue中,所以2先执行,4和2在同一个线程,所以4之后执行,5和3中间,5先被加入到queue中,所以不管5之前sleep了多久,都是5先执行,然后再执行3。