iOS开发技术iOS程序员

ios开发之线程

2017-09-12  本文已影响68人  残夜孤鸥

线程对操作系统来说就是一段代码以及运行时数据.操作系统回味每个线程保存相关的数据,当接收到来自CPU的时间片中断事件时,就会按一定规则从这些线程中选择一个,恢复它的运行时数据,这样CPU就可以继续执行这个线程了,也就是单核CPU并没有办法实现真正意义上的并发执行,只是CPU快速地在多条线程之间调度,CPU调度线程的时间足够快, 就造成了多线程并发执行的假象.并且就单核CPU而言多线程可以解决线程阻塞的问题, 但是其本身运行效率并没有提高, 多CPU的并行运算才真正解决了运行效率问题.
我们常用的线程
Pthreads

POSIX线程(POSIX threads), 简称Pthreads, 是线程的POSIX标准. 该标准定义了创建和操纵线程的一整套API. 在类Unix操作系统(Unix, Linux, Mac OS X等)中, 都使用Pthreads作为操作系统的线程.虽然高大上跨平台,但看似牛逼却基本用不到

举例用Pthreads创建一个线程去执行一个任务:

#import "pthread.h"
- (void)pthreadsDoTask {
/*
     pthread_t:线程指针
     pthread_attr_t:线程属性
     pthread_mutex_t:互斥对象
     pthread_mutexattr_t:互斥属性对象
     pthread_cond_t:条件变量
     pthread_condattr_t:条件属性对象
     pthread_key_t:线程数据键
     pthread_rwlock_t:读写锁
     //
     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(): 查询线程自身线程标识号
     //
     *创建线程
     int pthread_create(pthread_t _Nullable * _Nonnull __restrict, //指向新建线程标识符的指针
     const pthread_attr_t * _Nullable __restrict,  //设置线程属性。默认值NULL。
     void * _Nullable (* _Nonnull)(void * _Nullable),  //该线程运行函数的地址
     void * _Nullable __restrict);  //运行函数所需的参数
     *返回值:
     *若线程创建成功,则返回0
     *若线程创建失败,则返回出错编号
     */
   pthread_t thread = NULL;
   NSString *params = @"Hello World";
   int result = pthread_create(&thread, NULL, threadTask), (__bridge void *)(params));
   result == 0 ? NSLog(@"creat thread success") : NSLog(@"creat thread failure");
// 设置子线程的状态为detached, 则该线程运行结束后会自动释放所有资源
pthread_detach(thread);
}
void *threadTask(void *params) {
     NSLog(@"%@ - %@", [NSThread currentThread], (__bridge NSString *)(params));
    return NULL;
}

输出结果:

ThreadDemo[1197:143578] creat thread success
ThreadDemo[1197:143649] {number = 3, name = (null)} - Hello World

从打印的结果来看, 该任务是在新开辟的线程中执行的, 但是感觉用起来超不友好, 很多东西需要自己管理, 单单是任务队列以及线程生命周期的管理就够头疼的.之所以抛弃这套API很少用, 是因为我们有更好的选择:NSThread

NSThread是面对对象的, 所以操作起来会简便许多,一起来看看它的API吧

@interface NSThread : NSObject
//当前线程
@property (class, readonly, strong) NSThread *currentThread;
//使用类方法创建线程执行任务
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//判断当前是否为多线程
+ (BOOL)isMultiThreaded;
//指定线程的线程参数,例如设置当前线程的断言处理器。
@property (readonly, retain) NSMutableDictionary *threadDictionary;
//当前线程暂停到某个时间
+ (void)sleepUntilDate:(NSDate *)date;
//当前线程暂停一段时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//退出当前线程
+ (void)exit;
//当前线程优先级
+ (double)threadPriority;
//设置当前线程优先级
+ (BOOL)setThreadPriority:(double)p;
//指定线程对象优先级 0.0~1.0,默认值为0.5
@property double threadPriority NS_AVAILABLE(10_6, 4_0);
//服务质量
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
//线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
//栈区大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);
//是否为主线程
@property (class, readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
//获取主线程
@property (class, readonly, strong) NSThread *mainThread NS_AVAILABLE(10_5, 2_0);
//初始化
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;
//实例方法初始化,需要再调用start方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
//线程状态,正在执行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
//线程状态,正在完成
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
//线程状态,已经取消
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
//取消,仅仅改变线程状态,并不能像exist一样真正的终止线程
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//开始
- (void)start NS_AVAILABLE(10_5, 2_0);
//线程需要执行的代码,一般写子类的时候会用到
- (void)main NS_AVAILABLE(10_5, 2_0);
@end
另外,还有一个NSObject的分类,瞅一眼:
@interface NSObject (NSThreadPerformAdditions)
//隐式的创建并启动线程,并在指定的线程(主线程或子线程)上执行方法。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end

接下来我们下载一张图片,简单应用下:

- (void)createBigImageView {
    self.bigImageView = [[UIImageView alloc] initwithFrame:self.view.bounds];
   [self.view addSubview:self.bigImageView];
   UIButton *startButton = [UIButton buttonWithType:UIButtonTypeSystem];
    startButton.frame = CGRectMake(0, 0, self.view.frame.size.width / 2, 50);
    startButton.backgroundColor = [UIColor grayColor];
    [startButton setTitle:@"开始加载" forState:UIControlStateNormal];
    [startButton addTarget:self action:@selector(loadImage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:startButton];
     
    UIButton *jamButton = [UIButton buttonWithType:UIButtonTypeSystem];
    jamButton.frame = CGRectMake(self.view.frame.size.width / 2, 0, self.view.frame.size.width / 2, 50);
    jamButton.backgroundColor = [UIColor grayColor];
    [jamButton setTitle:@"阻塞测试" forState:UIControlStateNormal];
    [jamButton addTarget:self action:@selector(jamTest) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:jamButton];
} 
-(void)jamTest{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@
"线程阻塞"
 message:@
""
 delegate:nil cancelButtonTitle:@
"好"
 otherButtonTitles:nil, nil];
[alertView show];
}

-(void)loadImage{
NSURL *imageUrl = [NSURL URLWithString:@
"[http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg](http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg)"
];
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
[self updateImageData:imageData];
}

-(void)updateImageData:(NSData*)imageData{
UIImage *image = [UIImage imageWithData:imageData];
self.bigImageView.image = image;
}

把代码粘贴到你的工程内, 可以清楚看到,主线程阻塞了,用户不可以进行其他操作.所以我们要修改下:

-(void)creatBigImageView{
self.bigImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];  
[self.view addSubview:_bigImageView];
UIButton *startButton = [UIButton buttonWithType:UIButtonTypeSystem];
startButton.frame = CGRectMake(0, 20, self.view.frame.size.width / 2, 50);
startButton.backgroundColor = [UIColor grayColor];
[startButton setTitle:@
"开始加载"
 forState:UIControlStateNormal];
[startButton addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:startButton];
UIButton *jamButton = [UIButton buttonWithType:UIButtonTypeSystem];
jamButton.frame = CGRectMake(self.view.frame.size.width / 2, 20, self.view.frame.size.width / 2, 50);
jamButton.backgroundColor = [UIColor grayColor];
[jamButton setTitle:@
"阻塞测试"
 forState:UIControlStateNormal];
[jamButton addTarget:self action:@selector(jamTest) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:jamButton];
}

-(void)jamTest{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@
"阻塞测试"
 message:@
""
 delegate:nil cancelButtonTitle:@
"好"
 otherButtonTitles:nil, nil];
[alertView show];
}

-(void)loadImageWithMultiThread{
//方法1:使用对象方法
//NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
//??启动一个线程并非就一定立即执行,而是处于就绪状态,当CUP调度时才真正执行
//[thread start];
//方法2:使用类方法
[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];

}

-(void)loadImage{

NSURL *imageUrl = [NSURL URLWithString:@
"[http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg](http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg)"
];

    
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];

//必须在主线程更新UI,Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装),waitUntilDone:是否线程任务完成执行
[self performSelectorOnMainThread:@selector(updateImageData:) withObject:imageData waitUntilDone:YES];
//[self updateImageData:imageData];

}

-(void)updateImageData:(NSData*)imageData{
UIImage *image = [UIImage imageWithData:imageData];
self.bigImageView.image = image;
}

多线程确实解决了线程阻塞问题,并且NSThread比Pthreads好用, 但是如果我们有很多不同类型的任务, 每个任务之间还有联系和依赖,NSThread又不能很好的满足我们的需求了,于是GCD出现了.

GCD
GCD, 全名是Grand Central Dispatch, 小名叫共产党,是基于C语言的一套多线程开发API, 一听名字就知道非常NB,这也是目前[苹果官方推荐的多线程开发方式.方便使用又有逼格.它解决了我们上面直接操作线程带来的难题,它自动帮我们管理了线程的生命周期以及任务的执行规则.任务,其实就是你要执行的那段代码.
GCD任务管理方式--队列:简单的管理多个任务
两个通用队列

串行队列: 所有任务会在一条线程中执行(当前线程或者新开辟的线程), 并且一个任务执行完毕后, 才开始执行下一个任务.(等待完成,好比一个位置的厕所,轮流上).
并行队列: 可以开启多条线程并行执行任务(但不一定会开启新的线程), 并且当一个任务放到指定线程开始执行时, 下一个任务就可以开始执行了.(等待发生,一个厕所多个位置).

两个特殊队列

主队列: 系统为我们创建好的一个串行队列, 牛逼之处在于它管理必须在主线程中执行的任务, 属于有劳保的.
全局队列: 系统为我们创建好的一个并行队列, 使用起来与我们自己创建的并行队列无本质差别.

任务执行方式

任务除了管理,还得执行,并且在GCD中并不能直接开辟线程执行任务, 所以在任务加入队列之后, GCD给出了两种执行方式--同步执行(sync)和异步执行(async).
同步执行: 在当前线程执行任务, 不会开辟新的线程.必须等Block函数执行完毕后, dispath函数才会返回.
异步执行: 可以在新的线程中执行任务, 但不一定会开辟新的线程. dispath 函数会立即返回, 然后Block在后台异步执行.

任务队列组合方式
多线程最常见的问题就是线程死锁,例如

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1========%@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{///这里会崩
        NSLog(@"2========%@",[NSThread currentThread]);
    });
    NSLog(@"3========%@",[NSThread currentThread]);
}
打印结果:ThreadDemo[5615:874679] 1========{number = 1, name = main}

为什么会这样呢?因为1任务和2任务相互等待,永久的等待,所以就死锁了.
下面具体介绍下集中组合方式:

串行队列 + 同步执行

-(void)queue_taskTest{
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}
全部都在当前线程顺序执行,也就是说,同步执行不具备开辟新线程的能力。

串行队列 + 异步执行

-(void)queue_taskTest{
    dispatch_async(self.serialQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_async(self.serialQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_async(self.serialQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}
异步执行具有开辟新线程的能力,并且串行队列必须等到前一个任务执行完才能开始执行下一个任务,同时,异步执行会使内部函数率先返回,不会与正在执行的外部函数发生死锁。

并行队列 + 同步执行

-(void)queue_taskTest{
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}
未开启新的线程执行任务,并且Block函数执行完成后dispatch函数才会返回,才能继续向下执行,所以我们看到的结果是顺序打印的。

并行队列 + 异步执行

-(void)queue_taskTest{
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}
开辟了多个线程,触发任务的时机是顺序的,但是我们看到完成任务的时间却是随机的,这取决于CPU对于不同线程的调度分配,但是,线程不是无条件无限开辟的,当任务量足够大时,线程是会重复利用的。

GCD其他函数用法:

1. dispatch_after: 该函数用于任务延时执行,其中参数dispatch_time_t代表延时时长,dispatch_queue_t代表使用哪个队列。如果队列未主队列,那么任务在主线程执行,如果队列为全局队列或者自己创建的队列,那么任务在子线程执行.
2. dispatch: 保证函数在整个生命周期内只会执行一次.
3. dispatch_group_async & dispatch_group_notify: 队列组,当加入到队列组中的所有任务执行完成之后, 会调用dispatch_group_notify函数通知任务全部完成.
4.dispatch_barrier_async: 栅栏函数, 使用此方法创建的任务,会查找当前队列中有没有其他任务要执行,如果有,则等待已有任务执行完毕后再执行,同时,在此任务之后进入队列的任务,需要等待此任务执行完成后,才能执行.
5. dispatch_apply: 该函数用于重复执行某个任务, 如果任务队列是并行队列, 重复执行的任务会并发执行, 如果任务队列为串行队列, 则任务会顺序执行, 该函数为同步函数, 要防止线程阻塞和死锁.
6. dispatch_semaphore_create & dispatch_semaphore_signal & dispatch_semaphore_wait
看这几个函数的时候你需要抛开队列,丢掉同步异步,不要把它们想到一起,混为一谈,信号量只是控制任务执行的一个条件而已,相对于上面通过队列以及执行方式来控制线程的开辟和任务的执行,它更贴近对于任务直接的控制。类似于单个队列的最大并发数的控制机制,提高并行效率的同时,也防止太多线程的开辟对CPU早层负面的效率负担

NSOperation && NSOperationQueue

NSOperation 和 NSOperationQueue是苹果对于GCD的封装, NSOperation其实就是我们之前说的任务, 但是这个类不能直接使用, 我们要用他的两个子类, NSBlockOperation和NSInvocationOperation, 而NSOperationQueue呢,其实就是类似于GCD中的队列, 用于管理你加入到其中的任务.

NSOperation:
它提供了关于任务的执行, 取消, 以及随时获取任务的状态, 添加任务依赖以及优先级等方法和属性, 相对于GCD提供的方法来说, 更直观,更方便,并且提供了更多的控制接口.

@interface NSOperation : NSObject {
@private
    id _private;
    int32_t _private1;
#if __LP64__
    int32_t _private1b;
#endif
}
- (void)start;//启动任务 默认加入到当前队列
- (void)main;//自定义NSOperation,写一个子类,重写这个方法,在这个方法里面添加需要执行的操作。
@property (readonly, getter=isCancelled) BOOL cancelled;//是否已经取消,只读
- (void)cancel;//取消任务
@property (readonly, getter=isExecuting) BOOL executing;//正在执行,只读
@property (readonly, getter=isFinished) BOOL finished;//执行结束,只读
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);//是否并发,只读
@property (readonly, getter=isReady) BOOL ready;//准备执行
- (void)addDependency:(NSOperation *)op;//添加依赖
- (void)removeDependency:(NSOperation *)op;//移除依赖
@property (readonly, copy) NSArray *dependencies;//所有依赖关系,只读
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};//系统提供的优先级关系枚举
@property NSOperationQueuePriority queuePriority;//执行优先级
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);//任务执行完成之后的回调
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);//阻塞当前线程,等到某个operation执行完毕。
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);//已废弃,用qualityOfService替代。
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);//服务质量,一个高质量的服务就意味着更多的资源得以提供来更快的完成操作。
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);//任务名称
@end

NSOperation本身是个抽象类, 不能直接使用, 我们有三种方式赋予它新的生命

1. NSOperation自定义子类: 我们可以自定义继承与NSOperation的子类, 并重写父类提供的方法, 实现一波具有特殊意义的任务.
2. NSBlockOperation, 系统提供的NSOperation的子类NSBlockOperation
3. NSInvocationOperation, 同样也是系统提供给我们的一个任务类,基于一个target对象以及一个selector来创建任务.

最后来个总结吧
1.对于单核CPU来说,不存在真正意义上的并行,所以多线程执行任务,其实也只是一个人在干活,CPU的调度决定了非等待任务的执行速率, 同时对于非等待任务, 多线程并没有真正意义提高效率.

  1. 线程可以简单的认为就是一段代码+运行时数据.
  2. 同步执行会在当前线程执行任务, 不具备开辟线程的能力或者说没有必要开辟新的线程. 并且, 同步执行必须等到Block函数执行完毕, dispatch函数才会返回, 从而阻塞同一串行队列中外部方法的执行.
  3. 异步执行dispatch函数会直接返回, Block函数我们可以认为它再下一帧加入队列,并根据所在队列目前的任务情况无限下一帧执行, 从而不会阻塞当前外部任务的执行. 同时, 只有异步执行才有开辟新线程的必要,但是异步执行不一定会开辟新线程.
  4. 只要是队列, 肯定是FIFO(先进先出), 但是谁先执行完要看第一条.
  5. 只要是出耐性队列, 肯定要等上一个任务执行完成, 才能开始下一个任务. 但是并行队列当上一个任务开始执行后, 下一个任务就可以开始执行.
  6. 想要开辟新线程必须让任务在异步执行, 想要开辟多个线程, 只有让任务在并行队列中异步执行才可以. 执行方式和队列类型多层组合在一定程度上能够实现对于代码执行顺序的调度.
  7. 同步+串行: 未开辟新线程, 串行执行任务;
    同步+并行: 未开辟新线程, 串行执行任务;
    异步+串行: 新开辟一条线程, 串行执行任务;
    异步+并行: 开辟对跳新线程, 并行执行任务;
    在主线程中同步使用主队列执行任务, 会造成死锁.
  8. 对于多核CPU来说, 线程数量也不能无限开辟, 线程的开辟同样会消耗资源, 过多线程同时处理任务并不是我们想象中的人多力量大.
上一篇下一篇

猜你喜欢

热点阅读