美文网首页多线程
iOS中的多线程技术

iOS中的多线程技术

作者: limeng99 | 来源:发表于2019-12-06 10:08 被阅读0次

    从很多年前开始,CPU 的频率增长就出现停滞,转而向多核的方向发展。增加核心远远比提升制程、架构要更简单。因此多线程技术也有着越来越重要的地位。

    一、多线程相关知识

    1.1 进程

    • 进程是指在系统中正在运行的一个应用程序,比如同时打开微信和Xcode,系统会分别启动2个进程;
    • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内;

    1.2 线程

    • 一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程),是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位;

    • 一个程序有且只有一个主线程,程序启动时创建(调用main来启动),主线程的生命周期是和应用程序绑定,程序退出时,主线程也停止;

    1.3 多线程

    • 概念:一个进程中可以开启多条线程,每一条线程可以并行(同时)执行不同的任务
    • 原理:同一时间,CPU只能处理一条线程,只有一条线程在工作,多线程并发(同时)执行,其实是CPU快速的在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
    • 注意:如果线程很多,CPU会在N多线程之间调度,会消耗大量CPU资源,每条线程被调度执行的频次会降低(线程的执行效率会降低)

    1.4 多线程的优缺点

    • 优点: 能适当的提高程序的执行效率以及资源利用率(CPU、内存利用率)

    • 缺点: 每创建一个线程是会占用资源的,比如内存开销等;线程太多,会降低程序的性能; 程序开发复杂度上升

    1.5 主线程

    • 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”

    • 作用: 显示/刷新UI界面, 处理UI事件(点击事件,滚动事件,拖拽事件)

    • 使用注意:不要将耗时的操作放到主线程中,耗时操作应放在子线程(后台线程,非主线程); 凡是和UI相关的操作应放在主线程中操作

    1.6 iOS中多线程的实现方案

    • pthread :一套通用的多线程API,很少用到,c语言,线程生命周期由程序员管理。
    • NSTread:oc语言,面向对象,简单易用,可直接操作线程对象 ,线程生命周期由程序员管理
    • GCD: 常用,替代NSThread等线程技术,充分利用设备的多核,c语言,线程生命周期自动管理
    • NSOperation: 常用,是对GCD封装,使用更加面向对象,线程生命周期自动管理

    二、Pthreads

    Pthreads 是POSIX 多线程开发框架,是跨平台的 C 语言框架,需要自己管理线程的创建销毁等操作。这些 API 全都以 pthread_ 作为前缀。iOS 中 CFRunLoop就是基于Pthreads来管理的。更加详细的关于 Pthreads的学习可以参考 这里

    pthread_create():创建一个线程
    pthread_exit():终止当前线程
    pthread_cancel():中断另外一个线程的运行
    pthread_join():阻塞当前的线程,直到另外一个线程运行结束
    pthread_attr_init():初始化线程的属性
    pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
    pthread_attr_getdetachstate():获取脱离状态的属性
    pthread_attr_destroy():删除线程的属性
    pthread_kill():向线程发送一个信号
    pthread_equal(): 对两个线程的线程标识号进行比较
    pthread_detach(): 分离线程
    pthread_self(): 查询线程自身线程标识号
    ...
    

    三、NSThread

    一个NSThread对象就代表一条线程

    3.1 NSThread常用方法

    // 创建、启动线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
    
    // 创建线程后自动启动线程
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"OC"];
    
    // 隐式创建并启动线程
    [self performSelectorInBackground:@selector(run:) withObject:@"OC"];
    
    // 主线程相关用法
    [NSThread mainThread];      // 获得主线程
    [NSThread isMainThread];    // 是否为主线程
    [thread isMainThread];      // 是否为主线程
    
    // 获得当前线程
    NSThread *current = [NSThread currentThread];
    
    // 休眠线程
    [NSThread sleepForTimeInterval:2];  //休眠2s
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]]; //休眠2s
    
    // 强制退出线程,不推荐使用此方式退出子线程,可能会造成内存泄漏
    [NSThread exit];
    

    3.2 NSThread创建常驻线程

    当然,我们也可以增加一个特殊的线程常驻RunLoop,防止线程退出。

    NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(onRunLoop) object:nil];
    [runLoopThread start];
    [self performSelector:@selector(dothingOnRunLoop:) onThread:runLoopThread withObject:@[@"常驻RunLoop线程"] waitUntilDone:YES];
    
    // 常驻runLoop
    - (void)onRunLoop {
        @autoreleasepool {
            [NSThread currentThread].name = @"常驻RunLoop线程";
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    
    // 在runloop上执行操作
    - (void)dothingOnRunLoop:(id)param {
        // 可以在此打断点测试线程是否已经加入runloop
        NSLog(@"跑在runloop上的线程: %@", param);
    }
    

    3.3 NSThread线程间通讯

    NSThread通过以下四种方式进行线程之间的通信

    // 指定方法在主线程中执行, 参数1. SEL 方法  2.方法参数  3.是否等待当前执行完毕 4.指定的Runloop model
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    
    // 指定方法在某个线程中执行, 参数1. SEL 方法  2.方法参数 3.是否等待当前执行完毕 4.指定的Runloop model
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array 
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait 
    
    // 指定方法在开启的子线程中执行, 1. SEL 方法 2.方法参数
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg 
    

    四、GCD

    有关GCD相关内容,请参考另一篇文章 关于GCD的那些事儿

    五、NSOperation、NSOperationQueue

    有关NSOperation、NSOperationQueue相关内容,请参考另一篇文章 iOS多线程:NSOperation、NSOperationQueue总结

    六、 多线程的安全隐患

    一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源;或者说是多个线程访问同一个对象、变量、文件等等。这时候,如果不采取一定的措施,很容易引发数据错乱和数据安全的问题。

    6.1 iOS多线程安全问题及方案

    举个例子,银行账目上原先有1000,在取钱500的同时,又存钱500,这时候大家第一时间就是觉得,还剩余1000,这只是正常现象,如果存取操作同时进行的话,因为基础都是1000,所以,取完钱剩余500,存完钱剩余1500,这两个数据任何一个放回账目上都是不正确的。这就是多条线程同时操作一个对象所引发的问题。

    - (void)moneyTest {
        self.money = 100;
        
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 存钱任务
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self __saveMoney];
            }
        });
        
        // 取钱任务
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self __drawMoney];
            }
        });
    }
    
    // 存钱
    - (void)__saveMoney {
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    
    // 取钱
    - (void)__drawMoney {
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    
    

    最终日志:

    2019-11-22 17:05:18.577892+0800 多线程-demo[67341:21672333] 存50,还剩440元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
    

    在正常流程下,最后的结果是钱剩余400,票剩余0,可以看到,钱最终的结果是错的,这就是多线程同事操作同一个对象或者方法造成的隐患。这个,我们解决问题的方案就是:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)。而在iOS领域中,常见的线程同步技术就是:加锁。

    先来看看我们iOS线程同步技术的一些方案,也就是我们接下来要分析的方案:

    // 性能从高到低排序
    os_unfair_lock
    OSSpinLock
    dispatch_semaphore
    pthread_mutex
    dispatch_queue(DISPATCH_QUEUE_SERIAL)
    NSLock
    NSCondition
    NSRecursiveLock
    NSConditionLock
    @synchronized
    

    6.2 OSSpinLock

    第一种OSSpinLock,这个方案已经被苹果废弃,当然,现在仍然可用,只是苹果不推荐使用。OSSpinLock是一个自旋锁,通过加锁解锁,可以有效解决多线程隐患问题。

    // 首先,我们创建一个子类BLOSSpinLockDemo,继承BLBaseDemo,然后定义个锁,初始化对象的同时对它们进行初始化。然后重写卖票和存取钱的方法,在方法中加锁解锁:
    @interface OSSpinLockDemo()
    
    @property (assign, nonatomic) OSSpinLock moneyLock;
    
    @end
    
    @implementation OSSpinLockDemo
    
    - (instancetype)init {
        if (self = [super init]) {
            self.moneyLock = OS_SPINLOCK_INIT;
        }
        return self;
    }
    
    - (void)__drawMoney {
        OSSpinLockLock(&_moneyLock);
        
        [super __drawMoney];
        
        OSSpinLockUnlock(&_moneyLock);
    }
    
    - (void)__saveMoney {
        OSSpinLockLock(&_moneyLock);
        
        [super __saveMoney];
        
        OSSpinLockUnlock(&_moneyLock);
    }
    
    @end
    
    最终打印结果如下:
    2019-11-22 17:10:18.577892+0800 多线程-demo[67341:21672333] 存50,还剩400元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
    钱的结果正是我们想要的,并且是正确的结果
    

    6.3 os_unfair_lock

    OSSpinLock自旋锁会出现一种状况,当优先级低的线程加锁之后,优先级高的线程在等待的过程中,可能出现优先级高的线程会一直占着CPU资源,导致优先级低的线程没法释放锁,出现线程死锁状态。而互斥锁虽然在唤醒线程的时候会消耗CPU,但是不会出现死锁状态,相对比较安全,所以目前苹果从iOS10开始就推荐大家用os_unfair_lock来替换OSSpinLock。接下来,我们来看看os_unfair_lock的使用

    @interface OSUnfairLockDemo()
    
    @property (assign, nonatomic) os_unfair_lock moneyLock;
    
    @end
    
    @implementation OSUnfairLockDemo
    
    - (instancetype)init {
        if (self = [super init]) {
            self.moneyLock = OS_SPINLOCK_INIT;
        }
        return self;
    }
    
    - (void)__drawMoney {
        os_unfair_lock_lock(&_moneyLock);
        
        [super __drawMoney];
        
        os_unfair_lock_unlock(&_moneyLock);
    }
    
    - (void)__saveMoney {
        os_unfair_lock_lock(&_moneyLock);
        
        [super __saveMoney];
        
        os_unfair_lock_unlock(&_moneyLock);
    }
    
    @end
    
    最终打印结果如下:
    2019-11-22 17:13:18.577892+0800 多线程-demo[67341:21672333] 取20,还剩400元 - <NSThread: 0x600001807740>{number = 7, name = (null)}
    

    6.4 pthread_mutex

    pthread_mutex,c语言编写的锁,也是一种互斥锁。先看看基本的使用

    @interface MutexDemo()
    
    @property (assign, nonatomic) pthread_mutex_t moneyLock;
    
    @end
    
    @implementation MutexDemo
    
    - (void)initMutex:(pthread_mutex_t *)mutex {
        // 初始化属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
        
        // 初始化mutex,参数一是pthread_mutex_t(锁) 参数二是pthread_mutexattr_t(属性)
        pthread_mutex_init(mutex, &attr);
        //使用完属性之后 要销毁
        pthread_mutexattr_destroy(&attr);
    }
    
    - (instancetype)init {
        if (self = [super init]) {
            [self initMutex:&_moneyLock];
        }
        return self;
    }
    
    - (void)__drawMoney {
        pthread_mutex_lock(&_moneyLock);
        
        [super __drawMoney];
        
        pthread_mutex_unlock(&_moneyLock);
    }
    
    - (void)__saveMoney {
        pthread_mutex_lock(&_moneyLock);
        
        [super __saveMoney];
        
        pthread_mutex_unlock(&_moneyLock);
    }
    
    @end
    
    最终打印结果如下:
    2019-11-22 17:18:18.577892+0800 多线程-demo[67341:21672333] 取20,还剩400元 - <NSThread: 0x600001807740>{number = 6, name = (null)}
    

    6.5 dispatch_semaphore

    GCD中的信号量dispatch_semaphore,这也是同步技术方案中,比较方便的一直方案。

    @interface GCDSemaphoreDemo ()
    
    @property (nonatomic, strong) dispatch_semaphore_t semaphore;
    
    @end
    
    
    @implementation GCDSemaphoreDemo
    
    - (instancetype)init {
        if (self = [super init]) {
            self.semaphore = dispatch_semaphore_create(1);
        }
        return self;
    }
    
    - (void)__drawMoney {
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        [super __drawMoney];
        
        dispatch_semaphore_signal(self.semaphore);
    }
    
    - (void)__saveMoney {
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        [super __saveMoney];
        
        dispatch_semaphore_signal(self.semaphore);
    }
    
    @end
    
    最终打印结果如下:
    2019-11-22 20:43:02.167794+0800 多线程-demo[80757:22055581] 取20,还剩400元 - <NSThread: 0x600002f94e00>{number = 3, name = (null)}
    

    6.6 其它方案

    NSCondition,其实就是对pthread_cond_t加pthread_mutex_t的封装使用,而NSConditionLock,则是对NSCondition的进一步封装。

    对于dispatch_queue(DISPATCH_QUEUE_SERIAL)大家都知道,因为串行队列,不管你是同步还是异步,它都是依次执行任务的,所以可以达到加锁效果。

    对于@synchronized,可能大家在日常的项目中已经有所用到,大概就是@synchronized(self),其实,在()中,只要是同一个对象,就可以达到加锁的效果,这里我就不做过多说明了,但是这个加锁方式性能上不是很好,因为这简单一句话,封装了很多内容,不建议大家使用。


    本文首发于我的个人博客 https://limeng99.club/,转载请标明出处。

    相关文章

      网友评论

        本文标题:iOS中的多线程技术

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