美文网首页
对 NSOperation 和 GCD 的探究与思考

对 NSOperation 和 GCD 的探究与思考

作者: BlackStar暗星 | 来源:发表于2021-01-05 16:24 被阅读0次

    Demo地址
    Demo中包含本人学习的所有资料,还有一些封装的Pod组件,欢迎下载Star,如果有错误的地方,还请指出,详情查看 README.md


    主要思考问题:主线程和主队列是不是特殊的?我们是否可以使用自定义队列来模仿主队列?自定义串行队列是否可以和主队列一样可以通过返回队列来返回线程?死锁是因为什么?线程什么时候才会创建?


    分为三个方面总结:

    • 基本使用 :使用过程中根据打印分析线程的一些细节
    • 总结:什么时候开辟新的线程,队列和线程间的关系,等等
    • 坑点:在研究过程中出现的问题

    使用

    一、NSOperation 的使用,分为几种情况
    • Block 调用方式 : NSBlockOperation
    -(void)operatorBlock{
        
        NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"operation block0 = %@\n",[NSOperationQueue currentQueue]);
            NSLog(@"thread block0 = %@\n",[NSThread currentThread]);
    
        }];
    
        NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"operation block1 = %@\n",[NSOperationQueue currentQueue]);
            NSLog(@"thread block1 = %@\n",[NSThread currentThread]);
            
        }];
        
        NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"operation block2 = %@\n",[NSOperationQueue currentQueue]);
            NSLog(@"thread block2 = %@\n",[NSThread currentThread]);
        }];
        
        
        
        
        
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    //    [queue addOperation:operation];
    //    [queue addOperation:operation1];
        
        // waitUntilFinished = yes 将会等待 数组的 Operation 完成后,在执行后加入的 Operation
        // waitUntilFinished = no 时,所有 add 的 operation 执行顺序是随机的
        [queue addOperations:@[operation2,operation1] waitUntilFinished:YES];
        [queue addOperation:operation];
    
        
    }
    
    • addTarget 调用方式 : NSInvocationOperation
    -(void)operatorInvocation{
        
        //    打印结果目前只测出这两种情况,测试次数 30次左右
        //    invocation1 = <NSOperationQueue: 0x7ff90700cc70>{name = 'NSOperationQueue 0x7ff90700cc70'}
        //    invocation0 = <NSOperationQueue: 0x7ff90700cc70>{name = 'NSOperationQueue 0x7ff90700cc70'}
        //    thread invocation1 = <NSThread: 0x600003a4ac00>{number = 11, name = (null)}
        //    thread invocation0 = <NSThread: 0x600003a46800>{number = 13, name = (null)}
        //
        //    invocation0 = <NSOperationQueue: 0x7ff905648560>{name = 'NSOperationQueue 0x7ff905648560'}
        //    invocation1 = <NSOperationQueue: 0x7ff905648560>{name = 'NSOperationQueue 0x7ff905648560'}
        //    thread invocation0 = <NSThread: 0x600003a46800>{number = 13, name = (null)}
        //    thread invocation1 = <NSThread: 0x600003a43600>{number = 14, name = (null)}
        
        NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(invocation0) object:nil];
        
        NSInvocationOperation *operation1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(invocation1) object:nil];
        
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperation:operation];
        [queue addOperation:operation1];
    }
    
    
    -(void)invocation0{
        NSLog(@"invocation0 = %@\n",[NSOperationQueue currentQueue]);
        NSLog(@"thread invocation0 = %@\n",[NSThread currentThread]);
    }
    
    
    -(void)invocation1{
        NSLog(@"invocation1 = %@",[NSOperationQueue currentQueue]);
        NSLog(@"thread invocation1 = %@\n",[NSThread currentThread]);
    }
    
    • NSOperationQueue 直接使用Block
    -(void)operationQueue{
        
        /**
         * addBarrierBlock 作用,栅栏函数,iOS 13 之后可用
         * 5 个 操作 执行顺序 只有一个是固定的,就是 addBarrierBlock 的回调
         * 它会永远在第三个执行
         * 执行顺序:栅栏上边的随机执行,然后栅栏函数,然后栅栏下边的随机执行
         */
        
        NSOperationQueue *quque = [[NSOperationQueue alloc]init];
        
        [quque addOperationWithBlock:^{
            NSLog(@"operation block queue 0");
        }];
        
        [quque addOperationWithBlock:^{
            NSLog(@"operation block queue 1");
        }];
        
        if (@available(iOS 13.0, *)) {
            [quque addBarrierBlock:^{
                NSLog(@"operation block queue 2");
            }];
        }
    
        [quque addOperationWithBlock:^{
            NSLog(@"operation block queue 3");
        }];
        
        [quque addOperationWithBlock:^{
            NSLog(@"operation block queue 4");
        }];
        
    }
    
    二、GCD 的使用
    • 串行队列
    #pragma mark 串行队列 SERIAL
    -(void)serialQueue{
    
        //串行队列
        dispatch_queue_t serialqueue = dispatch_queue_create("serialqueue", DISPATCH_QUEUE_SERIAL);
        
        /// 只创建一条线程
        for (int i= 0; i<10; i++) {
            dispatch_async(serialqueue, ^{
                NSLog(@"serialqueue thread i = %d  %@ " ,i,[NSThread currentThread]);
            });
        }
        
        
        
        /// 死锁
        // 下边dispatch_sync如果使用主队列的话,会造成死锁,
        // 因为 serialQueue 方法 是在主队列进行的,而在主队列进行的同步操作,
        // 需要等待serialQueue方法执行完才会执行dispatch_sync,
        // 但是 serialQueue 方法也在等待 dispatch_sync 执行完才能完结方法,造成了相互等待,死锁
    
        
        /// 死锁案例1 ,主队列
        //[dispatch_sync(dispatch_get_main_queue(), ^{
        //  NSLog(@"syn serialqueue thread 1%@ " ,[NSThread currentThread]);
        //})] ;
        
        
        /// 死锁案例2 自定义串行队列
        //dispatch_queue_t serialqueueLock = dispatch_queue_create("serialqueueLock", DISPATCH_QUEUE_SERIAL);
        //
        //dispatch_async(serialqueueLock, ^{
        //
        //  dispatch_sync(serialqueueLock, ^{
        //      NSLog(@"死锁,无法打印");
        //  });
        //});
        
      
        /// 测试自定义串行队列和主队列的是否相同,验证主队列是否特殊
        //[self backSerialQueue];
    }
    
    • 并发队列
    /// CONCURRENT 并发队列
    /// 1、async 异步操作可能会创建新的线程,多个异步操作创建的线程数不确定,
    ///    可能有两个或两个以上的异步操作使用同一个线程
    /// 2、sync 同步操作不会创建新的线程,所以执行任务的时候是在当前线程执行的,也就是说,
    ///    当前如果是主线程,那么他就在主线程执行,当前如果是子线程,那么他就在子线程执行
    ///    对于队列,任务加到哪个队列就在哪个队列执行。典型案例:主线程执行其他队列任务
    #pragma mark 并发队列 CONCURRENT
    -(void)concurrentQueueTest{
        
        dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(concurrentQueue, ^{
            NSLog(@"async concurrentQueue thread 1%@ " ,[NSThread currentThread]);
        });
    
        dispatch_async(concurrentQueue, ^{
            NSLog(@"async concurrentQueue thread 2%@ " ,[NSThread currentThread]);
        });
        
        
        /// 这个打印结果证明: sync 同步操作不会创建新的线程,执行任务的时候是在当前线程执行的,也就是说,
        /// 当前如果是主线程,那么他就在主线程执行,当前如果是子线程,那么他就在子线程执行
    //    dispatch_async(concurrentQueue, ^{
    //
    //        NSLog(@"=== async concurrentQueue thread 1%@ " ,[NSThread currentThread]);
    //        NSLog(@"=== async queue = %@  == %p" ,dispatch_get_current_queue(),dispatch_get_current_queue());
    //
    //        dispatch_sync(concurrentQueue, ^{
    //            NSLog(@"=== sync concurrentQueue thread 3%@ " ,[NSThread currentThread]);
    //            NSLog(@"=== sync queue %@ == %p " ,dispatch_get_current_queue(),dispatch_get_current_queue());
    //        });
    //
    //    });
        
        
        /// sync 不会开启新的线程,所以会在 当前线程(concurrentQueueTest在主线程)
        /// 队列为 concurrentQueue 。但是有点需要注意,当使用
        /// [NSOperationQueue currentQueue]时,发现打印的是 主队列,
        /// 为什么是主队列不清楚,但是从堆栈来看,是在 concurrentQueue 队列中
        /// 而且我们在使用GCD 的时候,其实是不能使用 NSOperationQueue 来获取当前队列的
        /// [NSOperationQueue currentQueue] 只适用于 NSOperationQueue 类创建的队列
        /// 而 GCD 应该使用 dispatch_get_current_queue() 来获取。而 dispatch_get_current_queue()
        /// 在iOS 6时,已经被废弃,但是依然可以调用,调用的时候最好把他以临时变量的方式来使用
        /// 否则可能导致崩溃
    
    //    dispatch_sync(concurrentQueue, ^{
    //        NSLog(@"sync concurrentQueue thread 1%@ " ,[NSThread currentThread]);
    //    });
    
    }
    
    • 全局并发队列
    /// 对于全局并发队列 dispatch_get_global_queue
    /// 全局并发队列的第一个参数为 优先级,第二个参数是扩展参数,目前没有用
    /// 优先级有4个值,我们在 dispatch_get_global_queue 获取队列的时候,
    /// 4个值对应的是四个不同的队列,但是每个值,只会创建一个队列,也就是说实际上,
    /// 通过dispatch_get_global_queue最多只能创建4个队列,通过队列的地址可以
    /// 看出同一个优先级参数,对应的队列的内存地址是一个
    
    #pragma mark 全局队列 GLOBAL
    -(void)globalQueue{
        
        dispatch_queue_t globalqueue = dispatch_get_global_queue(0, 0);
        
        dispatch_async(globalqueue, ^{
            NSLog(@"async global thread %@ " ,[NSThread currentThread]);
            NSLog(@"%p",globalqueue);
        });
    
        
        dispatch_queue_t globalqueue1 = dispatch_get_global_queue(0, 0);
    
        dispatch_async(globalqueue1, ^{
            NSLog(@"async global thread 1%@ " ,[NSThread currentThread]);
            NSLog(@"%p",globalqueue1);
        });
    
        dispatch_queue_t globalqueue2 = dispatch_get_global_queue(0, 0);
    
        dispatch_async(globalqueue2, ^{
            NSLog(@"async global thread 2%@ " ,[NSThread currentThread]);
            NSLog(@"%p",globalqueue2);
        });
    
        
        dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
        dispatch_async(highQueue, ^{
            NSLog(@"async global thread 3%@ " ,[NSThread currentThread]);
            NSLog(@"HIGH %p",highQueue);
        });
    
    
        dispatch_queue_t highQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
        dispatch_async(highQueue1, ^{
            NSLog(@"async global thread 4%@ " ,[NSThread currentThread]);
            NSLog(@"HIGH %p",highQueue1);
        });
        
        
        
        
        dispatch_queue_t highQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
        dispatch_sync(highQueue2, ^{
            NSLog(@"==sync global thread 5%@ " ,[NSThread currentThread]);
            NSLog(@"HIGH %p",highQueue2);
        });
        
        
        
        dispatch_queue_t highQueue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
        dispatch_sync(highQueue3, ^{
            NSLog(@"==sync global thread 6%@ " ,[NSThread currentThread]);
            NSLog(@"HIGH %p",highQueue3);
        });
    }
    
    
    • 栅栏
    // 栅栏 GCD 的栅栏 ,只适用于自定义 并发队列,
    // 如果是 全局并发队列,效果和 dispatch_async 相同
    
    -(void)gcdBarrier{
        
        /// dispatch_barrier_async 的官方说明
        /// 大概意思是:只能用于 DISPATCH_QUEUE_CONCURRENT queues
        
        /*!
         * @function dispatch_barrier_async
         *
         * @abstract
         * Submits a barrier block for asynchronous execution on a dispatch queue.
         *
         * @discussion
         * Submits a block to a dispatch queue like dispatch_async(), but marks that
         * block as a barrier (relevant only on DISPATCH_QUEUE_CONCURRENT queues).
         *
         * See dispatch_async() for details and "Dispatch Barrier API" for a description
         * of the barrier semantics.
         *
         * @param queue
         * The target dispatch queue to which the block is submitted.
         * The system will hold a reference on the target queue until the block
         * has finished.
         * The result of passing NULL in this parameter is undefined.
         *
         * @param block
         * The block to submit to the target dispatch queue. This function performs
         * Block_copy() and Block_release() on behalf of callers.
         * The result of passing NULL in this parameter is undefined.
         */
        
    //    dispatch_queue_t myQueue = dispatch_get_global_queue(0, 0);
        dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    
        
        dispatch_async(myQueue, ^{
            
            NSLog(@"1");
        });
        
        dispatch_async(myQueue, ^{
            
            NSLog(@"2");
        });
        
        dispatch_barrier_async(myQueue, ^{
            NSLog(@"3");
        });
        
        dispatch_async(myQueue, ^{
            
            NSLog(@"4");
        });
        
        dispatch_async(myQueue, ^{
            
            NSLog(@"5");
        });
        
    }
    
    

    总结

    • GCD总结
    /// =====================================
    /// GCD研究结果
    ///
    /// 1、同一个串行队列下的两个任务如果存在调用关系,那么被调用的如果使用同步操作,
    /// 并将同步操作放在这个串行队列下,将会造成相互等待->死锁
    ///
    /// 2、线程是用来执行任务的,队列是用来存放任务的
    /// 队列决定了任务是否并发,async 和 sync 决定是否有开启新线程的能力
    /// async不是一定会开启新线程的,他只是有能力开启线程
    ///
    /// 3、iOS的队列分为串行队列、并发队列(强调:不是并行,不是并行)
    /// 并发是指有能力处理多个任务,不一定同时
    /// 并行是指有能力同时处理多个任务,强调的是同时
    ///
    /// 4、主队列 Main Queue ,无论是 同步操作还是异步操作,执行任务都是在主线程 、主队列。
    ///    说明了 主队列 是不能开辟子线程的。也就是说,子线程的队列永远也不可能是主队列
    ///
    ///    延伸1:同样情况的非主线程的串行队列,最多只能创建一个子线程,类似于主队列对应的主线程
    ///    即:串行队列,无论是不是主队列,只能有一个线程绑定(执行任务是可以在两个线程的:把当前线程下的同步操作放入新的队列下)
    ///    因为主队列系统已经帮我们创建了主线程,所以只有非主串行队列,才会去创建一个新的线程
    ///
    ///    延伸2:同为串行队列,主队列可以使用
    ///    dispatch_async(dispatch_get_main_queue(),^{})方式回到当前线程(主线程)
    ///    那么非主串行队列queue,在使用 dispatch_async(queue,^{}) 的时候,是否也会回到当前线程?
    ///    答案是:如果有runloop,那么代码不执行。如果没有runloop,和主线程一样,会回到队列所对应的线程。
    ///    原因未知,只能猜测,这种情况下,系统对主队列是有不同的处理方式的。
    ///
    /// 5、主队列不会出现在其他线程中,而非主队列可以在主线程中执行(同步操作),也可以在子线程中执行
    ///
    /// 6、dispatch_async(dispatch_get_main_queue(), ^{});方法,永远都会在当前作用域的最后调用
    ///    跟他调用的位置无关,即 dispatch_async 会将调用他的主方法里的所有其他操作执行完,才会执行
    ///    dispatch_async ,如果有多个dispatch_async(dispatch_get_main_queue(), ^{});方法
    ///    按照调用顺序依次执行
    ///
    /// =====================================
    
    • NSOperation 总结
    /// ====================================================
    /// 从打印的 thread 来看,Operation 这几种方式都可能产生新的线程
    /// 也就是说NSOperationQueue 其实是并发队列 ,执行的任务是异步执行
    /// ====================================================
    
    /**
     * block 和 invocation 两种 NSOperation 子类的测试打印顺序说明
     * currentQueue 的打印 和 thread 的打印是有先后顺序的,两个 currentQueue 打印完成后
     * 才会 打印 thread , 两个 currentQueue 打印顺序不确定,两个 thread 打印顺序不确定,
     * 是不是必然结果,无法确定
     * 测试次数 大概 30次
     *
     * 个人理解:因为是并发队列,在执行任务的时候,cpu会来回切换线程去执行,
     * 而切换的时机是随机的,或者说是在系统的一定规则下来回切换,
    所以对于currentQueue和thread打印顺序,也就是任务的执行顺序,
    可能是随机的,也可能是根据系统规则执行的,虽然出现了上边的结果,
     * 因为测试次数少,所以不能肯定打印结果的正确性,
    但是可以肯定的是,这种切换方式的概率是非常高的
     */
    

    坑点:

    1、在 GCD 中,我们不应该使用 [NSOperationQueue currentQueue]来获取当前队列,因为 NSOperationQueuedispatch_queue_t 他们不是一个类,是无法直接获取另外一个类所创建的队列的,在dispatch_async(dispatch_get_main_queue(), ^{}) 里边发现 [NSOperationQueue currentQueue]的值是主队列,这是不对的,我们可以查看堆栈信息来证明,而在其他情况下,[NSOperationQueue currentQueue]的值都是null,如果想要获取 GCD的队列,我们可以使用 dispatch_get_current_queue()(虽然可以用,但是苹果已经废弃了此方法)。打断点然后查看堆栈信息是最为靠谱的。

    2、很多人都说主队列和主线程不应该被区别对待,但是根据我的测试,主线程主队列就是被区别对待了。
    我们去自定义一个串行队列来把他当做主队列(我管他叫作子主队列),在开一个子线程和队列绑定,把他当做主线程(我管他叫作子主线程),然后在这个线程上,开启runloop,来把他当做主runloop,测试发现,我们无法通过 类似于 dispatch_async(dispatch_get_main_queue(), ^{}) 这种方式返回到子主线程、子主队列(block回调不执行)。但是如果我把runloop 注释掉,发现就可以回到子主线程、子主队列。
    还有一个例子:使用dispatch_async(dispatch_get_main_queue(), ^{})的任务,无论是在开头写,还是在最后写,执行的顺序一定会被放在作用域的最后一个执行,但是模仿的主线程主队列,是不会把任务放在最后执行的。

    勘误

    经测试,发现并不是 dispatch_async(self.queue, ^{}); 不执行,而是他被放到了runloop下边,也就是runloop执行完,他就可以执行了。当前线程开启runloop后,我们在串行队列下执行异步操作,任务的执行顺序为 普通任务->runloop->async 任务。但是 runloop 一直循环,不会结束,那么后边的 async 任务就无法执行。而我们一旦停掉runloop,async就会执行。至于主线程为什么不会出现如此情况,还不清楚。
    猜测大概是 runloop 和我这里创建的runloop 他们做线程常驻的方式不一样,主线程下,可以把 async 任务放在runloop前执行。
    详情看 demo 的 runloop 的测试

    相关文章

      网友评论

          本文标题:对 NSOperation 和 GCD 的探究与思考

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