基本定义
在早期只有单核的时候其实没有真正意义上的多线程,因为是CPU在时间片里不停地切换线程执行任务,时间片是非常小的以及CPU的调度能力让人觉得多线程的一种错觉,现在手机都是多核,存在真正的多线程。感谢wszcug指正。
进程和线程
**进程:**进程是在系统中正在运行的一个应用程序,每个进程都是独立且在其运行时使用专用保护的内存。
**线程:**线程是进程的基本单元,也是调度器的基本单元,一个进程所有的任务都在线程中执行,所以程序会在启动时默认创建一条线程,被称为主线程或UI线程。
进程和线程的关系
-
共享地址空间
-
共享资源
-
进程崩溃不会影响其他进程,线程奔溃会引起进程崩溃
-
在资源大的时候进程切消耗资源大而线程小
-
每个进程都有一个程序入口、执行序列,而线程是依赖于进程所以不能单独运行
多线程的意义
**优点:**可以适当提高程序的运行效率以及资源的利用率,线程执行完毕后会在短暂的停留后自动销毁。
**缺点:**开辟线程都需要消耗一定的内存(默认512kb),使用大量的线程会消耗大量的内存以及降低程序的运行效率、性能,好会增大CPU在调度线程上的开销。
多线程和Runloop的关系
-
Runloop和线程是一一对应的关系,一个Runloop对应一个核心的线程,虽然Runloop可以嵌套,但是核心的只有一个,以全局字典的关系保存着
-
Runloop管理线程。当线程的Runloop开启后,线程会开始执行任务并在结束后进入休眠状态等待新的任务。
-
Runloop在线程第一次获取时创建,在线程结束时被销毁。对于主线程来说,与其对应的Runloop在程序已启动的时候就默认创建好了;对于子线程来说Runloop是懒加载的,只有使用的时候才会进行创建,所以特别注意子线程的Runloop是否被创建。
多线程方式
| 线程 | 简介 | 语言 | 线程生命周期 | 使用频率 |
|---|---|---|---|---|
| pthread | 跨平台多线程API 使用难度大 |
C | 手动管理 | 几乎不用 |
| NSThread | 简单易用且可以直接操作线程 | OC | 手动管理 | 偶偶会用 |
| GCD | 充分利用设备的多核 | C | 自动管理 | 经常使用 |
| NSOperation | 基于GCD封装 | OC | 自动管理 | 经常使用 |
从上表我们知道我们一般都会使用GCD或者NSOperation,主要就是简单易用。但是因为涉及到语言不同就会有C和OC间的桥接:
__bridge我们都知道是类型转换,不修改对象(内存)管理权。__bridge_retained将对象转换为Core Foundation对象,对象内存管理交给我们手动管理其生命周期及销毁(CFRelease等),也可以使用CFBridgeRetain进行转换。__bridge_transfer和__bridge_retained类似,就是内存管理交给ARC管理。
多线程的生命周期


浅谈多线程中的饱和策略

从上面的多线程生命周期和线程池的流程可以知道在线程池满了并且没有空闲的工作队列时执行一个新的任务,由于没有资源创建线程和复用其他线程时,会进入一个饱和策略来处理这个问题。饱和策略有以下四个:
| 饱和策略 | 描述 |
|---|---|
| Abort | 默认策略,新任务提交时直接跑出未检查的异常RejectedExecutionException,该异常可悲调度者捕获 |
| CallerRuns | 一种调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会再线程池的线程中执行新任务,而是调用exector的线程中运行新的任务 |
| Discard | 新提交的任务被抛弃 |
| DiscardOldest | 抛弃队列里最近的任务,尝试提交新的任务 |
线程和队列
线程有同步线程和异步线程,队列有串行队列和并发队列。
| 串行队列 | 并发队列 | |
|---|---|---|
| 同步线程 | 不会开启线程 执行任务一个接一个 会堵塞 |
不会开启线程 执行任务一个接一个 |
| 异步线程 | 开启一条新线程 执行任务一个接着一个 |
开启线程 执行任务异步无序 |
GCD
单例
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
...
});
延迟
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
...
});
同步/异步
dispatch_sync(dispatch_queue_t _Nonnull queue, ^{ NSLog(@"这是同步队列") });
dispatch_async(dispatch_queue_t _Nonnull queue, ^{ NSLog(@"这是异步队列") });
栅栏函数
两者作用都是控制并发队列的执行顺序,执行完前面的任务才会来到dispatch_barrier_sync这里来。不同的是前者会堵塞影响后续任务执行,重要的是两者只能控制同一并发队列的顺序。并发队列必须是自定义队列。
dispatch_barrier_sync(dispatch_queue_t queue,
DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_barrier_async(dispatch_queue_t queue,
DISPATCH_NOESCAPE dispatch_block_t block);
调度组
创建组
dispatch_group_t dispatch_group_create(void);
组任务
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
组内任务执行完毕后通知回调
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
进组任务执行等待时间
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
任务进组、出组。使用的时候必须成对出现,否则会出现无法回调或崩溃的问题。
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
信号量
有些时候希望控制并发队列的的线程数,可以使用信号量来控制。在同步线程时也能当锁用。使用也是成对的使用,否则为0时就会一直堵塞线程。
创建信号量,值为0时永远等待,值大于1时才会执行。可以设置value为5就能控制最大并发数为5。
dispatch_semaphore_t dispatch_semaphore_create(long value);
信号量-1
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
信号量+1
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
DispatchSource
其 CPU 负荷非常小,尽量不占用资源的缘由于联结的优势。在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件句柄是一种指向指针的指针 它指向的就是一个类或者结构,它和系统有很密切的关系HINSTANCE(实例句柄),HBITMAP(位图句柄),HDC(设备表述句柄),HICON(图标句柄)等。这当中还有一个通用的句柄,就是HANDLE。
我们常见的就是DispatchSourceTimer,使用这个来封装计时器很好使。其次还可以使用DispatchSource这个来进行通讯。下面这个例子是可添加数据类型。
// 一种源事件数据类型,可以通过dispatch_source_merge_data添加数据,通过dispatch_source_get_data取得数据,每次添加值都会触发源事件回调。
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(self.source, ^{
NSUInteger value = dispatch_source_get_data(self.source); // 取回来值 1 响应式
self.totalComplete += value;
NSLog(@"进度:%.2f", self.totalComplete/100.0);
});
...
dispatch_source_merge_data(self.source, 1)
这是一些常用的DispatchSource常用函数列表。
| DispatchSource常用函数 | 意义 |
|---|---|
| dispatch_source_create | 创建源 |
| dispatch_source_set_event_handler | 设置源事件回调 |
| dispatch_source_merge_data | 源事件设置数据 |
| dispatch_source_get_data | 获取源事件数据 |
| dispatch_resume | 继续 |
| dispatch_suspend | 挂起 |
| dispatch_cancel | 取消 |
| dispatch_activate | 激活 |
浅谈GCD源码
苹果开源路径地址:官网、Github,有很多值得我们去看的。查看一些源码可以帮助我们更加了解一些系统的底层设计思想。本段只是简单的介绍一些源码,版本更新为1008,这东西也不太好写出来,需要自己去看然后消化。GCD的基类为dispatch_object_t,相当于NSObject。上面source时讲到GCD消耗很少内存的原因就是联合体,源码如下:
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_queue_t、dispatch_group_t等类型是什么样的呢?
typedef __typeof__(sizeof(int)) size_t;
typedef struct _opaque_pthread_t *pthread_t;
typedef void (*dispatch_function_t)(void *);
typedef struct Block_layout *dispatch_block_t;
typedef struct dispatch_continuation_s *dispatch_continuation_t;
typedef struct dispatch_queue_s *dispatch_queue_t;
typedef struct dispatch_source_s *dispatch_source_t;
typedef struct dispatch_group_s *dispatch_group_t;
typedef struct dispatch_object_s *dispatch_object_t;
想知道主线程的结构吗?
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_root_queues存着各种线程,这就是我们打印线程时的标记生成的地方。
struct dispatch_queue_global_s _dispatch_root_queues[] = {
//...删减了宏
_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,
),
};
主线程死锁的地方想瞧瞧不?
static void
_dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt,
dispatch_function_t func, uintptr_t top_dc_flags,
dispatch_queue_class_t dqu, uintptr_t dc_flags)
{
dispatch_queue_t top_dq = top_dqu._dq;
dispatch_queue_t dq = dqu._dq;
if (unlikely(!dq->do_targetq)) {
return _dispatch_sync_function_invoke(dq, ctxt, func);
}
pthread_priority_t pp = _dispatch_get_priority();
struct dispatch_sync_context_s dsc = {
.dc_flags = DC_FLAG_SYNC_WAITER | dc_flags,
.dc_func = _dispatch_async_and_wait_invoke,
.dc_ctxt = &dsc,
.dc_other = top_dq,
.dc_priority = pp | _PTHREAD_PRIORITY_ENFORCE_FLAG,
.dc_voucher = _voucher_get(),
.dsc_func = func,
.dsc_ctxt = ctxt,
.dsc_waiter = _dispatch_tid_self(),
};
_dispatch_trace_item_push(top_dq, &dsc);
__DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq); // 死锁位置在这里
if (dsc.dsc_func == NULL) {
dispatch_queue_t stop_dq = dsc.dc_other;
return _dispatch_sync_complete_recurse(top_dq, stop_dq, top_dc_flags);
}
_dispatch_introspection_sync_begin(top_dq);
_dispatch_trace_item_pop(top_dq, &dsc);
_dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
DISPATCH_TRACE_ARG(&dsc));
}
static void
__DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq)
{
uint64_t dq_state = _dispatch_wait_prepare(dq);
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
"dispatch_sync called on queue "
"already owned by current thread");
}
....
}
还有很多,感兴趣的同学可以自行去探索,我们还能查看GCD中单例、栅栏、调度组以及信号量的源码。看源码很枯燥很无味,也许看一次会忘,有时间多看几次多琢磨几次,学习设计思想是最好的。
NSOpration
NSOpration很多人都用过,特别实在些SDK中用到的会更多,因为NSOpration有很大的控制权,像最大并发数可以直接对maxConcurrentOperationCount赋值即可,不需要使用其他的锁之类的,而且对取消任务非常的方便,以及添加依赖、回调。都知道NSOpration是一个抽象类,有NSInvocationOperation、NSBlockOperation两个子类来使用,还有就是自定义NSOpration来使用。而且默认NSOpration是异步并发。简单用法就是下面这些:
// 最简单的用法
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"trg"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
//⚠️如果直接使用[op start]的话将会在主线程执行,但是不能和[queue addOperation:op];同时存在,否则将会崩溃,原因是加入到queue和start都是需要进入队列来执行,两个都在的时候就产生冲突了。在主线程可以直接用[[NSOperationQueue mainQueue] addOperation:op];即可。
// 这是一个使用闭包形式的NSOpration
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
sleep(3);
}];
[bo addExecutionBlock:^{
NSLog(@"这是一个执行代码块 - %@",[NSThread currentThread]);
}];
bo.completionBlock = ^{
NSLog(@"完成了!!!");
};
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo];
// 优先级的设置,bo1享有更大的优先执行的权利。当然也并不能保证bo2的任务全部优先于bo1,这些都是有系统来进行调度的。
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i++) {
NSLog(@"**第一个操作** %d --- %@", i, [NSThread currentThread]);
}
}];
// 设置优先级 - 最高
bo1.qualityOfService = NSQualityOfServiceBackground;
//创建第二个操作
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 10; i++) {
NSLog(@"第二个操作 %d --- %@", i, [NSThread currentThread]);
}
}];
// 设置优先级 - 最低
bo2.qualityOfService = NSQualityOfServiceUserInteractive;
//2:创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//3:添加到队列
[queue addOperation:bo1];
[queue addOperation:bo2];
// 最简单的线程通讯
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"oymuzi.download";
[queue addOperationWithBlock:^{
NSLog(@"%@ = %@",[NSOperationQueue currentQueue],[NSThread currentThread]);
//模拟请求网络
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%@ --%@",[NSOperationQueue currentQueue],[NSThread currentThread]);
}];
}];
// 控制最大并发数
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"oymuzi.download";
queue.maxConcurrentOperationCount = 2;
for (int i = 0; i<10; i++) {
[self.queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"%d-%@",i,[NSThread currentThread]);
}];
}
// 可以建立依赖,类似于GCD的栅栏作用
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"依赖测试";
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"请求token");
}];
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着token,请求数据1");
}];
NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着数据1,请求数据2");
}];
// 建立依赖 -- 循环乔涛
[bo2 addDependency:bo1];
[bo3 addDependency:bo2];
// ❌ 这样做会崩溃[bo1 addDependency:bo3];
[queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
NSLog(@"执行完了?我要干其他事");
以上就是NSOpration的基础用法,需要注意的地方是,我们知道可以取消所有任务。其中队列中取消后会让队列中的任务不会被调度,并且已被调度的任务将会继续执行,所以会出现有时cancel了还是有任务执行了。还有就是队列标记了取消后,队列未被调度的任务将会被移除,需要重新添加任务来执行。
[queue cancelAllOperations]; // 取消队列中的任务,如果已被调度的任务将不会取消。
[task cancel]; // 取消任务,如果任务已经被调度了就不能取消。
我们自定义NSOpration的话选择性的重写executing、finished、cancelled、concurrent、asynchronous以及start、main、cancel其中的属性以及方法,其实建议都重写。多接触NSOpration你就会越喜欢他。
@property (nonatomic, readwrite, getter=isExecuting) BOOL executing;
@property (nonatomic, readwrite, getter=isFinished) BOOL finished;
@property (nonatomic, readwrite, getter=isCancelled) BOOL cancelled;
@property (readonly, getter=isConcurrent) BOOL concurrent;
@property (readonly, getter=isAsynchronous) BOOL asynchronous
- (void)start;
- (void)main;
- (void)cancel;
线程安全
我们都知道在多线程很容易出现线程安全的问题,所以有很多锁来防止多线程争夺资源的问题,所很多种,如果分情况写很费劲的,个人常用的就是NSLock、NSConditionLock、dispatch_semapore、@synchronized、pthread_mutex这些,当然喽涉及的业务不同偏好锁也不同,所以发现一篇锁的文章:iOS多线程安全-13种线程锁🔒很不错,可以看下此篇文章。
多线程面试题
Q:请问输出a的值是啥?如何得到a真正的值?
var a = 0
while(a<10) {
DispatchQueue.global().async {
a += 1
}
}
print(a)
A: a >= 10 获取a真正的值可以通过下面的方法
var a = 0 while(a<10) { DispatchQueue.global().async { a += 1 } } print(a) DispatchQueue.global().async { print("a真正的值:\(a)") }
Q: 下面输出啥?
dispatch_queue_t queue = dispatch_queue_create("oymuzi", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
1 5 2 4 3 或1 2 5 4 3。 因为dispatch_async不会堵塞线程,但是调度需要时间,所以会1、5。然后在第一个异步函数里面的任务也是一样的,所以接下来是2、4、3。但是2快的话先再5,也有可能先5后2.
**Q:下面代码输出啥?**之前错误为并发队列已更正为串行队列,感谢wszcug
dispatch_queue_t queue = dispatch_queue_create("oymuzi", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
1 5 2或1 2 5。为啥没有3和4,因为输出2或5后已经死锁了。官方文档有dispatch_sync函数的 说明:Calling this function and targeting the current queue results in deadlock.
Q:下面代码输出啥?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
});
NSLog(@"4");
while(1){
}
NSLog(@"5");
4 1。 执行完1后进入死循环,无法执行后续任务。