OC总结篇 - 多线程

进程和线程
进程
1. 在系统中正在运行的一个应用程序
2. 每个进程之间是独立的,它们均运行在其专用且受保护的内存空间内.若你此时打开了微信,又打开了QQ音乐,则系统会分别启动两个进程.
3. iOS开发都是单进程,安卓可以支持多进程
4. 进程可以控制很多条线程来执行相应的任务
5. 进程至少要有一条线程,用来执行任务


线程
1. 是进程的基本执行单元,1个进程若想执行任务,至少要有一个线程
2. 系统会默认开启一条线程,称为主线程或UI线程
3. 线程上的任务执行完毕后,线程会自动销毁
4. 但是开辟线程要耗费一定的内存空间,会耗时,异步不会堵塞线程,会先执行下面的语句,就是因为辟线程耗时了
5. 我们开辟线程时有时会给他们命名,这样是为了打断点调试时能看到调用的堆栈信息



进程和线程的关系
1. 地址空间
   进程之间是独立的地址空间,但同一进程的线程共享本进程的地址空间

2. 资源拥有
   进程之间的资源是独立的,但同一进程的线程共享本进程的资源(如内存,I/O,cpu等)

3. 线程是处理器调度的基本单位,但进程不是
   意思是CPU调度是调度线程

4. 执行过程
   进程可以独立执行,每个独立的进程都有一个程序运行的入口
   线程不能独立执行,必须存活在应用程序中

5. 崩溃
   进程崩溃时,对其他进程没有影响
   线程崩溃时,整个进程就死掉了
   多进程比多线程健壮

6. 切换
   进程切换消耗资源大
   线程切换消耗资源小
重要: 线程是执行任务的
多线程的原理

CPU在单位时间片里快速的在各个线程之间切换

多线程意义
优点
1. 提高执行效率
2. 提高资源(CPU,内存)利用率
3. 线程执行完任务后,会自动销毁
缺点
1. 开启线程需要占用一定的内存空间,进行耗时
2. 线程越多,耗时越多,会降低程序的性能,也会增大CPU在调用线程上的开销
3. 程序设计会更加复杂,比如线程间的通信,多线程的数据共享
多线程的特点

多线程没有顺序
关于任务 : 先添加的不一定先执行完毕
这里先的意思只是先调度,就是先让你出去执行的意思 参考:排队买奶茶
先付钱的不一定先拿到奶茶
任务何时执行完毕依赖以下几点

1. 队列

2. CPU的调度: 在各个线程之间不断的切换来执行任务,CPU切到别的线程时,则当前线程就挂起了,所以有时会出现:任务A执行一半就不执行了

3. 线程(是执行任务的): 
线程状态:是否当前线程suspend,任务A先加到线程A中,任务B后加到线程B中,但可能在A加入到线程A的时候,CPU正好切到线程B,那么A就没有B结束任务快
线程优先级
线程池状态

4. 任务耗时
线程的生命周期

1.新建一个线程
2.必须start,否则没用,在start的时候,线程会进入runnable就绪状态,所以不能重复start
3.CPU调度当前线程,进行Running状态
4.CPU不会一直在这个线程执行,会间断的切换到其他线程执行(这就是多线程)
然后有两种情况
5.任务执行完毕,线程强制退出
6.线程堵塞,会等待同步锁等等,总有一个时间段是会执行完毕的,当执行完毕时,会获取同步锁,重新添加回可调度线程池

备注:第三步CPU是怎么调度线程的?
线程是被放到线程池中的,线程池中如果有线程,会开始执行,如果没有,会先判断线程池的大小,如果小于核心基本池大小,就创建线程来执行,如果大于核心基本池大小,那么就要等待被加入队列中,队列如果没满,就push进队列,队列如果是满的,就需要另外去等待

线程状态,建议根据状态来改变线程
start
execute
cancel
exit
wait
finish

YY,SD等第三方库会重写线程的start和main方法
当我下载图片时,我的请求已经发送了到线程了,但线程是否执行我不知道,因为有可能在堵塞,如果它一直不回来,我想把这个请求停掉的时候,就可以重写

还有就是我们发送GET或者POST请求,发出去你怎么终止呢? 无法终止,只能操作线程状态来终止

总结:我们可以通过掌握线程的生命周期来掌管程序的可控性

线程调度池的原理

先判断大小是否小于核心线程池的大小,小于直接执行
大于的话线程池就没有能力去开辟新的线程了,只能依赖于原有的线程去执行
这时候判断原有线程上的工作队列是否已满,假如线程13未满,则判断这个13线程是否正在执行,没执行的话就把任务放12上去执行
如果12上的队列已经满了,就会创建你刚创建的线程去执行

❤️如果大小超过线程核心池,其他线程上队列都满了,自己线程的队列也满了,就会进入饱和策略

当有任务进来,发现饱和了,那么我们怎么设计这个饱和策略呢?

  1. 排队 等待
  2. 抛异常, 不要了
  3. 直接push进去,将前面的久远的任务推出去
线程安全问题

例如卖票系统,100张票,好几个票点同时卖,数据就不对了
❤️造成这个的原因是多线程的资源抢夺

还有一个情况,就是你在查找数据库的过程中,又对数据库进行增删改查了,也是线程之间的资源抢夺问题

为了解决这种问题,需要加锁来解决

多线程与锁

有哪些锁

自旋锁和互斥锁的区别:
当发现有线程在操作任务时
自旋锁一直在转,忙等,一直问你做完了没有,等线程出来后它就会立刻进去执行,适合代码量较小,耗时较小的的
互斥锁会打盹,等线程出来后,会先醒盹,然后再执行
读写锁:多读单写,允许多条线程读,但只允许一条线程写,写影响读,读不影响写
例如很多地方都能调用set方法例如self.name,可以在set里面加一把互斥锁,保证在同一时间只有一条线程写入,其他线程等待
读的时候不加锁,随便读

- (void)setName:(NSString*)name{
synchronized(self){
_name = name;
}
}

常见的几种锁
1.互斥锁 @synchronized(self){里面写需要上锁的代码}
   互斥锁分递归锁和非递归锁,这是递归锁,非递归就是当你重复访问资源时会崩溃
2. atomic原子性,也是自旋锁
3. OSSpinLock自旋锁
4. NSLock
5. NSRecursiveLock递归锁
6. dispatch_semaphore_t信号量

下面分别介绍下这些锁

1. @synchronized
一般在创建单例对象的时候使用,来保证多线程情况下创建的对象是唯一的

2. atomic
修饰属性的关键字,可以对被修饰对象进行原子操作(不负责使用,意思是赋值操作可以保证线程安全,但其他操作例如增删改不能保证线程安全,需要我们做额外的线程保护)
虽然是线程安全的,但因为是自旋锁,一直忙等,会消耗大量的资源


3. OSSpinLock自旋锁: 尽量不要用,会产生一个优先级的问题,相对不再安全了
专门用于轻量级的数据访问,简单的int值,对引用计数进行+1-1操作
循环等待访问,并不释放当前资源,类似于一个while循环,一直在访问能不能获得当前这个锁,如果不能获得,就继续循环,直到有一次能获得到这个锁,才会停止这个循环
如果第一次获得到了,后续线程再想获得这个锁,是获取不到的,它会释放所持有的当前资源,然后对自己进行一个阻塞行为

4. NSLock
最经常用的,一般用来解决线程同步问题,lock是加锁,unlock是解锁,必须成对出现,但是可能会造成死锁
5. NSRecursiveLock递归锁
可以解决上面问题,它的特性是可以重入
6. dispatch_semaphore_t信号量

是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。

也是用来实现线程同步,包括对共享资源互斥访问的一个信号量机制

信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

注意: 信号量可以设置线程的并发量
若信号量设置并发线程为1,也不会出现资源抢夺,因为每次只出来一条线程

相关函数

dispatch_semaphore_create(1)
创建一个Semaphore并初始化信号的总量
函数内部创建了一个结构体Semaphore,里面一个是信号量的值,第二个是线程列表
dispatch_semaphore_wait(semaphone,DISPATCH_TIME_FOREVER)

可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。,参数(要等待的信号量是哪个,等待时长)

对value值进行-1,-1之后若小于0,意味着当前没有资源可以访问,或者说无法获取这个信号量,于是当前获取信号量的这个线程会主动把自己阻塞在S.List当中
dispatch_semaphore_signal(semaphore)
发送一个信号,让信号总量加1

对value进行+1,+1之后如果仍然小于等于0,意味着在释放信号之前有队列在排队,也就是有相应的线程需要唤醒,然后去唤醒即可,就会实现一个线程同步,唤醒是被动行为,由释放信号的线程来唤醒被阻塞的线程
线程和Runloop之间的关系

一一对应,关系保存在一个全局字典里
runloop是管理线程的,线程runloop开启后,当线程执行完任务runloop就休眠了,有了任务才会被唤醒
runloop在第一次获取时被创建,在线程结束时被销毁
主线程的runloop默认启动
子线程的runloop是懒加载的,只有使用时才会被调用

线程和runloop不是你死我亡的关系
只有Timer的运行时依赖于runloop的

线程之间的通信

NSThread方法

iOS如何实现多线程
  1. GCD: C语言写的一套API,自动管理线程的生命周期,你只需要告诉GCD想要执行什么任务,不需要写任何管理线程的代码
  2. NSOperation: 第三方常用,AFN和SDWebImage,AFN中所有网络请求都封装到NSOperation,提交到operationQueue中
  3. NSThread: 实现常驻线程
GCD

GCD中最重要的概念: 函数和队列
将任务添加到队列,并且指定任务执行的函数
队列是种数据结构,支持FIFO,先进先出,但并不是先进去先执行,只是先调度而已

同步任务dispatch_sync
阻塞线程,必须等待当前语句执行完毕,才会执行下一条语句
不开启线程


异步任务dispatch_async
不阻塞线程,不用等待当前语句执行完毕,就会执行下一条语句
开启线程
因为async可能会开辟线程以及底层一堆调用,它会比较耗时,但并不堵塞线程,所以会跳过它执行下一句语句
串行队列: FIFO(先进先出)
dispatch_queue_create(serial)
dispatch_get_main_queue
水龙头口小,一次只能放出一滴水,所以一个接一个的放到线程上去执行


并发队列
dispatch_get_global_queue
dispatch_queue_create(concurrent)
水龙头口大,一次可出来多滴,没有顺序,肆意妄为的放到线程上去执行
线程是执行任务的,同步和异步决定了是否会开启新线程去执行以及是否阻塞当前线程,队列是个数据结构,里面装着各个任务块,决定了任务何时被提交到线程,在应用程序加载的时候就创建了主队列
下面看同步异步串行和并发的N种情况
1. 同步串行, 可能会导致死锁
2. 其他只有一个原则: dispatch_sync阻塞线程,dispatch_async不阻塞线程
死锁

因为串行队列的先进先出原则,有可能会导致死锁
死锁:是由队列引起的循环等待,注意不是线程引起的循环等待

当两个同步任务被同步分配到同一个串行队列上并循环等待了,就会死锁,会直接崩溃,下面两个例子都是,viewDidLoad先被提交到主线程上执行,但block块在串行队列上堵塞了,后面的任务无法提交到线程上,viewDidLoad也无法在线程上结束,一切都是因为队列堵塞导致的


主队列死锁
自定义的串行队列死锁

若两个同步任务在两个串行队列上,因为不在同一个队列,就不会死锁,viewDidLoad先被提交到主线程上,但会先等待后提交到主线程的block在主线程执行完毕,它再去执行.这个例子进一步验证了,死锁是队列引起的循环等待!!!!!!!!!!!


两个串行队列的同步任务不会死锁
非同步串行队列
只有一个原则: dispatch_sync阻塞线程,dispatch_async不阻塞线程

下面看几道题目,按照这个原则去算


dispatch_sync阻塞线程
dispatch_async不阻塞线程



还有一种情况

异步并发,会开启线程,让任务在新线程去执行,但新线程默认是没有开启Runloop的,但perfprm方法,它是提交任务到Runloop上去执行的除非这个子线程开启了runloop,否则这种情况下,它会失效


栅栏和组
栅栏dispatch_barrier_async:一定要是自定义的并发队列,并且他堵塞的是同一个队列上的东西,缺点是太依赖于同一个队列,不利于封装(网络请求和数据显示基本不在一个队列上)

两个作用
1:保证顺序执行
2:保证线程安全

调度组

不依赖于队列,可以在全局并发队列中去下载,回主队列中去显示
更加灵活
一定要成对存在
每一个都要有进组和出组
进组和出租的写法内部是信号量,进组信号+1,出组信号-1,信号量为0时才会调用group的那个通知

信号量

1.GCD通过控制信号量来控制并发数
2.若并发数设置为1,一次一个线程运行,达到加锁效果

实现多读单写: 读的时候多并发,写的时候阻塞其他操作
原理
读者和读者并发,读者和写者互斥,写者和写者互斥


多线程组dispatch_group
可以实现A,B,C三个任务并发,完成后执行任务D
NSOperation

需要结合NSOperationQueue共同实现多线程

它的优势和特点
1.可以添加任务依赖
2.可以控制NSOperation任务的执行状态
3.可以控制最大并发量


它的状态:
isReady 当前任务是否处于就绪状态
isExecuting 当前任务是否处于正在执行中状态
isFinished 当前任务是否已执行完毕
isCancelled 当前任务是否已取消


下面看它是如何控制状态的
1.如果只重写了NSOperation的main方法,底层会自动帮我们控制任务状态
2.如果重写了NSOperation的start方法,需要我们自行控制任务状态,在合适的时机去修改对应的isFinished等

查看NSOperation的start方法源码,理解上面两点
start方法内,首先创造一个自动释放池,然后获取线程优先级
做一系列的状态异常判断,然后判断当前状态是否isExecuting
如果不是,那么我们手动变成isExecuting,然后判断当前任务是否有被取消
若未被取消就调用NSOperation的main方法
再之后,调用NSOperation的finish方法
在内部通过KVO的方式去变更isExecuting状态为isFinished状态
之后调用自动释放池的release

所以系统是在start方法里面为我们维护了任务状态的变更,若重写start,则没人帮我们维护了,只能自己手动维护

其他问题:系统是怎样移除一个isFinished = YES的NSoperation的
通过上面源码知道,是通过KVO方式来移除NSOperationQueue中的NSOperation的

NSThread

常用它结合RunLoop一起实现常驻线程

当我们创建一个NSThread后,会手动调用start()方法来启动线程,那么它的内部实现机制是什么呢?



start()内部会创建pthread线程,同时指定这个线程的启动函数,在启动函数中,通知观察者当前线程已经启动,设置线程名称,然后会调用NSThread的main()函数,之后再调用线程关闭函数来关闭线程

如果main里面什么都不做,那么这个线程启动后,执行完一段逻辑就退出了,如果我们想实现常驻线程,就需要在main里面去做一个Runloop的循环

下面看系统关于main的实现是什么,main内部先会做一个异常判断,
之后只有这一句调用[_target performSelector:_selector withObject:_arg], 会执行我们在创建NSThread时候所指定的选择器,也就是下图中的runRequest方法,就可以让我们在外部手动维护一个事件循环,进而实现常驻线程了

多线程的几个思考
主线程是用来做什么的?
更新UI,用户交互的


为什么要在主线程更新UI?
1.应该是苹果的一套设计标准,比如我们写程序会有异常,那么为什么错误是根据什么来定的呢,也是根据一套设计的标准
2.异步渲染(???不明白,要再研究下)

你可能感兴趣的:(OC总结篇 - 多线程)