MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的GCD并发队列与任务调度

2024-10-031.4k 阅读

GCD简介

Grand Central Dispatch(GCD)是苹果公司开发的一个多核编程的解决方案,它于2009年在Mac OS X 10.6和iOS 4.0中引入。GCD旨在简化并发编程,使得开发者能够更容易地利用多核处理器的性能,提升应用程序的运行效率。

GCD的核心概念包括队列(Queue)和任务(Task)。任务是需要执行的代码块,而队列则负责管理这些任务的执行顺序。GCD提供了两种类型的队列:串行队列(Serial Queue)和并发队列(Concurrent Queue)。

并发队列

并发队列允许多个任务同时执行,只要系统资源允许。在多核处理器上,多个任务可以真正地并行执行,从而提高程序的执行效率。

在Objective-C中,我们可以通过dispatch_get_global_queue函数来获取系统提供的全局并发队列。该函数接受两个参数:第一个参数是队列的优先级,第二个参数目前被忽略,应设置为0。

以下是获取全局并发队列的代码示例:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

这里DISPATCH_QUEUE_PRIORITY_DEFAULT表示默认优先级,除此之外,还有DISPATCH_QUEUE_PRIORITY_HIGH(高优先级)、DISPATCH_QUEUE_PRIORITY_LOW(低优先级)和DISPATCH_QUEUE_PRIORITY_BACKGROUND(后台优先级)可供选择。

任务调度

任务调度是指将任务添加到队列中,由队列按照其规则来执行这些任务。在GCD中,我们可以使用dispatch_async函数将任务异步添加到队列中,任务会在后台线程中执行,不会阻塞当前线程。

以下是将任务添加到全局并发队列的代码示例:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 这里是需要执行的任务代码
    NSLog(@"This is a task in the global concurrent queue.");
});

在上述代码中,dispatch_async的第一个参数是目标队列,第二个参数是一个块(block),块中的代码就是需要执行的任务。

自定义并发队列

除了使用系统提供的全局并发队列,我们还可以通过dispatch_queue_create函数创建自定义的并发队列。该函数接受两个参数:第一个参数是队列的名称,第二个参数用于指定队列的类型,如果为DISPATCH_QUEUE_CONCURRENT则表示创建并发队列。

以下是创建自定义并发队列并添加任务的代码示例:

dispatch_queue_t customConcurrentQueue = dispatch_queue_create("com.example.customConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(customConcurrentQueue, ^{
    // 任务1
    NSLog(@"Task 1 in custom concurrent queue");
});
dispatch_async(customConcurrentQueue, ^{
    // 任务2
    NSLog(@"Task 2 in custom concurrent queue");
});

在上述代码中,我们创建了一个名为com.example.customConcurrentQueue的自定义并发队列,并向其中添加了两个任务。这两个任务会并发执行(前提是系统资源允许)。

任务依赖

在实际开发中,有时我们需要确保某些任务在其他任务完成后再执行,这就涉及到任务依赖。在GCD中,我们可以使用dispatch_group来实现任务依赖。

以下是一个使用dispatch_group实现任务依赖的示例:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, globalQueue, ^{
    // 任务1
    NSLog(@"Task 1");
});

dispatch_group_async(group, globalQueue, ^{
    // 任务2
    NSLog(@"Task 2");
});

dispatch_group_notify(group, globalQueue, ^{
    // 任务1和任务2都完成后执行此任务
    NSLog(@"Both Task 1 and Task 2 are finished.");
});

dispatch_release(group);

在上述代码中,我们创建了一个dispatch_group,并通过dispatch_group_async将两个任务添加到全局并发队列中。然后通过dispatch_group_notify设置了一个通知,当group中的所有任务都完成后,会执行通知中的任务。

并发队列中的数据同步

当多个任务并发访问共享数据时,可能会导致数据竞争和不一致的问题。为了解决这个问题,我们需要进行数据同步。在GCD中,可以使用dispatch_queue的同步机制来确保数据的一致性。

一种常见的方法是使用串行队列来保护共享数据。例如,我们创建一个串行队列,当需要访问或修改共享数据时,将任务添加到这个串行队列中执行,这样就可以避免多个任务同时访问共享数据。

以下是一个简单的示例,展示如何使用串行队列来保护共享数据:

// 共享数据
int sharedData = 0;
// 创建串行队列用于保护共享数据
dispatch_queue_t dataProtectionQueue = dispatch_queue_create("com.example.dataProtectionQueue", DISPATCH_QUEUE_SERIAL);

// 在并发队列中执行任务,这些任务可能会访问共享数据
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    dispatch_sync(dataProtectionQueue, ^{
        // 访问和修改共享数据
        sharedData++;
        NSLog(@"Shared data incremented to %d", sharedData);
    });
});

dispatch_async(globalQueue, ^{
    dispatch_sync(dataProtectionQueue, ^{
        // 访问共享数据
        NSLog(@"Current value of shared data is %d", sharedData);
    });
});

在上述代码中,我们创建了一个串行队列dataProtectionQueue来保护共享数据sharedData。当并发队列中的任务需要访问或修改sharedData时,通过dispatch_sync将任务添加到dataProtectionQueue中执行,这样就保证了同一时间只有一个任务可以访问或修改sharedData,从而避免了数据竞争问题。

并发队列与性能优化

合理使用并发队列可以显著提升应用程序的性能。例如,在处理大量数据的计算任务时,如果将这些任务分配到并发队列中执行,多核处理器可以同时处理多个任务,从而加快计算速度。

然而,并发编程也并非没有代价。过多的并发任务可能会导致系统资源竞争,如CPU、内存等,反而降低性能。此外,数据同步的开销也需要考虑。因此,在使用并发队列进行性能优化时,需要根据具体的应用场景进行权衡和调优。

比如,在一个图像编辑应用中,我们可能有多个任务,如图片缩放、色彩调整、添加滤镜等。如果这些任务可以并行处理,我们可以将它们分配到并发队列中,这样可以大大缩短整个图像处理的时间。以下是一个简化的示例:

// 模拟图像处理任务
void processImage(void) {
    // 图片缩放任务
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        NSLog(@"Starting image scaling...");
        // 模拟缩放操作
        sleep(2);
        NSLog(@"Image scaling finished.");
    });

    // 色彩调整任务
    dispatch_async(globalQueue, ^{
        NSLog(@"Starting color adjustment...");
        // 模拟色彩调整操作
        sleep(2);
        NSLog(@"Color adjustment finished.");
    });

    // 添加滤镜任务
    dispatch_async(globalQueue, ^{
        NSLog(@"Starting adding filter...");
        // 模拟添加滤镜操作
        sleep(2);
        NSLog(@"Adding filter finished.");
    });
}

在上述代码中,我们将图像缩放、色彩调整和添加滤镜这三个任务添加到全局并发队列中,它们会并发执行,从而提高图像处理的效率。

并发队列中的异常处理

在并发任务执行过程中,可能会出现异常情况。在Objective-C中,我们可以使用@try@catch@finally块来处理异常。

以下是一个在并发任务中处理异常的示例:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    @try {
        // 可能会抛出异常的任务代码
        NSArray *array = @[@1, @2, @3];
        NSInteger value = [array objectAtIndex:5]; // 这里会抛出异常,因为索引越界
        NSLog(@"Value: %ld", (long)value);
    } @catch (NSException *exception) {
        NSLog(@"Caught exception: %@", exception);
    } @finally {
        NSLog(@"This will always be printed.");
    }
});

在上述代码中,我们在并发任务的块中使用了@try块来包含可能会抛出异常的代码。如果异常发生,@catch块会捕获异常并进行相应处理,而@finally块中的代码无论是否发生异常都会执行。

并发队列与线程安全

并发队列本身并不能保证线程安全,线程安全需要开发者通过合理的设计和同步机制来实现。如前文所述,使用串行队列来保护共享数据是一种常见的实现线程安全的方法。

此外,还可以使用其他同步工具,如信号量(dispatch_semaphore)、互斥锁(dispatch_mutex)等。例如,使用信号量来控制对共享资源的访问次数:

// 创建信号量,初始值为1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// 共享资源
int sharedResource = 0;

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 访问和修改共享资源
    sharedResource++;
    NSLog(@"Shared resource incremented to %d", sharedResource);
    dispatch_semaphore_signal(semaphore);
});

dispatch_async(globalQueue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 访问共享资源
    NSLog(@"Current value of shared resource is %d", sharedResource);
    dispatch_semaphore_signal(semaphore);
});

在上述代码中,我们创建了一个信号量,初始值为1。当任务需要访问共享资源时,先通过dispatch_semaphore_wait获取信号量,如果信号量的值大于0,则获取成功并将信号量的值减1,否则任务会等待直到信号量的值大于0。任务完成对共享资源的访问后,通过dispatch_semaphore_signal将信号量的值加1,以便其他任务可以获取信号量访问共享资源。

并发队列在多场景下的应用

  1. 网络请求:在进行多个网络请求时,可以将每个请求任务添加到并发队列中,这样可以同时发起多个请求,提高数据获取的效率。例如,一个新闻应用可能需要同时获取不同板块的新闻数据,就可以使用并发队列来并行发起这些网络请求。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 新闻板块1的网络请求任务
dispatch_async(globalQueue, ^{
    NSURL *url = [NSURL URLWithString:@"http://example.com/news/section1"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 处理新闻数据
});

// 新闻板块2的网络请求任务
dispatch_async(globalQueue, ^{
    NSURL *url = [NSURL URLWithString:@"http://example.com/news/section2"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 处理新闻数据
});
  1. 文件处理:在处理多个文件的读写操作时,并发队列可以提高处理速度。比如,一个文件管理应用可能需要同时对多个文件进行压缩或解压缩操作,就可以将这些文件处理任务添加到并发队列中。
dispatch_queue_t customConcurrentQueue = dispatch_queue_create("com.example.fileProcessingQueue", DISPATCH_QUEUE_CONCURRENT);
// 文件1的压缩任务
dispatch_async(customConcurrentQueue, ^{
    NSString *filePath1 = @"/path/to/file1";
    // 执行文件1的压缩操作
});

// 文件2的压缩任务
dispatch_async(customConcurrentQueue, ^{
    NSString *filePath2 = @"/path/to/file2";
    // 执行文件2的压缩操作
});
  1. 复杂计算:对于一些复杂的数学计算或数据处理任务,并发队列可以充分利用多核处理器的性能。例如,在一个科学计算应用中,可能需要同时计算多个数据集合的统计信息,就可以将这些计算任务分配到并发队列中。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 数据集合1的统计计算任务
dispatch_async(globalQueue, ^{
    NSArray *dataSet1 = @[@1, @2, @3, @4, @5];
    // 执行数据集合1的统计计算
});

// 数据集合2的统计计算任务
dispatch_async(globalQueue, ^{
    NSArray *dataSet2 = @[@6, @7, @8, @9, @10];
    // 执行数据集合2的统计计算
});

通过合理应用并发队列,我们可以在不同场景下优化应用程序的性能,提升用户体验。同时,在使用过程中要注意数据同步和线程安全问题,确保程序的正确性和稳定性。在实际开发中,需要根据具体的业务需求和系统资源情况,灵活选择和配置并发队列,以达到最佳的性能效果。

以上就是关于Objective - C中GCD并发队列与任务调度的详细介绍,希望能帮助开发者更好地理解和应用这一强大的并发编程工具。在实际项目中,不断实践和优化,将有助于编写出高效、稳定的应用程序。无论是小型应用还是大型项目,合理运用并发队列都可能成为提升应用性能的关键因素。通过精心设计任务调度和数据同步机制,我们可以充分发挥多核处理器的潜力,为用户带来更流畅、更快速的应用体验。同时,随着硬件技术的不断发展,多核处理器的性能将进一步提升,对并发编程的需求也会更加迫切,掌握GCD并发队列与任务调度的相关知识将显得尤为重要。在未来的开发工作中,开发者们需要不断探索和尝试,结合新的技术和需求,进一步优化并发编程的实现,为应用程序的发展注入新的活力。

在处理并发任务时,还需要关注一些潜在的问题。例如,死锁的发生。死锁是指两个或多个任务相互等待对方释放资源,导致所有任务都无法继续执行的情况。虽然GCD通过其设计在很大程度上减少了死锁的可能性,但如果使用不当,仍然可能出现死锁。比如,在使用同步函数(如dispatch_sync)时,如果调用顺序不当,就可能导致死锁。以下是一个可能导致死锁的示例:

dispatch_queue_t queue1 = dispatch_queue_create("com.example.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.queue2", DISPATCH_QUEUE_SERIAL);

dispatch_sync(queue1, ^{
    NSLog(@"In queue1");
    dispatch_sync(queue2, ^{
        NSLog(@"In queue2 from queue1");
    });
});

dispatch_sync(queue2, ^{
    NSLog(@"In queue2");
    dispatch_sync(queue1, ^{
        NSLog(@"In queue1 from queue2");
    });
});

在上述代码中,queue1中的任务等待queue2中的任务完成,而queue2中的任务又等待queue1中的任务完成,这就形成了死锁。为了避免死锁,在使用同步函数时,要仔细规划任务的调用顺序,确保不会出现相互等待的情况。

另外,资源竞争也是一个需要关注的问题。即使使用了同步机制来保护共享数据,但如果同步机制设计不合理,仍然可能出现资源竞争。例如,在一个多线程的数据库操作场景中,如果多个任务同时对数据库进行写入操作,即使使用了锁来同步,也可能因为锁的粒度不合适而导致性能下降或数据不一致。假设我们有一个简单的数据库操作类DatabaseManager,其中有一个方法insertData用于向数据库插入数据:

@interface DatabaseManager : NSObject
- (void)insertData:(NSString *)data;
@end

@implementation DatabaseManager
{
    dispatch_queue_t databaseQueue;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        databaseQueue = dispatch_queue_create("com.example.databaseQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)insertData:(NSString *)data {
    dispatch_sync(databaseQueue, ^{
        // 执行数据库插入操作
        NSLog(@"Inserting data: %@", data);
    });
}
@end

在上述代码中,我们使用串行队列databaseQueue来同步数据库插入操作,这在一定程度上保证了数据的一致性。但是,如果有大量的插入操作,串行执行可能会导致性能问题。此时,可以考虑使用更细粒度的锁或者优化同步机制,以提高性能。

在实际开发中,还需要考虑不同设备的性能差异。不同型号的iOS设备或Mac电脑,其处理器性能、内存大小等都有所不同。在设计并发任务时,要根据目标设备的性能来合理分配任务数量和资源。对于性能较低的设备,过多的并发任务可能会导致系统资源耗尽,应用程序出现卡顿甚至崩溃。例如,在一个需要处理大量图片的应用中,如果在性能较低的设备上同时启动过多的图片处理任务,可能会导致内存不足。因此,在应用启动时,可以通过检测设备的性能参数(如处理器核心数、内存大小等)来动态调整并发任务的数量。以下是一个简单的检测设备内存大小并根据结果调整任务数量的示例:

#import <sys/sysctl.h>

// 获取设备内存大小
unsigned long long getDeviceMemory() {
    int mib[2];
    mib[0] = CTL_HW;
    mib[1] = HW_MEMSIZE;
    int64_t size;
    size_t length = sizeof(size);
    sysctl(mib, 2, &size, &length, NULL, 0);
    return (unsigned long long)size;
}

// 根据内存大小调整任务数量
NSInteger adjustTaskCount() {
    unsigned long long memorySize = getDeviceMemory();
    if (memorySize < 1024 * 1024 * 1024) { // 小于1GB
        return 2;
    } else if (memorySize < 2 * 1024 * 1024 * 1024) { // 小于2GB
        return 4;
    } else {
        return 8;
    }
}

在上述代码中,我们通过sysctl函数获取设备的内存大小,然后根据内存大小返回一个合适的任务数量。在实际应用中,可以根据这个任务数量来动态调整并发队列中任务的添加数量。

同时,并发队列的优先级设置也会影响应用程序的性能和用户体验。对于一些对用户交互有重要影响的任务,如界面刷新、触摸响应等,应该设置较高的优先级,确保这些任务能够及时执行。而对于一些后台任务,如下载、数据处理等,可以设置较低的优先级,避免影响前台任务的执行。例如,在一个视频播放应用中,视频解码任务应该设置较高的优先级,以保证视频的流畅播放,而视频缓存任务可以设置较低的优先级,在后台进行。

dispatch_queue_t decodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t cacheQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

// 视频解码任务
dispatch_async(decodeQueue, ^{
    // 执行视频解码操作
});

// 视频缓存任务
dispatch_async(cacheQueue, ^{
    // 执行视频缓存操作
});

在上述代码中,我们将视频解码任务添加到高优先级的全局并发队列中,将视频缓存任务添加到低优先级的全局并发队列中,这样可以在保证视频流畅播放的同时,在后台进行视频缓存。

此外,随着应用程序功能的不断增加和复杂度的提高,并发编程的调试也变得更加困难。当出现问题时,很难确定是哪个任务出现了错误,或者是同步机制出现了问题。为了便于调试,可以在任务中添加详细的日志输出,记录任务的执行状态和关键数据。例如,在每个任务的开始和结束处记录任务的名称和执行时间,在访问共享数据时记录数据的状态等。以下是一个在任务中添加日志输出的示例:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    NSDate *startDate = [NSDate date];
    NSLog(@"Task started at %@", startDate);
    // 任务代码
    sleep(2);
    NSDate *endDate = [NSDate date];
    NSLog(@"Task finished at %@, duration: %f seconds", endDate, [endDate timeIntervalSinceDate:startDate]);
});

在上述代码中,我们在任务开始和结束时记录了时间,并计算了任务的执行时长,这样可以帮助我们分析任务的执行效率和性能瓶颈。

总之,在Objective - C中使用GCD并发队列与任务调度时,需要综合考虑各种因素,包括任务的依赖关系、数据同步、线程安全、设备性能、优先级设置以及调试等。只有全面、深入地理解这些知识,并在实际开发中灵活运用,才能编写出高效、稳定、用户体验良好的应用程序。在面对不断变化的技术和用户需求时,持续学习和探索并发编程的新方法和技巧,将有助于开发者保持竞争力,为用户带来更好的产品。