GCD基础

149 阅读12分钟

基础概念

线程与进程

  • 进程:是指在系统中正在运⾏的⼀个应⽤程序,每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内。通过活动监视器可以查看 Mac 系统中所开启的进程。
  • 线程:是进程的基本执⾏单元,⼀个进程的所有任务都在线程中执⾏,进程要想执⾏任务,必须得有线程,进程⾄少要有⼀条线程。程序启动会默认开启⼀条线程,这条线程被称为主线程UI线程

线程进程参数获取

  • 获取进程信息
// 创建系统进程信息对象
    NSProcessInfo *processInfo = [NSProcessInfo processInfo];
     
    // 返回当前进程的参数
    /*
    以 NSString 对象数组的形式返回当前进程的参数
    */
    NSArray *processArguments = [processInfo arguments];
     
    // 返回当前的环境变量
    NSDictionary *processEnvironment = [processInfo environment];
    
    // 返回进程标识符
    int processId = [processInfo processIdentifier];
     
    // 返回进程数量
    NSUInteger processCount = [processInfo processorCount];
     
    // 获取活动的处理器数量
    NSUInteger activeProcessCount = [processInfo activeProcessorCount];
     
    // 返回正在执行的进程名称
    NSString *processName = [processInfo processName];
     
    // 生成单值临时文件名
    /*
    每次调用这个方法时,都返回不同的单值字符串,可以用这个字符串生成单值临时文件名
    */
    NSString *uniqueString = [processInfo globallyUniqueString];
     
    // 返回主机系统的名称
    NSString *hostName = [processInfo hostName];
     
    // 返回操作系统的版本号
    NSOperatingSystemVersion osVerson = [processInfo operatingSystemVersion];
     
    // 返回操作系统名称
    NSString *osName = [processInfo operatingSystemVersionString];
     
    // 返回系统运行时间
    NSTimeInterval timeInterval = [processInfo systemUptime];
  • 获取线程信息
    //线程名称
    [NSThread currentThread].name;
    //线程优先级
    [NSThread currentThread].threadPriority;
    //线程是否正在执行
    [NSThread currentThread].executing;
    //线程是否结束
    [NSThread currentThread].finished;
    //线程是否被取消
    [NSThread currentThread].cancelled;
    //线程是否是主线程
    [NSThread currentThread].isMainThread;

进程与线程的关系

  • 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

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

  • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间

  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

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

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

多线程的意义

优点:

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU,内存)
  • 线程上的任务执行完成后,线程会自动销毁 缺点
  • 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU 在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

线程的⽣命周期

image.png

线程调度与时间⽚

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

  • (单核CPU)同一时间,CPU 只能处理 1 个线程
    • 换言之,同一时间只有 1 个线程在执行
  • 多线程同时执行:
    • 是 CPU 快速的在多个线程之间的切换
    • CPU 调度线程的时间足够快,就造成了多线程的同时执行的效果
  • 如果线程数非常多
    • CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源
    • 每个线程被调度的次数会降低,线程的执行效率降低

线程调度:⼀个CPU核⼼同⼀时刻只能执⾏⼀个线程。当线程数量超过 CPU 核⼼数量时,⼀个 CPU 核⼼往往就要处理多个线程,这个⾏为叫做线程调度

线程池

线程池是一种线程使用模式。 线程过多会带来调度开销,进而影响缓存局部性和整体性能。 而线程池维护着多个线程,等待着管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。

使用线程池有哪些优势

  • 线程和任务分离,提升线程重用性;
  • 控制线程并发数量,降低设备压力,统一管理所有线程;
  • 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

什么是GCD

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。纯 C 语言,提供了非常多强大的函数。

GCD的优势

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

GCD的中的队列

队列的作⽤是⽤来存储任务(任务就是要进行的操作,下文中会具体说明)。队列分类串⾏队列并发队列串⾏队列和并发队列都是 FIFO ,也就是先进先出的数据结构,这也是由队列的性质决定的。

  • 串行队列: 每次只有一个任务被执行,一个任务执行完成再执行下一个,一个接着一个的执行。其特点也就是每个任务只在前一个任务完成后才开始

image.png

  • 并发队列:可以让多个任务同时执行,并发队列下也不会等待任务处理结束,其特点为按照添加的顺序开始执行,以任意顺序完成

image.png 在并发队列下,不论是同步还是异步,队列中的任务都是以先进先出的方式出队列。

队列的创建获取

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

使用dispatch_queue_create方法创建队列Dispatch Queue

  • 参数1:传入队列名称
  • 参数2:传入队列类型,值一般为DISPATCH_QUEUE_CONCURRENT并行队列、DISPATCH_QUEUE_SERIAL串行队列
#define DISPATCH_QUEUE_CONCURRENT \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
_dispatch_queue_attr_concurrent)

#define DISPATCH_QUEUE_SERIAL NULL

获取队列: 获取到的主队列是串行队列,新添加的任务会被追加到主队列中。

//主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

获取全局并发队列

//获取全局并发队列的方法
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
  • 参数1:priority优先级,如下方代码,分四种优先级。
  • 参数2:保留字段,值一般为0
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 //高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 //默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) //低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN  //优先级最低

任务的创建

任务分为同步任务和异步任务,GCD提供了同步执行任务的创建方法dispatch_sync和异步执行任务的创建方法dispatch_async

同步执行任务:  等待预定任务完成后才返回。
异步执行任务:  调用操作后立即返回,预定的任务会完成但是不会等它完成。

//同步执行任务创建方法
dispatch_sync(queue, ^{
       // 这里放同步执行任务代码 
 });

//异步执行任务创建方法
dispatch_async(queue, ^{
      // 这里放异步执行任务代码
});

至此根据任务创建的不同和队列的不同,可以产生一下几种组合方式

区别并发队列串行队列
同步sync没有开辟新线程,串行执行任务没有开辟新线程,串行执行任务
异步async有开辟新线程,并发执行任务有开辟新线程,串行执行任务
  • 同步执行 + 并发队列 (在当前线程中串行执行任务,这一过程不会等待任务结束)
  • 异步执行 + 并发队列 (开辟新线程,并发执行任务,这一过程不会等待任务结束)
  • 同步执行 + 串行队列 (在当前线程中串行执行任务,新创建的串行队列不会死锁)
  • 异步执行 + 串行队列 (开辟新线程,串行执行任务,新线程一个一个的执行)

在日常开发当中,我们还经常用到全局并发队列与主队列,全局并发队列可以当做普通并发队列来使用,但是主队列是串行队列,有些特殊,在遇到同步任务与异步任务时又有以下情况

区别主队列
同步sync发生死锁
异步async不会开辟线程,串行执行任务
  • 同步 + 主队列 (在这里主队列中的任务是在主线程中进行的,当有新的同步执行的任务时,新任务会被追加到主队列中,同时,主线程(当前任务)会等待新添加的任务执行结束,两者相互等待,阻塞了主线程,导致死锁。)
  • 异步 + 主队列 (新任务也是会被追加到主队列中,但是由于是异步(当前任务不等待新添加任务执行结束),调用后就返回,一个一个去执行)。

面试题

根据以上分析,可以看下以下面试题

异步并发套同步任务

    dispatch_queue_t queue = dispatch_queue_create("fm", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
  • 主程序先自上而下运行,先执行打印1
  • 然后异步任务async({...})中的所有内容会被添加到并发队列queue中,由于是异步执行 + 并发队列,所以此时会开辟新线程,由新线程与执行async函数里边的内容。
  • 主线程此时打印5,但同时新线程此时先打印2,然后又把sync({...})中的内容添加到并发队列queue中,变成同步执行+串行队列,也就是在当前线程串行执行任务,打印3,然后打印4 至于说主线程打印的5与新线程的打印的内容谁前谁后,这都是不一定的。

同步并发套同步任务

 dispatch_queue_t queue = dispatch_queue_create("fm", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
  • 主程序先自上而下运行,先执行打印1
  • 然后异步任务async({...})中的所有内容会被添加到串行队列queue中,也就是异步执行 + 串行队列,所以此时会开辟新线程,由新线程去执行async函数里边的内容,但这一过程的每个任务不会等待结束。
  • 主线程此时打印5,但同时新线程此时先打印2,然后又把sync({...})中的内容添加到串行队列queue中,变成同步执行+串行队列,也就是在当前线程串行执行任务,由于同是一个队列queue,需要满足先进先出,此时在新线程中执行的async任务,就等待sync任务结束,sync任务又在等待async任务执行完毕后从队列中出来,造成了死锁。

源码中对于串行、并行的定义

在这里我们先看下并发队列和串行队列传入参数的区别:

并发队列

#define DISPATCH_QUEUE_CONCURRENT \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
_dispatch_queue_attr_concurrent)

串行队列

#define DISPATCH_QUEUE_SERIAL NULL

串行队列传入的值比较简单,直接是NULL。 而并发队列中DISPATCH_GLOBAL_OBJECT是一个宏函数,传入参数为dispatch_queue_attr_t_dispatch_queue_attr_concurrent 因此这里展开后就变成了:

dispatch_queue_create("fm", DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t,_dispatch_queue_attr_concurrent));
dispatch_queue_create("fm", (( __bridge dispatch_queue_attr_t)&(_dispatch_queue_attr_concurrent)));

也就是说DISPATCH_QUEUE_CONCURRENT参数传入的本质为_dispatch_queue_attr_concurrent的地址,只不过这里做了下桥接,封装成了对象。我们再看下队列创建。

我们在创建队列的时候一般通过dispatch_queue_create函数创建,如果在libdispatch源码中搜索,就可以找到其定义的地方。(搜索dispatch_queue_create(会发现很多地方,但队列的定义一般会放到queue文件中,这里也就是queue.c文件) image.png

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函数实现如下:

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

	dispatch_qos_t qos = dqai.dqai_qos;
#if !HAVE_PTHREAD_WORKQUEUE_QOS
	if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
		dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
	}
	if (qos == DISPATCH_QOS_MAINTENANCE) {
		dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
	}
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS

	...省略茫茫多代码...
	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_init,这里通过 dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1来设置是串行队列还是并行队列。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)

也就是说当dqai.dqai_concurrenttrue的时候,值为0x1000ull - 2,也就是8-2为6,当false的时候值为1.

而在_dispatch_queue_init函数内部又通过dqf |= DQF_WIDTH(width);来对dqf进行赋值来标记队列的并发数。

#define DQF_WIDTH(n)          ((dispatch_queue_flags_t)(uint16_t)(n))
dqf |= DQF_WIDTH(width);

那么dqai.dqai_concurrent又是何时赋值的呢?,回到该函数开头,看到改函数获取dqai的时候调用了_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;
	}
...省略茫茫多代码...
}

_dispatch_queue_attr_to_info函数中,我们看到有个dqa == &_dispatch_queue_attr_concurrent判断,在前文中我们也提到,DISPATCH_QUEUE_CONCURRENT参数的本质其实也就是_dispatch_queue_attr_concurrent的地址,因此,dqai_concurrent也就是在此处进行赋值操作。

总结来看:

  • 串⾏队列:它的DQF_WIDTH等于1,相当以它只有⼀条通道,任务只能一个一个的来。
  • 并发队列 :它的DQF_WIDTH⼤于1,相当于有多条通道。队列中的任务可以并发执⾏。