iOS多线程基本概念

多线程的基本概念

在计算机编程中有一个基本的概念就是对多个页面加以控制。最开始,线程只是用于分配单个处理器的处理时间的一种工具。处理器往返于多个任务之间,虽然从用户的角度看上去这个多任务是在同时执行的。然而,处理器只能同时处理一项任务,随着多核计算机的发展,多线程技术又有了新的活力。

intel的超线程技术

进程和线程

每个系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者程序的特殊段,他可以在程序中独立执行。也可以把他理解为代码中运行的上下文。所以基本上是轻量级的进程,负责在单个程序里执行多任务。

线程和UI

程序启动后系统会执行一个叫main的主线程。所有UI组件都必须运行在主线程中,因此主线程也叫UI线程。若将所有的任务都放在主线程去执行,很容易会造成UI阻塞。例如从网络上面加载一个图片到视图上面,一些网络请求。如果点击一个按钮之后发起一个同步请求,则会在进行网络请求期间,程序会停止响应其他操作。如果改成异步请求,则不会发生这种问题。使用多线程技术即可实现异步请求所达到的效果。

多线程编程及其意义

线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作成为多线程。
使用多线程的好处

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时候,整个系统都会等待这个操作,此时程序就不会响应键盘、鼠标、菜单的操作,而使用多线程技术,讲耗时长的操作置于一个新的线程,就可以避免这种尴尬的情况
  • 可以使多CPU系统更加优先。操作系统会保证当前线程数不大于CPU数目,不同的线程运行在不同的CPU上面。
  • 改善程序结构。一个即长又复杂的进程可以考虑分为多个线程,成为几个独立或者半独立的运行部分,这样的程序会有利于理解和修改。

iOS中实现多线程的几种方式

iOS系统支持多个层次的多线程编程,层次越高抽象度越高,使用起来越简单。也是苹果官方推荐的使用方法。下面就是根据抽象度从低到高的iOS所支持的多线程编程范式:ThreadCocoa operation以及Grand Central Dispatch(GCD)

Thread是这个三种范式中较为轻量级的,但也是使用起来最复杂的。你需要自己管理thread的生命周期,线程之间的同步。线程共享同一应用程序的部分内存弓箭,他们拥有对数据相同的访问权限。需要协调多个线程对同一数据的访问,一般做法是在访问之间加锁,这会导致一定的内存消耗。

Cocoa operation是基于Objective-C实现的,类NSOperation以面向对象的方式封装了用户需要执行的操作,我们只要聚焦于我们需要做的事情,而不必台操心线程的管理,同步等事情,因为NSOperation已经为我们封装了这些功能,NSOperation类是一个抽象基类,我们必须使用他的子类

Grand Central Dispatch(GCD),iOS4.0以后才开始支持,它支持一些新的特性,以及运行库来支持多核并行编程,它的关注点更高,如何在多个CPU上提效率。

使用NSThread
线程对象NSThread

一个NSThread对象控制了一个进程,当需要把Objective-C中的方法放到独立的线程中运行的时候,可以使用此类。多线程特别适用于:当需要执行一个长时间的任务却不想阻塞其他操作时候。

当你的UI线程产生用户交互,并需要处理一些动作时候,为了防止该处理过程阻塞主线程,你就可以使用多线程技术,将该处理放到另外的线程中执行。另外,多线程可以通过将大块的工作分解为若干个相对独立的小块来执行,以充分利用计算机的多核处理能力,提高运行效率。

NSThread实现多线程的几种方式

NSThread的创建主要有两种直接方式:

//方式1:
NSThread *t1 = [[NSThread alloc]initWithTarget:self selector:@selector(dowork) object:nil];
[t1 start];
//方式2:
[NSThread detachNewThreadSelector:@selector(dowork) toTarget:self withObject:nil];

这两种方式的区别在于:方式2,一调用就会立刻创建一个线程来执行self中的dowork方法;而方式1,需要我们手动调用start启动线程时才会创建线程。

还有一种简介的方法,更加方便,我们甚至不用显式编写NSThread相关代码。那就是利用NSObject类的实例方法performSelectorInBackground: withObject:这个方式与效果2一样。

线程之间的通信

线程在运行过程中,可能需要跟别的线程进行通信的。我们可以使用NSObject中的一些方法:

//在主线程做一些事情
performSelectorOnMainThread: withObject: waitUntilDone:
performSelectorOnMainThread: withObject: waitUntilDone:modes:
//在指定线程中做事情
performSelector: onThread: withObject: waitUntilDone:
performSelector: onThread: withObject: waitUntilDone:modes:
//在当前线程中做事情
performSelector: withObject: afterDelay:
performSelector: withObject: afterDelay:modes:

比如:我们在某个线程中下载数据,下载完成后通知主线程中更新界面等等。代码可能如下:

-(void)myThreadMainMethod{
    @autoreleasepool {
        //.....下载数据或者其他操作
        [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
    }
}
线程对象的局限性

线程对象具有局限性,你需要自己管理thread的生命周期以及处理线程同步问题。线程间共享同一应用程序的部分内存空间,他们拥有对数据相同的访问权限。你得协调多个线程对同一数据的访问,一般做法是在访问之前加锁,这会导致一定的性能开销。

当多个线程对同一个资源进行访问,用@synchronized关键字加锁,比如:

-(void)fetchMoneyAndDecreBalance:(NSString *)who{
    @synchronized(self) {
        NSLog(@"%@取钱:8000,取钱之前余额为%d",who,balance);
        [NSThread sleepForTimeInterval:0.1];
        balance-=8000;
        [NSThread sleepForTimeInterval:0.1];
        NSLog(@"%@取钱后余额为%d",who,balance);
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    balance = 20000;

    NSThread *husband = [[NSThread alloc]initWithTarget:self selector:@selector(fetchMoneyAndDecreBalance:) object:@"丈夫"];
    NSThread *wife = [[NSThread alloc]initWithTarget:self selector:@selector(fetchMoneyAndDecreBalance:) object:@"妻子"];
    [husband start];
    [wife start];

}

可以尝试去掉锁

使用NSThread来直接管理线程,实现起来比较复杂还容易出现问题,最常见的死锁问题,而且数据加锁会明显增加系统开销,建议使用苹果推荐的两种方式来实现多线程编程。

使用NSOperationQueue
NSOperation和NSOperationQueue

NSOperation是一个抽象类,用来封装一个独立任务的代码和数据。不能直接使用该类,通过子类化或者系统提供的子类来完成任务。iOS提供了NSoperation的子类叫做NSInvocationOperation,在这个类中你可以指定一个对象以及一个selector。

NSOperationQueue用来管理operation集合,决定他们的执行顺序。当一个operation被添加到queue后,一旦有多余的线程来执行这个opertaion,那么这个operation就会马上被执行。至于线程是何时创建并使用的,并不需要我们关心。就像我们到一员排队打疫苗,这个队伍就是一个queue,屋内有两个医生,这两个医生可以被看成两个线程。一旦有一个医生处于空闲状态,排在队首的人就会被叫进去打针。虽然这个比喻有点片面,不过对于这个复杂的概念,暂时了解这些就行了。

一般情况下,我们都是让operation queue来界定并发多少个线程,这样可以让你的硬件资源得到充分的利用。但有些情况下,你想对这个线程的数量进行控制,不如说取钱的例子,两个人不可能同时从一个账户中取钱,两个人必须有一个先后顺序。我们可以创建一个serial queue,也就是每次只能执行一个operation的queue。接下来是关于并发多线程操作队列与多线程顺序执行操作队列的示例。

示例:并发多线程操作队列

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
- (IBAction)fireButotnPress:(UIButton *)sender;
@end

创建一个NSOperation子类

#import <Foundation/Foundation.h>
#import "ViewController.h"

@interface MyOperation : NSOperation
@property (nonatomic,retain)ViewController *controller;
@end

#import "MyOperation.h"

@implementation MyOperation
-(void)main{
    for (int i = 0; i < 100; i++) {
        [NSThread sleepForTimeInterval:0.02];
        //有关UI的改动需要在主线程中执行
        [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:YES];
    }
}

-(void)updateUI{
    self.controller.progressView.progress += 0.01;
}
//当operation执行的时候,main方法会被自动调用,在这个方法中我们以固定的速度修改进度条的进度。
@end

然后实现按钮方法改变进度条进度

    MyOperation *my = [MyOperation new];
    my.controller = self;
    MyOperation *my2 = [MyOperation new];
    my2.controller= self;
    MyOperation *my3 = [MyOperation new];
    my3.controller = self;
    NSOperationQueue *queue = [NSOperationQueue new];
    [queue addOperation:my];
    [queue addOperation:my2];
    [queue addOperation:my3];

这里我们将MuOperation实例化了三遍,并将他们加入了操作队列中,这三个操作都是以固定的速度改变进度条的进度,并且都是并发执行的。三个操作仪器加入到操作队列中,他们会并发执行,进度条增加的速度回比只加入一个快很多

多线程顺序执行操作队列

还是以两人从同一银行账户中取钱为例子,在刚才的工程中添加一个银行账户类:

#import <Foundation/Foundation.h>

@interface BankAccount : NSObject
@property (nonatomic)int balance;
-(void)fetch:(NSString *)name andAmount:(int)amount;
@end

#import "BankAccount.h"

@implementation BankAccount
-(void)fetch:(NSString *)name andAmount:(int)amount{
    NSLog(@"%@取钱%d,取钱之前余额为%d",name,amount,self.balance);
    [NSThread sleepForTimeInterval:0.1];
    self.balance -= amount;
    [NSThread sleepForTimeInterval:0.1];
    NSLog(@"%@取钱之后余额为%d",name,self.balance);
}
@end

再添加一个操作类,执行取钱这一个动作

#import <Foundation/Foundation.h>
#import "BankAccount.h"

@interface Fetcher : NSOperation
@property (nonatomic,retain)NSString *name;
@property (nonatomic,retain)BankAccount *account;
@end


#import "Fetcher.h"

@implementation Fetcher
-(void)main{
    [self.account fetch:self.name andAmount:1000];
}
@end

为我们的视图中的一个适合的位置添加一个按钮。并配置它的操作:

- (IBAction)fetchMoney:(UIButton *)sender {
    BankAccount *account = [BankAccount new];
    account.balance = 10000;
    Fetcher *husband = [Fetcher new];
    Fetcher *wife = [Fetcher new];
    husband.name = @"丈夫";
    wife.name = @"妻子";
    husband.account = account;
    wife.account = account;
    NSOperationQueue *bankQueue = [NSOperationQueue new];
    [bankQueue addOperation:husband];
    [bankQueue addOperation:wife];
    //多线程顺序执行操作队列
    bankQueue.maxConcurrentOperationCount = 1;
}

点击按钮的时候,创建一个存有10000元的账户,以及两个取钱的操作,配置好的相关属性后将两个操作加入到同一个县城队列中,如果没有最后一句代码,两个操作并发执行,我们还得考虑一个数据同步的问题,加入最后一句代码后,这两个operation就会按照顺序执行,而不会出现同一时刻访问同一资源的问题。

NSOperation的优先级取消

Operation的相互依赖性

任何一个operation都能够选择性的和另外一个或者是几个operation存在一定的依赖性,所谓的依赖性,比方说一个operation要在另一个operation之前完成,那么operation queue就会知道先完成哪一个operation。你可以通过addDependency方法来添加依赖关系。比如:

    MyOperation *firstOperation = [[MyOperation alloc]init];
    MyOperation *secondOperation = [[MyOperation alloc]init];
    [secondOperation addDependency:firstOperation];

这里,如果有firstOperation和secondOperation都被加入到同一个operation queue里面的时候,即使queue有足够线程来执行这两个operation,这两个operation也不会同事执行,因为secondOperation是依赖于firstOperation的,在firstOperation执行之前是不会执行的。

你可以通过dependdencies这个方法以NSArray形式返回一个operation所有依赖关系,也可以利用removeDependency移除operation的一个依赖关系。

operation的优先级
我们可以使用setQueuePrority方法设置每个operation的优先级,有以下几种优先级可以选择:NSOperationQueuePriorityVeryLow,NSOperationQueuePriorityLow,NSOperationQueuePriority,NSOperationQueuePriorityNormal,NSOperationQueuePriorityHigh,NSOperationQueuePriorityVeryHigh我们所创建的operation的默认优先级是NSOperationQueuePriorityNormal。虽然优先级高的operation会比优先级低的operation先执行,但是任何一个operation在没有准备好的时候都不会执行。比如说,一个很高优先级operation在它所依赖的operation在他所依赖的operation之前是不会执行的,即使所依赖的operation的优先级很低。

取消一个Operation

你可以用cancel的方法取消一个operation。我们调用方法,会把operation的isCancelled属性设置为YES,而不是马上取消这个operation,什么时候取消这个operation是由这个operation的main方法决定的,当main方法检测到isCanceled属性为YES的时候就会返回,那么这个operation就才被取消掉。

这个cancel的的操作是在operation级别的,而不是operation queue级别的。所以这里看似有个奇怪的情况要说明下,如果一个operation在还开始的时候就被cancel掉了,那么这个operation还是会留在这个queue里面,如果从一个正处于挂起状态的operation调用cancel方法,那么这个operation也不会立刻从队列中移除,它要等到它下次执行的时候,main方法检测到它被cancel掉的时候才会返回,继而从queue中移除。

使用GCD
Grand Central Dispatch简介

Grand Central Dispatch简称GCD是苹果开发的技术,是对于多核编程的解决方案,它主要用于优化应用程序以支持多核处理器以及其他对称多核处理系统。

GCD提供了一种很简单的操作方式来实现并行处理,你可以将你要并发的代码放在一个block中,然后把这个block加入到一个queue当中。

我们在之前已经讨论过operation queue,在GCD中为我们需要执行block提供了三种队列:

  • Main:这个队列顺序执行我们的block,并保证这些block都在主线程中执行。
  • Concurrent:在这个队列会遵循FIFO的原则来执行其中的block,自动为你管理线程
  • Serial:这个类型的队列每次执行一个block,也遵循FIFO原则。
GCD的基本使用

创建一个队列(非Main型的队列),讲比较耗时的工作以block的形式放入到该队列中执行,以避免阻塞主线程,另外要注意的是必须在主线程中修改UI。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 100; i++) {
            [NSThread sleepForTimeInterval:0.02];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.progressView.progress += 0.01;
            });
        }
    });

第一句代码,创建一个队列的时候,需要两个参数,第一个参数是决定改队列的优先级,队列的优先级越高,被加入到该队列里的block越先被执行。参数的选择范围如下:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

数值越小,优先级越低,高优先级的队列中的block会遭遇优先级队列中的block执行,第二句函数调用dispatch_async函数试讲队列queue的block以异步的形式执行,若改为dispatch_sync则同步执行其中的block

GCD的其他使用方式

并发多线程的情况

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSString *str1 = [self dowork];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.textView.text = [self.textView.text stringByAppendingFormat:@"%@",str1];
        });
    });
    dispatch_async(queue, ^{
        NSString *str2 = [self dowork1];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.textView.text = [self.textView.text stringByAppendingFormat:@"%@",str2];
        });
    });
    dispatch_async(queue, ^{
        NSString *str3 = [self dowork2];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.textView.text = [self.textView.text stringByAppendingFormat:@"%@",str3];
        });
    });

首先创建一个Concurrent的队列,然后分别将要并发处理的block添加到这个队列中。分派组的使用:

    NSMutableString *totalString = [[NSMutableString alloc]initWithCapacity:10];
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        NSString *str1 = [self dowork];
        [totalString appendString:str1];
    });
    dispatch_group_async(group, queue, ^{
        NSString *str2 = [self dowork1];
        [totalString appendString:str2];
    });
    dispatch_group_async(group, queue, ^{
        NSString *str3 = [self dowork2];
        [totalString appendString:str3];
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        self.textView.text = totalString;
    });

可以如果你的某个task,需要依赖其他task执行完后的数据,你或者会用到分派组在这些task之间建立依赖关系,比如在上面代码,最后一句代码想要改变textView中的文本,但是目标文本需要执行完self中的dowork,dowork1,dowork2方法后才能够获得。我们首先建立了一个分组拍没然后在分派组中异步执行队列中的block。等到分派组中的任务全部实行完毕后,便会调用响应的dispatch_group_notify函数。

附带图片同步异步,线程队列的关系

这边文章大部分的内容囊括

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值