十九、多线程
线程
- 线程是进程的基本执⾏单元,⼀个进程的所有任务都在线程中执⾏
- 进程要想执⾏任务,必须得有线程,进程⾄少要有⼀条线程
- 程序启动会默认开启⼀条线程,这条线程被称为主线程或 UI 线程
进程
- 进程是指在系统中正在运⾏的⼀个应⽤程序
- 每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内
- 通过“活动监视器”可以查看 Mac 系统中所开启的进程
进程和线程的关系
- 地址空间:同⼀进程的线程共享本进程的地址空间,⽽进程之间则是独⽴的地址空间。
- 资源拥有:同⼀进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的
资源是独⽴的。
- ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进
程都死掉。所以多进程要⽐多线程健壮。 - 进程切换时,消耗的资源⼤,效率⾼。所以涉及到频繁的切换时,使⽤线程要好于进
程。同样如果要求同时进⾏并且⼜要共享某些变量的并发操作,只能⽤线程不能⽤进程 - 执⾏过程:每个独⽴的进程有⼀个程序运⾏的⼊⼝、顺序执⾏序列和程序⼊⼝。但是
线程不能独⽴执⾏,必须依存在应⽤程序中,由应⽤程序提供多个线程执⾏控制。 - 线程是处理器调度的基本单位,但是进程不是。
- 线程没有地址空间,线程包含在进程地址空间中
多线程
- 优点
- 能适当提⾼程序的执⾏效率
- 能适当提⾼资源的利⽤率(CPU,内存)
- 线程上的任务执⾏完成后,线程会⾃动销毁
- 缺点
- 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占 512 KB)
- 如果开启⼤量的线程,会占⽤⼤量的内存空间,降低程序的性能
- 线程越多,CPU 在调⽤线程上的开销就越⼤
- 程序设计更加复杂,⽐如线程间的通信、多线程的数据共享
时间⽚的概念:
CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚
- (单核CPU)同⼀时间,CPU 只能处理 1 个线程
- 换⾔之,同⼀时间只有 1 个线程在执⾏
- 多线程同时执⾏:
- 是 CPU 快速的在多个线程之间的切换
- CPU 调度线程的时间⾜够快,就造成了多线程的“同时”执⾏的效果
- 如果线程数⾮常多
- CPU 会在 N 个线程之间切换,消耗⼤量的 CPU 资源
- 每个线程被调度的次数会降低,线程的执⾏效率降低
//0: pthread
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4. 传递给第三个参数(函数)的`参数`
返回值:C 语言框架中非常常见
int
0 创建线程成功!成功只有一种可能
非 0 创建线程失败的错误码,失败有多种可能!
*/
// 1: pthread
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
// NSString *ocString = @"Gavin";
//延伸到: OC--C的混编 尤其在智能家居,SDK封装
//抛出一个问题: 在ARC需要这样操作,在MRC不需要
// OC prethread -- 跨平台
// 锁
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
NSLog(@"成功");
} else {
NSLog(@"失败");
}
// 2: NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
// 3: GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
// 4: NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
C与OC的桥接
-
__bridge
只做类型转换,但是不修改对象(内存)管理权; -
__bridge_retained
(也可以使⽤CFBridgingRetain
)将Objective-C的对象转换为
Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使⽤
CFRelease或者相关⽅法来释放对象; -
__bridge_transfer
(也可以使⽤CFBridgingRelease
)将Core Foundation
的对象
转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。
优先级问题
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
并非优先级越快速度越快,还要看资源的大小和任务的复杂度
互斥锁
- 互斥锁⼩结
- 保证锁内的代码,同⼀时间,只有⼀条线程能够执⾏!
- 互斥锁的锁定范围,应该尽量⼩,锁定范围越⼤,效率越差!
- 互斥锁参数
- 能够加锁的任意 NSObject 对象
- 注意:锁对象⼀定要保证所有的线程都能够访问
- 如果代码中只有⼀个地⽅需要加锁,⼤多都使⽤ self,这样可以避免单独再创建⼀个
锁对象
自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
临界区:每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
自旋锁:与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。"原地打转"。
自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态。
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
atomic与nonatomic 的区别
nonatomic ⾮原⼦属性
atomic 原⼦属性(线程安全),针对多线程设计的,默认值
保证同⼀时间只有⼀个线程能够写⼊(但是同⼀个时间多个线程都可以取值)
atomic 本身就有⼀把锁(⾃旋锁)
单写多读:单个线程写⼊,多个线程可以读取
atomic:线程安全,需要消耗⼤量的资源
nonatomic:⾮线程安全,适合内存⼩的移动设备
iOS 开发的建议
所有属性都声明为 nonatomic
尽量避免多线程抢夺同⼀块资源
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减⼩移动客户端的压⼒
@interface ViewController ()
/**
atomic 是原子属性,是为多线程开发准备的,是默认属性!
仅仅在属性的 `setter` 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行`写`操作
同一时间 单(线程)写多(线程)读的线程处理技术
nonatomic 是非原子属性
没有锁!性能高!
*/
@property (nonatomic, copy) NSString *name;
@end
// 在 OC 中,如果同时重写 了 setter & getter 方法,系统不再提供 _成员变量,需要使用合成指令
// @synthesize name 取个别名:_name
@synthesize name = _name;
#pragma mark - 模拟原子属性示例代码
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
/**
* 增加一把锁,就能够保证一条线程在同一时间写入!
*/
@synchronized (self) {
_name = name;
}
}
线程间通信
线程间通信常用方法
-
(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
-
(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
属性的weak、strong
从xib拖出来的控件是已经被持有了所以用weak
直接创建后直接被清空了
_imageView = [[UIImageView alloc] init];
如果用中间变量持有会维持到作用域结束
UIImageView imageView = [[UIImageView alloc] init];
_imageView = imageView