iOS之多线程-1
预备知识:
1.进程与线程
进程:进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。(进程的查看可以在活动监视器去查看)
线程:1个进程要想执行任务,必须得有线程,每个程序至少要有一条线程。一个进程中所有的任务都是在线程中执行的。如果一个线程有多个任务需要处理,这就需要一一处理这些任务,这就是是线程的串行。
线程与进程的比较:
- 线程是CPU调用(执行任务)的最小单位。
- 进程是CPU分配资源和调度的单位。
- 一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程。
- 同一个进程内的线程共享进程的资源。
2.多线程
多线程:1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。
串行:如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,1个线程只能执行1个任务(也就是说线程中同一时间段只能做一件事情)。
并行:多个线程都是执行,就是叫并行。
多线程图解
多线程原理(单个CPU的情况下):
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
线程也不是开的越多越好,开得较多的话反而会降低效率,线程一般就3~5条最好。
3.主线程
主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。
主线程的作用:
- 显示\刷新UI界面
- 处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意:
- 将耗时操作尽量不放在主线程中执行。(文件上传等都是耗时操作)
- 耗时操作会卡住主线程,会造成UI界面的卡顿,造成不好的用户体验。
由于在线程中是串行处理的,如果一个耗时操作放在主线程中执行,如果用户点击了按钮,而和UI相关的都是放在主线程的,所以只有当耗时操作完成后才会去执行按钮的点击事件,这样就会造成卡顿,用户体验不好。
耗时操作优化处理的方式:将耗时操作放在子线程(非主线程,后台线程)中进行处理。
由于子线程和主线程是同时执行的,这样耗时操作放在子线程中执行的,在点击按钮的时候,就不会造成卡顿的现象出现。
4.多线程的实现方案
多线程的实现方案- pthread
- (IBAction)btnClickAction:(id)sender {
// 创建线程对象
pthread_t thread;
// 创建线程
/**
创建线程
@param _Nonnull#> 线程对象传递地址 description#>
@param _Nullable#> 线程的属性(优先级等) description#>
@param _Nonnull 指向函数的指针
@return 函数需要传递的函数
*/
pthread_create(&thread, nil, task, NULL); // 创建一条线程,如果要创建多个线程就可以将上面的代码copy下就可以了。
pthread_t threadB;
pthread_create(&threadB, nil, task, NULL); // 第二个线程
pthread_equal(thread, threadB); // 判断两个线程是不是相等。
}
void * task(void *parameter) {
NSLog(@"%@ ---- ", [NSThread currentThread]);
// 可以将耗时操作放在这里执行。
return NULL;
}
// 打印结果
2017-08-06 22:47:48.854 1-pthread[4855:328489] <NSThread: 0x608000266d40>{number = 3, name = (null)} ----
2017-08-06 22:47:48.854 1-pthread[4855:328490] <NSThread: 0x608000265b80>{number = 4, name = (null)} ----
- NSThread
创建线程的几个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self creatNewThreadMethod];
}
- (void)creatNewThreadMethod {
// 开启一条后台线程
[self performSelectorInBackground:@selector(run:) withObject:@"开启后台子线程"];
}
- (void)creatDetachNewThreadMethod {
// 创建分离子线程->会自动启动子线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
}
- (void)creatNewThread {
// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
// 启动线程,默认是暂停状态的
[thread start];
}
- (void)run:(NSString *)param {
NSLog(@"---run--%@", [NSThread currentThread]);
}
设置线程的名字以及优先级等属性
// 创建多个线程->给每个线程命名以及优先级
- (void)creatMutilThread {
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
thread1.name = @"thread 1";
thread1.threadPriority = 0.6; // 设置优先级(0.0~1.0,默认的优先级是0.5)
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
thread2.name = @"thread 2";
thread2.threadPriority = 0.3;
NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
thread3.name = @"thread 3";
thread3.threadPriority = 1.0;
[thread1 start];
[thread2 start];
[thread3 start];
}
- (void)run:(NSString *)param {
for (int i = 0; i < 10; i++) {
NSLog(@"---run--%@", [NSThread currentThread].name);
}
}
// 打印结果
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.490 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.492 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.493 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.495 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.497 2-NSThread基本使用[5136:356389] ---run--thread 2
打印次数越多,打印的数字就越趋于准确的值。
- 线程状态
线程的控制状态等操作:
// 启动
- (void)start;
// 阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 强制停止线程
+ (void)exit; // 一旦线程停止(死亡)了,就不能再次开启任务
// 和break不一致,表示任务执行完毕后才退出的。
5.多线程安全隐患
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
例子:
同一时间存取钱
以上的图展示存取钱是同时处理的,造成了最终的钱变少了,造成了数据的不当处理。
处理的方案
苹果的文档说明的解决方案就是加上一个互斥锁,在访问数据的时候加上互斥锁,只要次个线程才能访问处理,在访问结束的时候打开把互斥锁打开,然后另一个线程进行访问,同样加上互斥锁,循环如此,这样就解决了数据安全的隐患。
互斥锁
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.totalCount = 100;
self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadA.name = @"售票员A";
self.threadB.name = @"售票员B";
self.threadC.name = @"售票员C";
[self.threadA start];
[self.threadB start];
[self.threadC start];
}
- (void)saleTicket {
while (1) {
// 锁必须是全局唯一的。
// 1.注意点,不能随便能乱加,加锁的位置要注意
// 2.加锁的前提条件,--多线程共享同一个资源
// 3.注意加锁是需要代价的--需要耗费性能的
// 4.加锁的结果是造成线程同步。->ABC ABC ABC的循环执行任务
@synchronized (self) {
NSInteger count = self.totalCount;
if (count > 0) {
self.totalCount = count - 1;
// 卖出去一张票,还剩下多少张
NSLog(@"%@卖出去一张票, 还剩下%ld张", [NSThread currentThread].name, self.totalCount);
} else {
NSLog(@"没票了");
break;
}
}
}
}
6.原子和非原子性
在oc中定义属性时有nonatomic和atomic两种选择;
atomic:原子属性,为setter方法加锁(默认就是atomic),意味着线程是安全的。
nonatomic:非原子属性,不会为setter方法加锁。
7.线程间的通信
有的时候,同一个进程跑了多个线程,有的子线程的输出的结果是另一个子线程的输入,这就需要两个子线程中进行一种通信。所以线程往往不是孤立存在的,多个线程之间需要经常进行通信。
线程之间的通信的方法:
// 回到主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 回到线程对象,可以回到主线程也可以回到子线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
例子:下载图片并展示
如果不做线程的处理,
- (void)wjDownloadImageCountTime {
NSDate *start = [NSDate date]; // 获得当前时间
NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/d/573d37cc14e3c.jpg"];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *img = [UIImage imageWithData:imageData];
self.imageView.image = [img imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
NSDate *end = [NSDate date];
NSLog(@"count time is: %f", [end timeIntervalSinceDate:start]);
}
以上能够完成图片的下载,图片的展示也能成功,但是一旦UI上的图片比较多的话,就会阻塞主线程,所以不采取。
优化的处理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 放在子线程去下载图片
[NSThread detachNewThreadSelector:@selector(wjDownloadImage) toTarget:self withObject:nil];
}
- (void)wjDownloadImage {
// http://img4.imgtn.bdimg.com/it/u=816246739,294523191&fm=214&gp=0.jpg
// 1.下载图片的url
NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/d/573d37cc14e3c.jpg"];
// 2.下载图片到本地->二进制数据 -> 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:url];
// 3.将二进制数据转为图片
UIImage *img = [UIImage imageWithData:imageData];
// 4.显示UI->回到主线程去刷新UI界面
[self performSelectorOnMainThread:@selector(wjShowImage:) withObject:img waitUntilDone:YES];
// 简便方法,就不需要写下面的那个方法了。
// [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:img waitUntilDone:YES];
}
- (void)wjShowImage:(UIImage *)img {
self.imageView.image = img;
}