OS开发基础——多线程的简单应用

目前最广泛被认知的几种多线程有:

  • pthread
  • NSThread
  • GCD
  • NSOperation

其中常用的是后面两种:GCD 和 NSOperation,这边文章就是对后两种常见用法的简单介绍,以及前两种 pthread 和 NSThread 的简单说明。

pthread

一套通用的多线程API,适用于Unix\Linux\Windows等系统,跨平台,可移植性强,由纯C语言编写的API,且线程的生命周期需要程序员自己管理,使用难度较大,所以在实际开发中通常不使用,在这里也不详细说明。

注意:在使用pthread的时候一定要手动把当前线程结束掉。如果有想从底层进行定制多线程的操作,可以使用ptherad。

NSThread

基于OC语言由苹果进行封装的API,使得其简单易用,完全面向对象操作。线程的声明周期由程序员管理,在实际开发中偶尔使用。

简单使用:

创建线程:

  • 使用NSThread的init方法显式创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
//线程名
[thread setName:@"thread"];
//优先级 优先级从0到1
[thread setThreadPriority:0.9];
//启动
[thread start];
复制代码
  • 使用NSThread类方法显式创建并启动线程
[NSThread detachNewThreadSelector:@selector(threadMethod:) toTarget:self withObject:nil];
复制代码
  • 隐式创建并启动线程
[self performSelectorInBackground:@selector(threadMethod:) withObject:nil];
复制代码

注意:添加线程的名字、更改优先级等操作,要使用第一种方式来创建线程。因为只有使用NSThread的init方法创建的线程才会返回具体的线程实例,此方法需要使用start方法来手动启动线程。

线程状态:

  • 启动
[thread start];
复制代码
  • 阻塞
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[NSThread sleepForTimeInterval:1];
复制代码
  • 结束(注意:当使用cancel方法时,只是改变了线程的状态标识,并不是结束线程,要配合isCancelled方法进行判断,退出线程使用)
[[NSThread currentThread] cancel];
if([[NSThread currentThread] isCancelled]) {
    [NSThread exit];//执行exit,后边的语句不再执行,可以通过 start 再次启动线程
}
if([[NSThread currentThread] isCancelled]) {
    return;//后边的语句不再执行,不可以通过 start 再次启动线程
}
复制代码

线程通讯:

线程间通信,最常用的就是开启子线程进行耗时操作,操作完毕后回到主线程,进行数据赋值以及刷新主线程UI。

[self performSelectorOnMainThread:@selector(backToMainThread:) withObject:image waitUntilDone:YES];
复制代码
[self performSelector:@selector(backToMainThread:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
复制代码

线程安全:

多线程操作会存在一定的安全隐患。原因是多线程会存在不同线程的资源共享,也就是说我们可能在同一时刻两个线程同时操作了某一个变量的值(资源竞争),但是线程的对变量的操作不同,导致变量的值出现误差。

例如:如果有一个变量x = 100,有两个线程A和B,A线程取x的值(x=100),B线程取x的值(x=100),B线程给x+1 (x=101),A线程给x+1 (x = 101),B 线程取x的值 (x = 101)或者( x = 102 )。变量出行了误差。

解决方案添加线程锁,有多种线程锁,在这里不多介绍。

GCD

基于C语言编写由苹果公司提供的的一套多线程开发解决方案,使用时会以函数形式出现,且大部分函数以dispatch开头。它会自动利用多核进行并发处理和运算,它能提供系统级别的处理,而不再局限于某个进程、线程,线程的生命周期由系统自动管理(创建,调度、运行,销毁),只需要告诉GCD执行什么任务,不需要编写管理线程的代码。

基本使用

创建列队和任务:

创建列队(queue)

  • 串行列队(一次执行一个任务)
dispatch_queue_t queue = dispatch_queue_create("10900900",DISPATCH_QUEUE_SERIAL);
复制代码
  • 并发列队(一次可执行多个任务)
dispatch_queue_t queue = dispatch_queue_create("10900901",DISPATCH_QUEUE_CONCURRENT);
复制代码
  • 全局列队(本质是一个并发队列,由系统提供,所有应用程序共享的,方便编程,可以不用创建就直接使用)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
复制代码
  • 主列队(专门调度主线程的任务,不开辟新的线程。在主队列下的任务不管是异步还是同步都不会开辟新线程,任务只会在主线程顺序执行)
dispatch_queue_t queue = dispatch_get_main_queue();
复制代码

创建任务(sync 和 async)

  • 同步任务(同步串行和同步并发,任务执行的方式是一样的,没有开辟新的线程,所有的任务都是在一条线程里面执行。)
dispatch_sync(queue, ^{
});
复制代码
  • 异步任务(异步串行和异步并发,任务执行的方式是有区别的,异步串行会开辟一条新的线程,队列中所有任务按照添加的顺序一个一个执行,异步并发会开辟多条线程,至于具体开辟多少条线程,是由系统决定的。)
dispatch_async(queue, ^{
});
复制代码

列队和任务的组合各种情况分析:

  • 同步任务,并发列队
  • 所有任务都是在当前线程中执行,没有开启新的线程(同步方法不具备开启新线程的能力)
  • 同步任务需要等候列队中的任务执行结束,才会执行下一个
  • 并发列队可以开启多线程,并且可以同时执行多个任务,但是同步任务无法创建新线程,所以只有当前一个线程,而且同步任务需要等待列队中前一任务执行结束才能继续执行下面的操作,因此任务只能一个一个顺序执行
  • 同步任务,串行列队
  • 和同步任务,并发列队相似
  • 所有任务在当前线程中执行,没有开启新的线程
  • 任务是按照顺序执行的,同步任务,线程需要等待列队中的任务执行完毕,才可以开启新的任务
  • 同步任务,主列队
  • 在主线程中调用会出现死锁,互相等待
  • 死锁原因:当我们在主线程中添加这个列队的时候,添加列队的这个操作本身就是一个任务,我们把它当作任务A,这个任务也被添加到了主线程的列队中。而同步任务,会等待当前列队中前面的任务执行完毕后接着执行,我们把添加到主线程中的列队中的任务称为任务B,这就产生了一个矛盾,任务B要执行需要等任务A执行完毕后才会执行,而任务A执行完毕需要任务B执行结束(因为任务B在任务A中),这就产生了任务互相等待的情况
  • 异步任务,并发列队
  • 有几个异步任务就开启了几个新的线程,任务也是同时执行的(异步方法具备开启新线程的能力,可以同时执行多个任务)
  • 异步执行,当前线程不等待,直接开启新的线程来执行,在新线程中执行任务(异步任务,添加异步任务的线程不做等待,可继续执行别的任务)
  • 异步任务,串行列队
  • 开启了一条新的线程来执行异步任务(异步任务可以开启新线程,串行列队只能开启一个线程)
  • 线程不会等待任务执行完毕,任务的执行是按照顺序来的,每次只有一个任务被执行,任务一个接一个的执行下去
  • 异步任务,主列队
  • 没有开启新线程,所有任务都是在主线程中执行的(虽然异步任务有开启新线程的能力,但因为是在主列队,所以无法开启新线程,所有任务都在主线程中执行)
  • 由于只有一个线程可以使用,所以所有任务都是按顺序一个个执行的,一个完毕,执行下一个

线程间的通信

线程间的通讯比较常用的就是在其他线程获取数据,然后返回主线程刷新UI界面

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    // 异步追加任务
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              
       NSLog(@"1---%@",[NSThread currentThread]);     
    }
    // 回到主线程
    dispatch_async(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];               
        NSLog(@"2---%@",[NSThread currentThread]);      
    });
});
复制代码

进阶使用

GCD栅栏

有时我们需要异步执行两组操作分别为A组和B组,当A组完成后,再执行B组操作,因此我们需要把把两组操作分割开来。这时可用 dispatch_barrier_async 方法来实现,在添加两组任务之间添加一个分栏,函数会先把分栏前添加的任务执行完毕之后,在把分栏后的任务添加到队列中

dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_barrier_async(queue, barrierBlk);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
复制代码

栅栏函数也可以执行队列上的操作(参数列表中有queue和block),也有对应的 dispatch_barrier_sync 函数。

栅栏函数中传入的参数队列必须是由 dispatch_queue_create 方法创建的队列,否则,与 dispatch_async 无异,起不到“栅栏”的作用了,对于 dispatch_barrier_sync 也是同理。

栅栏函数之前和之后的操作执行顺序都不固定,但是前面三个必然先执行,然后再执行栅栏函数中的操作,最后执行后面的三个

dispatch_barrier_syncdispatch_barrier_async 的区别:

  • 当栅栏前后添加的都是同步任务,两者没有区别,按照顺序依次执行
  • 当栅栏前后添加的是异步任务,sync 会先执行栅栏前的任务,然后不等待栅栏后部任务。async栅栏前后的任务都不等待

由此可见sync和async对于栅栏函数的区别作: dispatch_barrier_sync 将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们。 dispatch_barrier_async 将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务

GCD延迟

当遇到要求在指定时间后执行代码(例如5秒后执行代码),可用 dispatch_after 来实现,需要注意的是这个并不严谨,这个是指在指定时间后,再把代码加入列队中去,并不是严格的在多少时间后开始执行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(second * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // second秒后异步追加任务代码到主队列,并开始执行
    NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程
});
复制代码

GCD一次性代码

在创建单例,或者有代码要求在整个程序的运行过程中之执行一次的话可以使用GCD中的 dispatch_once 函数,这个函数保证即使在多线程的环境下也可以保证只调用一次,保证线程安全

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
    });
}
复制代码

GCD快速迭代

快速迭代函数 dispatch_apply 按照指定的次数把指定的任务加入到指定的列队中去,并等待全部的任务执行完毕后,结束

- (void)applyTime:(NSInteger)time {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(time, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}
复制代码

注意:由于是在并发列队中异步执行,所以里面的执行完成顺序不固定

GCD信号量

GCD中的信号量,是一种持有计数的信号,计数为0时,不可通过,要等待。计数为1或大于1时,可通过,不需等待。

三个函数来完成信号量的操作

dispatch_semaphore_create
dispatch_semaphore_signal
dispatch_semaphore_wait

实际开发中的作用:

  • 保持线程同步,使异步执行的任务转化为同步执行
  • 保护线程安全,为线程加锁

例如:当异步执行耗时操作时,需要使用该异步操作的结果进行一些额外的操作,例如:同时进行两个异步操作A和B,执行B的时候,需要对A的运行结果来进行操作,这个时候就可以对B加一个信号量,让B等待,当A执行完毕后,对B操作发送信号,继续执行B操作

- (void)semaphoreSync {
    NSLog(@"semaphore---begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建初始信号量 为 0 ,阻塞所有线程
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务A
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        number = 100;
        // 执行完线程,信号量加 1,信号总量从 0 变为 1
        dispatch_semaphore_signal(semaphore);
    });
    //原任务B
    ////若计数为0则一直等待,直到接到总信号量变为 >0 ,继续执行后续代码
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
复制代码

例如:线程安全方面:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。(可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。)

GCD队列组

有时候会遇到需要异步执行两个耗时任务,然后当两个任务都执行完毕后,在回到主线程执行任务,这时候我们可以用GCD的队列组的功能来实现这个要求

GCD队列组常用函数:

  • 调用队列组的 dispatch_group_async 先把任务放到列队中,然后把列队放入到列队组中。也可用 dispatch_group_enterdispatch_group_leave 两个组合来实现 dispatch_group_async
dispatch_group_enter
dispatch_group_leave

当group中的未执行完毕的任务数为0的时候才会执行 dispatch_group_notify 中的任务,以及不会使 dispatch_group_wait 堵塞当前线程(类似于信号量)

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_sync(queue, ^{
    // 追加任务A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    }
    dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
复制代码
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务(监听group中的所有任务的完成状态,当所有任务都完成后,把notify中的任务添加到group中,并执行任务)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任务A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    }
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任务B
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    }
});
    
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 追加任务Main
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"main---%@",[NSThread currentThread]);      // 打印当前线程
    }
});
复制代码
  • 调用队列组的 dispatch_group_wait 回到当前线程继续向下执行(暂停当前线程中的操作,阻塞当前线程,执行wait中的group操作,执行完后,继续执行当前线程中的操作)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任务A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    }
});
//执行A任务,执行完成后继续执行该线程后续任务
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"current---%@",[NSThread currentThread]);
复制代码
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章