多线程

iOS中的多线程技术

2019-12-06  本文已影响0人  limeng99

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

一、多线程相关知识

1.1 进程

1.2 线程

1.3 多线程

1.4 多线程的优缺点

1.5 主线程

1.6 iOS中多线程的实现方案

二、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/,转载请标明出处。

上一篇 下一篇

猜你喜欢

热点阅读