之前“写”过一篇关于多线程的博客,主要是综合各路大神的博文写得,回过头看其实对于多线程的相关知识点还是模糊的,从今天起重新认识多线程。
基础知识
进程、线程
进程,指的是系统中正在运行的一个应用程序,每个进程是独立的,简言之: 正在进行的程序即进程。
线程和线程是密不可分的,一个进程想要运行并执行任务,必须有线程才行。一个进程的所有任务都在线程中执行的。
例如: 使用迅雷下片儿都需要线程,一般我们在工程中,将耗时的任务放到子线程中执行。
串行
在同一时间内,一个线程只能执行一个任务,在串行中,如果要在一个线程中执行多个任务(串行),那么只能一个一个的按顺序执行这些任务。
多线程
一个进程中可以开启多条线程,每条线程可以并行(同时)进行,用于执行不同的任务。
如图:
多线程原理
同一时间,CPU 只能处理一条线程的任务,只有一条线程工作。多线程并发同时执行,其实是 CPU 快速地在多条线程之间调度,来回切换调度,造成了“同时”执行的假象,只不过 CPU 切换时间很快。其实也可以理解为“串行”。
多线程的缺点
- 多线程如果在很多条线程之间切换调度, CPU 会消耗大量的 CPU 资源,CPU 的开销会很大,很累;
- 多线程开的过多会降低每条线程的调动频次,造成线程执行效率低;
- 创建线程对内存是有开销的,而且需要时间,大约 90 毫秒的创建时间,所以创建的线程越多占用空间还有耗费时间也会越多、越久;
- 线程创建的越多,对于项目的设计会更加复杂,比如线程之间的通讯,多线程的数据共享。
常用多线程方案对比
以下为我们项目中的常用多线程方案,其中 GCD 多线程方案,使用相对多,相对“牛逼“,恩,是牛逼,面试没回答好 /(ㄒoㄒ)/ 。其中的 pthread 使用时需要导入 pthread 头文件,使用 C 语言实现。
NSThread 线程创建
我们在 viewController 中创建子线程,创建方法常用的有 ”3种“。
#pragma mark - 在touche方法中调用创建线程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self creatThread1];
[self creatThread2];
[self creatThread3];
}
#pragma mark -- 隐式创建子线程 (不能更详细的设置线程相关的信息)
- (void)creatThread3{
[self performSelector:@selector(run:) withObject:@"jake" ];
}
#pragma mark -- 自动创建线程 自动开启
- (void)creatThread2{
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"Mob"];
}
#pragma mark - 手动创建 线程创建完,会自动销毁,不用程序员管理
- (void)creatThread1{
//创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"run"];
//线程命名
thread.name = @"线程1";
//开启线程
[thread start];
}
- (void)run: (NSString *)pat{
NSLog(@"pat : %@ --- %@ ", pat , [NSThread currentThread].name);
[NSThread sleepForTimeInterval:2];//阻塞线程,让线程休眠两秒后执行
//遥远的未来 暂停到某个时刻
//[NSThread sleepUntilDate:[NSDate distantFuture]];
NSLog(@"阻塞线程,让线程休眠两秒后执行");
}
多线程安全
多条线程访问同一个地址,或是同时做同一件事情时,会出现访问错误的情况,例如买票、取款。
解决方法: 需要使用线程锁(互斥锁),在取值前添加,一般我们将 self 作为锁对象,锁对象一定要注意,只有一个,多个锁对象还会出现访问问题。
线程同步和线程锁为同一件事。
@synchronized (self) {
}
GCD 多线程
全称是Grand Central Dispath (牛逼的中枢调度器),纯C语言,提供非常多强大的函数,是目前苹果官网推荐的多线程开发方法,NSOperation便是基于GCD的封装。
GCD中有2个核心概念
(1)任务:执行什么操作
(2)队列:用来存放任务
容易混淆:同步、异步、并发、串行
同步和异步主要影响:能不能开启新的线程
同步:只是在当前线程中执行任务,不具备开启新线程的能力
异步:可以在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
并发:允许多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
并行和串行不会影响是否能开启线程。
GCD 自己可以创建 串行队列, 也可以创建并行队列.它有两个参数,第一个参数是标识符,用于 DEBUG 的时候标识唯一的队列,可以为空。第二个才是最重要的。第二个参数用来表示创建的队列是串行的还是并行的,传入DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。
//串行队列
dispatch_queue_t queue1 = dispatch_queue_create("GCD多线程,串行", DISPATCH_QUEUE_SERIAL);
// 异步任务
dispatch_async(queue1, ^{
NSLog(@"异步执行任务,开线程,在串行队列中执行");
});
}];
//并行队列
dispatch_queue_t queue2 = dispatch_queue_create("GCD多线程,并行", DISPATCH_QUEUE_CONCURRENT);
也可以:
dispatch_queue_t queue2 = dispatch_queue_create("GCD多线程,并行", NULL);
//同步任务
dispatch_sync(queue2, ^{
NSLog(@"同步任务:不会开启线程,在并行队列中执行");
});
GCD 官方已经提供了一个全局的并发队列,供整个应用使用
dispatch_queue_t queque3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_barrier_async 任务 “栅栏”
前面任务执行完之后才会执行它,执行完之后才会执行后面的。
// 全局并发队列 在 barrier 任务中不能使用全局队列 ×
dispatch_queue_t quent = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 要使用这个队列 √
dispatch_queue_t quent2 = dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(quent2, ^{
NSLog(@"---1--- %@",[NSThread currentThread]);
});
// “栅栏” 前面的任务执行完后,再执行 barrier 里面的,执行完之后才会执行后面的任务
dispatch_barrier_async(quent2, ^{
NSLog(@"---栅栏 2--- %@",[NSThread currentThread]);
[self run:@"li"];
});
dispatch_async(quent2, ^{
NSLog(@"---3--- %@",[NSThread currentThread]);
});
主队列 特殊的队列
在主队列中的任务都会在主线程中进行。
dispatch_queue_t queue4 = dispatch_get_main_queue();
由于队列和任务的不同,可以搭配出不同的组合,如上图,大体上的问题有两个,一个是否会创建新的子线程,还有是否会阻塞线程。
异步函数+并发队列
可以同时开启多条线程
同步函数 + 并发队列
不可以开新的线程,不能并发执行任务(只要不能开辟新的线程就不会并发执行任务)
异步函数 + 串行队列
可以开辟新的子线程,但是不能并发执行任务,只能开一条线程执行任务
异步函数 + 主队列
不管你是同步还是异步操作,只要是主队列,那么任务执行都会在主线程中执行,异步函数在主队列中将不会开辟线程。
同步函数 + 串行队列 同步函数 + 主队列
这两个组合比较特殊,一般不会在项目中这样干活搭配,这样的组合会造成堵塞线程。”你先“, ”还是你先“ 让来让去,谁都不能走了。
因为主队列要在主线程中执行,我们假设函数就在当前的主线程中,当执行主队列时,他想回到主线程,但是执行主线程的时候,需要同步的任务执行完才能执行主线程。然而此刻同步任务也想执行完,但是主线程还没执行完。把自己绕蒙逼了 /(ㄒoㄒ)/~~
简而言之: 主队列要执行,就得回到主线程,但是,同步任务还没执行完毕,无法完成祖国回归,回到主线程怀抱的梦想,所以就很尴尬了,最终隔岸相望。