iOS ------ 多线程 GCD

一,GCD简介

GCD是Apple开发的一个多线程的较新的解决方案。它主要用于优化应用程序以支持多核处理器以及其他对称处理系统。它是一个在线程池模式的基础上执行的并发任务。

为什么要使用GCD?

  • GCD!可用于多核的并行运算
  • GCD会自动利用更多的CPU内核(比如双核,四核)
  • GCD会自动管理线程的生命周期(创建线程,调度任务,销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

二,GCD任务和队列

  1. 任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。
  2. 队列:这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的 线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
    在这里插入图片描述
  3. 同步执行(sync):

同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1 -- %@", [NSThread currentThread]);
    // 串行队列的创建方法
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    NSLog(@"4 -- %@", [NSThread currentThread]);
    // Do any additional setup after loading the view.
}
输出
2024-05-29 20:53:55.168850+0800 GCD详解[69722:2675459] 1 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.168911+0800 GCD详解[69722:2675459] 2 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.168958+0800 GCD详解[69722:2675459] 3 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.169010+0800 GCD详解[69722:2675459] 4 -- <_NSMainThread: 0x600000b70100>{number = 1, name = m
  1. 异步执行(async):

异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1 -- %@", [NSThread currentThread]);
    // 串行队列的创建方法
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    NSLog(@"4 -- %@", [NSThread currentThread]);
    // Do any additional setup after loading the view.
}
输出
2024-05-29 21:02:04.468604+0800 GCD详解[69868:2682284] 1 -- <_NSMainThread: 0x60000376c540>{number = 1, name = main}
2024-05-29 21:02:04.468673+0800 GCD详解[69868:2682284] 4 -- <_NSMainThread: 0x60000376c540>{number = 1, name = main}
2024-05-29 21:02:04.468678+0800 GCD详解[69868:2682427] 2 -- <NSThread: 0x600003765580>{number = 6, name = (null)}
2024-05-29 21:02:04.468735+0800 GCD详解[69868:2682427] 3 -- <NSThread: 0x600003765580>{number = 6, name = (null)}
  1. 串行队列(Serial Dispatch Queue):

每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)

  1. 并发队列(Concurrent Dispatch Queue):

可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

⚠️注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1 -- %@", [NSThread currentThread]);
    // 并行队列的创建方法
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    NSLog(@"4 -- %@", [NSThread currentThread]);
    // Do any additional setup after loading the view.
}
输出
024-05-29 21:03:45.087904+0800 GCD详解[69913:2684176] 1 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.087962+0800 GCD详解[69913:2684176] 2 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.087992+0800 GCD详解[69913:2684176] 3 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.088034+0800 GCD详解[69913:2684176] 4 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}

三, GCD 的使用步骤

GCD 的使用步骤只有两步:

1,创建一个队列(串行队列或并发队列);
2,将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)。

3.1 队列的创建方法 / 获取方法

  • 可以使用 dispatch_queue_create 方法来创建队列。该方法需要传入两个参数:

第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
  • 对于串行队列,GCD 默认提供了:主队列(Main Dispatch Queue)。

1.所有放在主队列中的任务,都会放到主线程中执行。
2.可使用 dispatch_get_main_queue() 方法获得主队列。

⚠️注意:主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,平常写的代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象。

dispatch_queue_t queue = dispatch_get_main_queue();
  • 对于并发队列,GCD 默认提供了 全局并发队列(Global Dispatch Queue)。

可以使用 dispatch_get_global_queue 方法来获取全局并发队列。需要传入两个参数。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可。

// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

3.2 任务的创建方法

GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});

虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:

  1. 同步执行 + 并发队列
  2. 异步执行 + 并发队列
  3. 同步执行 + 串行队列
  4. 异步执行 + 串行队列

实际上,刚才还说了两种默认队列:全局并发队列主队列。全局并发队列可以作为普通并发队列来使用。但是当前代码默认放在主队列中,所以主队列很有必要专门来研究一下,这样就有六种不同的组合方式了。

  1. 同步执行 + 主队列
  2. 异步执行 + 主队列

3.3任务和队列不同组合方式的区别

3.3.1 同步执行 + 并发队列

在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。

NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
         [NSThread sleepForTimeInterval:2];  
         NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
         [NSThread sleepForTimeInterval:2];  
         NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
         [NSThread sleepForTimeInterval:2];  
         NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);

输出结果

2024-05-29 21:30:27.212376+0800 GCD详解[70435:2704510] 1 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212433+0800 GCD详解[70435:2704510] 2 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212479+0800 GCD详解[70435:2704510] 3 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212530+0800 GCD详解[70435:2704510] 4 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212569+0800 GCD详解[70435:2704510] 5 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}

从 同步执行 + 并发队列 中可看到:

  • 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
  • 所有任务都在打印的任务1和任务5之间执行的(同步任务 需要等待队列的任务执行结束)。
  • 任务按顺序执行的。按顺序执行的原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
3.3.2 异步执行 + 并发队列
  • 可以开启多个线程,任务交替(同时)执行。
NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);

输出结果

024-05-29 21:36:49.626519+0800 GCD详解[70567:2709956] 1 -- <_NSMainThread: 0x600003d9c040>{number = 1, name = main}
2024-05-29 21:36:49.626591+0800 GCD详解[70567:2709956] 5 -- <_NSMainThread: 0x600003d9c040>{number = 1, name = main}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710054] 2 -- <NSThread: 0x600003dd4c00>{number = 4, name = (null)}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710050] 4 -- <NSThread: 0x600003dd4e00>{number = 5, name = (null)}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710053] 3 -- <NSThread: 0x600003d9af40>{number = 3, name = (null)}

在 异步执行 + 并发队列 中可以看出:

  • 除了当前线程(主线程),系统又开启了 3 个线程,并且任务是交替/同时执行的。(异步执行 具备开启新线程的能力。且 并发队列 可开启多个线程,同时执行多个任务)。
  • 所有任务是在打印的任务1 和任务5之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行 不做等待,可以继续执行任务)。
3.3.3 同步执行 + 串行队列
  • 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);

输出结果

2024-05-29 21:40:14.290797+0800 GCD详解[70741:2714482] 1 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:16.291817+0800 GCD详解[70741:2714482] 2 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:18.292859+0800 GCD详解[70741:2714482] 3 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:20.293898+0800 GCD详解[70741:2714482] 4 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:20.293968+0800 GCD详解[70741:2714482] 5 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}

在 同步执行 + 串行队列 可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行 不具备开启新线程的能力)。
  • 所有任务都在打印的任务1和任务5之间执行(同步任务 需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
3.3.4异步执行 + 串行队列
  • 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);

输出结果:

2024-05-29 21:43:18.017856+0800 GCD详解[70835:2717791] 1 -- <_NSMainThread: 0x6000037b8000>{number = 1, name = main}
2024-05-29 21:43:18.017922+0800 GCD详解[70835:2717791] 5 -- <_NSMainThread: 0x6000037b8000>{number = 1, name = main}
2024-05-29 21:43:20.023459+0800 GCD详解[70835:2717899] 2 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}
2024-05-29 21:43:22.028921+0800 GCD详解[70835:2717899] 3 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}
2024-05-29 21:43:24.033872+0800 GCD详解[70835:2717899] 4 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}

在 异步执行 + 串行队列 可以看到:

  • 开启了一条新线程(异步执行 具备开启新线程的能力,串行队列 只开启一个线程)。
  • 所有任务是在打印的任务1和任务5之后才开始执行的(异步执行 不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
3.3.5同步执行 + 主队列
  • 同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。
  • 主队列:
    默认情况,平常所写的代码是直接放在主队列中的
    所有放在主队列中的任务,都会在主线程中执行
    可使用dispatch_get_main_queue() 获得主队列
3.3.5.1 在主线程中调用 同步执行 + 主队列
  • 互相等待卡住不可行
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);
}

这样会直接崩溃

这是因为我们在主线程中执行任务5方法,相当于把任务5放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务2追加到主队列中,任务2就在等待主线程处理完5任务。而任务5需要等待任务1 所在队列的所有任务执行完毕,才能接着执行。这样相互等待,就造成了死锁

3.3.5.2在其他线程中调用『同步执行 + 主队列』
  • 不会开启新线程,执行完一个任务,再执行下一个任务
- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
    
}
- (void)syncMain {
    NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);
 }

输出结果:

2024-05-29 21:59:50.168185+0800 GCD详解[71147:2730839] 1 -- <NSThread: 0x6000027bc700>{number = 7, name = (null)}
2024-05-29 21:59:52.197328+0800 GCD详解[71147:2730626] 2 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:54.203257+0800 GCD详解[71147:2730626] 3 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:56.210364+0800 GCD详解[71147:2730626] 4 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:56.210959+0800 GCD详解[71147:2730839] 5 -- <NSThread: 0x6000027bc700>{number = 7, name = (null)}

在其他线程中使用 同步执行 + 主队列 可看到:

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
  • 所有任务都在打印的任务1和任务5之间执行(同步任务 需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

这里任务2不用等待其他线程的任务5执行完,不会造成死锁。

3.3.6 异步执行 + 主队列
  • 只在主线程执行任务,执行完一个任务,在执行下一个任务
NSLog(@"1 -- %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4 -- %@",[NSThread currentThread]);
    });
    NSLog(@"5 -- %@", [NSThread currentThread]);

输出结果:

2024-05-30 19:47:21.114444+0800 GCD详解[96320:3468801] 1 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:21.114507+0800 GCD详解[96320:3468801] 5 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:23.140562+0800 GCD详解[96320:3468801] 2 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:25.142281+0800 GCD详解[96320:3468801] 3 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:27.143875+0800 GCD详解[96320:3468801] 4 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}

在 异步执行 + 主队列 可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的任务1和任务5之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(因为主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

四,GCD线程间的通信

在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //获取主队列
    dispatch_queue_t mainqueue = dispatch_get_main_queue();
    NSLog(@"begin---%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        //异步追加任务1
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1---%@", [NSThread currentThread]);
        
        //回到主线程
        dispatch_async(mainqueue, ^{
            //追加到主线程中2执行任务
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@", [NSThread currentThread]);
        });
        NSLog(@"3---%@", [NSThread currentThread]);
    });
    NSLog(@"end---%@", [NSThread currentThread]);

输出结果

2024-05-30 20:15:39.553857+0800 GCD详解[96959:3489760] begin---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
2024-05-30 20:15:39.553924+0800 GCD详解[96959:3489760] end---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
2024-05-30 20:15:41.559224+0800 GCD详解[96959:3490016] 1---<NSThread: 0x600000a4dd40>{number = 8, name = (null)}
2024-05-30 20:15:41.559701+0800 GCD详解[96959:3490016] 3---<NSThread: 0x600000a4dd40>{number = 8, name = (null)}
2024-05-30 20:15:43.560499+0800 GCD详解[96959:3489760] 2---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
  • 可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。

六,GCD信号量: dispatch_semaphore

Dispatch Semaphore(调度信号量)是 GCD(Grand Central Dispatch)提供的一种同步机制,用于控制并发访问资源或者在不同线程之间进行同步。信号量可以用来限制并发执行的任务数量或者让一个线程等待某个事件的发生

使用 Dispatch Semaphore

  • 创建信号量

dispatch_semaphore_create 用于创建一个信号量,初始计数可以是任意非负整数。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 创建初始值为1的信号量
  • 等待信号量

dispatch_semaphore_wait 用于等待信号量,如果信号量计数大于0,则减1并立即返回;否则阻塞当前线程,直到信号量计数大于0或者超时。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量,永远阻塞直到信号量计数大于0
  • 发送信号

dispatch_semaphore_signal 用于发送信号,即增加信号量的计数。如果有等待的线程,则唤醒一个等待的线程。

dispatch_semaphore_signal(semaphore); // 发送信号,增加信号量计数
控制并发任务数量

假设你想限制最多只有2个任务同时执行,可以使用信号量来实现:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 最多允许2个任务同时执行

for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
        NSLog(@"Task %d started", i);
        sleep(2); // 模拟任务耗时2秒
        NSLog(@"Task %d completed", i);
        dispatch_semaphore_signal(semaphore); // 发送信号
    });
}
实现线程同步,将异步执行任务转换为同步执行任务
NSLog(@"currentThread -- %@", [NSThread currentThread]);
    NSLog(@"semaphere --- begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphere = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1 --- %@", [NSThread currentThread]);
        
        number = 100;
        
        dispatch_semaphore_signal(semaphere);
    });
    dispatch_semaphore_wait(semaphere, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore --- end  number = %d", number);

输出结果:

2024-05-30 21:27:19.082058+0800 GCD详解[98426:3541645] currentThread -- <_NSMainThread: 0x6000030f4000>{number = 1, name = main}
2024-05-30 21:27:19.082108+0800 GCD详解[98426:3541645] semaphere --- begin
2024-05-30 21:27:21.087427+0800 GCD详解[98426:3541817] 1 --- <NSThread: 0x6000030b2380>{number = 4, name = (null)}
2024-05-30 21:27:21.087923+0800 GCD详解[98426:3541645] semaphore --- end  number = 100

semaphore—end 是在执行完 number = 100; 之后才打印的。而且输出结果 number 为 100。
执行顺如下:

  1. semaphore 初始创建时计数为 0。
  2. 异步执行 将 任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
  3. 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行,最后打印 semaphore—end,number = 100。

异步执行任务转换为同步执行任务,可以对异步的执行结果进行进一步操作

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

七,Dispatch Semaphore 线程安全和线程同步(为线程加锁)

非线程安全(不使用 semaphore)

先来看看不考虑线程安全的代码:

/**
 * 非线程安全:不使用 semaphore
 * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
 */
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });

/**
 * 售卖火车票(非线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
        
    }
}

线程安全(使用 semaphore 加锁)

/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

运行结果

2024-05-30 21:54:54.019858+0800 GCD详解[99036:3563863] 剩余票数:10 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:54.222849+0800 GCD详解[99036:3563864] 剩余票数:9 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:54.428311+0800 GCD详解[99036:3563863] 剩余票数:8 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:54.632216+0800 GCD详解[99036:3563864] 剩余票数:7 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:54.833208+0800 GCD详解[99036:3563863] 剩余票数:6 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.037057+0800 GCD详解[99036:3563864] 剩余票数:5 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:55.237681+0800 GCD详解[99036:3563863] 剩余票数:4 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.439402+0800 GCD详解[99036:3563864] 剩余票数:3 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:55.643820+0800 GCD详解[99036:3563863] 剩余票数:2 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.849417+0800 GCD详解[99036:3563864] 剩余票数:1 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:56.055054+0800 GCD详解[99036:3563863] 剩余票数:0 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:56.260555+0800 GCD详解[99036:3563864] 所有火车票均已售完
2024-05-30 21:54:56.261010+0800 GCD详解[99036:3563863] 所有火车票均已售完

semaphoreLock = dispatch_semaphore_create(1);这里的信号量设置为1,就是为了保证只有一个线程同时运行售卖火车票的这段代码。保证了线程安全。

相关推荐

  1. iOS ------ 线 pthread,NSThread

    2024-06-06 21:42:03       30 阅读
  2. Boost:asioio_service,线run

    2024-06-06 21:42:03       49 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-06 21:42:03       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-06 21:42:03       106 阅读
  3. 在Django里面运行非项目文件

    2024-06-06 21:42:03       87 阅读
  4. Python语言-面向对象

    2024-06-06 21:42:03       96 阅读

热门阅读

  1. Docker安全配置

    2024-06-06 21:42:03       26 阅读
  2. CMPSC473 malloclab: writing a dynamic storage allocator

    2024-06-06 21:42:03       27 阅读
  3. Flask的模块化实践

    2024-06-06 21:42:03       24 阅读
  4. Flask启动重复注册schedule问题

    2024-06-06 21:42:03       29 阅读
  5. LeetCode-239.滑动窗口最大值

    2024-06-06 21:42:03       26 阅读
  6. Napster诞生25周年:文件共享革命的引领者

    2024-06-06 21:42:03       31 阅读
  7. ZCC5429 异步升压芯片

    2024-06-06 21:42:03       35 阅读