多线程

作者: HotPotCat | 来源:发表于2021-08-09 17:12 被阅读0次

一、线程和进程的关系和区别

1.1 线程和进程的定义

线程(Thread):也被称为 轻量级进程(Lightweight Proces, LWP),程序执行流的最小单元。一个标准线程由 线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程。
  • 程序启动会默认开启一条线程,这条线程被称为主线程UI 线程

实际运用过程中线程也拥有自己的私有存储空间:

  • 栈:尽管并非完全无法被其它线程访问,一般情况下仍然可以认为是私有的数据。
  • 线程局部存储(Thread Local Storage, TLS):某些操作系统为线程单独提供的私有空间,通常只具有很有限的容量。
  • 寄存器(包括PC寄存器):寄存器是执行流的基本数据,因此为线程私有。

进程(Process):一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。

  • 进程是指在系统中正在运行的一个应用程序。
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内 。
  • 通过 活动监视器 可以查看 Mac 系统中所开启的进程。
    image.png
    能开启多少条线程与cpu性能以及任务执行程度有关。

在上图中taiosystemstats一个有图标一个没有图标。区别在于进程有没有访问mac桌面的权限。有获取桌面权限的才有图标,服务级别,后台运行的基本没有图标。

1.2 进程与线程的关系

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。也就是说进程有单独的地址空间,线程没有,同一进程的线程共享进程的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如 内存、I/Ocpu等,但是进程之间的资源是独立的。

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

二、多线程意义&原理

2.1多线程意义

单进程是一个任务接着一个任务执行,导致任务执行缓慢、效率低,这样就引出了多线程。比如如下耗时任务:

- (void)threadTest {
    NSLog(@"begin");
    NSInteger count = 1000 * 100;
    for (NSInteger i = 0; i < count; i++) {
        // 栈区
        NSInteger num = i;
        // 常量区
        NSString *name = @"HotpotCat";
        // 堆区
        NSString *result = [NSString stringWithFormat:@"%@ - %zd", name, num];
        NSLog(@"%@", result);
    }
    NSLog(@"end");
}
  • 循环的执行速度很快。
  • 栈区、常量区的内存操作也和快。
  • 堆区的内存操作有点慢。
  • I/O的操作速度是最慢的。

这样就涉及到了多线程,开辟一条线程将耗时的操作放入新的线程中执行。
iOS中有4个方案。
pthread
pthread:通过pthread_create创建线程。
函数定义如下:

#import <pthread.h>

int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
        const pthread_attr_t * _Nullable __restrict,
        void * _Nullable (* _Nonnull)(void * _Nullable),
        void * _Nullable __restrict);

参数说明:

  • pthread_t:要创建线程的结构体指针。开发的时候通常遇到 C 语言的结构体类型后缀一般为_t / Ref结尾,同时不需要*
  • pthread_attr_t:线程的属性,nil(空对象) / NULL(空地址)。
  • 线程要执行的函数地址。void *返回类型,表示指向任意对象的指针,与OC 中的 id 类似。(*) 函数名。(void *) 参数类型是void *
  • 传递给第三个参数(函数)的参数
  • 返回值int0表示创建成功,成功只有一种可能。非0创建线程失败的错误码,失败有多种可能。
pthread_t threadId = NULL;
char *cString = "HotpotCat";
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
    NSLog(@"thread creat success");
} else {
    NSLog(@"thread creat failure with code:%d",result);
}

void *pthreadTest(void *para){
    NSString *name = [NSString stringWithCString:para encoding:NSUTF8StringEncoding];
    NSLog(@"%@ %@", [NSThread currentThread],name);
    return NULL;
}

输出:

thread creat success
<NSThread: 0x600002cd8ac0>{number = 7, name = (null)} HotpotCat

NSThread

[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];

GCD

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self threadTest];
});

NSOperation

[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
    [self threadTest];
}];
  • __bridge只做类型转换,不修改对象(内存)管理权;相当于原对象拥有所有权。
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给CF,后续需要使用 CFRelease或者相关方法来释放对象;CF拥有所有权,OC交出控制权。
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARCOC拥有所有权,CF交出控制权。

技术方案对比:

方案 简介 语言 线程生命周期 使用频率
pthread • 一套通用的多线程 API
• 适用于 Unix/Linux/Windows等系统
• 跨平台,可移植
• 使用难度大
C 程序员管理 几乎不用
NSThread • 使用更面向对象
• 简单易用,可直接操作线程对象
OC 程序员管理 偶尔使用
GCD • 旨在替换 NSThread 等线程技术
• 充分利用设备的多核
C 自动管理 经常使用
NSOperation • 集于 GCD(底层是GCD)
• 比GCD多了更简单实用的功能
• 使用更加面向对象
OC 自动管理 经常使用

多线程的优缺点

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

2.2、多线程的原理

  • (单核CPU)同一时间,CPU 只能处理 1 个线程。也就是说同一时间只有 1 个线程在执行。对于单核CPU而言多线程并不是真正意义上的并发。
  • 多线程同时执行:
    • CPU 快速的在多个线程之间的切换(每次仅执行一小段时间,几十到几百毫秒)。
    • CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果(模拟出来的状态)。
  • 如果线程数非常多:
    • CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源。
    • 每个线程被调度的次数会降低,线程的执行效率降低。
  • 多核/多个CPU本身具备同时执行多个线程的能力。
    • 当线程数量小于等于处理器数量时(操作系统支持多处理器),线程的并发时真正的并发,不同的线程运行在不同的处理器上。此时彼此互不干扰。
    • 当线程数量大于处理器数量时,线程的并发会受到阻碍,至少有一个处理器会运行多个线程。
    • 多核才是解决并发的根本原因。
线程创建成本
  • 内核数据结构占用大小:1 KB
  • 栈空间大小:非主线程512 KB,主线程macOS 8 MBiOS 1 MB
  • 创建花费时间:大约90 us(微秒)。

更多关于多线程的介绍参考Threading Programming Guide

三、线程生命周期

时间片(Time Slice)CPU在多个任务直接进行快速的切换,运行中的线程有一段可以执行的时间,这个时间就是 时间片

线程调度(Thread Schedule
在单处理器对应多个线程的情况下,并发是模拟出来的状态,操作系统会让这些多线程程序轮流执行。每次仅执行一小段时间(通常是几十~几百毫秒),这样看起来是在同时执行。这样的在一个处理器上切换不同的线程的行为称为 线程调度

线程状态
在线程的调度中有如下状态:

  • 新建(New):新建线程。
//A:开辟线程
NSThread *thread = [[NSThread alloc] initWithTarget:self.hp selector:@selector(work:) object:@100];
//启动线程
[thread start];
thread.name = @"work";

//B: detach分离 不需要启动,直接分离出新的线程执行
[NSThread detachNewThreadSelector:@selector(work:) toTarget:self.hp withObject:@10000];

//C: 隐式的多线程调用方法,没有thread,也没有 start NSObject (NSThreadPerformAdditions)的分类
[self.hp performSelectorInBackground:@selector(work:) withObject:@5000];
  • 就绪(Runnable):当线程start后就进入了就绪状态。不代表运行。
  • 运行(Running):CPU调度当前就绪状态的线程就进入运行状态。
    • 运行状态的线程调用sleep等待同步锁丛可调度线程池移除当前线程就进入阻塞Blocked状态。
    • 运行状态的线程销毁后就进入死亡(Dead)状态
  • 阻塞(Blocked):运行状态的线程调用sleep、等待同步锁、丛可调度线程池移除就进入阻塞Blocked状态。
    • 阻塞状态sleep结束获取同步锁重新添加回可调度线程池线后进入就绪状态了。
  • 死亡(Dead):线程销毁后进入死亡状态。
线程状态转换

可调度线程池:我们开辟线程并不一定是按顺序来开辟的,有可能是乱序的。这是因为存在线程池。cpu在调度线程时会先判断是否有空闲线程(任何事务都依赖于线程执行),然后判断线程池工作队列是否饱和,再判断线程池中的线程是否都处于执行状态。都在执行状态的情况下交给饱和策略去处理。

可调度线程池

饱和策略
当线程池中线程处理不了任务的时候,系统会讲任务交给饱和策略,有如下4种处理方式:

  • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行。
  • CallerRunsPolicy:将任务回退到调用者。
  • DisOldestPolicy:丢掉等待最久的任务(丢掉的是其它任务)。
  • DisCardPolicy:直接丢弃任务(丢弃的是刚才要添加的任务)。

这四种拒绝策略均实现的RejectedExecutionHandler接口。

四、多线程优先级以及影响因素

4.1 任务执行速度的影响因素

  • cpu
  • 任务复杂度
  • 优先级
  • 线程状态

4.2 优先级调度

线程的优先级不仅可以由用户手动设置,系统还会根据不同线程的表现自动调整优先级。通常情况下频繁的进入等待状态的线程比频繁进行大量计算、以至于每次都要把时间片全部用尽的线程要受欢迎。频繁等待的线程通常占用很少的时间。

  • IO密集型线程(IO Bound Thread):频繁等待的线程。
  • CPU密集型线程(CPU Bound Thread):很少等待的线程。容易出现饿死。调度提高优先级。任务执行的可能性大大提升。

IO密集型线程总是比CPU密集型线程容易得到优先级提升。

饿死(Starvation)现象
在优先级调度下,存在一种饿死现象。一个线程被饿死,是说它的优先级较低,在它执行之前,总是有较高优先级的线程试图执行,所以这个低优先级的线程始终无法执行。当一个CPU密集型的线程获得较高的优先级时,许多低优先级的线程就很可能饿死。而高优先级的IO密集型线程由于大部分时间处于等待状态,因此不容易造成其他线程饿死。为了避免饿死,调度系统经常会逐步提高那些等待了过长时间得不到执行的线程的优先级。在这样的策略下,一个线程只要等待足够长的时间,其优先级一定会提高到足够让它执行的程度。

在优先级调度策略下,优先级影响因素有三种:

  • 用户指定优先级。比如qualityOfService
  • 根据进入等待状态的频繁程度提升或降低优先级。
  • 长时间得不到执行而被提升优先级。

五、自旋锁&互斥锁

自旋锁:当发现有其他线程正在执行,当前线程询问,忙等。
互斥锁:当发现有其他线程正在执行,当前线程休眠(就绪状态),等待唤醒。

  • 保证锁内的代码,同一时间,只有一条线程能够执行!
  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
  • 互斥锁参数:
    • 能够加锁的任意NSObject对象。
    • ⚠️:锁对象一定要保证所有的线程都能够访问。
    • 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象。

自旋锁耗费的性能比互斥锁大。一般小任务调度频繁适合用自旋锁。当然也与系统环境有关,比如单就macOSiOS来说macOSiOS更适合自旋锁。

5.1 atomic 与 nonatomic 区别

  • nonatomic 非原子属性,没有加锁性能高。非线程安全,适合内存小的移动设备
  • atomic 原子属性(线程安全),针对多线程设计的,默认值。线程安全,需要消耗大量的资源。
    • 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
    • atomic 本身就有一把锁(互斥锁) ,在setter & getter中加了锁。
    • 单写多读:单个线程写入,多个线程可以读取

atomic内部调用的是互斥锁os_unfair_lockiOS10以前用的是OSSpinLock自旋锁,为了解决优先级反转问题iOS10以后改为OSSpinLock)。
objc源码中:

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

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);
    }
    
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        //atomic 加锁 os_unfair_lock
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

atomic只是一个参数用来区分是否需要加锁,spinlock_t定义如下:

using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
   os_unfair_lock mLock;
   ……
}

本身是使用os_unfair_lock进行加锁。

iOS开发建议:

  • 所有属性都声明为 nonatomic
  • 尽量避免多线程抢夺同一块资源
  • 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

OC 中,如果同时重写 了 setter & getter 方法,系统不再提供 下划线成员变量,需要使用合成指令@synthesize声明一个,可以通过这个模拟atomic的实现:

@interface ViewController ()

@property (nonatomic, copy) NSString *name;

@end

@implementation ViewController

@synthesize name = _name;

- (NSString *)name {
    return _name;
}

- (void)setName:(NSString *)name {
    /**
     * 增加一把锁,就能够保证一条线程在同一时间写入!
     */
    @synchronized (self) {
        _name = name;
    }
}

@end

相关文章

  • iOS多线程 NSOperation

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程 pthread、NSThread

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程: GCD

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程运用

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程基础

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • 多线程介绍

    一、进程与线程 进程介绍 线程介绍 线程的串行 二、多线程 多线程介绍 多线程原理 多线程的优缺点 多线程优点: ...

  • iOS进阶之多线程管理(GCD、RunLoop、pthread、

    深入理解RunLoopiOS多线程--彻底学会多线程之『GCD』iOS多线程--彻底学会多线程之『pthread、...

  • iOS多线程相关面试题

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • 多线程之--NSOperation

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • iOS多线程之--NSThread

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

网友评论

      本文标题:多线程

      本文链接:https://www.haomeiwen.com/subject/enucvltx.html