GCD 是什么?

GCD,全称 Grand Central Dispatch,是异步履行使命的技能之一。一般将应用程序中记叙的线程办理用的代码在体系级中完成。开发者只需求界说想履行的使命并追加到恰当的 Dispatch Queue 中,GCD 就能生成必要的线程并计划履行使命。因为线程办理是作为体系的一部分来完成的,因而可统一办理,也可履行使命,这样就比以前的线程更有功率。

也便是,GCD 运用了很简洁的方法,完成了极为杂乱的多线程编程。

为什么要运用 GCD?

是因为之前用于多线程的方法太繁琐,写法不行简洁。

看一下在 GCD 之前的处理多线程的代码:

- (void)begin {
  // 建议使命履行
  [self performSelectorInBackground:@selector(asyncWork) withObject:nil];
}
- (void)asyncWork {
  // 耗时使命...
  // 履行完结后调用主线程处理成果
  [self performSelectorOnMainThread:@selector(asyncWorkDone) withObject:nil waitUntilDone:NO];
}
- (void)asyncWorkDone {
  // 只需在主线程能够履行的处理
  // 比方界面改写
}

而运用 GCD 只需求:

dispatch_async(queue, ^{
  // 耗时使命...
   
  dispatch_async(dispatch_get_main_queue(), ^{
    // 只需主线程才干履行的使命,如 UI 改写
  });
});

成果一望而知。

GCD 的 API

会提到的 API 有:

  • Dispatch Queue
  • dispatch_queue_create
  • Main Dispatch Queue / Global Dispatch Queue
  • dispatch_set_target_queue
  • dispatch_after
  • Dispatch Group
  • dispatch_barrier_async
  • dispatch_sync
  • dispatch_apply
  • dispatch_suspend / dispatch resume
  • Dispatch Semaphore
  • dispatch_once
  • Dispatch I/O

Dispatch Queue

开发者要做的仅仅界说想履行的使命并追加到恰当的 Dispatch Queue 中。

Dispatch Queue,便是履行处理的等候行列。这个行列依照先进先出的规则履行被追加到行列中的使命,就跟排队相同。

Dispatch Queue 分两种:

  • Serial Dispatch Queue:串行行列,需求等候当时履行处理完毕
  • Concurrent Dispatch Queue:并行行列,不需求等候当时履行处理完毕

啥意思呢?

举个栗子,某公司的厕所,只需一个坑位,有 A、B、C 三位搭档想摸鱼,A 先进厕所,然后 B,然后 C,只能顺次排队,这是串行行列。

dispatch_async(wc_serial, A);
dispatch_async(wc_serial, B);
dispatch_async(wc_serial, C);

因为串行行列能一起履行的履行数,也便是厕所的坑位只需 1 个,所以必定依照以下顺序履行:

A
B
C

另外一家公司的厕所,有三个坑位,这家公司也有 A、B、C 三位搭档,A、B、C 仍是按顺序进入了厕所,可是因为有三个坑位,所以他们能够一起开端摸鱼,B 不需求等 A 摸鱼完毕,C 也不需求等 B 摸鱼完毕。这是并行行列。

dispatch_async(wc_concurrent, A);
dispatch_async(wc_concurrent, B);
dispatch_async(wc_concurrent, C);

并行行列时,不需求等候当时履行完毕,A、B、C 仍是顺次添加进使命的,首先履行 A,可是 B 并不需求等 A 完毕,所以履行 A 后,也开端履行 B,C 也是同理。

并行处理的处理数量取决于当时体系的状况,即 iOS 和 OS X 基于 Dispatch Queue 中的处理数、CPU 核数以及 CPU 负荷等当时体系的状况来决议并行履行的处理数。所谓 “并行履行”,便是运用多个线程一起履行多个处理。

如何得到 Dispatch Queue

有两种方法:

  • dispatch_queue_create:自己创立一个
  • Main Dispatch QueueGlobal Dispatch Queue:获取体系为咱们供给的。

dispatch_queue_create

// 创立一个串行行列
dispatch_queue_t mySerialQueue =
    dispatch_queue_create("com.example.mySerialDispatchQueue", NULL);

需求留意的是,虽然串行和并行行列遭到体系资源的限制,但用 dispatch_queue_create 函数是能够生成恣意多个行列的,当生成多个串行行列时,各个串行行列将并行履行。

虽然在一个串行行列中只能一起处理一个使命,可是假如将使命分别加到 4 个串行行列中,每个串行行列都履行一个,即为一起处理 4 个使命。

能够生成恣意多个串行行列意思是,你想生成几个就生成几个,比方生成 2000 个行列,也行,那就会生成 2000 个线程,可是,过多运用多线程,会消耗很多内存,引起很多的上下文切换,大幅度下降体系的响应功用。

所以在运用串行行列的时分,必定要留意数量,不要很多的创立串行行列,并且一般只在多个线程更新相同资源导致数据竞赛的这种状况下运用串行行列。

关于并行行列来说,就没有这个忧虑,不管你生成多少个,体系都会帮你办理,只运用有用办理的线程,不会发生串行行列的那种问题。

// 创立并行行列
dispatch_queue_t myConcurrentQueue =
        dispatch_queue_create("com.example.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

Main Dispatch Queue / Global Dispatch Queue

这两个 Queue 是体系为咱们供给的。

Main Dispatch Queue 便是在主线程中履行的 Dispatch Queue,它是串行的。

追加到 Main Dispatch Queue 的处理会在主线程的 RunLoop 中履行.

另一个 Global Dispatch Queue 是一个全局的并行行列,它有四个优先级:

  • High Priority(高)
  • Default Priority(默认)
  • Low Priority(低)
  • Background Priority(后台)

不过这些优先级仅仅一个大致的判别,并不能保证实时性。

// main dispatch queue
dispatch_queue_t mainQueue =
  dispatch_get_main_queue();
// high global dispatch queue
dispatch_queue_t globalQueueHigh =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// default global dispatch queue
dispatch_queue_t globalQueueDefault =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// low global dispatch queue
dispatch_queue_t globalQueueLow =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
// background global dispatch queue
dispatch_queue_t globalQueueBackground =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

一个很常见的操作是:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  // 异步履行耗时使命
  // 回到主线程
  dispatch_async(dispatch_get_main_queue(), ^{
   // 改写 UI
  });
});

dispatch_set_target_queue

这是用于指定行列的优先级的,经过 dispatch_queue_create 创立的不管是串行行列仍是并行行列,都是运用的优先级,假如要改变优先级,就需求运用 dispatch_set_target_queue

dispatch_queue_t mySerialQueue =
  dispatch_queue_create("com.example.mySerialDispatchQueue", NULL);
dispatch_queue_t globalQueueBackground =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); 
dispatch_set_target_queue(mySerialQueue, globalQueueBackground);

dispatch_after

用于延迟履行一些使命。

// DISPATCH_TIME_NOW 指现在的时刻,`3 * NSEC_PER_SEC`,意思是距离现在时刻 3 秒后。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
  NSLog(@"waited at least three seconds.");
});

不过,这里并不是在指定时刻后就开端处理,而是在指定时刻后将使命追加到 Dispatch Queue 之中。

比方上述代码意思是 3 秒后将 NSLog 追加到主行列中,因为主行列是在主线程的 RunLoop 中履行的,所以在比方每隔 1/60 秒履行的 RunLoop 中,使命最快在 3 秒后履行,最慢在 3 秒 + 1/60 秒后履行,并且假如主行列有很多追加使命或主线程的处理本身有延迟时,这个时刻会更长。

Dispatch Group

这是用来处理咱们想要在多个行列中的使命悉数履行完结后,再做一个完毕的处理。一般是用于并行行列,因为串行行列的话,只需求将这些使命参加串行行列,然后在行列的最终追加咱们要处理的使命即可。

可是并行行列处理起这种状况就比较麻烦,所以 GCD 供给了 Dispatch Group 来处理这种状况。

举个栗子:

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"task0");});
dispatch_group_async(group, queue, ^{NSLog(@"task1");});
dispatch_group_async(group, queue, ^{NSLog(@"task2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});

task0task1task2 谁先输出是不确定的,可是 done 必定是在它们都输出完之后才会输出。

不管 queue 是不是同一个,只需你是同一个 group,不管你运用的 queue 是串行的仍是并行的,一个仍是多个,都能够保证在这些行列中的使命都履行完结,然后才会触发 dispatch_group_notify 的回调。

比方:

// 并行行列
dispatch_queue_t queue = 
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 串行行列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.mySerialQueue", NULL);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"task0");});
dispatch_group_async(group, serialQueue, ^{NSLog(@"task1");});
dispatch_group_async(group, queue, ^{NSLog(@"task2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});

done 都会是在最终输出的。

dispatch_group_notify 需求传入三个参数,第一个参数是要监听的 Dispatch Group,第二个参数需求传入一个行列,第三个参数是要履行的使命,在所监听的 Dispatch Group 中的悉数使命处理完毕后,将第三个参数所需求履行的使命,添加到第二个参数所指定的行列之中。

除了运用 dispatch_group_notify,还能够运用 dispatch_group_wait 来仅等候悉数处理履行完毕:

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"task0");});
dispatch_group_async(group, queue, ^{NSLog(@"task1");});
dispatch_group_async(group, queue, ^{NSLog(@"task2");});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

DISPATCH_TIME_FOREVER 意味着永久等候,只需 group 的处理还没完毕,就会一向等候。

dispatch_group_wait 是有返回值的,假如返回值为 0,阐明 group 中的处理已经悉数完毕,假如不为 0,阐明 group 还有某个使命正在履行中。当传入的时刻是 DISPATCH_TIME_FOREVER 时,返回值必定是 0,因为它会一向等候。假如传入的不是永久,而是其他的时刻,比方:

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"task0");});
dispatch_group_async(group, queue, ^{NSLog(@"task1");});
dispatch_group_async(group, queue, ^{NSLog(@"task2");});
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
  NSLog(@"all done");
} else {
  NSLog(@"working");
}

上述代码的意思是,等候 1 秒后,判别当时 group 中的使命是否已经悉数履行完毕,履行完毕输出 all done,否则输出 working

等候的意思是,在经过 dispatch_group_wait 中指定的时刻或 Dispatch Group 的处理悉数履行完毕之前,履行该函数的线程暂停。

dispatch_barrier_async

在拜访数据库或文件时,运用串行行列能够防止数据竞赛的问题。可是拜访数据库或文件,其实有两种操作,一个是写,一个是读,假如是读的话,那么运用并行履行是不会有问题的,因为它不会修改到数据源,只需写的时分,才需求保证只需一个人在写。

GCD 为咱们供给 dispatch_barrier_async 函数来处理这种状况。

dispatch_queue_t queue =
    dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, task0_for_reading);
dispatch_async(queue, task1_for_reading);
dispatch_barrier_async(queue, task_for_writing);
dispatch_async(queue, task2_for_reading);
dispatch_async(queue, task3_for_reading);

dispatch_barrier_async 函数会等候追加到并行行列上的并行履行处理悉数完毕后,再将指定的处理追加到该并行行列中,然后在由 dispatch_barrier_async 函数追加的处理履行完毕后,并行行列才恢复为一般的动作,追加到该并行行列的处理又开端并行履行。

Tip3 - 让我们搞定 GCD

dispatch_sync

一个经典的题目:在主线程履行下面的代码会有什么问题?

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{NSLog(@"Hello?");});

(一般我题都不必看全就说它死锁了。)

sync 意味着同步,dispatch_sync 是将指定的使命 “同步” 追加到指定的 Dispatch Queue 中,在追加使命完毕之前,dispatch_sync 函数会一向等候。

之前在说 dispatch_group_wait 函数的时分说到过,等候意味着当时线程停止。

死锁便是相互等候,dispatch_sync 在等候主行列的使命履行完毕,而主行列正在履行 dispatch_sync 这段代码,它需求比及 dispatch_sync 中的内容处理完毕才干完毕。这样互相等候,是没有成果的(突然有点悲伤?)。

主行列是一个串行行列,其实只需在串行行列去履行一个 sync 的操作,就会导致死锁。

dispatch_barrier_async 函数相应的也有 dispatch_barrier_sync,咱们在运用 sync 之类的函数时必定要小心,稍有不慎就会导致死锁。

dispatch_apply

dispatch_apply 函数是 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等候悉数处理履行完毕。

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
  NSLog(@"%zu", index);
});
NSLog(@"done");

index 的打印是不固定的,因为是在并行行列中履行的,可是 done 肯定是最终打印的。因为 dispatch_apply 会等候悉数处理履行完毕。

Block 带了一个参数,表示当时履行使命的下标。

这个能够用来对 NSArray 类目标的所有元素履行处理,比方:

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
  NSLog(@"%zu: %@", index, [array objectAtIndex: index]);
});

这样就能够简略的在行列中对所有的元素履行 Block。

因为 dispatch_applydispatch_sync 函数相同会等候处理履行完毕,所以引荐在 dispatch_async 函数中非同步的履行 dispatch_apply 函数。

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
  dispatch_apply([array count], queue, ^(size_t index) {
    NSLog(@"%zu: %@", index, [array objectAtIndex: index]);
  });
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"done");
  });
});

dispatch_suspend / dispatch resume

suspend 用于挂起行列,resume 用于恢复。

挂起行列,行列中尚未被履行的处理会被暂停,而恢复后则持续履行。

dispatch_suspend(queue);
dispatch_resume(queue);

Dispatch Semaphore

这是和信号量有关的,是用于多线程同步的一个东西,它会让你指定一个数,比方指定了 2,意思便是一起只能有两个并行履行的处理,假设有个厕所,你指定了坑位是 2,有 10 个人想蹲坑,可是因为坑位是 2,所以只能一起蹲两个人,两个坑都蹲了人的话,坑位是 0,但坑位是 0 时,不允许进人。一个人蹲完之后,坑位 +1,当坑位不为 0 时,能够再进人。直到所有的人都蹲完(老是举这种例子如同有点不雅)。

所以信号量便是这么个东西,它让你给个数,来一个履行,这个数减 1,减到 0 时,不能再来履行了,得候着。处理完一个履行,数 + 1,但这个数不为 0 时,就能够再进一个新的履行。它能够用来控制一起在履行的履行数。

了解了概念之后,咱们来看 GCD 中的信号量如何运用:

// 简单犯错的状况
dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
  dispatch_async(queue, ^{
    [array addObject:@(i)];
  });
}

这是不加办理的状况,一个厕所 100000 个人等着蹲,只需一个坑,还没人管,假如两个人一起来了,不好吧。

这个时分就能够用一下信号量:

dispatch_queue_t queue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 指定信号量为 1
dispatch_semaphore_t semphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
  dispatch_async(queue, ^{
    // 信号量 -1,因为指定的是 1,1-1=0,所以进入一个履行后,后续的履行需求等候
    dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
        // 履行
    [array addObject:@(i)];
    // 信号量 +1,0+1=1,不为0,后续的履行又能够进入
    dispatch_semaphore_signal(semphore);
  });
}

在没有 Serial Dispatch Queuedispatch_barrier_async 函数那么大粒度且一部分处理需求进行排他控制的状况下,Dispatch Semaphore 便可发挥威力。

dispatch_once

dispatch_once 很常用,once 便是只履行一次,它保障在应用程序履行中只履行一次指定处理的 API,写单例和运用 Method Swizzling 的时分都会用。

经过 dispatch_once 函数,即使在多线程的环境下履行,也能够保证百分百的安全。

Dispatch I/O

在读取较大文件时,假如将文件分红适宜的巨细并运用 Global Dispatch Queue 并排读取的话,会比一般的读取速度快不少。现在的输入 / 输出硬件已经能够做到一次运用多个线程更快的并排读取了。能完成这一功用的便是 Dispatch I/O 和 Dispatch Data。

dispatch_async (queue, ^{// 读取 0 ~ 8191 字节});
dispatch_async (queue, ^{// 读取 8192 ~ 16383 字节});
dispatch_async (queue, ^{// 读取 16384 ~ 24575 字节});
...

GCD 的完成

咱们能够自己完成多线程,可是功用不会有 GCD 的好,苹果的官方阐明是:

一般,应用程序中编写的线程办理用的代码要在体系级完成。

也便是说,GCD 是在体系级,也便是 iOS 和 OS X 的中心 XNU 内核级上完成的,不管咱们如何尽力的编写办理线程的代码,功用上也不会超过在内核级上完成的 GCD。

所以咱们应该尽量运用 GCD 或许封装了 GCD 的 Cocoa 结构中的 NSOperationQueue 类的 API。

这是 GCD 相关的 源码,都放在 libdispatch 库中,是用 C 言语写的。

Dispatch Queue 经过结构体和链表,被完成为 FIFO(先进先出)行列。FIFO 行列办理着经过 dispatch_async 等函数所追加的 Block(要履行的使命)。

Block 并不是直接参加到 FIFO 行列中,而是先参加 Dispatch Continuation 这一 dispatch_continuation_t 类型结构体中,然后再参加 FIFO 行列。这个 Dispatch Continuation 会记录 Block 所属的 Dispatch Group 和一些其他的信息,相当于一般常说的履行上下文。

Tip3 - 让我们搞定 GCD

也便是包了一层,添加了一些必要的信息,再参加到行列中,这种操作很常见。

Dispatch Queue 可经过 dispatch_set_target_queue 函数设定,能够设定履行该 Dispatch Queue 处理的 Dispatch Queue 为目标。该目标可像串珠子相同,设定多个衔接在一起的 Dispatch Queue,可是在衔接串的最终有必要设定为 Main Dispatch Queue,或各种优先级的 Global Dispatch Queue,或是预备用于 Serial Dispatch Queue 的各种优先级的 Global Dispatch Queue

Global Dispatch Queue 有如下 8 种:

  • Global Dispatch Queue(High Priority)
  • Global Dispatch Queue(Default Priority)
  • Global Dispatch Queue(Low Priority)
  • Global Dispatch Queue(Background Priority)
  • Global Dispatch Queue(High Overcommit Priority)
  • Global Dispatch Queue(Default Overcommit Priority)
  • Global Dispatch Queue(Low Overcommit Priority)
  • Global Dispatch Queue(Background Overcommit Priority)

Global Dispatch Queue 有两块,一共八种,一块是不带 Overcommit 的,一块是 Overcommit 的。

差异咱们等下说。

这 8 种 Global Dispatch Queue 各运用 1 个 pthread_workqueue。GCD 初始化时,运用 pthread_workqueue_create_up 函数生成 pthread_workqueue

pthread_workqueue 包含在 Libc 供给的 pthreads API 中。其运用 bsdthread_registerworkq_open 体系调用,在初始化 XNU 内核的 workqueue 之后获取 workqueue 信息。

XNU 内核持有 4 种 workqueue

  • WORKQUEUE_HIGH_PRIOQUEUE
  • WORKQUEUE_DEFAULT_PRIOQUEUE
  • WORKQUEUE_LOW_PRIOQUEUE
  • WORKQUEUE_BG_PRIOQUEUE

该履行优先级与 Global Dispatch Queue 的 4 种履行优先级相同。

它们的结构如下图:

Tip3 - 让我们搞定 GCD

OvercommitGlobal Dispatch Queue 是运用在 Serial Dispatch Queue (串行行列)中的,不带的是用于 Concurrent Dispatch Queue (并行行列)的。

咱们前面说过,运用 dispatch_queue_create 创立串行行列时,必定会发生一个新的线程,也便是带 Overcommit 优先级的 Queue,XNU 内核必定会给你搞个新的线程出来。可是并行行列的线程数,是受 XNU 内核控制的,它会依据体系状况那些,合理的运用线程数。

当在 Global Dispatch Queue 中履行 Block 时,libdispatchGlobal Dispatch Queue 本身的 FIFO 行列中取出 Dispatch Continuation,然后调用 pthread_workqueue_additem_np 函数(这个函数我在新版的源代码中已经搜不到了,或许换了名字,反正是一个添加的函数),将该 Global Dispatch Queue 本身,符合其优先级的 workqueue 信息以及为履行 Dispatch Continuation 的回调函数等传递给参数。

pthread_workqueue_additem_up 函数运用 workq_kernreturn 体系调用,告诉 workqueue 增加应当履行的项目。依据该告诉,XNU 内核基于体系状况判别是否要生成线程。当然,带 Overcommit 优先级的 Global Dispatch Queueworkqueue 是肯定会给你生成线程的。

workqueue 的线程履行 pthread_workqueue 函数,该函数调用 libdispatch 的回调函数,在该回调函数中履行参加到 Dispatch ContinuationBlock

Block 履行完毕后,进行告诉 Dispatch Group 完毕,释放 Dispatch Continuation 等处理,开端预备履行参加到 Global Dispatch Queue 中的下一个 Block

大约画了下流程:

Tip3 - 让我们搞定 GCD

因为 XNU 内核的参加,在编程人员自己办理的线程中,想发挥出 GCD 的功用是不或许的。

总结

最近重温了《Objective-C高档编程 iOS与OS X多线程和内存办理》(网上找的一个下载的链接),这是其中 GCD 的章节,结合自己的了解,就出现了这篇文章,经典好书,引荐咱们去读。