多线程

2021-08-09  本文已影响0人  HotPotCat

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

1.1 线程和进程的定义

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

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

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

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

1.2 进程与线程的关系

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

这样就涉及到了多线程,开辟一条线程将耗时的操作放入新的线程中执行。
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 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 自动管理 经常使用

多线程的优缺点

2.2、多线程的原理

线程创建成本

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

三、线程生命周期

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

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

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

//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];
线程状态转换

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

可调度线程池

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

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

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

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

4.2 优先级调度

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

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

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

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

五、自旋锁&互斥锁

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

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

5.1 atomic 与 nonatomic 区别

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开发建议:

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
上一篇 下一篇

猜你喜欢

热点阅读