多线程-GCD(一)

600 阅读11分钟

详细用法可以参考:GCD总结

一、GCD概念

1、什么是GCD

GCD全称是GrandCentral Central Dispatch(dispatch的意思是分发调度的意思),是用纯C语言,并且提供了非常多的强大的函数

GCD优势:

  • 是苹果公司为多核的并行运算提出的解决方案
  • 会自动利用更多的CPU内核,例如双核、四核
  • 会自动管理线程的生命周期,例如创建线程、调度任务、销毁线程、程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程代码

2、任务和队列

任务是使用block封装的,并且任务的block没有任何参数也没有返回值,执行任务的函数有了两个函数,童虎函数和异步函数

a、同步函数和异步函数

  • 同步函数:dispatch_sync,必须等待当前语句执行完毕,才会执行下一条语句,并且不会开启线程
  • 异步函数:dispatch_async,异步其实是多线程的代名词,会开启线程执行block的任务,不用等待当前语句执行完毕,就可以执行下一条语句

b、串行队列和并发队列

这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

在 GCD 中有两种队列:『串行队列』 和 『并发队列』。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue):

每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)

  • 并发队列(Concurrent Dispatch Queue):

可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

c、队列和任务的使用

  • 同步函数和串行队列:不会开启线程,在当前线程中执行任务,任务一个接一个的串行执行,会产生堵塞
  • 同步函数和并发队列:不会开启线程,在当前线程中执行任务,任务一个接一个的串行执行,会产生堵塞
  • 异步函数和串行队列:开启一个新线程,在新的线程中任务一个接一个执行
  • 异步函数和并发队列:开启线程,任务在新的线程中执行

苹果系统提供了全局的同步队列和全局的异步队列

  • 主队列:dispatch_get_main_queue()

    • 专门用来在主线程上调度任务的队列
    • 不会开启新线程
    • 如果当前主线程上有正在执行的任务,那么无论主队列中当前被添加了什么任务,都不会被调度
  • 全局队列:dispatch_get_global_queue(0,0)

    • 为了方便程序员的使用,苹果提供了全局队列
    • 是一个并发队列
    • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以使用全局队列,因为系统也会用到该并发队列,所以不能再该队列上使用栅栏任务

3、死锁

  • 主线程因为同步函数的原因等着先执行任务
  • 并且主队列等着主线程的任务执行完毕后再执行自己的任务,
  • 这样主线程就和主队列因为互相等待造成死锁

二、面试题

1、面试题一:

下面代码的打印是什么

- (void)textDemo{
    //创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    //任务一
    dispatch_async(queue, ^{
        NSLog(@"2");
        //任务二
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

答案是:15243

解释:

  • 因为任务一是异步的,并且任务一和任务二是加到自己创建的队列中的,跟目前执行的方法不再同一队列,5不会瞪大执行到2再执行,所以1过完5,5过完才是2
  • 2执行完了有把任务二加入到队列中,任务二是异步任务,所以4不会等到3执行后再执行
  • 所以最后打印是15243

2、面试题二

下面代码的输出是下面ABCD哪一项 // A: 1230789 // B: 1237890 // C: 3120798 // D: 2137890

- (void)wbinterDemo{
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // sleep(2);
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    // 堵塞 - 护犊子
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    // **********************
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
}

答案:AC

解释:

具体来说应该是3在0前面,0在789前面,12789几个的顺序不确定,我们把打印的数字当做任务几来解释哈!

  • 任务1、2加入到并发队列中就开辟线程执行去了,因为不知道任务的执行时间,所以无法判断1、2的打印时机,
  • 任务3通过同步的方式加入到队列中,就会在当前线程中直接执行,所以3在0前面
  • 0打印完后才把任务7、8、9异步方式加到并发队列中,所以7、8、9在0后面,并且打印顺序不确定

3、面试题三

关于串行队列的面试题,下面打印输出是啥

- (void)textDemo2{
    // 同步队列
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 异步函数-任务一
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步函数-任务二
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

答案:152---崩了

解释:

  • 1打印完后会将任务1通过异步方式加到同步队列中,因为此时queue没有任何任务,所以系统会直接开辟一个线程去执行任务一
  • 然后主线程往下执行,打印5,但是假如任务一执行够快,2可能在5前面执行,但是此题中因为打印2之前需要创建线程去执行任务一,所以5肯定在2前面打印
  • 执行任务一时候,打印完2后又将同步任务加到同步队列中,因为是同步任务,所以会阻塞当前线程,任务一需要将任务二执行完才能往下走,但是同步队列中任务一在任务二前面,队列遵循先进先出原则,所以任务二需要等到任务一执行完了,才能执行。这样就形成了经典的死锁

注意:

因为死锁是因为线程和队列互相等到导致的,所以破除的方法有两个:

  • 在别的线程中给同步队列添加同步任务
  • 在别的队列中给同步队列添加同步任务

因为开线程只能在别的队列中开辟,所有,解决方法就是在别的队列中给同步队列添加同步任务

4、面试题4

下面打印是多少:

void testMethod(){
    sleep(3);
}

- (void)wbinterDemo2{
    CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
    dispatch_queue_t queue = dispatch_queue_create("com.lgcooci.cn", DISPATCH_QUEUE_SERIAL);
    //任务一
    dispatch_async(queue, ^{
        testMethod();
    });
    //任务二
    dispatch_sync(queue, ^{
        testMethod();
    });
    //任务三
    testMethod();
        
    NSLog(@"%f",CFAbsoluteTimeGetCurrent()-time);
}

答案:9秒多一点点

解释:

  • 因为是同步队列,并且当前在主线程中执行的,所以任务一会在主线程中执行,会阻塞主线程3秒
  • 任务二是通过同步方式往同步队列中添加任务,但是因为目前执行的任务是在主队列中,而添加的同步队列是自己创建的,所以不会造成死锁
  • 因为是同步队列,所以任务二会在当前主线程中执行,造成堵塞
  • 所以打印的是9秒多一点,多一点那时间是执行任务的时间

5、面试题五

下面打印顺序:

- (void)wbinterDemo3{
    
    dispatch_queue_t queue1 = dispatch_queue_create("com.lgcooci2.cn", DISPATCH_QUEUE_CONCURRENT);
    //任务一
    dispatch_async(queue1, ^{
        NSLog(@"1");
    });
    //任务二
    dispatch_async(queue1, ^{
        NSLog(@"2");
    });
    //任务三
    dispatch_barrier_async(queue1, ^{
        NSLog(@"3");
    });
    //任务四
    dispatch_async(queue1, ^{
        NSLog(@"4");
    });
     NSLog(@"5");
}

答案:3在1、2后面,在4前面,5的位置不定

解释:

  • 任务一盒任务二通过异步方式加入到并发队列中,所以他们的执行完的先后顺序不确定
  • 任务3是以异步栅栏形式加入到并发队列中的,栅栏函数的作用就是堵塞队列,,让栅栏任务前的所有任务都执行完,然后再执行栅栏任务,栅栏任务执行完成后再执行后面的任务
  • 但是栅栏任务不堵塞线程,代码会继续往下执行,但是打印5的效率不确定是否比打印其他的快,所以不确定5在哪个位置

三、GCD源码解析

1、dispatch_queue_create

我们先通过汇编形式来找出dispatch_queue_create是在哪个库,然后去对应库找到相应的源码

运行,我们进入到

然后我们在dispatch_queue_create这个地方加个断点,然后通过control+in进入到该函数汇编流程

但是我们还是没找到dispatch_queue_create对应的库

这个时候我们就需要用符号断点来查找了,我们加一个dispatch_queue_create的符号断点

这个时候我们就会发现dispatch_queue_create就是在libdispatch.dylib库里面

我们从苹果官网上下载libdispatch的源码,在里面直接搜dispatch_queue_cteate的定义因为这样搜效率更快

dispatch_queue_create(const char *_Nullable label,
		dispatch_queue_attr_t _Nullable attr);

然后找到_dispatch_lane_create_with_target函数

我们创建队列分串行队列和并发队列,而这两种都是通过参数dispatch_queue_attr_t来区分创建的,并且我们发现在_dispatch_lane_create_with_target函数里面只有第一行用到dispatch_queue_attr_t参数的

所以我们具体分析下第一行代码,进入到_dispatch_queue_attr_to_info

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]) {
		DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
	}

	// 苹果的算法
	size_t idx = (size_t)(dqa - _dispatch_queue_attrs);

	// 位域赋值
	// 0000 000000000 00000000000 0000 000  1
	
	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 = -(idx % DISPATCH_QUEUE_ATTR_PRIO_COUNT);
	idx /= DISPATCH_QUEUE_ATTR_PRIO_COUNT;
	
	dqai.dqai_qos = idx % DISPATCH_QUEUE_ATTR_QOS_COUNT;
	idx /= DISPATCH_QUEUE_ATTR_QOS_COUNT;

	dqai.dqai_autorelease_frequency =
			idx % DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;
	idx /= DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;

	dqai.dqai_overcommit = idx % DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;
	idx /= DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;

	return dqai;
}

我们再看看dispatch_queue_attr_info_t结构

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;

我们发现:

  • 当dqa为空时候会返回一个空的结构体dqai,而创建串行队列时候参数dqa是NULL,所以我们知道_dispatch_queue_attr_to_info这个函数的作用是对并发队列的一个属性赋值的左右
  • dispatch_queue_attr_info_t是个结构体,对于这个结构体我们想到的是通过位域来赋值

然后我们找到创建队列的代码

因为构造方法是_dispatch_object_alloc,我们看下_dispatch_object是个什么样的东西,通过搜索发现

/*!
 * @typedef dispatch_object_t
 *
 * @abstract
 * Abstract base type for all dispatch objects.
 * The details of the type definition are language-specific.
 *
 * @discussion
 * Dispatch objects are reference counted via calls to dispatch_retain() and
 * dispatch_release().
 */

说明dispatch_object_t是一个抽象的类 然后我们找到

/*
 * Dispatch objects are NOT C++ objects. Nevertheless, we can at least keep C++
 * aware of type compatibility.
 */
typedef struct dispatch_object_s {
private:
	dispatch_object_s();
	~dispatch_object_s();
	dispatch_object_s(const dispatch_object_s &);
	void operator=(const dispatch_object_s &);
} *dispatch_object_t;

typedef union {
	struct _os_object_s *_os_obj;
	struct dispatch_object_s *_do;
	struct dispatch_queue_s *_dq;
	struct dispatch_queue_attr_s *_dqa;
	struct dispatch_group_s *_dg;
	struct dispatch_source_s *_ds;
	struct dispatch_mach_s *_dm;
	struct dispatch_mach_msg_s *_dmsg;
	struct dispatch_semaphore_s *_dsema;
	struct dispatch_data_s *_ddata;
	struct dispatch_io_s *_dchannel;
} dispatch_object_t DISPATCH_TRANSPARENT_UNION;

我们发现dispatch_object_t是一个联合体,可以很简单的保存各种值,因为互斥的原因,这样写可以简单的实现多态的特性,而且可以限制这个结构的多方面变化,并且这样写节省很多内存

我们先看一下队列的两种创建方式

    // 同步队列
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    //并发队列
    dispatch_queue_t queue1 = dispatch_queue_create("cooci1", DISPATCH_QUEUE_SERIAL);

因为NSLog只能打印队列的内存地址,而不能打印队列的结构,所以我们通过lldb打印出两种队列的结构体数据,

再往下走我们看到初始化函数_dispatch_queue_init

	// 构造方法
	/*
	 在方法_dispatch_queue_attr_to_info里面,
	 如果是串行队列dqai.dqai_concurrent是空
	 如果是并发队列dqai.dqai_concurrent有值
	 */
	_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
			DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
			(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

看一下参数DISPATCH_QUEUE_WIDTH_MAX

#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)

对应上面队列的结构,

  • 同步队列:width = 0x1
  • 并发队列:width = 0xffe 看一下参数DISPATCH_QUEUE_WIDTH_MAX = 0x1000 - 2 = 0xffe,这个参数就是最大并发数,为啥减2呢?
  • 并发数不能写满,要留点预留
  • 减1是给dispatch_queue_global_s用的

我们再看一下队列里面target的值

在创建时候 dq->do_targetq = tq;我们找一下tq

if (!tq) {
		tq = _dispatch_get_root_queue(
				qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 4
				overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq; // 0 1
		if (unlikely(!tq)) {
			DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
		}
	}

因为

#define DISPATCH_QOS_UNSPECIFIED        ((dispatch_qos_t)0)
#define DISPATCH_QOS_DEFAULT            ((dispatch_qos_t)4)

所以qos = 4,串行队列overcommit等于0,并发队列overcommit等于1

我们看一下_dispatch_get_root_queue汗湿

static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
	if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
		DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
	}
	// 4-1= 3
	// 2*3+0/1 = 6/7
	return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

因为qos = 4,overcommit等于0或者1

所以返回的是_dispatch_root_queues数组里面的第六或者第七个元素,我们再找下_dispatch_root_queues

struct dispatch_queue_global_s _dispatch_root_queues[] = {
#define _DISPATCH_ROOT_QUEUE_IDX(n, flags) \
		((flags & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) ? \
		DISPATCH_ROOT_QUEUE_IDX_##n##_QOS_OVERCOMMIT : \
		DISPATCH_ROOT_QUEUE_IDX_##n##_QOS)
#define _DISPATCH_ROOT_QUEUE_ENTRY(n, flags, ...) \
	[_DISPATCH_ROOT_QUEUE_IDX(n, flags)] = { \
		DISPATCH_GLOBAL_OBJECT_HEADER(queue_global), \
		.dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE, \
		.do_ctxt = _dispatch_root_queue_ctxt(_DISPATCH_ROOT_QUEUE_IDX(n, flags)), \
		.dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL), \
		.dq_priority = flags | ((flags & DISPATCH_PRIORITY_FLAG_FALLBACK) ? \
				_dispatch_priority_make_fallback(DISPATCH_QOS_##n) : \
				_dispatch_priority_make(DISPATCH_QOS_##n, 0)), \
		__VA_ARGS__ \
	}
	_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
		.dq_label = "com.apple.root.maintenance-qos",
		.dq_serialnum = 4,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.maintenance-qos.overcommit",
		.dq_serialnum = 5,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
		.dq_label = "com.apple.root.background-qos",
		.dq_serialnum = 6,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.background-qos.overcommit",
		.dq_serialnum = 7,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
		.dq_label = "com.apple.root.utility-qos",
		.dq_serialnum = 8,
	),
	//第六个元素
	_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.utility-qos.overcommit",
		.dq_serialnum = 9,
	),
	//第七个元素
	_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
		.dq_label = "com.apple.root.default-qos",
		.dq_serialnum = 10,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
			DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.default-qos.overcommit",
		.dq_serialnum = 11,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
		.dq_label = "com.apple.root.user-initiated-qos",
		.dq_serialnum = 12,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.user-initiated-qos.overcommit",
		.dq_serialnum = 13,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
		.dq_label = "com.apple.root.user-interactive-qos",
		.dq_serialnum = 14,
	),
	_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
		.dq_label = "com.apple.root.user-interactive-qos.overcommit",
		.dq_serialnum = 15,
	),
};

这就是为啥串行队列是com.apple.root.utility-qos.overcommit,并发队列是com.apple.root.default-qos

_dispatch_root_queues是dispatch_init时候创建的