iOS 多线程的简介及使用

作者: 堂吉诃德灬 | 来源:发表于2016-08-16 21:49 被阅读128次

    1 进程和线程

    1.1 进程

    进程是指程序在其内存地址的一次运行活动,各个进程之间是独立的,且各个进程都在其专用且受保护的内存空间运行。

    1.2 线程

    进程想要执行任务必须有线程,每一个进程必须要有一条线程,线程是进程的基本执行单元,线程依赖于进程而存活,一个进程里面可以有多个线程

    从技术角度来看,一个线程就是一个需要管理执行代码的内核级和应用级数据结构组合。内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。应用级结构包括用于存储函数调用的调用堆栈和应用程序需要管理和操作线程属性和状态的结构。

    多线程的优势:
    (1)多线程可以提高应用程序的感知响应
    (2)多线程可以提高应用程序在多核系统上的实时性能。

    多线程的劣势:
    (1)增加代码复杂性,带来大量的开销
    (2)每一个线程需要和其他线程协调其行为,以防止破坏应用程序的状态信息。
    (3)多线程访问其共享的内存空间可能破坏其数据结构。

    一个run loop是用来在线程上管理事件异步到达的基础设施。一个run loop为线程监测一个或多个事件源。当事件到达的时候,系统唤醒线程并调度事件到run loop,然后分配给指定程序。如果没有事件出现和准备处理,run loop把线程置于休眠状态

    1.3 线程的类型

    在iOS中,线程有两种类型,分别为Joinable和Detach,Joinable类型的线程可以被其他线程回收资源和终止。如果一个Joinable的线程与主线程结合,那么当主线程结束而该二级线程还没有结束的时候,主线程会被阻塞等待该二级线程,当二级线程结束后由主线程回收其占用资源并将其关闭。如果在主线程还没有结束而该二级线程结束了,那么它不但不会关闭,而且资源也不会被系统回收,只是等待主线程处理。而detach线程则刚好相反,会自行结束关闭线程并且由系统回收资源。

    2 串行队列和并行队列

    2.1 队列的特点

    队列只是负责任务的调度,而不负责任务的执行,任务是在线程中执行的。队列遵循先进先出原则,排在前面的任务最先执行

    2.2 串行队列

    任务按照顺序被调度,前一个任务不执行完毕,队列不会调度

    2.3 并行队列

    只要有空闲的线程,队列就会调度任务,不考虑前面是否有任务在执行,只要有线程可以利用,队列就会调度任务

    3 同步和异步

    同步执行:同步操作不管是在串行队列还是并行队列,不会新建线程,只会在当前线程按顺序执行。同步操作会阻碍当前线程并等待block中的任务执行完毕,然后当前线程才会往下执行。

    异步执行:所有的异步执行都会开启新的线程,不会阻碍当前线程

    4 队列的组合方式及特点

    串行队列同步执行:不会开启新的线程,在当前线程任务按照顺序执行
    串行队列异步执行:在其他线程任务按照顺序执行
    并行队列同步执行:在当前线程任务按照顺序执行
    并行队列异步执行:在多个线程任务并发执行

    5 IOS的多线程方案

    5.1 NSThread

    NSThread是苹果封装的面向对象的多线程方案,它可以直接操纵线程对象,但是需要自己管理线程的生命周期。
    用NSThread创建线程的两种方法:
    1.创建一个NSThread的对象,并调用Start方法启动

    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:nil];
    [thread start];
    
    //这样创建的线程是Detach类型的,当主线程结束之后,子线程如果没有开始执行,那么子线程将会被系统回收,永远不会被执行。
    

    2.直接创建线程并启动

    [NSThread detachNewThreadSelector:@selector(run:)toTarget:self withObject:nil];
    

    这两种方法都在应用里面创建了一个脱离的线程,脱离的线程意味着当线程退出的时候,线程的资源由系统自动回收。

    同时还可以用NSThread获取当前线程并阻塞线程

    NSThread *current = [NSThread currentThread]; //获取当前线程
    NSThread *main = [NSThread mainThread];
    //阻塞线程
    [NSThread sleepForTimeInterval:3];
    [NSThread sleepUntilDate:[NSDatedateWithTimeIntervalSinceNow:3]];
    

    当需要退出线程的时候需要调用cacel方法来把线程标记为取消,但是线程只是状态被标记为取消,如果需要线程并没有终止,如果需要终止线程,那么需要调用exit方法来退出线程。

    [thread cacel];//把线程标记为取消,但是线程并没有终止,只是状态改变了
    
    [thread exit];//退出终止线程
    

    5.2 NSOperation&NSOperationQueue

    5.2.1 NSOperation的优点

    NSOperation可以控制线程的并发数以及线程之间的依赖关系,因为线程也是需要消耗系统资源的

    5.2.2 NSOperation的使用方式

    将要执行的任务封装到一个NSOperation对象中,然后将创建好的NSOperation对象放入到NSOperationQueue队列中,OperationQueue便开始启动新的线程去执行队列中的操作

    NSOperation是一个抽象类,不能封装任务,它靠两个子类来进行任务的封装,分别是NSInvocationOperation和NSBlockOperation,创建一个NSOperation后需要调用start方法来启动任务,当然也可以调用cancel方法来取消任务,当创建完任务后,它会默认在当前队列同步执行(注意是在当前队列同步执行)
    使用NSInvocationOperation创建:

    NSInvocationOperation*operation = [NSInvocationOperation alloc]initWithTarget:self selector:@selector(run:) withObject:nil];
    [operation start];
    

    使用NSBlockOperation创建

    NSBlockOperation *operation =[NSBlockOperation blockOperationWithBlock:^{
         NSLog(@"%@", [NSThread currentThread]);
     }];
    [operation start];
    

    同时当在用NSBlockOperation的时候可以调用addExecutionBlock:方法来增加多个执行的block块,然后这些任务会在主线程和其他线程并发执行(此点需注意,当block内的任务多于一个的时候就会开启新线程)

    NSBlockOperation*operation3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"这是NSBlockOperation创建的线程");
            NSLog(@"现在的线程是%@",[NSThreadcurrentThread]);
        }];
        for (int i = 0; i < 10; i++) {
            [operation3 addExecutionBlock:^{
                NSLog(@"这是NSBlockOperation创建的线程");
                NSLog(@"现在的线程是%@",[NSThreadcurrentThread]);
            }];
        }
        [operation3start];
    

    另外addExecutionBlock必须在start方法之前调用,不然会报错

    5.2.3 NSOperationQueue

    NSOperationQueue只有主队列和其他队列,通过类方法来获得的是主队列,通过其他初始化方法获得的是其他队列,在主队列的任务会按照顺序执行,在其他队列的任务会在其他线程并发执行。
    调用类方法获取主队列:

    NSOperationQueue*main = [NSOperationQueue mainQueue];//使用类方法获取的是主队列
        NSBlockOperation *operation4 =[NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"这是加入主队列的");
        }];
    [mainaddOperation:operation4]; //加入队列的不用再调用start方法开始
    

    使用其他初始化方法获取其他队列:

    NSOperationQueue*other = [[NSOperationQueue alloc]init];//使用其他方法初始化的获取的是其他队列
        for (int i = 0; i < 5; i++) 
            NSBlockOperation *operation5 =[NSBlockOperation blockOperationWithBlock:^{
                NSLog(@"执行的是%d   现在的队列是%@",i,[NSThread currentThread]);
            }];
            [other addOperation:operation5];
    }  //加入其他队里的任务在其他队列并发执行,不会占用主线程
    

    在NSOperation里面没有串行队列,如果想要达到串行队列的效果可以调用setMaxConcurrentOperationCount设置最大并发数为1来达到串行效果
    在NSOperationQueue中还可以用addOperationWithBlock:快速添加任务:

    NSOperationQueue*other4 = [[NSOperationQueue alloc]init];
        [other4 addOperationWithBlock:^{
            NSLog(@"现在的线程是%@",[NSThreadcurrentThread]);
    }];
    

    NSOperation最大的特点就是可以设置线程间的依赖关系,因此,当某些操作必须按照顺序完成时可以用NSOperation直接设置依赖关系,不需要再去需找别的解决方案。

    NSOperationQueue*other3 = [[NSOperationQueue alloc]init];
        NSBlockOperation *operation7 =[NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"这是执行的第一个   现在的线程是%@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:3];
        }];
            NSBlockOperation *operation8 =[NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"这是执行的第二个     现在的线程是%@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:3];
        }];
        NSBlockOperation *operation9 =[NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"这是执行的第三个     现在的线程是%@",[NSThread currentThread]);
        }];
        [operation8 addDependency:operation7];
        [operation9 addDependency:operation8];
    [other3addOperations:@[operation7,operation8,operation9] waitUntilFinished:NO];
    

    当添加依赖关系后任务会在其他线程按照依赖关系执行(一般不会在主线程添加依赖关系执行),依赖是添加在任务上的和线程没关系,所以任务可以在不同的线程之间执行。但是添加依赖关系的时候不能添加相互依赖,这会造成死锁,同时也可以调用removeDependency解除依赖关系

    5.2.4 自定义NSOperation

    5.3 GCD

    Grand Central Dispatch是苹果为多核的并行运算提出的解决方案,它可以更合理的利用CPU内核,最大的好处是它可以帮助我们自动管理线程的生命周期(创建,调度和销毁),完全不需要我们管理

    在GCD中分为3种队列,主队列,全局队列和其他队列,在GCD中任务分为同步执行和异步执行

    使用GCD一般需要先创建队列,然后选择在队列里任务是同步执行的还是异步执行的

    5.3.1 GCD创建队列的方式
    主队列(串行队列):dispatch_queue_tqueue = dispatch_get_main_queue();
    
    全局队列(并行队列)dispatch_queue_t global =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    自己创建的队列:这种根据标识符的不同可以分为串行队列和并行队列

    串行队列:
    dispatch_queue_tqueue1 = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);    //注意后面标识符
    dispatch_queue_tqueue2 = dispatch_queue_create("serialQueue", nil);  //后面没有标示符默认为串行
    

    并行队列:

    dispatch_queue_t queue3 = dispatch_queue_create("concurrentQueue",DISPATCH_QUEUE_CONCURRENT);//注意后面的标示符,这种是并行队列
    
    5.3.2任务执行方式
    同步任务:dispatch_sync(queue1,^{
            for (int i  = 0; i <5; i++) {
    NSLog(@"现在的队列是%@   下标是%d",[NSThreadcurrentThread],i);
    
    异步任务:    dispatch_async(queue1, ^{
            for (int i  = 0; i <5; i++) {
    NSLog(@"现在的队列是%@   下标是%d",[NSThreadcurrentThread],i);
    
    5.3.3 队列组

    当我们某些情况下需要一些任务完成后再执行另外一任务后可以考虑用队列组来实现,队列组是将所有任务加入到一个组里,当组里所有的任务都执行完毕了会发出一个通知来告诉我们,然后我们在通知里进行所想要的事件处理

    dispatch_group_tgroup = dispatch_group_create();
        dispatch_queue_t queue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
      dispatch_group_async(group, queue, ^{
            NSLog(@"这是任务一");
        });
        dispatch_group_async(group, queue, ^{
             NSLog(@"这是任务二");
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"这是任务三");
        });
       dispatch_group_notify(group, queue, ^{
            NSLog(@"三个任务完成了");
           dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"去主线程处理某些事情");
            });
    });
    

    但是队列组我们需要注意的一种情况就是,当我们在队列组的某一个任务中又开启了一个异步执行的任务,那么通知发生的时刻可能并不符合我们的要求。比如在任务一里面加入一个网络请求的方法,当网络请求完成之后我们对界面做一下UI的处理,但是实际结果可能是当通知发出告诉我们所有的任务完成的时候,我们的网络请求可能还没有请求回来UI没有刷新。原因是网络请求是一个异步的任务,当执行到网络请求的时候,给网络请求单独开了一个子线程让他去进行网络请求,任务一的代码还继续往下执行,但是当任务一的代码执行完的时候,系统就认为任务一执行完了,但是其实这个时候网络请求还在执行。

    那么针对上面的情况我们该怎么办呢,其实思路就是当我们网络请求还在执行的时候,如果任务一代码执行完毕了,我们可以让任务一先阻塞,然后等网络请求完成之后再让任务一返回。此时我们可以结合信号量来达到我们的目的。我们可以设置一个初始值为0的信号量,当我们网络请求完成的时候让信号量加一,在任务一所有代码执行完之前让信号量减一,这样当网络请求未完成的时候线程就被阻塞了,当网络请求完成的时候信号量加一,整个任务就往下执行。

    5.3.4 dispatch_barrier_async的使用

    dispatch_barrier_async 当它被加入到自己创建的,标示符为DISPATCH_QUEUE_CONCURRENT的并行队列时,它所在的任务会等它所加入的队列排在它前面的任务执行完毕后才会执行,排在它后面的任务会等它执行完成后才会执行。当它被加入别的队列时,它的作用跟dispatch_async类似,任务会并发执行

    dispatch_queue_tqueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
       dispatch_async(queue, ^{
            NSLog(@"这是任务一");
            [NSThread sleepForTimeInterval:2];
        });
        dispatch_async(queue, ^{
            NSLog(@"这是任务二");
            [NSThread sleepForTimeInterval:3];
        });
            dispatch_barrier_async(queue, ^{
            NSLog(@"这是任务三");
            [NSThread sleepForTimeInterval:4];
        });
       dispatch_async(queue, ^{
            NSLog(@"这是任务四");
        });
        dispatch_async(queue, ^{
            NSLog(@"这是任务五");
    });//任务3会等任务1和2执行完毕后才会执行,任务4和5 等任务3执行完毕后才会执行
    
    dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
            dispatch_async(queue, ^{
                NSLog(@"这是任务一");
                [NSThread sleepForTimeInterval:2];
            });
            dispatch_async(queue, ^{
                NSLog(@"这是任务二");
                [NSThread sleepForTimeInterval:3];
            });
            dispatch_barrier_async(queue, ^{
                NSLog(@"这是任务三");
                [NSThread sleepForTimeInterval:4];
            });
           dispatch_async(queue, ^{
                NSLog(@"这是任务四");
            });
            dispatch_async(queue, ^{
                NSLog(@"这是任务五");
            });//任务并发执行,任务三可能在第一个执行,也可能在最后一个执行
    
    5.3.5 dispatch_once_t的使用

    dispatch_once_t**使用来保证只会创建一份实例,一般常用于单例,但是在使用时注意一定要加static来修饰,不然它并不会保证只创建一个实例对象

    staticdispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //单例代码
        });
    
    5.3.6dispatch_after的使用

    dispatch_after是延迟提交,并不是延迟执行,可以理解为这段代码式延迟了特定的时间才加入到队列,但是延迟时间到了后代码是否执行取决于队列的状态(比如队列休眠或者阻塞了那么这段代码肯定不会现在就执行只有等队列有多余的空闲才会执行)

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        NSLog(@"begin");
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:8];
            NSLog(@"second");
        });        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5 * NSEC_PER_SEC)), queue, ^{
            NSLog(@"thread");
        });//thread在5秒后不会立即执行,因为队列处于休眠中,只有等8秒后队列空闲,thread才会被打印
    
    5.3.6dispatch_souce的使用

    1.创建dispatch_souce源

    dispatch_source_t source = dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue)
    
    参数 意义
    type dispatch源可处理的事件
    handle 可以理解为句柄,索引或ID,假如要监听进程,需要传入进程的ID
    mask 可以理解为描述,提供更详细的描述,让它知道具体要监听什么
    queue 自定义源需要的一个队列,用来处理所有的响应句柄(block)

    dispatch_souce可以处理的所有事件:

    名称 内容
    DISPATCH_SOURCE_TYPE_DATA_ADD 自定义的事件,变量增加
    DISPATCH_SOURCE_TYPE_DATA_OR 自定义的事件,变量OR
    DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
    DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
    DISPATCH_SOURCE_TYPE_PROC 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
    DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应
    DISPATCH_SOURCE_TYPE_SIGNAL 接收到UNIX信号时响应
    DISPATCH_SOURCE_TYPE_TIMER 定时器
    DISPATCH_SOURCE_TYPE_VNODE 文件状态监听,文件被删除、移动、重命名
    DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应

    DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_DATA_OR有什么区别呢:对于DISPATCH_SOURCE_TYPE_DATA_ADD来说,当这个事件源在一段时间呢内不断被事件触发的时候,它会把这些事件进行累积,等到合适的时间进行处理,如果某段时间内事件触发的比较少,它会对每一个进行处理。而DISPATCH_SOURCE_TYPE_DATA_OR则是以或的关系来进行的。

    2.dispatch相关的函数

    dispatch_suspend(queue) //挂起队列
    
    dispatch_resume(source) //分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复(意思是想让事件源处理事件,那么必须先将其状态恢复调用)
    
    dispatch_source_merge_data //向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。(意思是如果需要调用处理事件就发送这个函数)
    
    dispatch_source_set_event_handler //设置响应分派源事件的block,在分派源指定的队列上运行(设置事件源处理事件的时候调用的block)
    
    dispatch_source_get_data //得到分派源的数据
    
    uintptr_t dispatch_source_get_handle(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第二个参数
    
    unsigned long dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数
    
    void dispatch_source_cancel(dispatch_source_t source); //取消dispatch源的事件处理--即不再调用block。如果调用dispatch_suspend只是暂停dispatch源。
    
    long dispatch_source_testcancel(dispatch_source_t source); //检测是否dispatch源被取消,如果返回非0值则表明dispatch源已经被取消
    
    void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t cancel_handler); //dispatch源取消时调用的block,一般用于关闭文件或socket等,释放相关资源
    
    void dispatch_source_set_registration_handler(dispatch_source_t source, dispatch_block_t registration_handler); //可用于设置dispatch源启动时调用block,调用完成后即释放这个block。也可在dispatch源运行当中随时调用这个函数。
    

    3.dispatch_souce的基础用法

    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));  
    //创建了一个信号源,这个信号源的处理事件的方式为累加
    
        dispatch_source_set_event_handler(source, ^{
    
            dispatch_sync(dispatch_get_main_queue(), ^{
    
                //更新UI
            });
        });   //设置信号源处理事件的block
    
        dispatch_resume(source);// 启动信号源
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            //网络请求
    
            dispatch_source_merge_data(source, 1); //通知信号源对事件进行处理
        });
    
    

    4.dispatch_souce实例

        dispatch_source_t souce = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));//创建一个处理方式为add的信号源,这个信号源处理的事件发生在全局队列中
        
        dispatch_source_set_event_handler(souce, ^{   //创建信号源处理事件时回调的block
            dispatch_async(dispatch_get_main_queue(), ^{
                long getdata = dispatch_source_get_data(souce);
                NSLog(@"输出的结果是%ld",getdata);
            });
        });
        
        dispatch_resume(souce); // 启动信号源
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 1; i <= 4; i ++) {
                dispatch_source_merge_data(souce, i); //在主线程通知信号源去处理事件,同时进行传值
                sleep(0.00003); //让线程休息一段时间,这样在短时间内触发事件,会使事件累积。如果事件触发的时间段分割较大,那么时间会一个一个处理。
            }
        });
    
    

    此处的输出结果随着线程休眠的时间的不一样会有不同的输出结果,当休眠时间越大越接近于我们的真实期望,当时间间隔越小,越与我们的期望不一样。

    6.设置线程属性

    线程也是有若干属性的,自然有一些属性是可配置的,在启动线程之前我们可以对其进行配置,比如线程占用的内存空间的大小,线程类型,线程的优先级等。

    6.1配置线程的栈空间大小

    在Cocoa框架中,我们可以通过修改NSThread类的stackSize属性来改变二级线程的线程栈的大小,不过要注意的是该属性的单位是字节,并且设置的大小必须是4KB的倍数,最小16KB。

    6.2配置线程的优先级

    在Cocoa框架中,我们可以通过修改NSThread类的setThreadPriority属性来改变二级线程的优先级。其优先级在0.0 - 1.0之间,默认为0.5。但是一般情况下我们不要去修改线程的优先级。因为如果优先级高的线程较多那么会导致优先级较低的线程可能没有机会执行。

    相关文章

      网友评论

        本文标题:iOS 多线程的简介及使用

        本文链接:https://www.haomeiwen.com/subject/jbnpsttx.html