iOS 多线程之 NSThread

1. 线程的概念

首先简单叙述一下这两个概念,我们在电脑上单独运行的每个程序就是一个独立的进程,通常进程之间是相互独立存在的,进程是系统分配资源的最小单元。进程中的最小执行单位就是线程,并且一个进程中至少有一个线程,进程中的所有线程共用这个进程的资源,线程也是系统进行调度的最小单元。

1.1 多线程在多核CPU中处理任务
多线程在多核CPU中处理任务的过程
1.2 线程状态的切换
线程状态切换
1.3 线程安全问题

线程安全问题是在多个线程处理任务的情况下产生。例如多个线程在同事执行下面的任务时,如果多个线程可以同时执行这段代码(任务),当多个线程同时在getTicket方法中执行count--时,count的结果就会出现不准确的情况,这个处理过程就不是线程安全的。

NSInteger ticketCount = 100;
- (void)getTicket() {

       count--;
       NSLog(@"剩余票数 = %d", ticketCount);
   }
1.4 iOS 中的多线程

在iOS中每个app启动后都会建立一个主线程,也称UI线程。由于除了主线程的其他子线程都是独立于Cocoa Touch的,所以一般只使用主线程来更新UI界面。iOS中的多线程有三种方式: NSThread、NSOperation、GCD,其中GCD是目前苹果比较推荐的方式。对于这篇文章,我们主要了解一下NSThread。

2. NSThread简介

NSThread是轻量级的多线程开发,优点是我们可以直接实例化一个NSThread对象并直接操作这个线程对象,但是使用NSThread需要自己管理线程生命周期。iOS开发过程中,NSThread最常用到的方法就是 [NSThread currentThread]获取当前线程,其他常用属性及方法如下:

// 线程字典
@property (readonly, retain) NSMutableDictionary *threadDictionary;
// 线程名称
@property (nullable, copy) NSString *name;
// 优先级
@property double threadPriority ; 
// 是否为主线程
@property (readonly) BOOL isMainThread
// 读取线程状态
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;


// 直接将操作添加到线程中并启动
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument

// 创建一个线程对象
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 

// 启动
- (void)start;

// 撤销
- (void)cancel;

// 退出
+ (void)exit;

// 休眠
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

在NSObject(NSThreadPerformAdditions)类中的几个常用方法,实现了在特定线程上执行任务的功能,该分类也定义在NSThread.h中:

// 在主线程上执行一个方法
- (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;

// 在后台执行一个操作,本质就是重新创建一个线程执行当前方法
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
2.1 NSThread的使用过程

例如在app从网络下载图片时,由于网络原因可能需要较长时间,这时如果只在主线程中进行下载,则这个过程中用户将无法进行其他操作,直到网络图片下载完成之前界面都处于卡死状态(线程阻塞)。我们在主线程中另起一个新线程来单独下载即可解决这一问题,不管资源是否下载完成都可以继续操作界面,不会造成阻塞。示例代码如下:

@interface MultiThread_NSThread ()

// 显示图片
@property (nonatomic, strong) UIImageView *imgView;

@end

@implementation MultiThread_NSThread

- (void)viewDidLoad {
    
    [super viewDidLoad];
    [self setTitle:@"NSThread"];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.edgesForExtendedLayout = UIRectEdgeNone;
    
    [self layoutViews];
}

- (void)layoutViews {
    
    CGSize size = self.view.frame.size;
    
    _imgView =[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, 300)];
    [self.view addSubview:_imgView];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(15, CGRectGetMaxY(_imgView.frame) + 30, size.width - 15 * 2, 45);
    [button setTitle:@"点击加载" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}


#pragma mark - 多线程下载图片

- (void)loadImageWithMultiThread {
    
    ////1. 对象方法
    //NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImage) object:nil];
    //[thread start];
    
    //2. 类方法
    [NSThread detachNewThreadSelector:@selector(downloadImg) toTarget:self withObject:nil];
}


#pragma mark - 加载图片

- (void)downloadImg {
    
    // 请求数据
    NSData *data = [self requestData];
    // 回到主线程更新UI
    [self performSelectorOnMainThread:@selector(updateImg:) withObject:data waitUntilDone:YES];
}


#pragma mark - 请求图片数据

- (NSData *)requestData {
    
    NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    return data;
}


#pragma mark - 将图片显示到界面

- (void)updateImg:(NSData *)imageData {
    
    UIImage *image = [UIImage imageWithData:imageData];
    _imgView.image = image;
}

@end

在请求数据的代码上打一个断点,可以看出NSThread是对pthread的封装:


NSThread是对pthread的封装
2.2 使用NSThread实现多线程并发

下面我们使用NSThread实现多线程加载多张网络图片,来了解NSThread多线程处理任务的过程。示例代码如下:

@implementation NSThreadImage

@end

#define ColumnCount    4
#define RowCount       5
#define Margin         10

@interface MultiThread_NSThread1 ()

// imgView数组
@property (nonatomic, strong) NSMutableArray *imgViewArr;
// thread数组
@property (nonatomic, strong) NSMutableArray *threadArr;

@end

@implementation MultiThread_NSThread1

- (void)viewDidLoad {
    
    [super viewDidLoad];
    [self setTitle:@"NSThread1"];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.edgesForExtendedLayout = UIRectEdgeNone;
    
    [self layoutViews];
}

- (void)layoutViews {
    
    CGSize size = self.view.frame.size;
    CGFloat imgWidth = (size.width - Margin * (ColumnCount + 1)) / ColumnCount;
    
    _imgViewArr = [NSMutableArray array];
    for (int row=0; row<RowCount; row++) {
        for (int colomn=0; colomn<ColumnCount; colomn++) {
            UIImageView *imageView=[[UIImageView alloc] initWithFrame:CGRectMake(Margin + colomn * (imgWidth + Margin), Margin + row * (imgWidth + Margin), imgWidth, imgWidth)];
            imageView.backgroundColor=[UIColor cyanColor];
            [self.view addSubview:imageView];
            [_imgViewArr addObject:imageView];
        }
    }
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(15, (imgWidth + Margin) * RowCount + Margin, size.width - 15 * 2, 45);
    [button addTarget:self action:@selector(loadImgWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"点击加载" forState:UIControlStateNormal];
    [self.view addSubview:button];
}


#pragma mark - 多线程下载图片

- (void)loadImgWithMultiThread {
    
    _threadArr = [NSMutableArray array];
    for (int i=0; i<RowCount*ColumnCount; ++i) {
        NSThreadImage *threadImg = [[NSThreadImage alloc] init];
        threadImg.index = i;
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(loadImg:) object:threadImg];
        thread.name = [NSString stringWithFormat:@"myThread%i",i];
        //// 优先级
        //thread.threadPriority = 1.0;
        [thread start];
        [_threadArr addObject:thread];
    }
}


#pragma mark - 加载图片

- (void)loadImg:(NSThreadImage *)threadImg {
    
    //// 休眠
    //[NSThread sleepForTimeInterval:2.0];
    //// 撤销(停止加载图片)
    //[[NSThread currentThread] cancel];
    //// 退出当前线程
    //[NSThread exit];
    
    // 请求数据
    threadImg.imgData =  [self requestData];
    // 回到主线程更新UI
    [self performSelectorOnMainThread:@selector(updateImg:) withObject:threadImg waitUntilDone:YES];
    
    // 打印当前线程
    NSLog(@"current thread: %@", [NSThread currentThread]);
}


#pragma mark - 请求图片数据

- (NSData *)requestData{
    
    NSURL *url = [NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/products/apple-products-section1-one-holiday-201811?wid=2560&hei=1046&fmt=jpeg&qlt=95&op_usm=0.5,0.5&.v=1540576114151"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    return data;
}


#pragma mark - 将图片显示到界面

- (void)updateImg:(NSThreadImage *)threadImg {
    
    UIImage *image = [UIImage imageWithData:threadImg.imgData];
    UIImageView *imageView = _imgViewArr[threadImg.index];
    imageView.image = image;
}


//#pragma mark 停止加载网络图片
//
//- (void)stopLoadingImgs {
//
//    for (int i=0; i<RowCount*ColumnCount; ++i) {
//
//        NSThread *thread = _threadArr[i];
//        if (!thread.isFinished) {
//            [thread cancel];
//        }
//    }
//}

@end

3. 关于NSThread线程状态的说明

NSThread类型的对象可以获取到线程的三种状态属性isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经撤销),其中撤销状态是可以在代码中调用线程的cancel方法手动设置的(在主线程中并不能真正停止当前线程)。isFinished属性标志着当前线程上的任务是否执行完成,cancel一个线程只是撤销当前线程上任务的执行,监测到isFinished = YES或调用cancel方法都不能代表立即退出了这个线程,而调用类方法exit方法才可立即退出当前线程。

例如在加载多张网络图片时,中途停止加载动作的执行:

#pragma mark 停止加载网络图片

- (void)stopLoadingImgs {
    
    for (int i=0; i<RowCount*ColumnCount; ++i) {
        
        NSThread *thread = _threadArr[i];
        if (!thread.isFinished) {
            [thread cancel];
        }
    }
}

PS:

  1. 更新UI需回到主线程中操作;
  2. 线程处于就绪状态时会处于等待状态,不一定立即执行;
  3. 区分线程三种状态的不同,尤其是撤销和退出两种状态的不同;
  4. 在线程死亡之后,再次点击屏幕尝试重新开启线程,则程序会挂;
  5. NSThread可以设置对象的优先级thread.threadPriority,threadPriority取值范围是0到1;
  6. NSThread并没有提供设置线程间的依赖关系的方法,也就不能单纯通过NSThread来设置任务处理的先后顺序,但是我们可以通过设置NSThread的休眠或优先级来尽量优化任务处理的先后顺序;
  7. 在自己试验的工程中,虽然NSThread实例的数量理论上不受限制,但是正常的处理过程中需要控制线程的数量。

参考文章:http://www.cocoachina.com/articles/20404

工程源码GitHub地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 1、简介:1.1 iOS有三种多线程编程的技术,分别是:1.、NSThread2、Cocoa NSOperatio...
    LuckTime阅读 1,333评论 0 1
  • NSThread: NSThread 是一个控制线程执行的对象,它不如 NSOperation抽象,通过它我们可以...
    蓝白自由阅读 607评论 0 4
  • 国庆之后真的很糟糕,有时候在深夜偷偷的问自己是不是不适合写代码?是不是不适合当程序员?是不是要转行才能见到未来?是...
    土鳖不土阅读 644评论 0 0
  • iOS多线程开发基础概念 进程 VS 线程 进程:程序的一次执行,是正在执行的程序的实例,它是Unix的一个基本概...
    qingmarch阅读 358评论 0 1
  • 悄悄的我喜欢上了你 你如同兰花般 宁静 优雅 散发着无尽的美丽 迷人的淡香 扑面而来 无论我在哪里 始终萦绕在我的...
    我是一片叶儿阅读 262评论 0 0