爬爬爬之路:UI(十九) 多线程

来源:互联网 发布:2015网络歌曲排行榜 编辑:程序博客网 时间:2024/06/05 16:15

多线程原理:

CPU工作时 同一时间只能执行一个任务, 之所以可以造成多条线程一起执行的假象 是CPU高速的在线程之间切换(调度) 来达到多个任务一起执行的效果.

进程和线程:

  • 正在活动或者(运行的应用程序, 就是一个进程)
  • 每一个进程 都至少有一条线程 叫主线程
  • 除了主线程以外的都叫子线程
  • 子线程可以有很多个 但是线程是耗费资源的
  • 在iOS程序中 子线程一般最多不超过5条 注:正常来说3条最佳

主线程的的任务

UI界面 按钮点击 屏幕的滚动(一切用户看到见的 都要在主线程当中去操作)

若让主线程完成比较耗时的操作, 就会导致屏幕的假死.
比较大的耗时操作 或者用户看不到的操作 可以放到子线程当中去操作. 比如下载 解压缩 读取大型数据等. 可以在子线程中操作

多线程的优点:

  1. 可以大大提高执行任务的效率
  2. 可以让用户有更好的用户体验

多线程的缺点:

  1. 如果大量开辟线程会造成程序的卡顿 (耗费过量的资源)
  2. 耗费资源比较大

开辟子线程的方法

OC中开辟子线程的方法大致有4种:

NSObject

NSObject中提供了开辟子线程和回到主线程方法

在后台中完成某一任务

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
在后台完成某一任务, 实际上就是开辟一条子线程, 在子线程中完成该任务
通常, 都是在子线程中完成了某一耗时较大的任务后, 需要回到主线程中进行界面的刷新

回到主线程中完成某一任务

回到主线程的方法有两种

方法一:

```

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

“`
最后一个参数是控制是否立即回到主线程中, 若YES则立即回到主线程. 等该任务完成后再执行子线程中本方法后的操作. 若NO则等子线程中的本方法全部走完后再回到主线程中.

方法二:

“`
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait

“`

NSThread

NSThread提供了两种开辟子线程的方法

对象初始化方法:

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument

利用对象初始化方法创建的子线程需要手动调用start方法开启子线程

类初始化方法

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

用类方法初始化的子线程不需要手动调用start方法, 会自动开启子线程.

NSThread自身没有回到主线程方法, 若需要回到主线程, 需要用到NSObject的回到主线程方法.

NSThread类的常用方法

事实上开辟子线程的方法通常不用NSThread和NSObject提供的方法, 因为他们没有办法对一组子线程任务进行管理. 无法批量完成多个任务. 常用的方法接下来会介绍.

常用方法一: 获取当前线程

+ (NSThread *)currentThread;

常用方法二: 判断当前线程是否是主线程

+ (BOOL)isMainThread;

NSOperation和GCD

NSOpration和GCD相比于前两种开辟子线程的方法而言, 更为高效. 他们可以通过一组队列来管理多个子线程的任务.

通过队列来管理子线程的优势

不用程序员管理 线程的生命周期
系统会根据队列的类型 开启线程去完成任务

线程队列的分类

  1. 串行队列
  2. 并行队列

串行队列

串行队列是让队列里的任务依次执行的队列. 遵循FIFO原则(First in first out 先进先出)

并行队列

并行队列是可以让队列中的任务同时进行的队列. 当然任务能否同时进行, 还跟它的任务种类有关.

什么是任务:

任务其实就是一个代码完成的功能. 比如一段代码的作用是输出一段文字, 那么就可以说这个任务就是输出一段文字. 如果一段代码的作用是请求一个数据, 那么就可以说这个任务就是请求一个数据.
简单而言, 任务就是一段可以实现的目标功能的代码.

任务的分类:

任务分为两种类型: 同步任务 和异步任务.

  • 同步: 没有开启子线程的能力
  • 异步: 拥有开启子线程的能力

NSOperation

NSOperation本身是一个抽象类. 它的功能通常由其子类NSInvocationOperation和NSBlockOperation完成.

NSInvocationOperation 和 NSBlockOperation本身是同步请求的 需要调用start方法开启子线程.
NSInvocationOperation和NSBlockOperation最大的区别在于NSInvocationOperation是将执行方法封装在了@select里, 而NSBlockOperation是将执行方法写在了自身的Block里.

代码示例如下:

// 开启线程所在的方法{    NSInvocationOperation *operation = [NSInvocationOperation alloc];    [operation initWithTarget:self                     selector:@selector(download:)                       object:@"123"];    [operation start];}// 线程调用的方法- (void)download:(NSString *)str {    NSLog(@"任务0%@",[NSThread currentThread]);}// 开启线程所在的方法{    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{    NSLog(@"任务2%@", [NSThread currentThread]);    }];    [BlockOperation start];}

除了开启线程之外, NSOperation还提供了一些控制线程在线程任务队列中进行的方法

比如控制线程在队列中的的优先级:

- (NSOperationQueuePriority)queuePriority;- (void)setQueuePriority:(NSOperationQueuePriority)p;/*优先级的取值:NSOperationQueuePriorityVeryLow = -8L,NSOperationQueuePriorityLow = -4L,NSOperationQueuePriorityNormal = 0,NSOperationQueuePriorityHigh = 4,NSOperationQueuePriorityVeryHigh = 8 优先级大的线程会有更大的几率先执行. (注意只是较大几率, 当前的任务若是已经执行, 就算后面的任务优先级更大也不会强制抢夺当前线程任务的资源)*/

控制线程任务的依赖关系

- (void)addDependency:(NSOperation *)op;// 作用是当前的线程依赖于op线程, 当线程op执行完后再执行本线程

线程任务完成后完成的执行语句

@property (copy) void (^completionBlock)(void);// 利用本属性, 可以给completionBlock赋值, 赋值内容会在该任务执行完毕后被执行.

最重要的是, 可以停止当前线程任务

- (void)cancel;

任务队列NSOperationQueue

NSOperationQueue是用于管理一组NSOperation或其子类对象的队列.

将NSOperation添加到队列中的方法

- (void)addOperation:(NSOperation *)op;

暂停所有任务的方法

@property (getter=isSuspended) BOOL suspended;// YES是暂停   NO是恢复执行

停止所有任务的方法

- (void)cancelAllOperations;

可以通过以上方法对添加到队列中的任务进行统一的批量操作.

值得注意的是

添加到队列中的NSOperation都会由在主线程进行变为在子线程中执行. 默认情况下队列中的任务是并发执行的.
可以通过

@property NSInteger maxConcurrentOperationCount;

这个属性来控制并发执行的数量. 一般为3为最佳.设为1则本队列是串行执行, 若不设置. 系统会自动根据当前的内存来觉得目前的最大并发数. 内存大则并发数大, 内存小则并发数小.

GCD

GCD本身是无法像NSOperation一样独立的创建一个线程运行, 必须依托一个队列才能进行.
GCD可以最大化的发挥多核CPU的效率 用的是C语言的函数
相比于NSOperation, 它所占的资源会更少. 因为NSOperation是OC语言的方法, 它的执行必须依靠对象来调用. 但是对象本身是或多或少占用资源的.

GCD中有三种队列

分别是:

并行队列

// 创建一个并行队列// dispatch_queue_t 是队列的类型. // dispatch_queue_create()是创建队列的方法 第一个参数是一个任意的标识符, 第二个参数是队列的类型(并行和串行两种)dispatch_queue_t queue = dispatch_queue_create("identifier", DISPATCH_QUEUE_CONCURRENT);// 系统本身自带一个并行队列, 也可以获取系统自带的并行队列dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

串行队列

dispatch_queue_t queue = dispatch_queue_create("identifier", DISPATCH_QUEUE_SERIAL);

主队列

dispatch_queue_t mainQueue = dispatch_get_main_queue();// 主队列就是主线程运行的队列. 本质上其实也是一个串行队列.

GCD中不同队列和不同任务的组合

NSOperation在处理队列和任务的组合时并没有GCD来的方便.
利用GCD可以轻松的实现以下几种组合:

  1. 并行队列 — 添加异步任务
  2. 并行队列 — 添加同步任务
  3. 串行队列 — 添加异步任务
  4. 串行队列 — 添加同步任务
// 1. 并行队列 --- 添加异步任务- (void)asyncGlobalQueue {    // async异步, global全局的 CONCURRENT并发的    // 获取系统提供的全局并发队列    // 队列的类型: dispatch_queue_t    // 参数一: CPU切换的频率高低(切换的优先级) 通常设置default即可    // 参数二: 预留的参数 可以填0    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    // 添加异步任务    // 参数1 给哪一个队列添加任务    dispatch_async(queue, ^{        // Block里是添加任务的地方        NSLog(@"任务1%@", [NSThread currentThread]);    });}// 2. 并行队列 --- 添加同步任务- (void)syncGlobalQueue {    // 创建一个并行队列    // 参数1: 可以填一个队列的标识符    // 标识符通常使用反向域名 比如百度的反向域名是com.baidu.www    // 参数2: 填要创建的队列的类型:(串行还是并行)    dispatch_queue_t queue = dispatch_queue_create("identifier", DISPATCH_QUEUE_CONCURRENT);    // 添加同步任务    dispatch_sync(queue, ^{        NSLog(@"同步任务1%@", [NSThread currentThread]);    });    // 自己手动创建的队列 在MRC下是要手动释放的 ARC下释放是不用自己管的    dispatch_release(queue);}// 3. 串行队列 --- 添加异步任务- (void)asyncSerialQueue {    // 创建一个串行队列    dispatch_queue_t queue = dispatch_queue_create("identifier", DISPATCH_QUEUE_SERIAL);    dispatch_async(queue, ^{        NSLog(@"异步任务1%@", [NSThread currentThread]);    });    dispatch_release(queue);}// 4. 串行队列 --- 添加同步任务- (void)syncSerialQueue {    dispatch_queue_t queue = dispatch_queue_create("123", DISPATCH_QUEUE_SERIAL);    dispatch_sync(queue, ^{        NSLog(@"同步串行任务1%@", [NSThread currentThread]);    });    dispatch_release(queue);}/* GCD中, 并发队列添加异步任务是最为常用的. 它的结果就是可以开辟多个子线程来同时开启任务.  开辟的子线程数是系统通过合理的计算决定的, 不受也不需要程序员控制. 并发队列添加同步任务的结果是所有同步任务都会在主线程中顺序进行. 串行队列添加异步任务的结果是所有异步任务会在同一个子线程中进行, 顺序不定 串行队列添加同步任务的结果和并发队列添加同步任务的结果相同, 一样是在主线程中顺序进行.*/

线程之间的通信及线程互斥(线程保护)

线程通信

在子线程中 完成耗时的操作 完成后需要回到主线程 进行UI刷新

主线程与子线程是独立的

开启子线程 请求图片或者数据 请求完成后回到主线程显示图片(刷新UI)

其实主要涉及到的数据就是子线程中完成了某个步骤以后, 需要回到主线程中调用UI刷新的方法.

利用GCD来完成方法如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_async( queue, ^{        // 请求的耗时操作 这里以请求一张图片为例(图片是百度图库上随机找的)        // 在子线程中的同步请求 其实就相当在主线程中的异步请求        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://pic2.ooopic.com/01/03/51/25b1OOOPIC19.jpg"]];        UIImage *image = [UIImage imageWithData:data];        // 回到主线程刷新界面        // 取出主线程dispatch_get_main_queue();        dispatch_async(dispatch_get_main_queue(), ^{            // 刷新界面操作.比如: [self.tableView reloadData];            // 此处的相应操作如下:            self.imageView.image = image;        });    });

线程互斥

应对场景就是当多个子线程同时访问同一个属性的时候, 会由于子线程之间独立进行的特点导致被访问属性的数据操作异常的问题.

比如属性@property(nonatomic, assign) NSInteger number;

设置number的初值为10.

当多个(比如4个)子线程同时(理想状态下的并发, 也就是各线程之间没有任何延迟)访问该属性,
该属性的对应操作是self.number--;.

此时由于子线程独立运行, 所以在每个子线程自身的角度来看, 刚访问数据的时候都是10, 进行-1操作后结果应该是9.
但是对于number本身而言, 它其实已经被减了4次. 若是此时子线程中有特定的操作. 比如子线程需要对number进行判断, 若number = 7时由一个子线程来完成另一项操作(这个操作只需要被触发一次). 这时候就很有可能因为数据异常而无法触发到该操作. 或是多个子线程均触发了该操作.

解决该问题的方法是对number值进行锁定操作. 同一个时间内只能有一个子线程可以对其进行操作.

一个方法是将属性的nonatomic改成atomic.
另一个方法是手动给属性加锁.
方法如下:

// 在viewDidLoad中给NSLock对象进行初始化self.lock = [[NSLock alloc] init];// 给共同访问的属性操作的时候进行[self.lock lock];self.number --;[self.lock unlock];

利用线程互斥的原理实现单例的带线程保护的初始化方法

这里需要用到一个函数

dispatch_once();

本函数的作用是在整个程序运行期间只执行一次, 类似与static变量的赋值. 可以说是自带线程保护的一种方法

具体如下:

+ (MyHandle *)shareHandle {    static MyHandle *handle = nil;    static dispatch_once_t onceToken;    // 带线程保护的单例初始化方法    dispatch_once(&onceToken, ^{        // 执行的任务 在整个程序运行期间 只执行一次        // 并且只允许 一个线程访问 (自带 线程保护)        handle = [[MyHandle alloc] init];    });    return handle;}
0 0