一、什么是多线程
-
多线程
是指从软件或者硬件上实现多个线程并发执行
的技术。 - 由于一条线程
同一时间
只能处理一个任务
,所以线程里的任务必须按顺序
执行。 - 如果遇到耗时操作(网络通信、耗时计算、音乐播放等),等上一个操作完成再执行下一个任务,在此段时间内,用户得不到任何响应,这个体验无疑是极糟的。
- 因此,在iOS编程中,通常
将耗时操作
单独放在一个线程中
,而把用户交互
的操作放在主线程中
。
二、进程与线程
- 进程:
进程是指系统中正在运行的应用程序。这个'运行中的程序'就是一个进程。
每个进程都拥有着自己的地址空间。
进程有3个主要特征:
独立性:
进程是一个能够独立运行的基本单位,它既拥有自己独立的资源,又拥有着自己私有的地址空间。
在没有经过进程本身允许的情况下,一个用户的进程是不可以直接访问其他进程的地址空间的。
动态性:
进程的实质是程序在系统中的一次执行过程。
程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。
在进程中加入了时间的概念,它就具有了自己的生命周期和各自不同的状态,进程是动态消亡的。
并发性:
多个进程可以在单个处理器中同时进行,而不会相互影响。
- 线程:
多线程扩展了多进程的概念,使得同一进程可以同时并发处理多个任务。
一个程序的运行至少需要一个线程,一个进程想要执行任务,也必须要有至少一个线程,而这个线程就被称作主线程。
通常来说,只有一个主线程。
当进程被初始化后,主线程就被创建了,主线程是其他线程最终的父线程,所有界面的显示操作必须在主线程进行。
三、多线程的优势
-
进程间不能共享内存,但线程之间的共享内存是很容易的。
-
当硬件处理器的数量有所增加时,程序运行的速度更快,无需做其他调整。
-
充分发挥了多核处理器的优势,将不同的任务分配给不同的处理器,真正进入“并行运算”的状态。
-
将耗时、并发需求高的任务分配到其他线程执行,而主线程则负责统一更新界面,这样使得应用程序更加流畅,用户体验更好。
四、多线程的劣势
- 开启线程需要占用一定的内存空间
[默认情况下,主线程最大占用1M的栈区空间、子线程最大占用512K的栈区空间]
,如果要开启大量的线程,势必会占用大量的内存空间,从而降低程序的性能。 - 开启的线程越多,CPU在调度线程上的开销就越大,一般
最好不要同时开启超过5个线程
。 - 程序的设计会变得更加复杂,如线程之间的通信、多线程间的数据共享等。
五、线程的串行和并行
- 串行:
如果在一个进程中只有一个线程,而这个进程要执行多个任务,
那么这个线程只能一个一个的按照顺序执行这些任务,也就是说,
在同一时间内,一个线程只能执行一个任务,这样的线程执行方式称为线程的串行。
- 并行:
如果一个进程中包含的线程不止一条,
每条线程之间可以并行执行不同的任务,这叫做线程的并行,也叫多线程。
- 补充:
假如一个进程有3个线程,每个线程执行1个任务,3个下载任务没有先后顺序。可以同时执行。
同一时间,CPU只能处理一个线程,也就是只有一个线程在工作。
由于CPU快速的在多个线程之间调度,人眼无法察觉到,就造成了多线程并发执行的假象。
六、线程的状态
当线程被创建并启动之后,既不是一启动就进入执行状态,也不是一直处于执行状态,即便程序启动运行之后,它也不可能一直占用CPU独自运行。
由于CPU需要在多个线程之间进行切换,造成了线程的状态会在多次运行、就绪之间进行切换。
线程的状态主要有5个:
1. 新建New
当程序新建一个线程之后,该线程就处于新建状态。
和其他对象一样,只是由系统分配了内存,并初始化了内部成员变量的值。
此时的线程没有任何动态特征
2. 就绪Runable
当线程被start之后,该线程就处于就绪状态。
系统会为其创建 方法调用的栈和 程序计数器。
3. 运行Running
当CPU调度当前线程的时候,将其他线程挂起,当前线程变为运行状态。
当CPU调度其他线程时,当前线程回到就绪状态。
测试线程是否在运行,调用isExecuting方法,若返回YES,则处于运行状态。
4. 终止Exit
* 线程执行方法执行完成,线程正常结束
* 线程执行的过程出现异常,线程崩溃结束
* 直接调用NSThread类的exit方法,终止当前正在运行的线程
* 测试线程是否结束,调用isFinished方法判断,若返回YES,则已终止。
5. 阻塞Blocked
如果当前正在执行的线程需要暂停一段时间,并进入阻塞状态,通过NSThread类的两个方法:
//让当前执行的线程暂停到date参数代表的时间,并且进入阻塞状态
+ (void)sleepUntilDate:(NSDate *)date;
//让正在执行的线程暂停ti秒,并进入阻塞状态
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
七、线程间的安全隐患
进程中的一块资源可能会被多个线程共享,也就是多个线程可能访问同一块资源,这里的资源包括对象
、变量
、文件
等。
当多个线程同时访问同一块资源时,会造成资源抢夺
,很容易引发数据错乱
和数据安全
的问题。
为了解决这个问题,实现数据的安全访问,可以使用线程间的加锁
。
- 数据混乱示例:
/** 售票处理 */
- (void)saleTickets{
while (true) {
//模拟延时
[NSThread sleepForTimeInterval:1.0];
//判断是否还有票
if (self.leftTickets > 0) {
self.leftTickets--;
NSLog(@"%@卖了一张票,还剩下%lu张票",[NSThread currentThread].name,self.leftTickets);
} else{
NSLog(@"票已售完");
return;
}
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//设定总票数
self.leftTickets = 100;
//创建3个线程,启动后执行saleTickets方法卖票
NSThread *t1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
t1.name = @"1号窗口";
[t1 start];
NSThread *t2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
t2.name = @"2号窗口";
[t2 start];
NSThread *t3 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
t3.name = @"3号窗口";
[t3 start];
}
未加锁.png
可以看到,由于3个线程的并发操作
,同一时间抢夺一个资源
leftTickets,造成了剩余票数统计的混乱。
- 加锁示例:
@synchronized (obj)
{
//插入被修饰的代码块
}
- 使用同步锁的时候,要尽量让
同步代码块包围的代码范围最小
,而且要锁定共享资源的全部读写部分
的代码。
obj
就是加锁对象
,添加了锁对象后,锁对象实现了对多线程的监管,保证同一时刻只有一个线程执行
,当同步代码块执行完成后
,锁定对象就会释放同步监视器的锁定。
/** 售票处理 */
- (void)saleTickets{
while (true) {
//模拟延时
[NSThread sleepForTimeInterval:1.0];
//判断是否还有票
@synchronized(self){
if (self.leftTickets > 0) {
self.leftTickets--;
NSLog(@"%@卖了一张票,还剩下%lu张票",[NSThread currentThread].name,self.leftTickets);
} else{
NSLog(@"票已售完");
return;
}
}
}
}
加锁.png
线程添加同步锁后,实现了线程的同步运行,也就是说,使多线程按顺序执行任务。需要注意的是,同步锁会消耗大量的CPU资源。
网友评论