基础概念
线程与进程
- 进程:是指在系统中正在运⾏的⼀个应⽤程序,每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内。通过
活动监视器可以查看 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 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
线程的⽣命周期
线程调度与时间⽚
时间片的概念: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 ,也就是先进先出的数据结构,这也是由队列的性质决定的。
- 串行队列: 每次只有一个任务被执行,一个任务执行完成再执行下一个,一个接着一个的执行。其特点也就是
每个任务只在前一个任务完成后才开始。
- 并发队列:可以让多个任务同时执行,并发队列下也不会等待任务处理结束,其特点为
按照添加的顺序开始执行,以任意顺序完成。
在并发队列下,不论是同步还是异步,队列中的任务都是以先进先出的方式出队列。
队列的创建获取
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文件)
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_concurrent为true的时候,值为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,相当于有多条通道。队列中的任务可以并发执⾏。