iOS-多线程相关

多线程:GCD

2019-10-12  本文已影响0人  意一ineyee
一、GCD的两对儿主要概念及它们的六种组合
 1、dispatch_syncdispatch_async
 2、串行队列和并发队列、主队列和全局队列
 3、它们的六种组合
二、GCD的死锁
三、GCD的其它常用API
 1、dispatch_once
 2、dispatch_after
 3、GCD定时器
 4、GCD信号量
 5、dispatch_group
 6、dispatch_barrier_async——读写安全方案(多读单写方案)

GCD是实现多线程的一种方案,我们开发者只需要定义想执行的任务,追加到特定的队列中,GCD就会根据情况来决定是否开辟新线程来执行任务。

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

一、GCD的两对儿主要概念及它们的六种组合


1、dispatch_syncdispatch_async

同步和异步修饰的是追加,即同步追加和异步追加,它们俩的主要区别就是是否会阻塞当前线程是否具备开辟新线程的能力,即:

  • dispatch_sync函数会阻塞当前线程,不具备开辟新线程的能力。
  • dispatch_async函数不会阻塞当前线程,具备开辟新线程的能力,但不是一定会开辟新线程。
// 同步追加函数
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

dispatch_sync函数,是指把一个任务同步追加到特定的队列中,所谓同步追加是指dispatch_sync函数在把一个任务追加到特定的队列中后不会立马返回,而是会阻塞当前线程——即dispatch_sync函数所在的线程,一直等到它追加的任务执行完毕才会返回,这个时候当前线程的代码才能继续往下执行。

GCD设定dispatch_sync函数不具备开辟新线程的能力。

// 异步追加函数
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_async函数,是指把一个任务异步追加到特定的队列中。所谓异步追加,是指dispatch_async函数在把一个任务追加到特定的队列中后会立马返回,而不会阻塞当前线程——即dispatch_async函数所在的线程,当前线程的代码可以继续往下执行。

GCD设定dispatch_async函数具备开辟子线程的能力,但不是一定会开辟新线程。

2、串行队列和并发队列、主队列和全局队列

串行和并发修饰的是队列,即串行队列和并发队列,它们俩的主要区别就是队列里的任务是串行执行的还是并发执行的,即:

  • 串行队列里的任务是串行执行(挨个执行)的,即必须得等上一个任务执行完毕,才会去拿下一个任务执行。
  • 并发队列里的任务是并发执行(同时执行)的,即不会等上一个任务执行完毕,就会去拿下一个任务执行,多个任务可以同时执行。


(可见GCD里队列的先进先出原则体现在“拿任务”的先后顺序上,而不是“任务执行完毕”的先后顺序上。)

// 串行队列:第一个参数是该队列的唯一标识符,第二个参数是队列的类型
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
// 并行队列:第一个参数是该队列的唯一标识符,第二个参数是队列的类型
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

主队列是一个特殊的串行队列,我们追加到主队列里的任务都会被放到主线程中去执行。

// 全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

全局队列就是一个并发队列,它没什么特殊的,只不过是系统已经为我们提供好的而已,所以通常情况下我们没必要专门去创建一个并发队列,直接用全局队列就可以了,除非项目中要用到多个并发队列。

3、它们的六种组合

因为全局队列就是一个并发队列,它没什么特殊的,所以我们把它归到并发队列里了,而主队列是一个特殊的串行队列,和自己创建的串行队列有些差别,所以我们把它单独拎出来了。

自己创建的串行队列 并发队列 主队列
dispatch_sync • 会阻塞当前线程
• 不会开辟新线程


• 队列里的任务在当前线程上串行执行
• 会阻塞当前线程
• 不会开辟新线程


• 队列里的任务在当前线程上串行执行
• 会阻塞当前线程
• 不会开辟新线程


• 队列里的任务在主线程上串行执行
dispatch_async • 不会阻塞当前线程
• 会开辟新线程,会开辟一个新线程(因为任务是串行执行的,所以没必要开辟那么多)


• 队列里的任务在这个新线程上串行执行
• 不会阻塞当前线程
• 会开辟新线程,会开辟多个新线程(因为任务是并发执行的,所以可以开辟多个)


• 队列里的任务在这些新线程上并发执行
• 不会阻塞当前线程
• 不会开辟新线程(因为主队列里的任务都会被放到主线程中去执行,所以没必要开辟新线程)


• 队列里的任务在主线程上串行执行

可见要想用GCD实现多线程开发,就必须得用dispatch_async,当把任务追加到串行队列时,只会开辟一个子线程,当把任务追加到并发队列时,会开辟多个子线程。

它们的六种组合要从四个角度去考虑:

  • 会不会阻塞当前线程:完全由dispatch_syncdispatch_async决定,dispatch_sync肯定会阻塞当前线程,dispatch_async肯定不会阻塞当前线程。
  • 会不会开辟新线程:则由dispatch_syncdispatch_async和队列共同决定,但dispatch_syncdispatch_async占主导地位,dispatch_sync肯定不会开辟新线程,dispatch_async + 主队列不会开辟新线程,dispatch_async + 其它队列会开辟新线程。
  • 会开辟线程的前提下,会开辟几个新线程:此时则完全由队列决定了,串行队列只会开辟一个新线程,并发队列会开辟多个新线程。
  • 队列里的任务在哪个线程上执行、串行执行还是并发执行:也是由dispatch_syncdispatch_async和队列共同决定,但队列占主导地位,串行队列里的任务肯定是串行执行,开辟了新线程时则在新线程上串行执行,没开辟新线程时则在当前线程上串行执行,并发队列里的任务还要看是否开辟了新线程,开辟了新线程时则在新线程上并发执行,没开辟新线程时则在当前线程上串行执行,主队列里的任务永远是在主线程上串行执行。

我们随便举两个例子来练习分析一下。

1、dispatch_sync + 自己创建的串行队列

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"0,%@", [NSThread currentThread]);

    for (int i = 1; i <= 3; i++) {
        
        dispatch_sync(queue, ^{
            
            NSLog(@"%d,%@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"4,%@", [NSThread currentThread]);
}


// 控制台打印:
0,<NSThread: 0x6000018bd580>{number = 1, name = main}
1,<NSThread: 0x6000018bd580>{number = 1, name = main}
2,<NSThread: 0x6000018bd580>{number = 1, name = main}
3,<NSThread: 0x6000018bd580>{number = 1, name = main}
4,<NSThread: 0x6000018bd580>{number = 1, name = main}

分析一下:

2、dispatch_async + 并发队列

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"0,%@", [NSThread currentThread]);

    for (int i = 1; i <= 3; i++) {
        
        dispatch_async(queue, ^{
            
            NSLog(@"%d,%@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"4,%@", [NSThread currentThread]);
}


// 控制台打印:
0,<NSThread: 0x600003076140>{number = 1, name = main}
4,<NSThread: 0x600003076140>{number = 1, name = main}
2,<NSThread: 0x600003073d40>{number = 5, name = (null)}
1,<NSThread: 0x600003014b00>{number = 6, name = (null)}
3,<NSThread: 0x6000030360c0>{number = 4, name = (null)}
// 或者:
0,<NSThread: 0x600003076140>{number = 1, name = main}
2,<NSThread: 0x600003073d40>{number = 5, name = (null)}
3,<NSThread: 0x6000030360c0>{number = 4, name = (null)}
1,<NSThread: 0x600003014b00>{number = 6, name = (null)}
4,<NSThread: 0x600003076140>{number = 1, name = main}

分析一下:

二、GCD的死锁


什么情况下会造成死锁:dispatch_sync、往它自己所在的队列里追加任务、并且它自己所在的这个队列还是个串行队列,就会造成死锁。(“它自己所在的队列”是指“dispatch_sync追加任务”这个操作本身所在的队列)

怎么打破死锁:我们只要打破其中任意一个条件,就可以解决死锁问题。比如我们可以用dispatch_async追加任务,而不是用dispatch_sync追加任务,这就绝对不会造成死锁;又比如我们可以用dispatch_sync把任务追加到别的队列里——串行、并发随你便,而不是追加到dispatch_sync它自己所在的队列里,也绝对不会造成死锁;又比如我们可以让dispatch_sync自己所在的队列是个并发队列,而不是个串行队列,那用dispatch_sync往它自己所在的队列里追加任务时,也绝对不会造成死锁。

我们随便举两个例子来练习分析一下。

1、例一

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        
        NSLog(@"block任务,%@", [NSThread currentThread]);
    });
}


// 控制台打印:没有打印,崩溃在dispatch_sync函数处......

分析一下:

使用dispatch_sync追加任务,追加到的队列是主队列,是它自己所在的队列,而且主队列是个串行队列,三个条件都齐了,所以会发生死锁。

打破死锁:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        
        NSLog(@"block任务,%@", [NSThread currentThread]);
    });
}


// 控制台打印:
block任务,<NSThread: 0x60000137e1c0>{number = 1, name = main}

任务添加阶段:block任务依旧是被追加到主队列里,而且依旧是追加到viewDidLoad后面。

任务执行阶段:但是viewDidLoad执行过程中dispatch_async函数不会阻塞主线程,所以viewDidLoad不必等待block任务执行完毕就可以继续往下执行,执行完毕后再执行block任务。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
        
        NSLog(@"block任务,%@", [NSThread currentThread]);
    });
}


// 控制台打印:
block任务,<NSThread: 0x600002a34340>{number = 1, name = main}

任务添加阶段:block任务是被追加到别的队列里,而不是dispatch_sync自己所在的主队列里。

任务执行阶段:所以viewDidLoad执行过程中dispatch_sync函数虽然会阻塞主线程来执行block任务,但是block任务和viewDidLoad根本就不在一个队列里,所以block任务不必等待viewDidLoad执行完毕就可以拿来执行,执行完毕后继续往下执行viewDidLoad

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        
        NSLog(@"任务1,%@", [NSThread currentThread]);
        
        dispatch_sync(queue, ^{

            NSLog(@"block任务,%@", [NSThread currentThread]);
        });
        
        NSLog(@"任务2,%@", [NSThread currentThread]);
    });
}


// 控制台打印:
任务1,<NSThread: 0x60000179c080>{number = 3, name = (null)}
block任务,<NSThread: 0x60000179c080>{number = 3, name = (null)}
任务2,<NSThread: 0x60000179c080>{number = 3, name = (null)}

任务添加阶段:dispatch_async函数把“dispatch_sync追加任务”这个操作放到了一个并发队列里,而不再是在主队列——串行队列里,所以首先block任务肯定不会再和viewDidLoad发生死锁问题了,但现在却有可能跟任务1、2发生死锁问题,因为任务1、2也都是被添加在这个并发队列里的,而且block任务也确实是被追加到了任务2后面。

任务执行阶段:任务执行阶段dispatch_sync函数虽然会阻塞线程来执行block任务,但由于这个队列是个并发队列,所以任务2不必等待block任务执行完毕就可以拿出来执行。

2、例二

但是我们把上面方案三的并发队列改成串行队列,block任务就会和任务2发生死锁问题了。

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("myOtherQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{

        NSLog(@"任务1,%@", [NSThread currentThread]);
        
        dispatch_sync(queue, ^{

            NSLog(@"block任务,%@", [NSThread currentThread]);
        });
        
        NSLog(@"任务2,%@", [NSThread currentThread]);
    });
}

分析一下:

使用dispatch_sync追加任务,追加到的队列queue和它自己所在的队列是同一个队列,而且这个队列是个串行队列,三个条件都齐了,所以会发生死锁。

三、GCD的其它常用API


1、dispatch_once

dispatch_once用来保证一段代码在程序的整个生命周期中只执行一次,所以它里面的代码是百分之百线程安全的

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
    // 代码...
});

2、dispatch_after

dispatch_after用来延时多长时间后执行某个任务,和GCD定时器原理一样,都是基于系统内核实现的。而performSelector:afterDelay:NSTimer的原理一样,都是基于RunLoop实现的。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    // 任务...
});

3、GCD定时器

我们知道NSTimer是基于RunLoop实现的,它可能不准时,所以如果我们想要保证定时器准时,可以使用GCD定时器。它不是基于RunLoop实现的,不是Timer事件源,而是基于系统内核实现的,时间到后直接把任务追加到当前相应的队列中执行,更加准时。

@interface ViewController ()

@property (nonatomic, strong) dispatch_source_t timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
    /**
     * 创建定时器
     *
     * dispatchQueue:定时器的回调要放在什么队列里
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    /**
    * 设置时间
    *
    * start:多少秒后开始执行
    * intervalInSeconds:时间间隔
    * leewayInSeconds:误差,通常填0就可以
    */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    // 回调
    dispatch_source_set_event_handler(timer, ^{
        
        NSLog(@"11");
    });
    // 启动定时器
    dispatch_resume(timer);
    
    // 要用强引用把timer保住
    self.timer = timer;
}

4、GCD信号量

dispatch_semaphore——GCD信号量用来控制GCD线程的最大并发数,它有一个初始值,这个值就用来指定GCD线程的最大并发数。

dispatch_semaphore的原理是利用dispatch_semaphore_waitdispatch_semaphore_signal两个函数来实现的。具体地说,每当一个线程进来执行到dispatch_semaphore_wait函数,函数内部就会判断信号量的值,如果发现信号量的值 > 0,就让信号量的值 - 1,并让这条线程往下执行代码,如果发现信号量的值 <= 0,就会让线程阻塞在这里休眠,等待信号量的值再次 > 0时被唤醒;而每当一个线程进来执行到dispatch_semaphore_signal函数,就代表这个线程执行完任务了,函数内部会让信号量的值 + 1,如果上面有休眠的线程,就可以唤醒那个休眠的线程进来执行代码了。这样就达到了控制线程最大并发数的效果。

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) dispatch_semaphore_t semaphore;// 信号量

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建信号量,并设置信号量的初始值为3,即设置线程的最大并发数为3
    self.semaphore = dispatch_semaphore_create(3);
        
    // 开辟10个新线程来执行任务
    for (int i = 0; i < 10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self test];
        });
    }
}

- (void)test {

    // 每个线程进来执行到dispatch_semaphore_wait函数时,函数内部有如下操作:
    // 如果发现信号量的值 > 0,就让信号量的值 - 1,并让这条线程往下执行代码
    // 如果发现信号量的值 <= 0,就会让线程阻塞在这里休眠等待,直到信号量的值 > 0,才再次让信号量的值 - 1,并唤醒一条休眠的线程进去执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2); // 睡眠2s,为了更直观地看到最大并发数
    NSLog(@"%s,%@", __func__, [NSThread currentThread]);
    
    // 每个线程执行完有效代码后,执行到dispatch_semaphore_signal函数时,函数内部有如下操作:
    // 会让信号量的值 + 1,代表这个线程执行完任务了,此时上面就可以唤醒一个休眠的线程进来执行代码了
    dispatch_semaphore_signal(self.semaphore);
}

@end

5、dispatch_group

dispatch_group通常用来实现“先并发执行多个任务,等多个任务并发执行完毕后,再执行某个结束操作”这样的需求。

例如,先并发执行任务1和任务2,等任务1和任务2并发执行完毕后,再回到主线程执行结束任务3。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建group
    dispatch_group_t group = dispatch_group_create();
    // 创建并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    // 先并发执行任务1和任务2
    dispatch_group_async(group, queue, ^{
        
        NSLog(@"执行任务1,%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        
        NSLog(@"执行任务2,%@", [NSThread currentThread]);
    });
    
    // 等任务1和任务2并发执行完毕后,再回到主线程执行结束任务3
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        NSLog(@"执行结束任务3 - done,%@", [NSThread currentThread]);
    });
}


// 控制台打印:
执行任务2,<NSThread: 0x600003190100>{number = 6, name = (null)}
执行任务1,<NSThread: 0x600003186800>{number = 5, name = (null)}
执行结束任务3 - done,<NSThread: 0x6000031e2200>{number = 1, name = main}

6、dispatch_barrier_async——读写安全方案(多读单写方案)

我们在开发中经常会遇到IO操作——即文件读写操作,那么在多线程开发的情况下,就很容易出现:

“同一时间,多个线程都在执行写入操作”肯定会出现数据竞争,导致写入的数据错乱;“同一时间,有一个线程在执行写入操作,一个线程在执行读取操作”也肯定会出现数据竞争,导致读取出来的数据有可能不是原来的数据或者最新的数据(我们可能想读的就是原来的数据或者最新的数据);“同一时间,多个线程都在执行读取操作”这个是没有问题的,因为大家都是读取,而不是写入,所以不会出现数据错乱。

因此针对多线程下的IO操作,我们就得有一套读写安全方案(或者叫多读单写方案)来解决上面的问题:

我们很容易就想到用线程同步来实现这套方案,举个例子:

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) dispatch_semaphore_t semaphore;// 信号量

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建信号量,并设置信号量的初始值为1,即设置线程的最大并发数为1,以此达到线程同步的效果
    self.semaphore = dispatch_semaphore_create(1);

    // 有10个线程在执行读取操作,10个线程在执行写入操作
    for (int i = 0; i < 10; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self read];
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self write];
        });
    }
}

- (void)read {
    
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"读取操作,%@", [NSThread currentThread]);
    
    dispatch_semaphore_signal(self.semaphore);
}

- (void)write {
    
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"写入操作,%@", [NSThread currentThread]);
    
    dispatch_semaphore_signal(self.semaphore);
}

@end

这样的确实现了:

也确实解决了数据竞争的问题,但线程同步并非完美的读写安全方案,因为它没有实现:

这就导致读写操作的效率不是最高的,因为多个读取操作完全可以并发执行嘛,那我们尝试把读取操作的信号量去掉。

- (void)read {
    
//    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"读取操作,%@", [NSThread currentThread]);
    
//    dispatch_semaphore_signal(self.semaphore);
}

这样的确可以实现“同一时间,允许多个线程都执行读取操作”了,但又同时打破了“同一时间,不能既有写入操作,也有读取操作,只能是其中之一”,因此使用线程同步是无法完美实现读写安全方案的,需要另寻它路——dispatch_barrier_async

  • 我们只需要把写入操作(即不能并发执行的操作)通过dispatch_barrier_async追加到队列中,把读取操作(即能并发执行的操作)通过dispatch_async追加到队列中,就能实现完美的读写安全方案。原理大概是执行到dispatch_barrier_async里面的任务时,就会在这个任务的前后插入两个栅栏,以此保证这一时刻只能执行这一个任务,任务执行完后拿走栅栏,dispatch_async里面的任务就又可以并发执行了。
  • 不过这种读写安全方案的队列必须是自己创建的并发队列,不能是全局队列。
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建并发队列,不能是全局队列
    dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

    // 有10个线程在执行读取操作,10个线程在执行写入操作
    for (int i = 0; i < 10; i++) {

        dispatch_async(queue, ^{

            [self read];
        });
        
        dispatch_barrier_async(queue, ^{

            [self write];
        });
    }
}

- (void)read {
    
    NSLog(@"读取操作,%@", [NSThread currentThread]);
}

- (void)write {
        
    NSLog(@"写入操作,%@", [NSThread currentThread]);
}

@end

此外我们捎带提一下另一种读写安全方案:读写锁——pthread_rwlock

#import "ViewController.h"
#import <pthread.h> // 导入头文件

@interface ViewController ()

@property (nonatomic, assign) pthread_rwlock_t rwlock; // 锁

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建锁
    pthread_rwlock_init(&_rwlock, nil);

    // 有10个线程在执行读取操作,10个线程在执行写入操作
    for (int i = 0; i < 10; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self read];
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self write];
        });
    }
}

- (void)read {
    
    // 读加锁
    pthread_rwlock_rdlock(&_rwlock);
    
    NSLog(@"读取操作,%@", [NSThread currentThread]);
    
    // 解锁
    pthread_rwlock_unlock(&_rwlock);
}

- (void)write {
    
    // 写加锁
    pthread_rwlock_wrlock(&_rwlock);
        
    NSLog(@"写入操作,%@", [NSThread currentThread]);
    
    // 解锁
    pthread_rwlock_unlock(&_rwlock);
}

- (void)dealloc {
    
    // 销毁锁
    pthread_rwlock_destroy(&_rwlock);
}

@end
上一篇下一篇

猜你喜欢

热点阅读