一、进程、线程及关系
一、什么是进程?
进程是具有一定独立功能程序关于某次数据集合的一次运行活动,操作系统分配资源的基本单元也是最小单位。
进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app就是一个进程。
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。
二、什么是线程?
线程是CPU调度的最小单位。
一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
三、进程和线程的关系
进程和线程的关系,可以简单的形象的做个比喻:进程=火车,线程=车厢
线程须在进程下才能运行的(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)
不同进程间的数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车中一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,线程最多适合多核不同火车(可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
二、任务、队列及执行任务的方式
一、任务
- 任务就是要执行的操作,也就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。
二、队列
一、队列有两种:
-
串行队列:
- 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
-
并行队列:
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
二、执行队列的方式
-
同步执行(sync):
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
异步添加任务到指定的队列中,它不会造成主线程的等待,可以继续执行任务.
可以在新的线程中执行任务,具备开启新线程的能力。
队列和任务的关系
任务和队列的关系可以打一个形象的比喻,比如小时后玩的四驱车,队列=跑道,串行队列=一条跑道,并行队列=多条跑道,四驱车=任务)。
基本规则就是每条跑道上只能有一辆车在上面跑。串行队列由于只有一条跑道,所以每次只能跑一辆车(一个任务),等这辆车跑完,别的车(任务)才能跑。并发队列由于有多个跑道,所以可以供多辆车(多个任务)一起跑。
执行队列的方式就相当于我们如何把车(任务)放到跑道(队列)里
-
同步执行不开启新线程:
就相当于我们只有主线程一只手,每次只能把一辆车(任务)放到跑道上跑,等车跑完,把车收了以后,才能把下一辆车(任务)放到跑道上跑
所以不管我们是把车(任务)放到单条跑道(串行队列)还是把车(任务)放到多条跑道(并发队列),每次都只能控制一辆车。并且由于主线程在玩车(执行任务),也就干不了别的事,所以同步执行会造成主线程等待。
-
异步执行可以开启新线程:
就相当于我们除了有主线程这只手外还邀请了很多一起玩的小伙伴(分线程),我们每个人都能拿起一辆车(任务)放到对应的跑道(队列)上,然后一起跑。所以在多条跑道(并发队列)上多辆车(多个任务)可以一起跑。
如果车(任务)很多,跑道只有一个(串行队列),那么还是得排队玩,每次只能跑一辆车(任务)。但是,由于邀请了小伙伴(开启了线程),主线程这只手就可以让小伙伴(分线程)先玩车,自己去处理别的事情。所以异步执行不会造成主线程等待。
四、多线程引起的死锁
死锁就是队列引起的循环等待。
产生死锁的情况:
- 一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
}
在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。
viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
- 二、在同串行队列下异步任务里调用同步任务
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
外面的函数无论是同步还是异步都会造成死锁。
这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决
二、在不同的串行队列,异步block调用同步不会引起死锁
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue2, ^{
NSLog(@"serialQueue2");
});
NSLog(@"serialQueue");
});
serialQueue、serialQueue2是两个不同队列,所以不存在队列引起的循环等待。
这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。
打印结果:
serialQueue2
serialQueue
五、GCD任务执行顺序
- 1、串行队列先异步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(serialQueue, ^{
NSLog(@"4");
});
NSLog(@"5");
代码打印结果为:1 3 2 4 5
首先先打印1,
接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
然后是任务4,将任务4添加到串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行。所以先打印2
又因为任务4是同步任务,会阻塞线程,所以再打印4.
最后打印5
- 2、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});
这里的test方法是不会去执行的,原因在于
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay;
这个方法要创建提交任务到runloop上的。
gcd底层创建的子线程是默认没有开启对应runloop的,所有这个方法就会失效。
解决方法:
将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行。
或者将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的,如果不是就还是不执行)。
在就是开启子线程的run,在perfomselector下开发runloop就行代码如下:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
[[NSRunLoop currentRunLoop] run];
});
开启子线程的runloop就会执行了。
六、自旋锁与互斥锁
一、自旋锁
自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
使用场景:对持有锁较短的程序
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
二、互斥锁
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕。
当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。
三、各自优缺点:
自旋锁优点:
自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。
所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
自旋锁缺点:
- 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
- 自旋锁不能实现递归调用。
自旋锁:
- atomic
- OSSpinLock
- dispatch_semaphore_t 等
互斥锁如:
- pthread_mutex
- @ synchronized
- NSLock、NSConditionLock
- NSCondition
- NSRecursiveLock等
六、NSThread+runloop实现常驻线程
一、为什么去实现常驻线程,常驻线程使用背景?
由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用。
那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程。
一、实现常驻线程的步骤
- 首先常驻线程既然是常驻,那么可以用GCD实现一个单例来保存NSThread 代码如下:
+ (NSThread *)shareThread {
static NSThread *_shareThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
[_shareThread setName:@"threadTest"];
[_shareThread start];
});
return _shareThread;
}
这样创建的thread就不会销毁了吗?
[self performSelector:@selector(test) onThread:[ViewController shareThread] withObject:nil waitUntilDone:NO];
- (void)test {
NSLog(@"test:%@", [NSThread currentThread]);
}
执行结果并没有打印,说明test方法没有被调用。
当创建的NSThread没有开启runloop的话,常驻线是不会有效的。
因为新建的子线程默认没有开启runloop
因此需要给这个线程添加了一个runloop,并且加了一个NSMachPort端口监听,防止新建的线程由于没有活动直接退出。
- 其次给线程开启runloop。
需要给这个线程添加了一个runloop,并且加了一个NSMachPort端口监听,防止新建的线程由于没有活动直接退出。。
- (void)threadTest
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
二、常驻线程的使用
performSelector是封装在NSObject的NSThreadPerformAdditions类别里,只要是NSObject直接调用
[self performSelector: onThread: withObject: waitUntilDone:]
三、常驻线程退出
只有从runloop中移除我们之前添加的端口,这样runloop没有任何事件,所以直接退出。方法如下:
[NSRunLoop currentRunLoop]removePort:<#(nonnull NSPort *)#> forMode:<#(nonnull NSRunLoopMode)#>