深入浅出iOS多线程(一)——线程的概念
深入浅出iOS多线程(二)——pthraed和NSThread的使用
深入浅出iOS多线程(三)——GCD多线程
深入浅出iOS多线程(四)——NSOperation多线程
深入浅出iOS多线程(五)——多线程锁
pthread
pthread简介
pthread 是属于 POSIX 多线程开发框架,POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),如果想学习这套API,在网上是可以找到相关的资料等,由于在iOS中有NSThrad,如果不考虑移植性,那么在iOS开发中基本上不回去使用,所以只是了解,pthread是多线程的一种技术实现。
iOS中的pthread
在iOS中需要导入头文件pthread.h才能够使用pthrad的Api
#import <pthread.h>
pthraed的特点
- 一套通用的多线程API
- 跨平台可移植
- 使用难度比较大
- 基于C语言的开发
pthread的简单使用
/**
参数:
1.指向线程标示的指针
2.线程的属性
3.指向函数的指针
4.传递给该函数的参数
返回值
- 如果是0,标示正确
- 如果非0,标示错误代码
void * (*) (void *)
返回值 (函数指针) (参数)
void * 和OC中的 id 是等价的!
*/
pthread_t pthreadId ;
NSString *str = @"敲代码";
int result = pthread_create(&pthreadId,
NULL,
&doing,
(__bridge void *)(str)
);
if(result == 0){
NSLog(@"开启成功");
}else{
NSLog(@"开启失败");
}
void * doing(void * param){
NSLog(@"%@,%@",[NSThread currentThread],param);
return NULL;
}
NSThread
iOS的多线程NSThread简介
NSThread是苹果官方提供面向对象操作线程的技术,简单方便,可以直接操作对象,需要手动控制线程的生命周期,平时iOS开发较少使用,使用最多的是获取当前线程
NSThread特点
- 面向对象的多线程编程
- 简单易用,可直接操作线程对象
- 需要手动管理线程的生命周期
NSThread的详细使用介绍
如何开启NSThread线程
NSThread初始化API
//初始化的API
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
- (instancetype)initWithBlock:(void (^)(void))block
//类对象方法
+ (void)detachNewThreadWithBlock:(void (^)(void))block
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
上述实例方法以及类对象方法,一样都是创建一个新的线程,不一样的是,类对象方法不需要创建完成以后调用start
方法,而alloc创建的线程需要手动start
开启。
NSThread代码实现
//方法一:
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(demo1Doing:) object:@"hello"];
[thread start];
//方法二:
NSThread *thread1 = [[NSThread alloc]initWithBlock:^{
NSLog(@"%s",__func__);
}];
[thread1 start];
//方法三:
[NSThread detachNewThreadSelector:@selector(demo1Doing:) toTarget:self withObject:@"hello"];
//方法四
[NSThread detachNewThreadWithBlock:^{
NSLog(@"%s",__func__);
}];
NSThread主线程的API和获取主线程
-
判断是否是主线程,获取主线程
+ (NSThread *)mainThread; // 获得主线程 - (BOOL)isMainThread; // 是否为主线程 + (BOOL)isMainThread; // 是否为主线程
-
获取当前线程
NSThread *current = [NSThread currentThread];
-
设置和获取线程的名字
- (void)setName:(NSString *)n; - (NSString *)name;
其他创建线程的方式
-
自启动线程
[NSThread detachNewThreadSelector:@selector(demo1Doing:) toTarget:self withObject:@"hello"]; [NSThread detachNewThreadWithBlock:^{ NSLog(@"%s",__func__); }];
-
隐式创建并启动线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait; - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
总结
上述两种创建线程方式的优缺点:
- 优点:简单快捷
- 缺点:无法对线程进行更详细的设置和管理
控制线程状态
-
启动线程
// 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态 - (void)start;
-
阻塞线程
//进入阻塞线程状态 休眠 + (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti;
-
强制停止线程
//进入死亡状态 + (void)exit;
线程的优先级
-
线程的优先级API
+ (double)threadPriority; + (BOOL)setThreadPriority:(double)p;
-
优先级设置
- Priorit的值 0.0~1.0之间
- 优先级只能保证CPU调度的可能性会高,归根究底你还是无法控制多线程的顺序,如果就靠优先级,来误认为控制多线程额顺序是不严谨的。
- 多线程的目的:是不阻塞UI线程
- 建议不要修改优先级
- 多线程开发中不能相信一次的运行结果
- 优先级翻转,优先级低的任务太耗时放到最后面,然后后面排的任务比较多,优先级高的任务被堵死了
多线程的安全隐患问题
安全隐患?
- 资源共享
- 一块资源可能会被多个线程共享,也就是说多个线程可能会访问同一块资源
- 例如:多个线程同时访问修改同一个变量、同一个文件、同一个对象
- 数据错乱
- 同一个线程修改一个数据,不同线程不同的运行结束时间,有可能得到不一样结果
安全隐患问题分析
- ThreadA去访问一块内容中的integer数据,得到数据17,17+1 = 18写入内存
- ThreadB也在同一时间访问了integer数据,得到数据17,17+1 = 18写入内存
-
结果非常有意思的是+1了两次应该是19才对,最终结果是18,这就是多线程的安全隐患问题,如下图所示
如何解决多线程的安全隐患(线程锁)
互斥锁
当ThreadA去访问一块内容中的integer数据的时候,首先上一把锁
lock
得到数据17,17+1 = 18写入内存,最后在unlock
-
ThreadB也在同一时间访问了integer数据:
由于ThreadA已经在数据上面加了锁
lock
,所以必须等到ThreadA完成以后才能去访问这个数据-
ThreadA完成,访问integer数据时,时候
lock
然后在获取数据18,18+1 = 19写入内存,最后在unlock
//互斥锁 @synchronized (self) { } ``
-
@synchronized
的参数:- 任意OC对象都可以加锁
- 加锁一定要加锁共有的对象,一般用self
-
互斥锁的优缺点:
- 优点:能有效防止因多线程抢夺资源造成的数据安全问题
- 缺点:需要消耗大量的CPU资源
-
互斥锁的使用前提
- 多条线程抢夺同一块资源
-
线程同步
- 线程同步的意思就是:多条线程同一条线上工作(按顺序地执行任务)
- 互斥锁,就使用了线程同步技术
互斥锁需要注意的地方
保证代码内的代码,同一时间,只有一条线程执行
互斥锁的范围应该尽量小,范围大了,效率就差
-
结果本身是一个多线程开发,最后结果变成了同步去执行,互斥锁,也就是同步线程技术,如下图所示:
如何解决多线程的安全隐患(原子与非原子对象)
nonatomic 非原子属性
- 不会为setter方法加锁
- 非原子属性,因为
atomic
所有对这个对象的操作之前会加锁,所以会很耗费资源,在没有安全隐患的问题上在加锁,是不必要的
atomic 原子属性
为setter方法加锁(默认是atomic)
-
原子属性,保证这个属性的安全性(线程安全),多线程写入这个对象的时候,保证同一时间只有一个线程能够执行!
- 模拟一个
atomic
原子属性
//模拟原子属性 - (void)setMyAtomic:(NSObject *)myAtomic{ @synchronized (self) { _myAtomic = myAtomic; } }
- 实际上,原子属性内部有一个锁,自旋锁:
- 自旋锁和互斥锁不一样的地方
- 共同点:都能够保证线程的安全
- 不同点:互斥锁:如果线程被锁到外面,线程就会进入休眠状态,等待锁打开,打开之后被唤醒;自旋锁:如果线程被锁在外面,就会用死循环的方式,一直等待锁打开。
- 无论什么锁,都会消耗新能,效率不高
- 线程安全
- 在多个线程进行读写操作时,仍然保证数据正确
- 模拟一个
-
UI线程
- 共同的约定,所有更新UI的操作都放在主线程执行
- 因为UIKit 框架都是线程不安全的(因为线程安全效率低下)
-
nonatomic和atomic对比
- atomic:线程安全,需要消耗大量的资源
- nonatomic:非线程安全,适合内存小的移动设备
-
iOS开发使用nonatomic和atomic
- 所有属性都应声明nonatomic
- 尽量避免多线程抢夺同一块资源
- 尽量将加锁、资源抢夺的业务逻辑交给服务器处理,减小移动客户端的压力
注意一个小细节
- OC中:定义一个属性,通常会生成成员变量,如果同时重写了getter、setter那么成员变量就不会自动生成
- 如果想要同时重写了getter、setter,那么就直接使用 @synthesize myAtomic = _myAtomic;
NSThread自定义
在NSThread的有init
初始化方法:
//用alloc init 适用于自定义NSThread (子类)
NSThread * t = [[NSThread alloc]init];
需要创建一个新的子类继承NSThread方法,然后重写main方法
多线程下载网络图片
-(void)loadView{
}
如果重新了上述方法,SB和XIB都无效
代码如下:
#import "ViewController.h"
@interface ViewController ()<UIScrollViewDelegate>
@property(nonatomic,strong)UIScrollView * scrollView;
@property(nonatomic,weak) UIImageView * imageView;
@property(nonatomic,strong) UIImage * image;
@end
@implementation ViewController
/**
加载视图结构的,纯代码开发
功能 SB&XIB 是一样
如果重写了这个方法,SB和XIB 都无效
*/
-(void)loadView{
//搭建界面
self.scrollView = [[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.view = self.scrollView;
//MARK:- 设置缩放属性
self.scrollView.delegate = self;
self.scrollView.minimumZoomScale = 0.1;
self.scrollView.maximumZoomScale = 2.0;
//imageView
UIImageView * iv = [[UIImageView alloc]init];
//会调用View的getter方法. loadView方法在执行的过程中!如果self.view == nil,会自动调用loadView加载!
[self.view addSubview:iv];
self.imageView = iv;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage) object:nil];
[t1 start];
}
//MARK: - 下载图片
-(void)downloadImage{
NSLog(@"%@",[NSThread currentThread]);
//NSURL -> 统一资源定位符,每一个URL 对应一个网络资源!
NSURL * url = [NSURL URLWithString:@"https://images.unsplash.com/photo-1496840220025-4cbde0b9df65?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80"];
//下载图片(在网络上传输的所有数据都是二进制!!)
//为什么是二进制:因为物理层!!是网线!!网线里面是电流!!电流有高低电频!!高低电频表示二进制!!!
NSData * data = [NSData dataWithContentsOfURL:url];
//将二进制数据转成图片并且设置图片
//提示:不是所有的更新UI在后台线程支持都会有问题!!!
//重点提示:不要去尝试在后台线程更新UI!!!出了问题是非常诡异的!!
// self.image = [UIImage imageWithData:data];
//在UI线程去更新UI
/**
* 1.SEL:在主线程执行的方法
* 2.传递给方法的参数
* 3.让当前线程等待 (注意点!! 如果当前线程是主线程!哥么YES没有用!!)
*/
// 线程间通讯
[self performSelectorOnMainThread:@selector(setImage:) withObject:[UIImage imageWithData:data] waitUntilDone:NO];
}
//这种写法 省略一个 _image ,主要原因是因为image 保存在了imageView里面了!
-(UIImage *)image{
return self.imageView.image;
}
-(void)setImage:(UIImage *)image{
NSLog(@"更新 UI 在====%@",[NSThread currentThread]);
//直接将图片设置到控件上
self.imageView.image = image;
//让imageView和image一样大
[self.imageView sizeToFit];
//指定ScrollView 的contentSize
self.scrollView.contentSize = image.size;
NSLog(@"\n\n\n\n\n\n\n\n\n\n\n%@",self.image);
}
#pragma mark - <scrollView代理>
//告诉 ScrollView 缩放哪个View
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return self.imageView;
}
/**
* transform 矩阵
* CGFloat a(缩放比例), b, c, d(缩放比例); 共同决定角度!
* CGFloat tx(x方向位移), ty(y方向的位移);
*
*/
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
NSLog(@"%@",NSStringFromCGAffineTransform(self.imageView.transform));
}
@end
提示:不是所有的更新UI在后台线程支持都会有问题!!!
重点提示:不要去尝试在后台线程更新UI!!!出了问题是非常诡异的!!
-
在UI线程去更新UI
/** * 1.SEL:在主线程执行的方法 * 2.传递给方法的参数 * 3.是否让当前线程等待 (注意点!! 如果当前线程是主线程!YES没有用!!) * NO当前线程不需要等待@selector(setImage:)执行完成,YES当前线程需要等待@selector(setImage:)执行完成 */ // 线程间通讯 [self performSelectorOnMainThread:@selector(setImage:) withObject:[UIImage imageWithData:data] waitUntilDone:NO];
线程间的通信
@interface NSObject (NSThreadPerformAdditions)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
@end
NSPort实现线程通信
代码如下:
@interface ViewController () <NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;
@property (nonatomic, strong) NSPort* mainThreadPort;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mainThreadPort = [NSPort port];
self.mainThreadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
[self task];
}
- (void) task {
NSThread* thread = [[NSThread alloc] initWithBlock:^{
self.subThreadPort = [NSPort port];
self.subThreadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread setName:@"子线程"];
[thread start];
}
- (void)handlePortMessage:(id)message {
NSLog(@"%@", [NSThread currentThread]);
if (![[NSThread currentThread] isMainThread]) {
NSMutableArray* sendComponents = [NSMutableArray array];
NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
[sendComponents addObject:data];
[self.mainThreadPort sendBeforeDate:[NSDate date] components:sendComponents from:self.subThreadPort reserved:0];
return;
}
sleep(2);
NSMutableArray* components = [message valueForKey:@"components"];
if ([components count] > 0) {
NSData* data = [components objectAtIndex:0];
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", str);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
@end
NSThread需要注意的地方
子线程执行太快,还会调用线程通信的代码吗?
不会
有时候会出现这个问题,代码如下:
NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
[t1 start];
//不执行地方原因,是因为 demo 方法执行的快!""
[self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
-(void)demo{
NSLog(@"%@",[NSThread currentThread]);
}
-(void)otherMethod{
self.finished = YES;
}
- Demo执行额太快,因为子线程是没有RunLoop的,当demo执行完成以后就消失了,所以不会在执行
otherMethod
。
如何解决上述问题
在子线程开启RunLoop,
NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
[t1 start];
//不执行地方原因,是因为 demo 方法执行的快!""
[self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
-(void)demo{
NSLog(@"%@",[NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
}
-(void)otherMethod{
NSLog(@"%s %@",__FUNCTION__,[NSThread currentThread]);
}
RunLoop开启了循环,这样就会无限制的进行循环,这样这个子线程就永远不会释放
改进的办法就是,从外面创建一个
BOOL
来判断是否需要关闭RunLoop
@interface ViewController ()
/** 循环条件 */
@property(assign,nonatomic,getter=isFinished)BOOL finished;
@end
@implementation ViewController
NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
[t1 start];
self.finished = NO;
//不执行地方原因,是因为 demo 方法执行的快!""
[self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
-(void)demo{
NSLog(@"%@",[NSThread currentThread]);
//启动当前RunLoop 哥么就是一个死循环!!
//使用这种方式,可以自己创建一个线程池!
// [[NSRunLoop currentRunLoop] run];
//在OC中使用比较多的,退出循环的方式!
while (!self.isFinished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
-(void)otherMethod{
for (int i = 0; i<10; i++) {
NSLog(@"%s %@",__FUNCTION__,[NSThread currentThread]);
}
self.finished = YES;
}