1. Target of Networking and multi-thread
2. Today goal
3. 同步和异步
4. 进程和线程
4.1 进程
4.2 线程
4.3 多线程
4. 多线程实现方案
5. pthread
5.1 pthread创建子线程步骤
6. NSThread
6.1 创建线程的三种方式
6.1.1 对象方法创建
6.1.2 类方法创建
6.1.3 NSObject(NSThreadPerformAdditions) 的分类创建
6.2 target & @selector relationship
6.3 thread state(life cycle)
6.3.1 新建
6.3.2 就绪
6.3.3 运行
6.3.4 阻塞
6.3.5 死亡
6.3.5.1 关于exit
6.3.5.2 取消线程(在线程执行的方法的外部取消)
6.4 thread常用属性
6.4.1 线程的优先级
6.4.2 QualityOfService
6.5 资源共享 & thread security
6.6 atomic属性
1. Target of Networking and multi-thread
-
掌握多线程原理
-
熟悉iOS开发中的常用多线程技术
-
知道SDWebImage的原理
2. Today goal
-
多线程的概念
-
多线程的原理
-
pthread创建线程的函数
-
NSThread三种方法
-
掌握线程状态和属性
-
知道线程安全
-
NSThread线程间通信
3. 同步和异步
-
同步和异步是任务执行的两种方式
-
同步:顺序执行,即多个任务按序依次执行,就是同步执行。
-
异步:多线程的带名字,多个任务同时执行。
4. 进程和线程
4.1 进程
-
进程:正在运行的应用程序叫进程
-
进程之间都是独立的,运行在专用且受保护的内存空间中
-
两个进程之间无法通讯
4.2 线程
-
线程:进程想要执行任务,必须要有线程,每个进程至少有一条线程
-
线程就是用来干活的
-
程序一启动,就会启动进程。进程默认开启一条线程
4.3 多线程
-
单核CPU同一时间,CPU只能处理1个线程,只有1个线程在执行任务.
-
多线程的同时执行 : 其实是CPU在多条线程之间快速切换(调度任务).
-
如果CPU调度线程的速度足够快,就造成了多线程同时执行的假象
-
如果线程非常多,CPU会在多条线程之间不断的调度任务,结果就是消耗了大量的CPU资源,效率下降:
-
每个线程调度的频率会降低
-
线程的执行效率会下降
-
4. 多线程实现方案
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 跨平台、可移植。Unix\Linux\ Windows | C | 程序员管理 | 几乎不用 |
NSThread | 面向对象,可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | 为了替代NSThread的技术方案,充分利用了设备的多核 | C | 自动管理 | 经常使用 |
NSOperation | 创建时间比GCD早,但是后期基于GCD做了重写。比GCD多了一些更简单实用的功能 | OC | 自动管理 | 经常使用 |
-
NSThread封装性最差,最偏向于底层,主要基于thread使用
-
GCD是基于C的API,直接使用比较方便,主要基于task使用
-
NSOperation是基于GCD封装的NSObject对象,对于复杂的多线程项目使用比较方便,主要基于队列使用
5. pthread
-
实现多线程的技术方案之一.
-
pthread是POSIX thread的简写。表示跨平台的线程接口.
5.1 pthread创建子线程步骤
-
导入头文件
-
创建子线程
pthread_create(pthread_t _Nullable * _Nonnull __restrict,
const pthread_attr_t * _Nullable __restrict,
void * _Nullable (* _Nonnull)(void * _Nullable),
void * _Nullable __restrict);
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan = %@",[NSThread currentThread]);
[self pthreadDemo];
}
- (void)pthreadDemo
{
NSLog(@"pthreadDemo = %@",[NSThread currentThread]);
/*
一, 参数
参数1 : 新线程的标示符,传入其地址;
参数2 : 新线程的属性,传入NULL; (nil : 表示空对象,常用于OC代码; NULL : 表示空地址,常用于C语言代码;)
参数3 : 新线程要执行的函数(任务),传入函数地址,即函数名;
void * (*) (void *)
函数返回值类型 函数名 函数参数
注意 : void * : 表示可以指向任何地址的指针;类似于OC的id;
参数4 : 传入到函数的参数;
二, 返回值 :
int类型;如果返回0,表示创建新线程成功;反之,表示创建新线程失败,并返回失败的编号;
C语言里面有时候并不是非零即真的原则;
因为成功的结果只有一个,但是,失败的原因有很多;
三, 查看主线程和子线程
当(number != 1) 表示是子线程;
当(number = 1, name = main) 表示主线程;
提示 : 不要纠结 number 到底等于几,它就是CPU给线程的编号而已,不由程序猿管理;
四, 桥接 : __bridge
在ARC环境下,编译器不会处理C语言申请的内存空间;
在ARC环境下,当出现C语言和OC语言的混合开发时,就需要使用桥接,实现数据类型的转换和自动内存管理;
*/
// 参数1 : 新线程的标示符
pthread_t ID;
// 定义函数的参数
// char *cStr = "hello";
NSString *ocStr = @"hello";
int result = pthread_create(&ID, NULL, demo, (__bridge void *)(ocStr));
if (result == 0) {
NSLog(@"创建新线程成功");
} else {
NSLog(@"创建新线程失败");
}
}
/// 新线程执行的函数
void *demo(void *param) {
// 查看当前函数执行的线程
// NSLog(@"demo = %s %@",param,[NSThread currentThread]);
NSString *str = (__bridge NSString *)(param);
NSLog(@"demo = %@ %@",str,[NSThread currentThread]);
return NULL;
}
-
内存管理
-
在 OC 中,如果是 ARC 开发,编译器会在编译时,根据代码结构,自动添加 retain/release/autorelease
-
ARC 只负责管理 OC 部分的内存管理,而不负责 C 语言 代码的内存管理
-
在ARC环境下,如果使用的 C 语言框架出现 retain/create/copy/new 等字样的函数,大多都需要 release,否则会出现内存泄漏
-
-
在混合开发时,如果在 C 和 OC 之间传递数据,需要使用 __bridge 进行桥接,桥接的目的就是为了告诉编译器如何管理内存
-
桥接的添加可以借助 Xcode 的辅助功能添加
-
MRC 中不需要使用桥接
-
6. NSThread
- NSThread是四种多线程技术方案中唯一基于thread的,每一个NSThread对象代表着一个线程
6.1 创建线程的三种方式
- 三种创建线程的方式,各有不同.随意选择.
使用哪种方式需要根据具体的需求而定.比如 : 如果需要线程对象,就使用对象方法创建.
6.1.1 对象方法创建
-
实例化线程对象的同时指定线程执行的方法
@selector(demo:)
-
需要用
- start
方法手动开启
线程
- (void)threadDemo1
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手动启动线程
[thread start];
}
6.1.2 类方法创建
-
detach方法直接创建并
自动开启
一个线程去@selector()
-
由于没有返回值,如果需要获取新创建的Thread,需要在执行的Selector中调用
[NSThread currentThread]
获取 -
无法获取到线程对象
- (void)threadDemo2
{
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}
6.1.3 NSObject(NSThreadPerformAdditions)
的分类创建
-
方便任何继承自NSObject的对象,都可以很容易的调用线程方法
-
无法获取到线程对象
-
自动开启线程执行
@selector()
.
- (void)threadDemo3
{
[self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}
6.2 target & @selector relationship
-
NSThread 的实例化方法中的 target 指的是开启线程后,在线程中执行 哪一个对象 的 @selector 方法
-
target : 指方法从属于的对象.
- 比如 : 本对象–self;其他对象–self.person.
-
@selector : 指对象里面的方法.
- 比如 : 要执行的是self中或者self.person中的哪个方法.
-
target和@selector的关系 : 执行哪个对象上的哪个方法.
6.3 thread state(life cycle)
6.3.1 新建
内存中创建一个线程对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
6.3.2 就绪
-
将线程放进可调度线程池,等待被CPU调度
-
注意:部分线程属性需要在启动前设置,线程启动之后再设置会无效。如
qualityOfService
属性
[thread start];
6.3.3 运行
-
CPU负责调度可调度线程池中的处于就绪状态 的线程
-
线程执行结束之前,状态可能会在
就绪状态
和运行状态
之间来回的切换 -
就绪状态
和运行状态
之间的状态切换由CPU来完成,程序员无法干涉
6.3.4 阻塞
-
正在运行的thread,当满足某个条件时候,可以用休眠或者锁来阻塞线程的执行
-
sleepForTimeInterval:休眠指定时长
[NSThread sleepForTimeInterval:1.0];
- sleepUntilDate:休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
- 上锁
synchronized(self)
-
看到
sleepUntilDate
:会想起runloop
的runUntilDate:
。他们都有阻塞线程的效果,但是阻塞之后的行为又有不一样的地方。-
sleepUntilDate
:相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务 -
runUntilDate
:虽然会阻塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate
:之后的代码
-
6.3.5 死亡
-
正常死亡:线程之行结束
-
非正常死亡
-
程序突然崩溃
-
满足某个条件后,线程内部强制退出,调用
exit
方法
-
6.3.5.1 关于exit
+ (void)exit;
-
终极API,使当前线程立刻退出。非常有可能导致内存泄漏等严重问题,一般不推荐使用
-
不能在主线程中调用该方法.会使主线程退出.
-
当前线程死亡之后,这个线程中的剩下的所有代码都不会被执行.
-
在调用此方法之前一定要注意释放之前由C语言框架创建的对象.
对于有runloop的线程,可以使用
runMode:beforeData:
启动,然后配合使用CGRunLoopStop()
退出。
6.3.5.2 取消线程(在线程执行的方法的外部取消)
//取消线程的方法
- (void)cancel NS_AVAILABLE(10_5, 2_0);
-
使用
[thread cancel]
-
这个方法只是修改了线程的状态而已,并没有真正的取消线程。仅仅只是将
cancelled
属性设置为YES
。线程取消的功能需要在main函数中自己实现。 -
如果要实现取消的功能,需要在线程的main函数中定期检查
isCancelled
状态来判断线程是否需要退出。如果不检查isCancelled
状态,那么调用- cancel
将没有任何意义。
if ([NSThread currentThread].isCancelled) {
NSLog(@"该线程已经被取消");
return;
}
6.4 thread常用属性
名称 | 用途 |
---|---|
name | 给线程命名,方便查找 |
stackSize | 栈区大小,看看线程在栈区占了多大空间 |
isMainThread | 是否是主线程 |
threadPriority | 线程的优先级,取值范围0~1.0 |
qualityOfService | 服务质量,iOS8.0推出,为了取代优先级。 |
6.4.1 线程的优先级
-
设置线程优先级threadPriority,取值范围0 ~1
-
0 表示优先级最低,1表示优先级最高
-
默认为0.5
-
优先级能够提高线程被调度的频率
-
优先级并不能保证优先级高的执行完代码。
6.4.2 QualityOfService
由于threadPriority是一个比较抽象的东西,很难区分0.3和0.6之间的区别。所以iOS8之后新增了qualityOfService枚举属性,通过枚举值设置优先级,减少了程序员的选择恐惧症。
-
设置线程的qualityofservice可以提高线程被调度的频率,但是不能保证较高等级的能够先执行完代码
-
NSQualityOfServiceUserInteractive
最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上 -
NSQualityOfServiceUserInitiated
次高优先级,用户需要,主要用于执行需要立即返回的任务 -
NSQualityOfServiceDefault
默认值,介于用户需要和实用工具之间 -
NSQualityOfServiceUtility
用户不需要立即得到结果 -
NSQualityOfServiceBackground
后台,用于完全不紧急的任务
关于优先级和服务质量
多线程的目的:是将耗时的操作放在后台,不阻塞主线程和用户的交互。
多线程开发的原则:简单
在开发时,最好不要修改优先级
内核调度算法在决定该运行哪个线程时,会把线程的优先级作为考量因素:
-
较高优先级的线程会比较低优先级的线程具有更多的运行机会
-
较高优先级不保证线程具体执行的时间,只是相比较低优先级的线程,更有可能被调度器选择执行而已
6.5 资源共享 & thread security
-
在多线程的环境下,共享的资源可能会被多个线程共享,也就是多个线程可能会操作同一块资源.
-
当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.
解决方法 :添加thread lock
-
可以保证被锁定的代码,同一时间,只能有一个线程可以操作.
-
锁对象 : self是最方便使用的锁对象。锁对象要满足两个条件:
-
一定要是全局的锁对象,要保证所有的线程都能够访问。
-
锁对象要求继承自NSObject。
-
-
锁定的范围应该尽量小,但是一定要锁住资源的读写部分。
-
加锁后,程序执行的效率会变低。牺牲了性能保证了安全性.
// 添加互斥锁
@synchronized(self) {
//下面就是需要所著的内容。就是资源的读写部分
......
}
6.6 atomic属性
- 获取执行到这行代码的精准时间。