美文网首页
iOS开发-线程和队列

iOS开发-线程和队列

作者: Super超人 | 来源:发表于2017-07-06 18:26 被阅读82次

随着开发的深入,不可避免的要涉及到项目运行速度和内存管理,有时我们也不可避免的遇到数据冲突的问题,多线程操作,什么加锁......那么你不妨看看下面的文章,也许能从另一个角度帮你解决问题,今天就来聊聊队列的调度和线程管理(本文章针对OC的CGD来些浅谈,这段时间在网上看了很多相关的文章,有很多优秀的解释也有很多不合理的,今天在这里谈下我自己总结出来的理解)

串行与并行

队列根据调度方式可以分为串行Serial和并行Concurrent,在GCD中分别对应DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT,在使用GCD的时候我们会把任务通过block的方式追加到队列里面,然后队列按其特性去执行任务

  • 串行队列:需要等待队列里面的上个任务执行完才会去执行下个任务,可以是看做一个排着一条长队等待入场的人群,里面的每个人检票入场对应通过block追加进来的任务
  • 并行队列:不需要等待上个任务执行完就去执行下个任务,可以看做一群不等排队,围成一团(一排更合适)等待入场的人群
同步和异步(synchronization/asynchronization)

针对任务的执行方式可以分为同步执行和异步执行,同步和异步是针对线程来讲,在GCD中分别对应 同步dispatch_sync 异步dispatch_async

  • 同步执行:对于队列里面的任务执行放在同一个线程中,同步执行要阻塞当前线程,必须要等待同步线程中的任务执行完返回以后,才能继续执行下一任务
  • 异步执行:对于队列里面的任务,根据需求可以把不同的任务放在不同的线程里面执行,所以说异步执行不需要等待
就这四种组合举一些简单的立即方便理解

下面我们用一个排队等待入场的生活场景来描述下线程和队列
任务:等待入场的人要给一个人检票让他入场是一个任务,为了后面好描述和理解,这里写成一个等待入场的人是一个任务,执行这个任务是给这个人检票,任务执行完对应这个人检完票
队列:等待入场的人群
线程:检票口

  • 串行队列同步执行:一群人排着一条长队等待入场(不允许插队是肯定的),检票口也只有一个,所以里面的人只能一个一个的入场
  • 串行队列异步执行:一群人排着一条长队等待入场(不允许插队是肯定的),检票口有多个,但是虽然有多个检票口,人群还是排着队的,必须等一个检票口检完票,这个检票口或者另一个检票口才能去检下个人的票,平时写简单的异步执行串行队列代码跑起来似乎始终在同一个线程里面,但这种说法是错误的,往队列里面加任务不是一次性加完,而是有任务的时候才会往对列里面加(就好比来排队入场的人是陆陆续续来),执行这个队列的一个线程有空闲的时候(没人的时候检票员可以休息下),这个时候线程可能会被系统的其他任务所占用(检票员被领导支开干其他事了),这时有任务追加到队列里面执行则需要在一个闲置线程里面去执行(要去其另一个检票口检票入场)
    在这里网上有些说法是只要是串行队列不管怎么执行都始终是在同一个线程中,这种说法是不对的,亲身实验,在一个大工程里面,数据处理量大的时候异步执行串行队列不只一个线程里面
  • 并行队列同步执行:人群在一个检票口围成一群等待入场,虽然不用排队,但是检票口只有一个,人群还是只能一个一个的进
  • 并行队列异步执行:人群围着多个检票口,等待入场,感觉这么比喻有点不合适,通过代码思维来理解,往并行队列里面添加任务,异步执行,添加一个任务,系统里面会找一个闲置的线程/创建一个线程立马来执行任务
//获取系统提供的一个默认优先级别的并行队列,往里面添加一个任务,然后异步执行
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"添加一个待执行任务");
        
        //需要执行的任务,例如我需要计算下当前时间戳
        NSTimeInterval timeInterval = [NSDate date].timeIntervalSince1970;
        NSLog(@"时间戳是: %f",timeInterval);
        
        NSLog(@"任务执行完毕");
        
    });
下面我们来个案例与分析

在做项目时,我遇到了这么个问题,我是做智能家居相关的,插座或者灯我们下面统称为设备,APP里面有设备列表,想控制哪个设备点哪个设备,控制设备有两种方式,一种是跟设备连接直接控制,二是通过服务器转发指令控制设备,当能收到设备散发的多播包或者广播包的时候,说明跟设备是在同一个路由器下,控制设备可以直接跟设备建立TCP然后发送控制指令,同时设备散发的广播包或者多播包里面也包含有设备的状态相关参数(开关状态、实时功率、名字...),这些参数都要解析,为了保证设备状态显示的及时性,我们这边规定设备1s可能要发5次甚至更多的广播或者多播包
假如说设备量足够大的时候,每个数据包包含的数据又很长的时候,就出现一个这样的问题:放在主线程中可能会出现线程阻塞卡顿,放在分线程中解析数据可能会出现线程不好把控,同时要确保广播包和多播包的解析要在同一个线程中,因为有可能同时收到设备的广播和多播,同时解析一个设备的数据会出现数据冲突,设备的数据包是不定时发的,陆陆续续接收的,怎么能保证始终在一个分线程中来解决问题呢......这么来做的话我想不到好办法
换个角度,从队列出发,你会看到不一样的天空,这里我用到了一个很优秀的第三方socket CocoaAsyncSocket,是国外的一个大神写的,支持ARC和MRC,到现在支持IPV6连接,话不多说,老样子,上代码

/// 本地UDP代理队列,ARC以后系统会帮忙管理队列,所以这么可以用Strong来修饰
@property (atomic,strong, nullable) dispatch_queue_t udpDelegate_queue;
/// 接收广播包对象
@property (nonatomic,strong) GCDAsyncUdpSocket *radioUdp;
/// 新接收广播包对象
@property (nonatomic,strong) GCDAsyncUdpSocket *radioNewUdp;

如果需要支持MRC需要这么来写

#if OS_OBJECT_USE_OBJC
@property (atomic, strong, nullable) dispatch_queue_t udpDelegate_queue;
#else
@property (atomic, assign, nullable) dispatch_queue_t udpDelegate_queue;
#endif
///创建一个UDP代理回调串行队列(后面参数填NULL也默认是串行队列)
_udpDelegate_queue = dispatch_queue_create("WeconnPlug.udpSocketDelegate.queue", DISPATCH_QUEUE_SERIAL);
    
    //实例化广播包对象,并开启监听
    if (!self.radioUdp) {
        //设置代理,传进去代理的队列,socket队列传NULL时他会默认创建一个串行的队列
        self.radioUdp = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:self.udpDelegate_queue socketQueue:NULL];
    }
    if (self.radioUdp.localPort==0) { //放在这检测下防止系统回收端口/自己释放了端口
        NSError *error = nil;
        [self.radioUdp bindToPort:kRadioPort error:&error]; //kRadioPort为监听多广播端口
        if (error) NSLog(@"绑定广播端口:%d 失败:%@",kRadioPort,error);
    }
    NSError *rReceiveError = nil;
    [self.radioUdp beginReceiving:&rReceiveError];
    if (rReceiveError) NSLog(@"开启接收广播:%d 失败:%@",kRadioPort,rReceiveError);
    
    //实例化多播包对象,并开启监听
    if (!self.multicastUdp) {
        self.multicastUdp = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:self.udpDelegate_queue socketQueue:NULL];
    }
    if (self.multicastUdp.localPort==0) {  //放在这检测下防止系统回收端口/自己释放了端口
        NSError *error = nil;
        [self.multicastUdp bindToPort:KMulticastPort error:&error];   //组播端口
        if (error) NSLog(@"绑定多播包端口:%d 失败:%@",KMulticastPort,error);
        NSError *mJoinGroupError = nil;
        [self.multicastUdp joinMulticastGroup:KMulticastIp error:&mJoinGroupError]; //组播地址
        if (mJoinGroupError) NSLog(@"加入多播组:%@ 失败:%@",KMulticastIp,mJoinGroupError);
    }
    NSError *mReceivingError = nil;
    [self.multicastUdp beginReceiving:&mReceivingError];
    if (mReceivingError) NSLog(@"开启接收多播包失败:%@",mReceivingError);

收到UDP包回调协议方法 GCDAsyncUdpSocketDelegate

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(nullable id)filterContext
{
    NSString *host;
    uint16_t port;
    [GCDAsyncUdpSocket getHost:&host port:&port fromAddress:address];
    NSLog(@"广播的ip地址为%@ 端口:%d",host,port);

  这个data就是广播或者多播数据,进行对应的解析即可
}

下面我们来看下GCDAsyncUdpSocket里面掉协议方法的部分

- (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context
{
    LogTrace();
    
    SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:);
    
    __strong id theDelegate = delegate;
    if (delegateQueue && [theDelegate respondsToSelector:selector])
    {
        //delegateQueue队列就是创建时传给他的代理队列
        dispatch_async(delegateQueue, ^{ @autoreleasepool {
            
            [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context];
        }});
    }
}

流程是:我们传给他一个串行队列作为代理回调队列,他接收好数据后,把代理回调数据的任务异步加进我们传的这个串行队列里面
总结:这个串行队列异步执行里面的任务,因为是串行队列,所以不管是广播包的数据解析,还是收到的多播包数据解析,都是按加入顺序一个一个执行,又因为是异步执行,所以执行任务不会被卡顿,如果先前的线程被占用了,那么下个任务来了之后系统会找一个新的线程来执行任务,尽管有可能数据是在不同的线程里面被解析,但是是按顺序一个一个来得,所以不会出现数据冲突,这里不得不提下GCD的强大,任务来了系统帮我们找闲置的线程,没有则创建,一个任务执行完,可以通知到另一个不同线程里面的下个任务开始执行,而不用我们自己动手去监听,也不用手动去分发线程,强大到没话说

最后咱们来聊下线程死锁问题和预防措施

线程死锁主要发生在串行队列同步执行上,下面看个例子

    //手动创建一个串行的队列
    dispatch_queue_t queue = dispatch_queue_create("test.temp.queue", NULL);
    
    NSLog(@"1"); //任务1
    dispatch_sync(queue, ^{

        NSLog(@"2"); //任务2
        dispatch_sync(queue, ^{
            NSLog(@"3"); //任务3
        });
        NSLog(@"4"); //任务4
    });

    //输出循序: 1 -> 2 ->crash

分析:执行任务2 -> 添加任务3然后执行任务3 -> 执行任务4是在同一个线程里面,一个线程里面执行顺序肯定是由上到下,下一级等待上一级执行完再接着往下走,但是添加任务3是把任务3添加到对列尾,队里里面任务4在任务3前面,队列是串行,所以任务3要等待任务4执行完才会执行,但是在线程里面,任务4要等任务3执行完才会执行,所以会造成一个死循环的等待之中

一般发开中常见

//当前在主线程里面
dispatch_sync(dispatch_get_main_queue(), ^{  //往主线程里面添加任务
        NSLog(@"执行一些任务");
    });

一些常见的避免措施
现在我写数据库一般都用FMDB的操作队列来写,这样数据对数据库的增删查改都会有一个单独的队列来执行,这样就可以在任何线程里面调用增删查改的方法,而且数据库的执行也不会占用主线程的资源,强大到何乐而不为
我们来看下FMDB里面的方法

//初始化FMDatabaseQueue时的内部

//创建了一个串行的操作队列
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
//定义一个键
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//将FMDatabaseQueue对象通过这个键与其使用的dispatch_queue_t关联起来
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
//对数据库的操作部分
- (void)inDatabase:(void (^)(FMDatabase *db))block {
   //根据键取出当前队列关联的对象,与自己比较,看看是不是自己,是自己这个就给个断言,不给断言的话接下来程序就应该进入死锁状态了
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
    assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
    
    FMDBRetain(self);
    //对于自己创建的串行队列同步执行(将后面{xx}里面的类容当做任务添加到队列里面,执行或者等待执行)
    dispatch_sync(_queue, ^() {
        //获取数据库对象,通过block回调对象,并执行block里面的代码
        FMDatabase *db = [self database];
        block(db);
       
    });
}

相关文章

  • iOS开发之GCD并发队列

    iOS开发多线程之GCDiOS开发之GCD同步任务加强iOS开发之GCD串行队列iOS开发之GCD并发队列 03 ...

  • iOS开发多线程之GCD

    iOS开发多线程之GCDiOS开发之GCD同步任务加强iOS开发之GCD串行队列iOS开发之GCD并发队列 GCD...

  • iOS开发之GCD同步任务加强

    iOS开发多线程之GCDiOS开发之GCD同步任务加强iOS开发之GCD串行队列iOS开发之GCD并发队列 004...

  • iOS开发之GCD串行队列

    iOS开发多线程之GCDiOS开发之GCD同步任务加强iOS开发之GCD串行队列iOS开发之GCD并发队列 实例d...

  • iOS主线程和主队列的区别

    iOS主线程和主队列的区别 iOS主线程和主队列的区别

  • iOS 队列与线程

    队列和线程是在iOS开发中不可避免的,那么队列与线程有哪些关系呢? 给队列添加任务有四种方式:串行队列中执行同步任...

  • iOS开发-线程和队列

    随着开发的深入,不可避免的要涉及到项目运行速度和内存管理,有时我们也不可避免的遇到数据冲突的问题,多线程操作,什么...

  • iOS并行开发:从NSOperation和调度队列开始

    iOS并行开发:从NSOperation和调度队列开始 iOS并行开发:从NSOperation和调度队列开始

  • iOS线程、队列与死锁

    iOS中关于线程和队列,有一些概念:队列、串行队列、并发队列、主线程、主队列、任务、同步、异步。这些概念的意义和联...

  • GCD

    GCD 队列与线程的关系 主队列和主线程 『ios』主线程 和 主队列的关系,绝对安全的UI操作,主线程中一定是主...

网友评论

      本文标题:iOS开发-线程和队列

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