多线程

线程和进程

线程
  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程
  • 程序启动会默认开启一条线程,这条线程被称为主线程或者UI线程
进程
  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 通过“活动监视器”可以查看mac系统中所开启的线程
  • MAC是多进程的,iOS是单进程的。

进程中包含多个线程,进程负责任务的调度,线程负责任务的执行。在iOS中并不支持多进程,所有程序都是单一进程运行,进程之间相互独立

线程与进程的关系
  • 地址空间:同一进程的线程共享本进程的地址空间,而进程与进程之间是独立的地址空间;
  • 资源拥有:同一进程的线程共享本进程的资源,如:内存、IO、CPU等,而进程与进程之间的资源是独立的。

两者的使用特点:

  1. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但一个线程崩溃会导致整个进程都死掉。所以多进程要比多线程健壮
  2. 进程切换时消耗的资源大效率高。所以涉及到频繁的切换时,使用线程要好于进程。如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程;
  3. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
  4. 线程是处理器调度的基本单位,但是进程不是;
  5. 线程没有地址空间,线程包含在进程地址空间中;
线程局部存储TLS

线程局部存储全称:Thread Local Storage:线程是没有地址空间的,但是存在线程局部存储。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有有限的容量
类的加载篇章中分析在objc源码中,_objc_init方法中包含了对tls的初始化操作,源码如下

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    // 线程绑定,例如线程数据的析构函数
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

//tls_init源码实现
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

多线程原理

iOS中多线程同时执行的本质是CPU在多个任务之间快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片。所以多线程并不是真正的并发,而真正的并发必须建立在多核CPU的基础上。

多线程意义
  • 优点
  1. 能适当提高程序的执行效率
  2. 能适当提高资源的利用率,如CPU、内存
  3. 线程上的任务执行完成后,线程会自动销毁
  • 缺点
  1. 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB
  2. 如果开启大量线程,会占用大量的内存空间,降低程序的性能
  3. 线程越多,CPU在调用线程上的开销就越大
  4. 程序设计更加复杂,比如线程间的通信多线程的数据共享
时间片

时间片的概念:CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片

  • 单核CPU同一时间,CPU只能处理一个线程
    换言之,同一时间只有一个线程在执行
  • 多线程同时执行:
    CPU快速的在多个线程之间的切换
    CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果
  • 如果线程数非常多
    CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源
    每个线程被调度的次数会降低,线程的执行效率降低

多线程官方文档

  • 线程创建成本


    image.png
  • 多线程技术方案

image.png

以上四种方案的简单示例

// *********1: pthread*********
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
 pthread_create 创建线程
 参数:
 1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
 同时不需要 `*`
 2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
 3. 线程要执行的`函数地址`
 void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
 (*): 函数名
 (void *): 参数类型,void *
 4. 传递给第三个参数(函数)的`参数`
 */
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];
}];

/**
 1. 循环的执行速度很快
 2. 栈区/常量区的内存操作也挺快
 3. 堆区的内存操作有点慢
 4. I(Input输入) / O(Output 输出) 操作的速度是最慢的!
 * 会严重的造成界面的卡顿,影响用户体验!
 * 多线程:开启一条线程,将耗时的操作放在新的线程中执行
 */
- (void)threadTest{
    NSLog(@"begin");
    NSInteger count = 1000 * 100;
    for (NSInteger i = 0; i < count; i++) {
        // 栈区
        NSInteger num = i;
        // 常量区
        NSString *name = @"zhang";
        // 堆区
        NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
        NSLog(@"%@", myName);
    }
    NSLog(@"over");
}

void *pthreadTest(void *para){
    // 接 C 语言的字符串
    //    NSLog(@"===> %@ %s", [NSThread currentThread], para);
    // __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);
    
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    
    return NULL;
}
C与OC的桥接
  • __bridge只做类型转换,但是不修改对象(内存)管理权;
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象;
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。

线程生命周期

线程的生命周期主要分为五部分:新建 - 就绪 - 运行 - 阻塞 - 死亡

线程生命周期
  • 新建:主要是实例化线程对象
  • 就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法并不会立即执行,进入就绪状态,需要等待一段时间经CPU调度后才执行,也就是从就绪状态进入运行状态
  • 运行:CPU负责调度可调度线程池中线程的执行,在线程执行完成之前,其状态可能会在就绪运行之间来回切换,这个变化是由CPU负责,开发人员不能干预。
  • 阻塞:当满足某个预定条件时,可以使用休眠即sleep,或者同步锁阻塞线程执行。当进入sleep时,会重新将线程加入就绪中。下面关于休眠的时间设置,都是NSThread的API
    sleepUntilDate: 阻塞当前线程,直到指定的时间为止,即休眠到指定时间
    sleepForTimeInterval: 在给定的时间间隔内休眠线程,即指定休眠时长
    同步锁:@synchronized(self)
  • 死亡:分为两种情况
    正常死亡,即线程执行完毕
    非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
线程池原理
线程池原理
  • 判断核心线程池是否都正在执行任务:
    如果返回NO,创建新的工作线程去执行;
    如果返回YES,进入下一步;
  • 判断线程池工作队列是否饱满
    如果返回NO,将任务存储到工作队列,等待CPU调度
    如果返回YES,进入下一步;
  • 判断线程池中的线程是否都处于执行状态
    如果返回NO,安排可调度线程池中空闲的线程去执行任务
    如果返回YES,进入下一步;
  • 交给饱和策略去执行,分为以下四种拒绝策略
    AbortPolicy:抛出RejectedExecutionExeception异常,阻止系统正常运行
    CallerRunsPolicy:将任务回退到调用者
    DisOldestPolicy:丢掉等待最久的任务
    DisCardPolicy:直接丢弃任务
    这四种拒绝策略均实现的RejectedExecutionHandler接口。

多线程面试题

任务执行速度的影响因素有哪些?

这个问题从四个角度分析:CPU的调度情况任务的复杂度任务的优先级线程状态
目前iOS中,线程优先级的threadPriority属性已经弃用,被NSQualityOfService类型的qualityOfService所代替,看先底层的枚举设置

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));

开发者自己指定NSQualityOfService服务质量,用于表示工作的性质和对系统的重要性。当存在资源竞争时,使用高质量的服务类比使用低质量的服务类获得更多的资源

  • NSQualityOfServiceUserInteractive:用于直接涉及提供交互式UI的工作。例如:处理控制事件或在屏幕上绘图;
  • NSQualityOfServiceUserInitiated:用于执行用户明确要求的工作,并且为了允许进一步的用户交互,必须立即显示这些工作的结果。例如:在用户邮件列表中选择邮件后加载邮件;
  • NSQualityOfServiceUtility:用于执行用户不太可能立即等待结果的工作。这项工作可能是由用户请求的,也可能是自动启动的,并且通常使用非模式进度指示器在用户可见的时间尺度上操作。例如:定期内容更新或批量文件操作,如媒体导入;
  • NSQualityOfServiceBackground:用于非用户发起或不可见的工作。通常,用户甚至不知道正在进行这项工作。例如:预抓取内容、搜索索引、备份或与外部系统同步数据;
  • NSQualityOfServiceDefault:表示没有明确的服务质量信息。只要可能,适当的服务质量是根据可用的资源确定的。否则,使用NSQualityOfServiceUserInteractive和NSQualityOfServiceUtility之间的服务质量级别。
优先级反转
  • 线程分为以下两种:
    IO密集型,频繁等待的线程;
    CPU密集型,很少等待的线程;
  • IO密集型比CPU密集型更容易得到线程优先级的提升
    I(Input输入) / O(Output输出) 操作的速度是最慢的,并且等待频繁,如果它的优先级又低,很容易被饱和策略所淘汰;
    为了避免这种情况,当CPU发现一个频繁等待的线程,会将其优先级提升,从而提升线程被执行的可能性。
优先级的影响因素
  • 用户指定线程的服务质量;
  • 根据线程等待的频繁程度提高或降低;
  • 长时间不执行的线程,提升它的优先级。

自旋锁和互斥锁

当多个线程同时访问同一块资源时,很容易引发资源抢夺,造成数据错乱数据安全问题,有以下两种解决方案:

  • 互斥锁(同步锁):@synchronized
  • 自旋锁

例如多窗口卖票时,会产生资源的抢夺(如下图)。这时我们的常规操作就是加锁

image.png
互斥锁
  • 用于保护临界区,确保同一时间,只有一条线程能够执行;
  • 如果代码中只有一个地方需要加锁,大多都使用self,这样可以避免单独再创建一个锁对象
  • 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。

使用互斥锁的注意事项:

  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差
  • 能够加锁的任意NSObject对象;
  • 锁对象一定要保证所有的线程都能够访问。
自旋锁
  • 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态;
  • 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁;
  • 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
自旋锁与互斥锁
  • 相同点:
    同一时间保证只有一条线程执行任务,即保证了相应同步的功能。
  • 不同点:
    互斥锁:发现其他线程执行,当前线程休眠(即就绪状态),进入等待执行,即挂起。一直等其他线程打开之后,然后唤醒执行;
    自旋锁:发现其他线程执行,当前线程一直询问(即一直访问),处于忙等状态,耗费的性能比较高
使用场景
  • 根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理;
  • 当前的任务状态比较短小精悍时,用自旋锁;
  • 反之的,用互斥锁。
atomic原子锁与nonatomic非原子锁的作用

atomicnonatomic主要用于属性的修饰

  • atomic是原子属性,是为多线程开发准备的,是默认属性!
  1. 仅仅在属性的setter方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行写操作
  2. 同一时间 单(线程)写多(线程)读的线程处理技术
  3. Mac开发中常用
  • nonatomic是非原子属性
    没有锁!性能高!
    移动端开发常用
iOS开发的建议
  • 所有属性都声明为nonatomic
  • 尽量避免多线程抢夺同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
atomic与nonatomic的区别
  • nonatomic
  1. 非原子属性
  2. 非线程安全,适合内存小的移动设备
  • atomic
  1. 原子属性(线程安全),针对多线程设计的,默认值
  2. 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
  3. atomic 本身就有一把锁(自旋锁) 单写多读:单个线程写入,多个线程可以读取
  4. 线程安全,需要消耗大量的资源
objc4-818.2源码分析
  • 全局搜索objc_setProperty的方法实现,源码如下
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
  • 进入reallySetProperty方法查看源码
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    // atomic修饰,增加了spinlock_t的锁操作;
    // 所以atomic是标示,自身并不是锁。而atomic所谓的自旋锁,由底层代码实现。
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}
线程与Runloop的关系
  • runloop线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的。但是核心的只能有一个,他们的关系保存在一个全局的字典里
  • runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。
  • runloop在第一次获取时被创建,在线程结束时被销毁。
  • 对于主线程来说,runloop在程序一启动就默认创建好了。
  • 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。
线程间通讯

Threading Programming Guide文档,线程间的通讯有以下几种方式

线程间通讯

你可能感兴趣的:(多线程)