iOS开发高级进阶(13-16)-多线程

这周就学了多线程,现在还在云里雾里....

线程的概念

  • 执行调度单位
  • 并发资源访问控制
    • 原子操作 Atomic(任务不可分隔,保证数据一致性)
    • 用锁同步,锁的类型
      • Semaphore 信号量:系统多个资源
      • Mutex 互斥量:单个资源
      • Critical Section 临界区:反复访问的情况
      • Read-Write Lock 读写锁 只有1个写,其它是读,不受锁的影响
      • Condition Variable 条件变量 符合条件后激活

iOS中多任务API

iOS有三种多线程编程的技术:

  1. NSThread
  2. Cocoa NSOperation
  3. GCD 全称:Grand Central Dispatch
    抽象度层次是从低到高的,抽象度越高的使用越简单

三种方式的优缺点介绍:

NSThread:

优点:NSThread 比其他两个轻量级
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销

NSThread实现的技术有下面三种:

  • Technology
  • Description
  • Cocoa threads
    一般使用cocoa thread 技术。

Cocoa operation

优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。

Cocoa operation 相关的类是 NSOperation ,NSOperationQueue。

NSOperation是个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。

创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行。

GCD

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。在iOS4.0开始之后才能使用。GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。

参考:http://blog.csdn.net/totogo2010/article/details/8010231


Main Thread (主线程)

主线程管理界面,也叫 UI 线程
长时间操作线程的应该另外开一个线程进行

NSThread

//新开线程执行 
//import
@import Foundation;

//方法一:直接创建线程并且开始运行线程
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];

//方法二:
NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                    selector:@selector(doSomething:)
                                    object:nil];
[myThread start];

参数说明:

  • selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。

  • target :selector消息发送的对象

  • argument:传输给target的唯一参数,也可以是nil

    //通知主线程更新界面
      NSObject *Obj = [[NSObject alloc]init];
      [Obj performSelectorOnMainThread:@selector(doSomething:) withObject:self waitUntilDone:NO];
    
    //查询是否是主线程
      NSLog(@"%@",[NSThread isMainThread]?@"YES":@"NO");
    

Demo

  1. 为了访问 HTTP 协议,在Info.plist中添加

     <key>NSAppTransportSecurity</key>
     <dict>
         <key>NSAllowsArbitraryLoads</key>
         <true/>
     </dict>
    
  2. 在 View 中添加imageView

  3. .m文件
    #import "ViewController.h"
    #define kURL @"http://upload.jianshu.io/daily_images/images/myb4DiWJtEcFVgHwgdhh.jpg"
    @interface ViewController ()
    @property (weak, nonatomic) IBOutlet UIImageView *imageView;
    @end

    @implementation ViewController
    
    -(void)loadImage:(NSString *) url{
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
        UIImage *image = [UIImage imageWithData:data scale:2.0];
        [self performSelectorOnMainThread:@selector(dispalyImage:) withObject:image waitUntilDone:NO];
    }
    
    -(void)dispalyImage:(UIImage*) image{
        self.imageView.image = image;
    }
    
    - (void)viewDidLoad{
        [super viewDidLoad];
    
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:kURL];
       [thread start];
    }
    
    - (void)didReceiveMemoryWarning{
        [super didReceiveMemoryWarning];
    }
    
    @end
    

创建一个线程的开销

  1. 内核中占据1KB 内存保存线程资料
  2. stack space堆中占据 512KB 内存( osx 主线程8MB\ios 主线程1MB)
  3. 需要耗费90毫秒,费电
    因此线程越少越好

线程属性设定

thread.name = @"image";
thread.stackSize = 1024 * 4 * 5 ;//20K
thread.threadPriority=1.0;//0.0-1.0 ,1.0为高优先级
//?thread.threadDictionary;//没弄清楚怎么用

线程的控制

   [thread start];  
   [thread cancel];  
   NSLog(@"1--%@",[NSThread currentThread]);//当前线程
   [NSThread sleepForTimeInterval:2];
   NSDate * sleepTime =[[NSDate alloc]initWithTimeIntervalSinceNow:60];
   [NSThread sleepUntilDate:sleepTime];
   [NSThread exit];//直接取消,慎用

Runloop (循环执行)

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里

线程刚创建时并没有 RunLoop

RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。

只能在一个线程的内部获取其 RunLoop(主线程除外)

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer

Timer :CFRunLoopTimerRef/NSTimer

Observer:CFRunLoopObserverRef:观察 runloop 状态变化

每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。

如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

两种 source :

  • source0:

    • 只包含一个回调
    • 不能主动触发事件
    • 需要手工标记和唤醒
      CFRunLoopSourceSignal(<#CFRunLoopSourceRef source#>)
      CFRunLoopWakeUp(<#CFRunLoopRef rl#>)
  • source1:

    • 一个 mach_port 和一个回调
    • mach消息到达时,主动唤醒 Runloop 线程

实际上 RunLoop 内部是一个 do-while 循环。

当调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode

这两个 Mode 都已经被标记为"Common"属性

DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态

当创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。

"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

NSRunloop 生命周期

待补充


NSOperation

NSThread中,依赖\同步\调度需要写的东西比较多
参考:http://blog.csdn.net/totogo2010/article/details/8013316

  • NSOperation:封装任务部分
    • 用 selector(选择器)编写任务: NSInvocationOperation
    • 用 block 编写任务:NSBlockOperation
    • 任务可以用依赖关系串起来
  • NSOperationQueue: 封装线程管理部分

demo

  1. 为了访问 HTTP 协议,在Info.plist中添加

     <key>NSAppTransportSecurity</key>
     <dict>
         <key>NSAllowsArbitraryLoads</key>
         <true/>
     </dict>
    
  2. 在 View 中添加imageView

  3. .m文件

@interface NSOperationTemplateVC ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation NSOperationTemplateVC


-(void)loadImage:(NSString *) url{
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
    self.image = [UIImage imageWithData:data scale:2.0];

}

-(void)dispalyImage{
    self.imageView.image = self.image;
}


- (void)viewDidLoad
{
    [super viewDidLoad];


    NSInvocationOperation *opLoadImage = [[NSInvocationOperation alloc]initWithTarget:self
                                                                       selector:@selector(loadImage:)
                                                                         object:kURL];
    NSInvocationOperation *opDispalyImage = [[NSInvocationOperation alloc]initWithTarget:self
                                                                       selector:@selector(dispalyImage)
                                                                         object:nil];
    [opDispalyImage addDependency:opLoadImage];

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];

    [queue addOperation:opLoadImage];
    [[NSOperationQueue mainQueue] addOperation:opDispalyImage];

    - (void)didReceiveMemoryWarning
    {
        [super didReceiveMemoryWarning];
    }

    @end

NSOperation 的直接使用

是个抽象基类,一般不直接创造实例

NSOperation *op = [[NSOperation alloc]init];
[op start];
[op waitUntilFinished];
[op cancel];

[op main];
//默认不做
//在 autoreleasepool 里运行
//定义子类时覆盖
//可以通过继承重写NSOperation 的main 使用

NSInvocationOperation

创建

//方法一
     NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self
                             selector:@selector(loadImage:)
                             object:kURL];
//方法二
    NSInvocation * invocation =[[NSInvocation alloc]init];
    NSInvocationOperation *operation2 = [[NSInvocationOperation alloc]initWithInvocation:invocation];

NSBlockOperation

//创建
[NSBlockOperation blockOperationWithBlock:<#^(void)block#>);

//追加
 NSBlockOperation *blockOperation = [[NSBlockOperation alloc]init];
 [blockOperation addExecutionBlock:<#^(void)block#>];

NSOperation 依赖关系

[op addDependency:<#(nonnull NSOperation *)#>];

依赖完成后本Operation才会执行

移除(必须手工移除):

[op removeDependency:<#(nonnull NSOperation *)#>];

NSOperation 状态

op.name = @"image";

NSLog(@"%@",op.ready?@"YES":@"NO");
NSLog(@"%@",op.finished?@"YES":@"NO");
NSLog(@"%@",op.cancelled?@"YES":@"NO");

NSLog(@"%@",op.asynchronous?@"YES":@"NO");

[op setCompletionBlock:<#(void (^ _Nullable)(void)()completionBlock#>];

NSOperationQueue

//add 操作
    [queue addOperation:operation];
    [queue addOperations:<#(nonnull NSArray<NSOperation *> *)#> waitUntilFinished:NO];
    [queue addOperationWithBlock:<#^(void)block#>];


    [queue setMaxConcurrentOperationCount:5];//可跨队列,但必须保证没有阻塞和挂起
    [queue cancelAllOperations];
    [queue setSuspended:YES];//暂停

    [queue waitUntilAllOperationsAreFinished];

mainQueue

  NSOperationQueue *operationQueue = [NSOperationQueue mainQueue];

GCD

(作业相关内容,必须好好学)

用函数的方法调用进程,是一个 C语言 API

Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统

参考:http://blog.csdn.net/totogo2010/article/details/8016129

dispatch queue分为下面三种:

  • Serial
    又称为private dispatch queues,同时只执行一个任务。Serial queue通常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。

  • Concurrent
    又称为global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。

  • Main dispatch queue
    它是全局可用的serial queue,它是在应用程序主线程上执行任务的。
    我们看看dispatch queue如何使用

用法:

  1. 拿到一个 queue
  2. 把 block 放到 queue 中去

用dispatch_async处理后台任务

dispatch_sync:等执行结束再返回
dispatch_async:不等结束就返回
如果要获知任务结束信息,只要自己写就好了
现在在执行的 queue
dispatch_get_current_queue()

全局队列
      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{...}

  //1
  DISPATCH_QUEUE_PRIORITY_HIGH
  DISPATCH_QUEUE_PRIORITY_DEFAULT
  DISPATCH_QUEUE_PRIORITY_LOW
  DISPATCH_QUEUE_PRIORITY_BACKGROUND

  //2
   QOS_CLASS_USER_INTERACTIVE//响应用户         
   QOS_CLASS_USER_INITIATED//用户发起               
   QOS_CLASS_DEFAULT//
   QOS_CLASS_UTILITY//程序内部维护
   QOS_CLASS_BACKGROUND//后台
   QOS_CLASS_UNSPECIFIED//
主队列(串行)
dispatch_get_main_queue(), ^{...}
//dispatch_main(); 在 OSX 服务程序中使用,iOS 不使用

Demo

 - (void)viewDidLoad
{
    [super viewDidLoad];

//新建线程,然后通知主线程更新界面
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSURL * url = [NSURL URLWithString:@"http://upload.jianshu.io/daily_images/images/myb4DiWJtEcFVgHwgdhh.jpg"];
    NSData * data = [[NSData alloc]initWithContentsOfURL:url];
    UIImage *image = [[UIImage alloc]initWithData:data];
    if (data != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
    }
});

}

dispatch_queue_create(串行)

dispatch_queue_create("com.my.q",NULL);//name=com.my.q

操作
dispatch_suspend()/dispatch_resume()
dispatch_set_finalizer_f(func)//clean

传递参数

dispatch_set_context(q,voidPtr)
dispatch_get_context(q,voidPtr)

dispatch_once让单例线程安全

dispatch_once(<#dispatch_once_t *predicate#>, <#^(void)block#>)

 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)); // 1

用 dispatch_after 延后工作

 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2
});

dispatch_apply(5, globalQ, ^(size_t index) {
// 执行5次
});

dispatch_group_async

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"group1");
});
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"group2");
});
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:3];
    dispatch_group_wait(group,10);
    NSLog(@"group3");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"updateUi");
});

用 dispatch_barrie\dispatch_semaphore 处理读者与写者的冲突(临界区)

  • dispatch_barrier_async
  • dispatch_barrier_sync
dispatch_barrier

用 Dispatch Sources 来监视事件(进度条)

用dispatch_semaphore(信号量)来处理多个资源

在GCD中有三个函数是semaphore的操作,分别是:
  dispatch_semaphore_create   创建一个semaphore
  dispatch_semaphore_signal   发送一个信号
  dispatch_semaphore_wait    等待信号
第一个函数有一个整形的参数,可以理解为信号的总量
dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1
dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1

int data = 3;
__block int mainData = 0;
__block dispatch_semaphore_t sem = dispatch_semaphore_create(0);


dispatch_queue_t queue = dispatch_queue_create("StudyBlocks", NULL);

dispatch_async(queue, ^(void) {
    int sum = 0;
    for(int i = 0; i < 5; i++)
    {
        sum += data;
        
        NSLog(@" >> Sum: %d", sum);
    }
    
    dispatch_semaphore_signal(sem);
});
//dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);//可以看看取消注释前后的区别

for(int j=0;j<5;j++)
{
    mainData++;
    NSLog(@">> Main Data: %d",mainData);
}

用 Dispatch Sources 来监视事件(进度条)

//1
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,     dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
    [progressIndicator incrementBy:dispatch_source_get_data(source)];
});
dispatch_resume(source);

dispatch_apply([array count], globalQueue, ^(size_t index) {
// do some work on data at index
dispatch_source_merge_data(source, 1);
});


//2
dispatch_queue_t globalQueue =     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t stdinSource =    dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                                                   STDIN_FILENO,
                                                   0,
                                                   globalQueue);
dispatch_source_set_event_handler(stdinSource, ^{
    char buf[1024];
    int len = read(STDIN_FILENO, buf, sizeof(buf));
    if(len > 0)
        NSLog(@"Got data from stdin: %.*s", len, buf);
});
dispatch_resume(stdinSource);


//创建

dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                                                   STDIN_FILENO,
                                                   0,
                                                   globalQueue);

DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_MACH_SEND/RECV
DISPATCH_SOURCE_TYPE_PROC 监控进程
DISPATCH_SOURCE_TYPE_VNODE 监控文件系统对象
DISPATCH_SOURCE_TYPE_SIGNAL
DISPATCH_SOURCE_TYPE_READ/WRITE 从描述符中读取数据 向描述符中写入字符

//配置
dispatch_source_set_event_handler(stdinSource, ^{...}]
dispatch_source_set_event_get_data/handle/mask(stdinSource)
dispatch_source_set_cancel_handler(stdinSource, ^{...})
//启动
dispatch_resume(stdinSource, ^{...})

//撤销
dispatch_source_cancel(stdinSource)

计时器Demo

__block int timeout=300; //倒计时时间
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
    if(timeout<=0){ //倒计时结束,关闭
        dispatch_source_cancel(_timer);

        dispatch_async(dispatch_get_main_queue(), ^{
            
          
        });
    }else{
        int minutes = timeout / 60;
        int seconds = timeout % 60;
        NSString *strTime = [NSString stringWithFormat:@"%d分%.2d秒后重新获取验证码",minutes, seconds];
        dispatch_async(dispatch_get_main_queue(), ^{
           
            self.lbTimer.text = strTime;
         
        });
        timeout--;
        
    }  
});  
dispatch_resume(_timer);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容