算法

稳定与不稳定:如果a原本在b的前面并且a=b,排序之后a仍然在b的前面那就是稳定排序,如果排序之后a可能会出现在b的后面则是不稳定排序。所以冒泡排序是稳定的。选择排序可能不稳定。

空间复杂度:是指算法在计算机内执行时所需的存储空间与n的规模之间的关系。

时间复杂度:排序的的总操作次数,与n的规模之间的关系。

以下是我们在面试中要问的主要问题:

1.数据结构:
a、 链表
b、 堆叠
c、 排队
d、 二叉树
e、 哈希表

2.算法:
a、 排序(像快速排序和归并排序),二分搜索法,贪婪算法。
b、 二叉树(比如遍历、构造、操作…)

iOS 开发中常用的排序(冒泡、选择、快速、插入、希尔、归并、基数)算法

排序算法 平均时间复杂度 其他时间复杂度 平均空间复杂度 是否是稳定性的
冒泡排序 O(n^2) 最好:O(n),已是有序的
最坏:O(n^2),是逆序的
O(1) 稳定
选择排序 O(n^2) —— O(1) 不稳定
快速排序 O(nlogn) 最好:O(nlogn),每次划分都非常平衡时
最坏:O(n^2),每次划分非常不均匀时
O(nlogn) 不稳定
不稳定发生在交换的时刻;
归并排序 O(nlogn) —— O(n) 稳定

因为冒泡和选择排序都是原地排序算法,不需要额外的存储空间。所以其空间复杂度都是O(1)。

归并排序需要额外的空间来存储合并过程中的临时数组,其空间复杂度O(n)。

11.5.3 快速排序为什么快

在Object-C中学习数据结构与算法之排序算法

二分搜索法,是一种【在有序数组中】查找某一特定元素的搜索算法。

iOS查找算法之二分查找

漫画:五分钟学会贪心算法

有一个背包,最多能承载150斤的重量,现在有7个物品,
重量分别为[35, 30, 60, 50, 40, 10, 25],
价值分别为[10, 40, 30, 50, 35, 40, 30],
应该如何选择才能使得我们的背包背走最多价值的物品?

局部最优解

1、每次都尽量选择当前【价值最高】的物品
重量分别为[35, 30🚗, 60, 50🚗, 40🚗, 10🚗, 25], 130
价值分别为[10, 40②✅, 30, 50①✅, 35④✅, 40③✅, 30], 165

2、每次都尽量选择当前【重量最小】的物品
重量分别为[35④, 30③, 60, 50, 40⑤, 10①, 25②], 140
价值分别为[10✅,40✅, 30, 50, 35✅, 40✅, 30✅], 155

3、每次都尽量选择当前【价值密度最高】的物品
重量分别为[35🚗, 30🚗, 60, 50🚗, 40, 10🚗, 25🚗], 150
价值分别为[10✅, 40✅, 30, 50✅, 35, 40✅, 30✅], 170
价值密度 [0.285⑤, 1.333②, 0.5, 1.0④, 0.875, 4.0①, 1.2③]

想象一下下列场景:

  1. 从通讯录中寻找某个联系人
  2. 从一大堆文件中寻找某个文件
  3. 到了影厅之后,寻找电影票上指定的座位

如果以上情况中,联系人、文件、影厅座位这些“数据”没有按照需要的顺序组织,如何找到想要的特定“数据”呢?会非常麻烦!所以说,对于需要搜索的数据,往往应该先排个序!

排序比较 & 对比归并排序与快速排序

image-20240902163103782

前言

1
2
3
4
5
6
// 交换数组中的两个元素
void swap(NSMutableArray *array, NSInteger i, NSInteger j) {
NSNumber *temp = array[i];
array[i] = array[j];
array[j] = temp;
}

算法图解

034-Data-AlgorithmCollect-iOS

Data-AlgorithmCollect-1

一、冒泡排序

实现一个冒泡排序或者快速排序

从小到大排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
假设45132,按从小到大排序
i = 0 的时候,j的相邻两个位置都要比较排一下位置:
j = 0 的时候:arr_M = 45132(a[0]和a[1]比得到的结果)
j = 1 的时候:arr_M = 41532(a[1]和a[2]比得到的结果)
j = 2 的时候:arr_M = 41352(a[2]和a[3]比得到的结果)
j = 3 的时候:arr_M = 41325(a[3]和a[4]比得到的结果)
i=0已经将第一个最大的值放到最后了。

i = 1 的时候,j的相邻两个位置都要比较排一下位置:
j = 0 的时候:arr_M = 14325
j = 1 的时候:arr_M = 13425
j = 2 的时候:arr_M = 13245
i=1已经将第二个最大的值放到最后了。

冒泡排序代码实现

二、选择排序

**从数组的第a[i+1]个元素开始到第n个元素,寻找最小的元素(的位置)**。(具体过程为:先设最小位置为i=0,从该位置往后逐一比较,若遇到比之小的则记下该最小值的位置,结束后交换);

先为a[0]取到最小值,再为a[1]取到最小值,再继续…..

从小到大排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
假设45132,按从小到大排序
i = 0 的时候,j的相邻两个位置都要比较排一下位置:
j = i+1=1 的时候:arr_M = 45132(此时a[0]的4和a[1]的5比较:得到的结果)
j = 2 的时候:arr_M = 15432(此时a[0]的4和a[2]的1比较:得到的结果)
j = 3 的时候:arr_M = 15432(此时a[0]的1和a[3]的3比较:得到的结果)
j = 4 的时候:arr_M = 15432(此时a[0]的1和a[4]的2比较:得到的结果)
i=0已经将第一个最小的值放到第一位了。

i = 1 的时候,j的相邻两个位置都要比较排一下位置:
j = 2 的时候:arr_M = 14532(a[1]和a[2]比得到的结果)
j = 3 的时候:arr_M = 13542(a[1]和a[3]比得到的结果)
j = 4 的时候:arr_M = 12543(a[1]和a[4]比得到的结果)
i=1已经将第二个最小的值放到最第二位了。

选择排序代码实现

三、快速排序

漫画:什么是快速排序?(完整版)

快速排序(Quicksort)是对冒泡排序的一种改进。

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。这种思路就叫做分治法

1
2
3
4
5
6
7
8
// 快速排序函数 quickSort(array, 0, [array count] - 1);
void quickSort(NSMutableArray *array, NSInteger left, NSInteger right) {
if (left < right) {
NSInteger pivotIndex = partition(array, left, right);
quickSort(array, left, pivotIndex - 1);
quickSort(array, pivotIndex + 1, right);
}
}

快速排序代码实现

1、排序原理:

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,

快排图快排图

然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。

2、排序流程

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

四、归并排序

Hello 算法 11.6 归并排序

图解排序算法(四)之归并排序

1、划分阶段:通过递归不断地将数组从中点处分开(以把序列分成元素尽可能相等的两半),将长数组的排序问题转换为短数组的排序问题。当子数组长度为 1 时终止划分,开始合并。

归并排序

归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。

2、合并阶段:持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。

归并排序的合并过程如下:

  1. 初始化:创建两个指针,分别指向两个已排序数组的起始位置。
  2. 比较元素:比较两个指针所指向的元素。
  3. 选择较小元素将较小的元素放入临时数组,并移动该元素所在数组的指针。
  4. 重复比较:重复步骤2和3,直到其中一个数组的所有元素都被比较过。
  5. 复制剩余元素:如果一个数组的所有元素都已经被比较并复制到临时数组中,将另一个数组中剩余的元素直接复制到临时数组的末尾。
  6. 返回临时数组:临时数组现在是一个合并后的有序数组。

示例:要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

img

附:深度优先遍历(DFS)和广度优先遍历(BFS)

深度优先遍历(Depth First Search, 简称 DFS)

img

广度优先遍历(Breath First Search, 简称 BFS) 也叫层序遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。

BFS 一般是解决最短路径问题。

img

iOS算法篇-leetcode题目记录

附:排序算法代码

1、冒泡排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)methodMaopao {
int array[5] = {4, 5, 1, 3, 2};

for (int i = 0; i < 5-1; i++) { //需要比较几遍
for(int j = 0; j < 5-1-i; j++) { //每一遍都从0开始到倒数第-i个
if (array[j] > array [j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}

for(int i = 0; i < 5; i++) {
printf("%d\n",array[i]);
}
}

2、选择排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sort(items: Array<Int>) -> Array<Int> {
var list = items
for i in 0..<list.count {
//记录当前最小的数,比较i+1后更大的数进行记录
var minIndex = i
for j in i+1..<list.count {
if list[j] < list[minIndex] {
minIndex = j
}
}
// 交换
let temp = list[minIndex]
list[minIndex] = list[i]
list[i] = temp
}
return list
}

3、快速排序代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//单趟 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
if (left >= right) return left;

int begin = left;
int end = right;
int key = a[left]; //保存要排序的值
while (begin < end) //当左右指针相遇时结束
{
// begin是坑,从后往前找比key小的值填到坑里
while (begin < end && a[end] >= key) {
end--;
}
a[begin] = a[end];

// 此时end位置是坑,从前往后比key大的值填到坑中
while (begin < end && a[begin] <= key) {
begin++;
}
a[end] = a[begin];
}

//begin和end相遇的地方是key对应的位置
a[end] = key;
return end; //返回排好位置的元素的下标
}

END

< 返回目录

即时通信

以下内容摘自:开发即时通信是选择UDP还是TCP协议

大规模即时通讯软件的总体架构:

1。以UDP协议作为主要数据传输协议

2。服务端使用一个数据库保存信息(分布式数据库)

3。服务端是是分布式的,通过异步多线程技术,集群服务等 通过服务器集群共同运行服务端,对外进行海量信息处理(转发,暂存,广播消息)

4。客户端也属于分布式应用程序,具有一些服务端的功能 在进行语言,视频,文件传输的时候,可由服务端协调 在两个客户端直接进行点对点通信

目录

# 目录 * [一、为什么说Objective-C是一门动态的语言?](#Objective-C)
1

1
2
3
4
5
>线程间通信的体现:
>1. 一个线程传递数据给另一个线程
>2. 在一个线程中执行完特定任务后,转到另一个线程继续执行任务

>附:其他传值方法总结
1
2
3
4
5
6
7
1、通常的做法/不合理的做法
2、合理的做法(使用多线程异步执行)
1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程)
2)、真正的线程同步问题(异步线程里再开启异步线程)
3)、其他补充
附1:多个请求依次执行
附2:并发数控制
1
...
1
2
3
1、SDWebImage在图片下载及缓存的处理方法
2、SDWebImageDownloader 异步的图片下载器
3、SDWebImageDownloaderOperation 下载操作
1
2
3
1、一次完整的HTTP请求过程
2、TCP三次握手
3、TCP四次挥手
1
2
3
4
5
6
7
8
9
附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用
1
2
3
4
5
6
7
8
9
10
11
12
13
1、RunLoop概念

2、RunLoop和线程的关系?

3、RunLoop相关各类关系
3.1 CFRunLoopSourceRef
3.1.附 上题中button点击后,关于RunLoop的过程
3.2 CFRunLoopTimerRef
3.3 CFRunLoopObserverRef

4、RunLoop的应用
附:autoreleasepool 自动释放池
runloop、autorelease pool以及线程之间的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1、内存管理
(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?
(2)、那怎么知道对象已经没有被引用了呢?
(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池) 
(5)、autoreleasepool里面对象的内存什么时候释放?
(6)、runloop、autorelease pool以及线程之间的关系
(7)、自动释放池怎样创建
(8)、自动释放池使用注意
(9)、自动释放池的应用/什么时候要用@autoreleasepool

2、如何监测内存泄漏

3、循环引用
(1)、NSTimer的循环引用
(2)、block的循环引用
(2.1)、Objective-C中block为何得用copy修饰,能否用其他
(2.2)、block的循环引用
(2.3)、判断该block是否会发生循环引用
(2.4)、为什么masonry的block里引用self不需要weak?
(3)、委托delegate的循环引用
1
2
3
4
5
6
7
1、NSTimer和NSRunLoop的关系?
2、NSTimer使用细节
3、NSTimer的创建
4、NSTimer的循环引用
5、NSTimer使用的优化
6、NSTimer的销毁问题
6.1子线程中NSTimer的创建和销毁问题
  • 十一、谈谈设计模式
  • 十二、如何优化过于臃肿的Controller
  • 十三、谈谈性能优化(功耗)
  • 十四、UITableView的优化、重用
  • 十五、布局 layoutsubview、drawrect等
  • 十六、有逼格的代码
  • 十七、单元测试
  • 十八、APP审核

时间计时器等

NSTimer、NSDate

###iOS时间那点事
经典看前必先看:iOS时间那点事NSCalendar + NSDateComponents
iOS 时区日期处理及定时 (NSDate,NSCalendar,NSTimer,NSTimeZone)

NSTimer

NSTimer 定时器用法总结

自动加一秒(dateByAddingComponents和dateByAddingTimeInterval)

参考:时间与日期处理
涉及知识点:
NSDate的生成
方法①通过NSDateFormatterdateFromString:方法对dateString生成NSDate;

1
2
3
4
5
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];

NSString *dateString = @"2016-08-15 22:55:03";
NSDate *date = [dateFormatter dateFromString:dateString];

方法②通过NSCalendardateFromComponents:方法对dateComponents生成NSDate;(dateComponents由分个字段生成)
[greCalendar dateFromComponents:dateComponents];

1
2
3
4
5
6
7
    NSCalendar *greCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
[dateComponents setYear:2016];
[dateComponents setMonth:8];
[dateComponents setDay:15];
NSDate *operationDate = [greCalendar dateFromComponents:dateComponents];

NSDate转NSString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
NSDate *operationDate;

- (void)updateTimeExample {
NSCalendar *greCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
[dateComponents setYear:2016];
[dateComponents setMonth:8];
[dateComponents setDay:15];
operationDate = [greCalendar dateFromComponents:dateComponents];

if (!self.timer) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: self selector:@selector(addOneSecondToDate:) userInfo:nil repeats:YES];
}
}

- (void)addOneSecondToDate:(NSTimer *)timer {
// NSDate *operationDate = [timer userInfo];
NSCalendar *greCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

NSDateComponents *offsetDateComponents = [[NSDateComponents alloc] init];
[offsetDateComponents setMinute:1];
[offsetDateComponents setSecond:1];
NSDate *finalDate = [greCalendar dateByAddingComponents:offsetDateComponents toDate:operationDate options:0];
operationDate = finalDate;

//NSDate转NSString
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy年MM月dd HH:mm:ss"];
NSString *dateString = [dateFormatter stringFromDate:finalDate];

self.networkTimeLabel.text = dateString;
}

#####NSJSONSerialization 去除不必要的空格回车
NSJSONSerialization介绍
NSJSONSerialization四个枚举什么意思

1
2
3
4
5
6
// 要去除不必要的空格回车,只需将参数Option设置为0即可。如果设置NSJSONWritingPrettyPrinted会使格式化的json更加可读,即添加空格和换行,但如果设为0,则不添加空格和换行
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary
options:0
error:nil];
NSString *JSON = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];

######颜色渐变 CAGradientLayer
CAGradientLayer简介

线程-①线程GCD

线程与网络-①线程GCD

知识架构

iOS知识库

Android知识库

## 六、GCD > [< 返回目录](#目录)

精: iOS开发多线程篇—GCD介绍

0、dispatch_sync、dispatch_async

1.GCD中有2个用来执行任务的函数

说明:把右边的参数(任务)提交给左边的参数(队列)进行执行

(1)用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

(2)用异步的方式执行任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

以下参考自:dispatch_group_async 使用详解

1、dispatch_group_async (可实现队列同步)

要实现监听一组任务是否全部完成,等到全部完成后执行其他的操作可以使用dispatch_group_async(这个方法很有用)。比如异步下载多张图片,等所有图片下载完成之后,再去做一些事情(比如才通知界面说完成的)。

如果想在dispatch_queue中所有的任务执行完成后在做某种操作,在串行队列中,我们只需把该操作放到最后一个任务来执行就可以,但是在并行队列中怎么做呢。答案是用dispatch_group任务组来做。

思路:创建一个任务组,然后将异步操作都放进任务组里面,在最后用notify 告知所有任务完成,并做相应处理,一般最后的处理都是在主线程里面刷新UI来提示用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建并行队列queue
dispatch_group_t group = dispatch_group_create(); //创建任务组group

dispatch_group_async(group, queue, ^{
NSLog(@"group1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"group2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"group3");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"updateUi");
});

dispatch_release(group);

附:下面的两种调用其实是等价的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A)

dispatch_group_async(group, queue, ^{

  // 。。。

});

B)

dispatch_group_enter(group);

dispatch_async(queue, ^{

  //。。。

  dispatch_group_leave(group);

});

其他:

使用gcd怎么让两个线程执行完结束后再去执行另一个线程

多线程并发流程控制之dispatch_group 有关函数

2、dispatch_barrier_async(可实现队列同步)

dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_queue_t queue = dispatch_queue_create("**test.abc.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"dispatch_async1");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:4];
NSLog(@"dispatch_async2");
});

dispatch_barrier_async(queue, ^{ //dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
NSLog(@"dispatch_barrier_async");
[NSThread sleepForTimeInterval:4];
});

dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_async3");
});

3、dispatch_apply

dispatch_apply执行某个代码片段N次。

1
2
3
dispatch_apply(5, globalQ, ^(size_t index) {
// 执行5次
});

4、小结dispatch_group_async、dispatch_barrier_async、dispatch_apply分别是什么?

dispatch_group_async 可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了
dispatch_barrier_async 是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
dispatch_apply 执行某个代码片段N次。

5、GCD四大队列

iOS GCD四大队列

(1)、串行队列
(2)、并发队列
(3)、全局队列

全局队列:
和并发队列,执行效果一样,只是说,并发队列,需要我们程序员,自己创建
而全局队列,是由于系统提供

(4)、主队列

## END > < [返回目录](#目录)

线程-①线程基础

线程与网络-①线程基础

# 目录
1
2
3
4
>1、多线程的原理
>2、多线程的优缺点
>3、多线程在iOS开发中的应用
>4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD)
1
2
3
4
5
>线程间通信的体现:
>1. 一个线程传递数据给另一个线程
>2. 在一个线程中执行完特定任务后,转到另一个线程继续执行任务

>附:其他传值方法总结
1
2
3
4
5
6
7
>1、通常的做法/不好的做法
>2、合理的做法(使用多线程异步执行)
>1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程)
>2)、真正的线程同步问题(异步线程里再开启异步线程)
>3)、其他补充
>附1:多个请求依次执行
>附2:并发数控制
1
>1、线程安全
1
2
>1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决?
>2、串行队列与并行队列的区别

## 一、多线程介绍 > [< 返回目录](#目录)

1、多线程的原理

同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况?
答:CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。

2、多线程的优缺点

1
2
3
4
5
6
7
8
多线程的优点
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)

多线程的缺点
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享

3、多线程在iOS开发中的应用

主线程:

一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”

主线程的主要作用:

显示\刷新UI界面,处理UI事件(比如点击事件、滚动事件、拖拽事件等)

主线程的使用注意:

别将比较耗时的操作放到主线程中。耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验。

4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD)

1.NSThread
  1. 使用nsthread对象建立一个线程非常方便
  2. 但是!要使用nsthread管理多个线程非常困难,不推荐使用
  3. 技巧!使用[NSThread currentThread]跟踪任务所在线程,适用于这三种技术
2.NSOperation/NSOperationQueue

NSOperation和NSOperationQueue相关

  1. 是使用gcd实现的一套objective-c的api
  2. 是面向对象的线程技术
  3. 提供了一些在gcd中不容易实现的特性,如:限制最大并发数量、操作之间的依赖关系
1
2
3
4
5
6
7
8
9
NSOperation`是系统提供的抽象的基类,我们使用的时候需要使用继承于它的子类。系统为我们提供了两种继承于`NSOperation`的子类,分别是`NSInvocationOperation`和`NSBlockOperation。

当将操作添加到主操作队列时,所有操作会按照添加到队列中的先后顺序串行依次执行。(主队列是GCD自带的一种特殊串行队列。)
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:op1];

当将任务添加到自定义队列的时候,会开启子线程,操作会并发执行。
NSOperationQueue *customQueue = [[NSOperationQueue alloc]init];
[customQueue addOperation:op1];
3.GCD —— grand central dispatch
  1. 是基于c语言的底层api
  2. 用block定义任务,使用起来非常灵活便捷
  3. 提供了更多的控制能力以及操作队列中所不能使用的底层函数

NSOperationQueue与GCD的使用原则和场景

NSOperationQueue与GCD的使用原则和场景

## 三、线程等待(线程同步)、线程依赖、任务顺序问题 > [< 返回目录](#目录)

多个任务中,某个线程的执行依赖其他线程的执行完毕,或者某个线程的执行需等待其他线程执行完毕。

常见场景:
某个页面加载时通过网络请求获得相应的数据,再做某些操作。有时候加载的内容需要通过好几个请求的数据组合而成,比如有两个请求A和B。

多线程同步需求分析图

1、通常的做法/不好的做法:把并发队列变成串行

常见的有:为了省事,会将B请求放在A请求成功的回调中发起,将C请求放在B请求成功的回调中发起,在C的成功回调中将数据组合起来,这样做有明显的问题:

1
2
①、请求如果多了,需要写许多嵌套的请求
②、请求被变成了同步的方式了,这是最大的问题,在网络差的情况下,如果有n个请求,意味着用户要等待n倍于并发请求的时间才能看到内容

2、合理的做法(使用多线程异步执行)

1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程)

首先我们先来看简单的多线程同步问题(异步线程里没有再开启异步线程),这种时候,我们可以使用的方法有

使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题。其中notify的作用就是在group中的其他操作全部完成后,再操作自己的内容。
② 使用NSOperationQueue中operation的addDependency
③使用dispatch_barrier_async方法。

①、使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题

使用dispatch_group方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
NSLog(@"Request_1");
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
NSLog(@"Request_2");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
②使用使用NSOperationQueue中operation的addDependency,则是
1
2
3
4
5
6
7
//4.设置依赖
[operation3 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
③dispatch_barrier_async

2)、真正的线程同步问题(异步线程里再开启异步线程)

对于上面的例子,当将上面三个操作改成真实的网络操作后,这个简单的做法突然变得无效了,这是为什么呢?

其实这个道理很简单,因为我们开启的网络请求,是一个异步线程,所谓的异步线程,就是告诉系统你不要管我是否完成了,你尽管执行其他操作,开一个线程让我到外面操作去执行就行了,对我的处理你已经完成了,也就是说线程只负责将请求发出去,就认为自己的任务算完成了。所以,当三个请求都发送出去后,不会管网络操作是否完成,就会执行notify中的内容,但由于请求结果本身需要一定的时间,所以导致了界面都刷新了,而请求结果才返回。

即上面的问题,总结为如果dispatch_group_async里执行的是异步代码dispatch_group_notify会直接触发而不会等待异步任务完成。

所以,对于这种异步线程里开异步线程的线程同步问题,该怎么处理呢?

为了应对这种异步线程里开异步线程的线程同步问题,其实我们需要多处理的只是监控异步线程里的所有操作(包括异步线程里的异步线程)都结束后,才算这个异步线程结束

这里我们的解决方法常见的有:

方法A:直接使用dispatch_group_enter和dispatch_group_leave,即只需要在任务开始前enter和结束后leave即可达到线程同步的效果。

dispatch_group_enter(group):下面的任务由group组管理,group组的任务数+1
dispatch_group_leave(group):相应的任务执行完成,group组的任务数-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)testGroupSync_userEnterLeave {
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_group_leave(group);
});
});

dispatch_group_enter(group);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_group_leave(group);
});
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

方法B:使用信号量,控制异步线程的结束,来达到多线程同步的效果。

在GCD中有三个函数是semaphore的操作,分别是:

1
2
3
  dispatch_semaphore_create   创建一个semaphore
  dispatch_semaphore_signal   发送一个信号(信号量+1)
  dispatch_semaphore_wait    等待信号(wait执行完后,信号量-1)

实际使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)testGroupSync_semaphore_signal_wait {
dispatch_group_t group = dispatch_group_create();

// 创建一个控制线程同步的信号量,初始值为0(红灯)
dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,原本阻塞住的任务1线程才会恢复畅通,才代表任务1结束了。
//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(syncSemaphore);
});

// 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯)
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(syncSemaphore);
});

// 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯)
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

附:实际上,这段代码是一个常用的不控制并发数,只控制线程阻塞,实现线程同步的例子。在这里它是将等待信号dispatch_semaphore_wait放在线程执行后。往下看,待会会介绍控制并发数的例子。

上面例子中,先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,恢复畅通,才代表任务1结束了。

3)、其他控制任务顺序的例子

附1:多个请求依次执行

例子:三个任务分别为下载图片,打水印和上传图片,三个任务需异步执行但需要顺序性。

对于这个问题通常会通过线程依赖进行解决。

  • 1、如果是GCD的话,设置线程依赖比较繁琐,所以这里就不讨论了。
  • 2、如果是NSOperation的话,我们直接通过addDependency来设置一个操作队列NSOperationQueue中的线程依赖就可以了。

代码如下(此例子不讨论线程里开线程,如果需要的话,请将信号量的使用添加上去即可解决):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 //1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
[self request_C];
}];

//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

四、多线程并发数控制

当我们在处理一系列线程的时候,当数量达到一定量,我们需要处理并发数控制。

  • 1、如果是GCD的话,怎么快速的控制并发呢?答案就是dispatch_semaphore。这个比较复杂,但我们这边还是谈一下
  • 2、如果是NSOperation的话,我们直接使用NSOperationQueue来控制并发,这个就不谈了。

GCD使用信号量dispatch_semaphore控制并发

说道信号量,这里顺便谈下信号量的概念:

信号量就是一个资源计数器,它是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。

上面的例子中,信号量初始值为0,其先执行到dispatch_semaphore_wait,此时由于信号量为0,所以造成了阻塞,而使得线程1没能结束它的工作。而到线程1里的异步线程执行完后,给信号量发送了一个通知,使得信号量的值加上了1,此时刚才线程1里的dispatch_semaphore_wait发现信号量大于等于1了,它就不再阻塞,而是继续执行下去,从而使得了线程1这时候真正结束了它所应该处理的工作。wait执行完后,信号量又减1了。

也就是通过这种线程阻塞/等待的方法,我们实现了线程的同步。

当然信号量的用途,除了可以用来实现线程同步外,还可以用来实现控制GCD的并发数

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)testBingfa {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建信号量,并且设置值为10
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++)
{ // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次发送信号则semaphore会+1,
dispatch_semaphore_signal(semaphore);
});
}
}

比较这段代码与上面的代码,控制并发一般都是将等待信号dispatch_semaphore_wait放在线程执行前。而我们常用的只控制线程阻塞,实现线程同步的,都是将等待信号dispatch_semaphore_wait放在线程执行后。所以,如果一个多线程,它需要同时控制线程同步和线程并发数的话,那它就需要创建两个信号量来分别控制。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)testGroupSyncAndBingfa_semaphore_signal_wait {
dispatch_group_t group = dispatch_group_create();

// 创建一个控制线程并发的信号量,初始值为最大并发数2(红灯)
dispatch_semaphore_t bingfaSemaphore = dispatch_semaphore_create(2);

// 创建一个控制线程同步的信号量,初始值为0(红灯)
dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0);

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(6);
NSLog(@"任务三完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

通过所附加的两个例子,我们明显的看出NSOperation在处理线程依赖以及并发数的问题上,明显更高级。所以,如果有类似的问题,我们优先使用NSOperation来处理。

## 常见笔试/面试题 [< 返回目录](#目录)
1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决?

答:(1)容易造成系统崩溃
(2)解决方案:开启串行模式,使用一个类(单例方式)操作数据库。

## END > < [返回目录](#目录)

线程-①线程间通信

线程与网络-①线程

## 二、谈谈你对线程间的通信的了解 > [< 返回目录](#目录)

在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信。

线程间通信的体现:

1
2
1. 一个线程传递数据给另一个线程
2. 在一个线程中执行完特定任务后,转到另一个线程继续执行任务

线程间通信应用举例:(即在一个线程中执行完特定任务后,转到另一个线程继续执行任务,并将该线程数据传递给另一个线程的应用举例)

以下例子摘自:线程间通讯

NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了。

1
2
3
4
5
6
7
8
9
10
>回到主线程执行,执行self的showImage方法,参数是arg

>- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

>回到后台线程执行aSelector方法,参数是arg
>- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg

>回到xx线程执行aSelector方法,参数是arg

>- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

②、GCD一个线程传递数据给另一个线程。

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"donwload---%@", [NSThread currentThread]);
// 1.子线程下载图片
NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 2.回到主线程设置图片
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"setting---%@ %@", [NSThread currentThread], image);
[self.button setImage:image forState:UIControlStateNormal];
});
});

③、NSOperation线程通信的举例:在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。这里有两种方法,一种是线程里转线程,另一种是线程间使用依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.创建一个新的队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.添加任务(操作)
[queue addOperationWithBlock:^{
// 2.1在子线程中下载图片
NSURL *url = [NSURL URLWithString:@"http://imgcache.mysodao.com/img2/M04/8C/74/CgAPDk9dyjvS1AanAAJPpRypnFA573_700x0x1.JPG"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 2.2回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];

首先我们先来说说线程与线程间的关系,其引入了我们下面的第二点线程等待(线程同步)问题、线程依赖问题。

然后我们再来说说,有时候我们还常经常在多个线程中操作一个全局变量,这个的话,也算线程间通信吗?不管他算不算(应该是不算的,个人觉得),但是他的存在使得我们引入了另一个线程安全的问题,即多个线程操作同一个变量。也就是下面说的第三点,线程安全问题。

附:其他传值方法总结

参考:其他传值方法总结

1
2
1、A界面到B界面的正向传值,即A界面传值给B界面使用
2、。。。

## END > < [返回目录](#目录)

线程-①进程与线程

线程与网络-①线程

知识架构

iOS知识库

Android知识库

5、进程和线程的区别

(1)、进程和线程的区别

以下区别摘自:进程和线程的区别

一个程序至少一个进程,一个进程至少一个线程。

一个线程只能属于一个进程,而一个进程可以有多个线程,且至少有一个线程。

进程线程的区别:

1
2
3
4
5
6
7
8
资源拥有:同一进程内的所有线程共享该进程的资源。如内存、I/O、cpu等,但是进程之间的资源是独立的。
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。

两者均可并发执行。
(2)、一个app运行时只有一个进程吗?有没有多进程?

一个正在运行的程序可以看做一个进程。(例如:正在运行的QQ就是一个进程),进程拥有独立运行所需的全部资源。

iOS (APP)进程间8中常用通信方式总结

(3)、什么时候用进程?什么时候用线程?

进程与线程的选择取决以下几点:

1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。

2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应

3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;

4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;

5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

END

线程-②队列与任务或线程

线程与网络-①队列与任务或线程

知识架构

iOS知识库

Android知识库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>1、死锁探究:GCD死锁及报错提示(EXC_BAD_INSTRUCTION)
>(1)、死锁举例:通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁
>(2)、死锁结论:往串行队列里添加的同步任务不能卡住该串行队列,否则会造成死锁(这句话非常重要)
>(3)、死锁原因分析

>2、主队列中的死锁:在主队列开启同步任务,一定为什么会阻塞线程?

>3、iOS多线程中,队列和执行的排列组合结果分析
>(1)、队列的类型
>(2)、四个比较容易混淆的术语:同步、异步、并发、串行
>(3)、队列和线程的关系
>(4)、自己延伸的问题:一个队列同时存在同步任务和异步任务?

>4、并发是并发队列,并行又是什么鬼???
>(1)、多个处理器和多核处理器的区别
>(2)、并发和并行的区别

>5、进程和线程的区别
>(1)、进程和线程的区别
>(2)、一个app运行时只有一个进程吗?有没有多进程?
>(3)、什么时候用进程?什么时候用线程?

五、队列与任务执行

< 返回目录

1、队列的类型(并发队列(Concurrent Dispatch Queue)& 串行队列(Serial Dispatch Queue))

GCD的队列可以分为2大类型:并发队列和串行队列。

(1)并发队列(Concurrent Dispatch Queue)

可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_async)函数下才有效
并发队列

####(2)串行队列(Serial Dispatch Queue)

让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
串行队列

问:并发是并发队列,并行又是什么鬼???

答案:下文会详细介绍。

2、队列是用来干嘛的?说一说【队列与任务 】或【队列与线程】的关系?(重要)

答:

1、队列是用来添加任务/保存以及管理任务的,它只是负责任务的调度(是按串行队列还是按并发队列调度),而不负责任务的执行
2、任务是添加到队列里的,可以添加同步任务或者异步任务。

3、对队列所添加的任务是在线程中执行的/由线程负责进行执行,至于在什么线程根据是同步任务还是异步任务,以及当前是什么队列共同决定。

(所以不管是将同步任务添加到串行队列还是并行队列,因为同步任务都不会创建新线程。所以并行同步队列和串行同步队列其实是一样的,它们都不会创建新的线程而且会是顺序执行)

4、补充:在一个线程内可能有多个队列。

问:往队列里面添加任务,是否会创建新线程是由什么决定的,是任务类型是同步还是异步,还是队列是串行还是并发?

答:是否会创建新线程,主要由队列的类型(串行 or 并发)以及任务类型(同步 or 异步)共同决定。即异步 + 并发,才会创建新线程,并发执行任务。

同步(dispatch_sync) 异步(dispatch_async)
串行队列(Serial Queue) ❌ 不创建新线程,任务按顺序在当前线程执行 ❌ 不创建新线程,任务按顺序在当前线程执行
并发队列(Concurrent Queue) ❌ 不创建新线程,任务按顺序在当前线程执行 ✅ 可能创建新线程,并发执行任务

⚠️ 例外情况:

​ • 主队列 (dispatch_get_main_queue()) 永远不会创建新线程,不管是 sync 还是 async,任务都在主线程执行。

​ • dispatch_sync 只会在当前线程执行,不管队列是不是并发的,都不会开新线程。

2.1、理解队列是用来添加任务/保存以及管理任务的,它只是负责任务的调度,而不负责任务的执行的代码

看以下代码,我们通过它来方便我们理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//  NSOperationQueueCJHelper.m

/**
* 创建队列
*
* @param operations 队列的操作数组
* @param lastOperation 队列的最后一条(最后一条,会等到前面都结束后才会结束)
*
* return 队列
*/
+ (NSOperationQueue *)createOperationQueueWithOperations:(NSArray<NSOperation *> *)operations lastOperation:(NSOperation *)lastOperation {

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"this is a queue";

for (NSBlockOperation *operation in operations) {
[queue addOperation:operation];

[lastOperation addDependency:operation];
}

[queue addOperation:lastOperation];

return queue;
}

// 或者
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTask) object:nil];
[operationQueue addOperation:operation];

附:队列的取消、暂停、恢复
取消队列的所有操作使用NSOperationQueue的 - (void)cancelAllOperations;

也可以调用NSOperation的- (void)cancel方法取消单个操作

暂停和恢复队列
- (void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列

2.2、iOS主线程、主队列(串行队列)、全局队列

iOS 主队列 全局队列

主队列是系统为我们创建的串行队列,而且每个APP只有一个主队列,负责调度处理APP的唯一主线程的UI事件。

注意:主队列没有办法开辟新的线程。所以,在主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行。

主队列异步任务:先将任务放在主队列中,但是不是马上执行,等到主队列中的其它所有除我们使用代码添加到主队列的任务的任务都执行完毕之后才会执行我们使用代码添加的任务。

主队列同步任务:容易阻塞主线程,所以不要这样写。原因:我们自己代码任务需要马上执行,但是主线程正在执行代码任务的方法体,因此代码任务就必须等待,而主线程又在等待代码任务的完成好去完成下面的任务,因此就形成了相互等待。整个主线程就被阻塞了。

全局队列:本质是一个并发队列,由系统提供,方便编程,可以不用创建就直接使用。

2.3、插入问:NSOperationQueue是什么队列?

答:NSOperationQueue是操作队列。结合实际才知道它是并发队列,还是串行队列。

[NSOperationQueue mainQueue]是主队列,和GCD中的主队列一样,是串行队列

[[NSOperationQueue alloc]init]是非主队列,非常特殊(同时具备并发和串行的功能)。默认情况下,非主队列是并发队列。

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
当operationQueue.maxConcurrentOperationCount > 1,那么operationQueue就是并发队列;
当operationQueue.maxConcurrentOperationCount == 1,那么operationQueue就是串行队列。

3、总结四个比较容易混淆的术语:并发队列、串行队列、同步任务、异步任务

3.1、并发和串行决定了任务的执行方式(队列)。

并发队列:多个任务并发(同时)执行,指一个处理器同时处理多个任务
串行队列:一个任务执行完毕后,再执行下一个任务

3.2、同步和异步决定了要不要开启新的线程(线程)

1
2
>同步:在当前线程中执行任务,不具备开启新线程的能力
>异步:在新的线程中执行任务,具备开启新线程的能力

4、并发是并发队列,并行又是什么鬼(多核处理器同时执行任务)???

(1)、多个处理器和多核处理器的区别

多个处理器:多个单核处理器,就是说电脑和处理器有多个,但是这个电脑的处理器是单核的;

多核处理器:单个多核处理器,也就是说电脑有一个处理器,但是这个处理器是多核的;

附:在生活中,我们的电脑上常见的处理器都是单处理器,但是这个处理器是多核的,一般是四核,有些需要玩高级游戏的,可能还是八核的处理器。

详情查看:多个处理器和多核处理器的区别

(2)、并发和并行的区别

并发(concurrency)是指一个处理器同时处理多个任务。即并发指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并发(concurrency)

并行(parallel)是指多个处理器或者是多核的处理器同时处理多个不同的任务。即并行是指在同一时刻,有多条指令在多个处理器上同时执行。

并行(parallel)

个人理解:是否可以理解为在出现多核处理器或者只有一个处理器的时候,我们那时候就只还有并发的概念。

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)

5、iOS多线程中,队列和执行的排列组合结果分析

该文章非常重要,且主要总结点有

1
2
3
4
5
6
7
1. 开不开线程,取决于执行任务的函数,同步不开,异步开。
2. 开几条线程,取决于队列,串行开一条,并发可以开多条(即并发队列里有异步任务,则可以开多条,如果都只有同步任务,那由于同步任务并不会开新线程,所以此时也就不会开新线程)
3. 主队列:专门用来在主线程上调度任务的"队列",主队列不能在其他线程中调度任务!
4. 如果主线程上当前正在有执行的任务,主队列暂时不会调度任务的执行!主队列同步任务,会造成死锁。原因是循环等待
5. 同步任务可以队列调度多个异步任务前,指定一个同步任务,让所有的异步任务,等待同步任务执行完成,这是依赖关系。
6. 全局队列:并发,能够调度多个线程,执行效率高,但是相对费电。 串行队列效率较低,省电省流量,或者是任务之间需要依赖也可以使用串行队列。
7. 也可以通过判断当前用户的网络环境来决定开的线程数。WIFI下6条,3G/4G下2~3条。

6、代码:使用GCD为队列添加任务

实例1:并发队列添加异步任务/用异步函数往并发队列中添加任务的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)viewDidLoad
{
[super viewDidLoad];

//1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//2.添加任务到队列中,就可以执行任务
//异步函数:具备开启新线程的能力
dispatch_async(queue, ^{
​ NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
​ NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
​ NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);

}
上述方法说明.GCD中有2个用来执行任务的函数

说明:把右边的参数(任务)提交给左边的参数(队列)进行执行

(1)用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

(2)用异步的方式执行任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

## 常见笔试/面试题 [< 返回目录](#目录)
2、以下两种GCD队列创建有什么不同?
1
2
3
4
5
6
7
dispatch_queue_t queue = dispatch_queue_create("MyQueue",DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue =dispatch_queue_create(@“MyQueue", DISPATCH_QUEUE_CONCURRENT);

//生成一个串行队列,队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。第一个参数是队列的名称,在调试程序时会非常有用,所有尽量不要重名了。

//生成一个并发执行队列,block被分发到多个线程去执行

## END > < [返回目录](#目录)

线程-③任务或线程与锁

四、线程安全问题

1、线程安全

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。

参考:iOS中保证线程安全的几种方式与性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
性能对比
对以上各个锁进行1000000此的加锁解锁的空操作时间如下:
OSSpinLock: 46.15 ms
dispatch_semaphore: 56.50 ms
pthread_mutex: 178.28 ms
NSCondition: 193.38 ms
NSLock: 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock: 157.44 ms
NSConditionLock: 490.04 ms
@synchronized: 371.17 ms

总的来说:

OSSpinLock 自旋锁和 dispatch_semaphore 信号量的效率远远高于其他。
@synchronized和NSConditionLock效率较差。

OSSpinLock 自旋锁原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过最近YY大神在自己的博客不再安全的 OSSpinLock中说明了OSSpinLock已经不再安全,请大家谨慎使用。
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。


鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。
如果不考虑性能,只是图个方便的话,那就使用@synchronized。

NSDateFormatter在iOS7之后(包括iOS7)才是线程安全的

2、死锁

死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。

死锁的4个必要条件

一、@synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSObject *obj = [[NSObject alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要线程同步的操作2");
}
});

@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞,

二、NSLock

1
2
3
4
5
6
7
8
9
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock]; // 加锁
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock]; // 解锁

});

三、NSRecursiveLock递归锁的使用

我们先写一个典型的死锁情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NSLock *lock = [[NSLock alloc] init];	// 此锁在本代码中会造成死锁
//NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; //要想下面的递归调用不会造成死锁,只要这里将锁改成递归锁NSRecursiveLock就可以了。NSRecursiveLock递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1); // 此时lock还没解锁,就有执行这个block,而block里又给加了次锁,从而造成了死锁
}
[lock unlock];
};

RecursiveMethod(5);
});

在是跟你面的代码中,在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
};

[lock lock];
RecursiveMethod(5);
[lock unlock];
});

四、NSCondition & NSConditionLock 条件锁

wait方法是傻等;

waitUntilDate:方法是等一会;

signal是唤起一个在等待的线程。

通过wait()waitUntilDate(limit: NSDate) -> Bool这两个方法都可以实现线程阻塞即线程睡眠。

不同之处在于

wait()会使线程一直处于休眠状态,直到收到signal()为止;

waitUntilDate(limit: NSDate) -> Bool在使线程睡眠的同时会设置睡眠的终止时间。

如果在终止时间前收到了signal()就会唤醒线程;
当到达终止时间的时候,即使没有收到signal(),也会直接唤醒线程,而不会像wait()方法那样一直睡眠下去。

1、NSCondition

基本的条件锁,手动的控制。wait

2、NSConditionLock 条件锁

条件锁,这里的条件并不是bool表达式中的条件,而是一个特定的int值。

条件锁,不是简单的加锁/解锁, 而是需要根据一定条件满足后进行 加锁/解锁.

以一个生产中与消费者的例子,介绍条件锁的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static NSInteger CONDITION_NO_DATA        //条件一: 没有数据
static NSInteger CONDITION_HAS_DATA //条件二: 有数据

// 初始化锁时,指定一个默认的条件
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

//生产者,加锁与解锁的过程
while (YES) {

//1. 当满足 【没有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_NO_DATA];

//2. 生产者生成数据
//.....

//3. 解锁,并设置新的条件,已经有数据了
[locker unlockWithCondition:CONDITION_HAS_DATA];
}


//消费者,加锁与解锁的过程
while (YES) {

//1. 当满足 【有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_HAS_DATA];

//2. 消费者消费数据
//.....

//3. 解锁,并设置新的条件,没有数据了
[locker unlockWithCondition:CONDITION_NO_DATA];
}

五、OSSpinLock自旋锁 & os_unfair_lock

由于OSSpinLock不再安全,所以这里我们就直接说一下os_unfair_lock,这个是苹果用于代替OSSpinLock的。效率很高用法很简单如下

1
2
3
4
5
os_unfair_lock_t lock = OS_UNFAIR_LOCK_INIT;

os_unfair_lock_lock(&lock);
NSLog(@"os_unfair_lock_t");
os_unfair_lock_unlock(&lock);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "ViewController.h"
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property (nonatomic,assign) int ticket;
//@property (nonatomic,assign) OSSpinLock lock;
@end

@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self.lock = OS_SPINLOCK_INIT;
self.ticket=50;
[self ticketsTest];
// Do any additional setup after loading the view.
}

-(void)ticketsTest{
// 这里我们开两个线程处理同一件事
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
}

-(void)saleTicket{
//静态创建、则不需要新建属性
static OSSpinLock lock = OS_SPINLOCK_INIT;
//若后面是个函数、则需
/*
static OSSpinLock lock = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = OS_SPINLOCK_INIT;
});
*/
//其他线程执行到这里时,发现锁被加锁了 就会再这排队等待、直到这个锁被打开
//加锁
OSSpinLockLock(&lock);
int ticket = self.ticket;
sleep(.2);
ticket--;
self.ticket=ticket;
NSLog(@"%d",self.ticket);
//解锁
OSSpinLockUnlock(&lock);
}

iOS - 互斥锁&&自旋锁 多线程安全隐患

iOS - 互斥锁&&自旋锁 多线程安全隐患

一、多线程安全隐患

资源共享
一块资源可能会被多个线程共享,也就是多个线程可能会访问到一块资源
比如多个线程访问同一个对象,同一个变量,同一个文件。
当多线程访问同一块资源的时候,很容易引发数据错乱和数据安全问题
二、原子和非原子属性
1>OC 在定义属性的时候有nonatomic和atomic两种选择
* atomic:原子属性,为 setter 方法加锁
* nonatomic:非原子属性,不会为 setter 方法加锁
普通情况下都是在主线程做操作,所以一般都不会加锁。
对比:
*** atomic:线程安全,需要消耗大量的资源
*** nonatomic
:非线程安全,适合内存小的移动设备
2>synchronized 与 atomic
* synchronized:互斥锁
* atomic:自旋锁
共同点:都能保证同一时刻只能有一个线程操作锁住的代码
区别
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会. 自动唤醒然后执行任务。
自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
自旋锁应用场景:比较适合做一些不耗时的操作
三、互斥锁
· 注意点:
- 如果多线程访问同一个资源,那么必须使用同一把锁才能锁住
- 在开发中,尽量不要加锁,能在服务端做尽量在服务端做,如果必须要加锁,一定要记住,锁的范围不能太大,哪里有安全隐患就加在哪里。
技巧:因为必须使用同一把锁,开发中如果需要加锁,直接使用 self 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@synchronized(self) {
//线程1进来之后,锁住,2和3都再外面等待
//1、查询剩余的票数 NSUInteger count = self.totalCount;
//2、判断是否还有余票
//2.1卖票
//3 、提示客户,没有票了
if (count>0) {
[NSThread sleepForTimeInterval:0.1];
self.totalCount = count-1;
NSLog(@"%@卖了一张票,还剩%zd票",[NSThread currentThread].name,self.totalCount);
}
else
{
NSLog(@"没票了");
break;
}
}
//解锁

四、自旋锁
注意点:
只会给 setter 方法加锁,并不会给getter方法加锁。

ObjC 多线程简析(一)-多线程简述和线程锁的基本应用

在iOS10之后apple废弃了OSSpinLock自旋锁,使用os_unfair_lock互斥锁来替代。

在iOS10之后apple已经不再建议使用OSSpinLock自旋锁了,它的替代方案是一个互斥锁,所以一般情况下我们使用互斥锁来解决线程同步的问题才是比较合理的。

1
2
3
4
5
6
7
8
9
10
11
// 初始化OSSpinLock
_osspinlock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&_osspinlock);

// 操作数据
// ...

// 解锁
OSSpinLockUnlock(&_osspinlock);
1
2
3
4
5
6
7
8
9
10
11
// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;

// 加锁
os_unfair_lock_lock(&(_osunfairLock));

// 操作数据
// ...

// 解锁
os_unfair_lock_unlock(&(_osunfairLock));

iOS中的线程同步方案

自旋锁、互斥锁比较:

a, 什么情况使用自旋锁比较划算?

预计线程等待锁的时间很短

加锁的代码(临界区)经常被调用,但竞争情况很少发生

CPU资源不紧张

多核处理器

b,什么情况使用互斥锁比较划算?

预计线程等待锁的时间较长

单核处理器

临界区有IO操作

临界区代码复杂或者循环量大

临界区竞争非常激烈

(4)、自己延伸的问题:什么队列能同时存在同步任务和异步任务?(并发队列)

eg:主线程的主队列是串行队列,不可能在该队列上添加同步任务,否则会造成死锁。所以,同时存在同步任务和异步任务的队列应该只能是并发队列。

最终的执行顺序,还是看这些各式各样的任务所在的队列是什么队列,串行的还是并发的。

1、死锁探究:GCD死锁及报错提示(EXC_BAD_INSTRUCTION)

(1)、死锁举例:通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (IBAction)testDeadlock1:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
NSLog(@"1(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
dispatch_sync(q1, ^{ // 添加的同步任务和外面的任务是同一个串行队列
sleep(2);
NSLog(@"2(属任务②).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
NSLog(@"3(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
}
(2)、死锁结论:往串行队列里添加的同步任务不能卡住该串行队列,否则会造成死锁(这句话非常重要)
(3)、死锁原因分析:

要了解通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁,或者说往串行队列里添加的同步任务卡住该串行队列的时候会发生死锁的原因,我们首先需要知道几个概念:

知识点①:创建同步任务的操作,在创建完之后,需要立马执行所创建的同步任务。即创建完同步任务的操作后,只有连其所创建的同步任务也结束了才能继续执行下去;

知识点②:而创建异步任务的操作,在创建完之后,即可继续执行下去,不需要等待所创建的异步任务结束。

附:在线程组的使用中,我们会遇到需要监听多个子线程中的所有任务都结束后,才去执行最后的更新UI的操作。假设最后更新UI的操作,除了需要子线程的任务结束,还需要子线程中的异步线程,如网络请求也结束后,才能更新UI,那么为了不让子线程提前退出,而是等到网络请求也结束后才退出,我们通常会通过使用gcd的enter leave或者信号量来等待。

知识点③:串行队列里的任务是一个执行完,才接着执行下一个;

所以,由以上①②③知识点,我们就能分析出其造成死锁的原因如下:

首先,对于标记123,先只看是同步任务还是异步任务,而不管是串行队列,还是并行队列,或者说是哪个队列。

从代码上我们很容易看出其是同步任务,所以由以上同步的知识点②,可以知道,最终标记输出的正确顺序应该依次是标记1、标记2、标记3

即我们的关注点是标记2能否在标记1之后输出。

答,在上述代码中,标记2处的同步任务是被添加到串行队列的,而且还是当前的串行队列。

我们知道串行队列里的任务是一个执行完,才接着执行下一个的,也就是说,往串行队列里添加的任务要执行的条件一定是在所添加的新任务之前的所有任务都已经全部执行完了后,才会执行到这一个的。

这里我们依次往串行队列里添加了第一个任务块和第二个任务块。

要完成第一个任务块需要同时完成任务1+第二个任务块2+任务3

第二个任务块要执行,根据串行队列的性质,我们知道第二个任务块要等待第一个任务块结束才会执行。(如果两个任务即使都是添加到串行队列,但是他们是不同串行队列的时候就不会需要等待)

由此死锁。

显然这里任务②需要等到任务①真正完成,而任务①的真正完成需要等任务②完成,这样的一个互相等待也就构成了一个死锁,导致我们** EXC_BAD_INSTRUCTION**的崩溃了。

那么以上死锁的问题,怎么解决呢?

答:其实只要解决标记2即任务②,可以在标记1执行之后执行就可以了。
解决方法有:将任务②改成异步任务,或者将任务②这个同步任务添加到非本串行队列下,可以是其他串行队列,也可以是其他并行队列都可以。即以下这种修改方案是能够解决死锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (IBAction)testDeadlock2:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
​ NSLog(@"1(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ dispatch_queue_t q2 = dispatch_queue_create("serial_queue_2", DISPATCH_QUEUE_SERIAL);
​ dispatch_sync(q2, ^{
​ sleep(2);
​ NSLog(@"2(属任务②).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ });
​ NSLog(@"3(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
});
}

2、主队列中的死锁:在主队列开启同步任务,一定为什么会阻塞线程(看同步任务是不是加在主队列里去了)?

回答本问题前,我们需要先了解的知识点是:

知识点①:主线程和主队列的关系:

主队列是主线程中的一个串行队列。每一个应用程序只有唯一的一个主队列用来update UI。所有的和UI的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。

因为主线程是一个串行队列,所以往主队列里添加同步任务(如果不是往主队列添加同步任务就不会)是很有可能发生死锁卡死的。如以下代码就会发生死锁。代码如下:

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"打印1");

dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"打印2");
});
NSLog(@"打印3");
}

由引述,我们已经知道往串行队列里添加的同步任务,如果卡住的是该串行队列,则会发生死锁,所以显然即执行往这个串行队列里添加同步任务的该任务也是在这个串行队列里的话,那么由于相互等待会造成死锁。

最简单的解决方法:将sync同步方法,替换成异步方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"标记1");

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"标记3");
});

NSLog(@"标记5");
}
//输出结果为 标记1、标记5、标记3

其他修改方法:将同步任务卡住的队列改成并发队列。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testDeadLock {
NSLog(@"标记1");
dispatch_queue_t other_queue = dispatch_queue_create("other_queue", DISPATCH_QUEUE_SERIAL);//不管是串行队列还是并发队列,都能解决这个死锁问题,因为它同步方法没有卡住这个other_queue。
dispatch_async(other_queue, ^{
​ NSLog(@"标记2");
​ dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"标记3");
​ });
​ NSLog(@"标记4");
});
sleep(2);
NSLog(@"标记5");
}
//输出结果为 标记1、标记2、标记5、标记3、标记4。

有了这些基础,你再看以下文章中的例子时,就能轻松判断是否会造成死锁了 。

iOS中自旋锁与互斥锁的区别

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。(附:获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active活跃状态的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快)

其他参考文章:iOS中自旋锁与互斥锁的区别

pthread_mutex 表示互斥锁。互斥锁可以传入不同参数,实现递归锁pthread_mutex(recursive)。

NSLock,NSCondition,NSRecursiveLock,NSConditionLock都是内部封装的pthread_mutex,即都属于互斥锁。@synchronized是NSLock的一种封装,牺牲了效率,简洁了语法。

OSSpinLock 表示自旋锁,从上图可以看到自旋锁的效率最高,但是现在的iOS因为优先级反转的问题,已经不安全,所以推荐使用pthread_mutex或者dispatch_semaphore。

总结
  自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
  互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

优缺点
  自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,cpu时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。