多线程

作者: 半边枫叶 | 来源:发表于2020-02-23 16:56 被阅读0次

一、线程的生命周期

首先创建线程,然后调用线程的start,此时线程进入runable就绪状态,等待CPU的调度。CPU调度该线程后,线程进入running状态,CPU去调度其他的线程时,该线程会进入runabel状态。当线程出现以下情况:调用了sleep、等待同步锁或者从可调度线程池移除,就会进入Blocked阻塞状态。当sleep到时,获得同步锁或者重新加入可调度线程池时,重新进入runable状态。任务执行完毕或者强制性退出,线程进入Dead状态。


线程生命周期

二、线程池原理

使用线程执行任务的时候,需要到线程池中去取线程进行任务分配。首先判断线程池大小是否小于核心线程池大小,如果小于的话,创建新的线程执行任务;如果当前小城池大小大于了核心线程池大小,然后开始判断工作队列是否已满,如果没满,将任务提交到工作队列。如果工作队列已满,判断线程池的线程是否都在工作,如果有空闲线程没有在工作,就交给它去执行任务。而如果线程池中的线程都在工作,那么就交给饱和策略去执行。

线程池
饱和策略分为下面四种:
  • AbortPolicy 直接抛出RejectedExecutionExeception 异常来阻止系统正常运行;
  • CallerRunsPolicy 将任务回退到调用者;
  • DisOldestPolicy 丢掉等待最久的任务‘;
  • DisCardPolicy 直接丢弃任务。

线程之间的通信
线程之间的通信除了我们常用的直接同步或者异步向任务队列添加任务外,还可以通过NSPort端口的形式进行发送消息,实现不同的线程间的通信。使用的时候注意需要将NSPort加入的线程的RunLoop中去。

三、GCD

GCD的特点:自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需要告诉GCD需要执行什么任务,而不必关心任何线程管理代码。
GCD分为串行任务队列和并发任务队列,同步函数和异步函数。
注意:在串行队列中同步的往该串行队列中添加任务,会导致死锁。
下面我们来看下GCD相关的题目

  • 题目1、下面代码的打印结果是什么?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1 5 2 4 3
1 和 5 都是在主线程打印的,因为调用dispatch_async进行异步调用,需要耗费一些时间,而在主线程直接打印是很快的。所以1打印后,接着打印出了5,然后才会执行异步任务。接着就是2、4、3。

  • 题目2、下面代码的打印结果是什么?从ABCD选中中选择所有的可能。
    A: 1230789
    B: 1237890
    C: 3120798
    D: 2137890
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // sleep(2);
    NSLog(@"1");
});
dispatch_async(queue, ^{
    NSLog(@"2");
});
// 堵塞 - 护犊子
dispatch_sync(queue, ^{
    NSLog(@"3");
});
// **********************
NSLog(@"0");

dispatch_async(queue, ^{
    NSLog(@"7");
});
dispatch_async(queue, ^{
    NSLog(@"8");
});
dispatch_async(queue, ^{
    NSLog(@"9");
});

答案是:A和C。
因为我们再打印3的时候使用的同步函数。所以一定是先执行3,然后执行0,然后(789)再执行所以3、0、(789)这个顺序是可以确定的,所以A和C都可能出现。

  • 题目3、下面代码的打印结果是什么?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1、5、2 然后就会死锁卡死。因为在串行队列中同步往该队列中添加了任务,会导致添加的任务和后面的任务互相等待。

四、GCD的使用

barrier的使用

barrier的特点:堵塞指定的queue队列;只能使用自定义的并发队列,不能使用global队列。
题目一、下面代码的打印结果是什么?

dispatch_queue_t concurrentQueue = dispatch_queue_create("rzf", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"123");
});
dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"----------------- 分割------");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"456");
});
NSLog(@"我在主线程");

结果如下

2020-02-26 13:22:20.571436+0800 004--GCD进阶使用[1696:82355] 我在主线程
2020-02-26 13:22:21.571781+0800 004--GCD进阶使用[1696:82445] 123
2020-02-26 13:22:21.572109+0800 004--GCD进阶使用[1696:82445] ----------------- 分割------
2020-02-26 13:22:21.572305+0800 004--GCD进阶使用[1696:82445] 456

barrier栅栏函数,可以堵塞指定的queue任务队列,而对其他的queue没有影响。queue中凡是在barrier任务添加之添加的任务,会在barrier任务之执行;凡是在barrier任务添加之添加的任务,会在barrier任务之执行。barrier就像在一个并发队列中放了一个闸口,将前面添加的任务和后面添加的任务完全分割来。
如果我们将上面的并发队列换成系统的global并发队列可以吗?我们看下结果:

2020-02-26 13:37:00.735816+0800 004--GCD进阶使用[3560:94316] 我在主线程
2020-02-26 13:37:00.735821+0800 004--GCD进阶使用[3560:94366] ----------------- 分割------
2020-02-26 13:37:00.735830+0800 004--GCD进阶使用[3560:94368] 456
2020-02-26 13:37:01.738121+0800 004--GCD进阶使用[3560:94365] 123

答案是不可以,系统的global不允许使用栅栏函数进行堵塞。如果换成global的话结果如下,显然不起作用了,而且还有可能崩溃。

题目二、下面代码可以正产运行吗?

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i<2000; i++) {
    dispatch_async(concurrentQueue, ^{
        NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
        NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        
        [self.mArray addObject:image];
    });
}

答案是不可以,因为在for缓存中创建了好多线程去对self.mArray进行操作,显然self.mArray是线程不安全的。最后的结果有有可能和预期的不一样,也有可能崩溃。所以我们需要进行加锁处理。我们可以通过synchronized来保护。

@synchronized (self) {
    [self.mArray addObject:image];
}

也可以通过栅栏来实现

dispatch_barrier_async(concurrentQueue, ^{
    [self.mArray addObject:image];
});

这样就可以保证得到正确的结果了,避免了多线程的数据竞争。

dispatch_group的使用

创建一个group,以及两个任务队列queue和queue1。然后通过dispatch_group_async分别往两个队列中添加block任务。最后通过dispatch_group_notify配置组任务完成后的回调。这样我们就可以是实现多个任务全部完成后的一个汇总操作,而且不同的任务可以不在一个队列中。

//创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue1 = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);

// SIGNAL
dispatch_group_async(group, queue, ^{
    NSString *logoStr = @"http://p.qpic.cn/qqcourse/QFzQYCgCrxlq7n5Jats36WGb4wxzJIYmFplnUUgdxk4gy66xicbmGCDM5DXDuVNQP/";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});

dispatch_group_async(group, queue1, ^{
    sleep(2);
    NSString *logoStr = @"https://www.baidu.com/img/baidu_resultlogo@2.png";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});

__block UIImage *newImage = nil;
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"数组个数:%ld",self.mArray.count);
    for (int i = 0; i<self.mArray.count; i++) {
        UIImage *waterImage = self.mArray[i];
        newImage = [KC_ImageTool kc_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 100, 40)];
    }
    self.imageView.image = newImage;
});

我们还可以通过dispatch_group_enterdispatch_group_leave向group添加任务。注意enter和leave要成对出现,如果 dispatch_group_enterdispatch_group_leave多,dispatch_group_notify不会被调用;如果dispatch_group_enterdispatch_group_leave 少,就会奔溃。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"第一个走完了");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"第二个走完了");
    dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务完成,可以更新UI");
});

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)同步等待group任务的完成。会堵塞当前线程。可以设置等待的时间,当超过设置的时间如果任务还没有完成,就会直接返回非0值。如果任务在设置的时间内完成就会返回0。

long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC));
if (timeout == 0) {
    NSLog(@"回来了");
}else{
    NSLog(@"等待中 -- 转菊花");
}
dispatch_semaphore的使用

我们可以通过semaphore信号量实现控制并发队列中任务的并发数,就像NSOperationQueue中的maxConcurrentOperationCount一样。下面代码设置任务的并发数为3。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 信号量 -- gcd控制并发数
// 同步
//总结:由于设定的信号值为3,先执行三个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过3
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

//任务1
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务1");
    sleep(1);
    NSLog(@"任务1完成");
    dispatch_semaphore_signal(semaphore);
});

//任务2
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务2");
    sleep(1);
    NSLog(@"任务2完成");
    dispatch_semaphore_signal(semaphore);
});

//任务3
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务3");
    sleep(1);
    NSLog(@"任务3完成");
    dispatch_semaphore_signal(semaphore);
});
dispatch_source的使用

dispatch_source的特点:效率高,不依赖于runloop。使用dispatch_source创建的time相比于NSTimer可以非常的精确。
dispatch_source相关的api:
dispatch_source_create 创建source
dispatch_source_set_event_handler 设置事件的回调
dispatch_source_merge_data 添加数据,会触发DISPATCH_SOURCE_TYPE_DATA_ADD 和DISPATCH_SOURCE_TYPE_DATA_OR类型source的事件回调。
dispatch_source_get_data 获取上面merge_data添加的数据。
dispatch_resume 启动
dispatch_suspend 挂起

下面我们来演示创建一个DISPATCH_SOURCE_TYPE_DATA_ADD类型的source。该类型的source可以根据添加数据进行响应。首先我们创建source,然后设置source的句柄,也就是回调代码块,在回调中获取传递过来的data,我们这里的data是一个进度。最后resume这个source。
我们还可以使用DISPATCH_SOURCE_TYPE_TIMER这个type来实现定时器的功能。

//创建source
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

// 封装我们需要回调的触发函数 -- 响应
dispatch_source_set_event_handler(self.source, ^{
    
    NSUInteger value = dispatch_source_get_data(self.source); // 取回来值 1 响应式
    self.totalComplete += value;
    NSLog(@"进度:%.2f", self.totalComplete/100.0);
    self.progressView.progress = self.totalComplete/100.0;
});

//启动source
dispatch_resume(self.source);

上面我们在回调中处理了数据,下面我们需要使用dispatch_source_merge_data传输数据,触发回调句柄。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_async(self.queue, ^{
            sleep(2);
            dispatch_source_merge_data(self.source, 1); // source 值响应
        });
    }
}

我们在touchesBegan方法中,循环merge_data。每次传输一个1过去。然后回调句柄那可以收到该数据1。
我们还可以在一个按钮的点击方法中控制该source的暂停和继续:

- (IBAction)didClickStartOrPauseAction:(id)sender {
    if (self.isRunning) {// 正在跑就暂停
        dispatch_suspend(self.source);
        dispatch_suspend(self.queue);
        [sender setTitle:@"暂停中..." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        [sender setTitle:@"加载中..." forState:UIControlStateNormal];
    }
}

因为self.queue是用来为source传输数据用的,所以暂停source后,queue也对应的挂起。

五、GCD的源码实现

  • 1、主队列是个全局的静态变量,所以才可以全局直接调用。
struct dispatch_queue_static_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
    .do_targetq = _dispatch_get_default_queue(true),
#endif
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
            DISPATCH_QUEUE_ROLE_BASE_ANON,
    .dq_label = "com.apple.main-thread",
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
    .dq_serialnum = 1,
};
  • 2、自定义队列以及global队列都是模式化创建的,
    我们调用create方法进行创建队列。
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

creat方法会调用下面的init方法,进行构造queue。

// 构造方法
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
......
dq->do_targetq = tq;

我们看到上面方法构造的dq的属性do_targetq=tq,然后我们来看下tq的创建。

tq = _dispatch_get_root_queue(
        qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 4
        overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq; // 0 1
if (unlikely(!tq)) {
    DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
}

然后我们跟踪_dispatch_get_root_queue这个方法。

static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
    if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
        DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
    }
    // 4-1= 3
    // 2*3+0/1 = 6/7
    return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

通过上面的方法我们看到,_dispatch_get_root_queue方法实际上就是去_dispatch_root_queues这个数组里面去值。至此我们可以总结:自定义队列和global队列都是通过_dispatch_root_queues这个数组来进行初始化创建的。
_dispatch_root_queues这个数组是在dispatch_init方法中初始化创建的。

相关文章

  • iOS多线程 NSOperation

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程 pthread、NSThread

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程: GCD

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程运用

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程基础

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • 多线程介绍

    一、进程与线程 进程介绍 线程介绍 线程的串行 二、多线程 多线程介绍 多线程原理 多线程的优缺点 多线程优点: ...

  • iOS进阶之多线程管理(GCD、RunLoop、pthread、

    深入理解RunLoopiOS多线程--彻底学会多线程之『GCD』iOS多线程--彻底学会多线程之『pthread、...

  • iOS多线程相关面试题

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • 多线程之--NSOperation

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • iOS多线程之--NSThread

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

网友评论

      本文标题:多线程

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