当前位置: 首页 > article >正文

Effective Objective-C 2.0 读书笔记——大中枢派发

Effective Objective-C 2.0 读书笔记——大中枢派发

多用派发队列,少用同步锁

说到同步锁,我们不难想起我们前面在学习线程之中的内容时学习到的关键字@synchronized,使用这个同步块可以让我们这段程序实现加锁的操作,即在不同线程之中这个关键字的内容只能有一条线程运行。自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。以下是例子

 -(void) synchronizedMethod {
 	@synchronized (self) {
 		///Safe
 	}
 
 } 

但是滥用@synchronized (self)会很危险,因为所有同步块都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完 毕才能执行,这也许并不是开发者想要的效果。我们只是想令每个属性各自独立地同步。

替代方案就是使用 GCD,它能以更简单、更高效的形式为代码加锁。比方说,属性就是开发者经常需要同步的地方,这种属性需要做成“原子的”。

初步代码

有种简单而高效的办法可以代替同步块或锁对象,那就是使用“ 串行同步队列” (serial synchronization queue)。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。 其用法如下:

// 创建同步队列(通常在初始化方法中创建)
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

由于写操作(setter)不需要返回值,所以可以考虑使用 异步派发(dispatch_async) 替代同步派发,这样调用者就不必等待写操作完成,从而可能提升设置方法的执行速度。

异步写入

- (void)setSomeString:(NSString *)someString {
    // 异步派发写操作,调用者不会被阻塞
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

优点:

  • 调用者性能提升:因为写操作是异步执行的,调用者立即返回,不需要等待 block 执行完毕。

缺点:

  • Block 拷贝开销:dispatch_async 会拷贝 block。如果拷贝 block 的成本超过 block 实际执行的时间,在简单的例子中可能会导致整体性能反而变慢。
  • 适用场景:对于比较轻量的操作,异步写可能没有明显优势,但如果 block 内执行的任务较重或耗时,那么异步方式能使得调用者更快返回,提升整体效率。

用栅栏函数再优化

我们先前使用的就是串行队列,我们知道如果我们使用并行队列,那么性能将会更好,但是如果使用并行队列似乎就没办法完成安全读写这个点,于是我们就想起我们之前学习过的栅栏函数,将读写两个操作分开

// 创建一个并发队列,使用 barrier 确保写操作的独占性
_syncQueue = dispatch_queue_create("com.example.syncQueue", DISPATCH_QUEUE_CONCURRENT);

- (NSString *)someString {
    __block NSString *localSomeString;
    // 使用同步派发保证读取时数据一致
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    // 使用 barrier 异步派发确保写操作独占执行
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

image-20250214180228383

我们多个读取操作变为异步执行,而写入操作则变成了单独执行,既避免了读取发生错误,又提高了程序的效率。

多用 GCD,少用performSelector系列方法

我们在讲述OC之中的动态性介绍过,performSelector与选择子结合用于动态绑定,可简化复杂的代码

该方法与直接调用选择子等效。所以下面两行代码的执行效果相同:

[object performSelector: @selector (selectorName) ];
[object selectorName] ;

看似多余之举,实则如果选择子在运行期才能决定,就能体现其强大之处了

SEL selector;
if (/* some condition */) {
    selector = @selector(newObject);
} else if (/* some other condition */) {
    selector = @selector(copy);
} else {
    selector = @selector(foo);
}

[object performSelector:selector];

但是这样写的代码也会出现一定的问题,我们前面也知道了在 Objective-C 中,根据方法命名规范(比如以 allocnewcopymutableCopy 开头的方法返回的对象属于调用者,需要手动释放),调用方法时返回的对象的内存管理责任是有约定的。那么不难理解这段代码之中的ret对象,在前两种情况需要被手动释放,最后一种则无需释放。那么在运行期才确定的内容,那么ARC就无法用简单的内存管理规则来管理相应内存,那么ARC在这种情况下会使用比较谨慎的做法,即不添加释放操作,那就会造成内存泄漏。

另外一点,这些方法的返回值只能是void或者是对象类型。如果调用的方法实际上返回了一个基本数据类型(如整数或浮点数),则必须通过一些复杂且容易出错的转换才能正确处理,因为这些数据类型与指针大小可能不一致。

由于 id 只是一个指向 Objective-C 对象的指针:

  • 在 32 位系统上,返回值的大小只能是 32 位以内的数据。
  • 在 64 位系统上,则只能是 64 位以内的数据。
  • 如果返回的是一个 C 语言的结构体,而该结构体的大小超过了指针的大小,就不能使用 performSelector方法来调用,因为它无法正确返回超出指针大小的数据。

由于参数类型是id,所以传入的参数必须是对象才行。如果选择子所接受的参数是整数或浮点数,那就不能采用这些方法了。此外,选择子 最多只能接受两个参数,也就是调用performSelector: withObject: withObject:这个版本。 而在参数不止两个的情况下,则没有对应的 performSelector方法能够执行此种选择子。

书中给了两组示例展示了如何用 GCD 的块来实现与 performSelector 系列方法相同的功能,同时也解决了它们在内存管理和返回值类型上的一些限制。

延后执行任务

方案一:使用 performSelector:withObject:afterDelay:

// 延后 5 秒执行 doSomething 方法
[self performSelector:@selector(doSomething)
           withObject:nil
           afterDelay:5.0];

方案二(推荐):使用 dispatch_after:

// 计算延迟时间(5秒)
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));

// 在主队列中延后 5 秒后执行 doSomething 方法
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
    [self doSomething];
});

在主线程上执行任务

方案一:使用 performSelectorOnMainThread:withObject:waitUntilDone:

// 在主线程上异步执行 doSomething 方法
[self performSelectorOnMainThread:@selector(doSomething)
                       withObject:nil
                    waitUntilDone:NO];

方案二(推荐):使用 dispatch_async:(若需要等待则用 dispatch_sync:)

// 在主队列上异步执行 doSomething 方法
dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

通过DispatchGroup 机制,根据系统资源状况来执行任务

dispatch_group_async

void dispatch_group_async (dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block) ;

下面是一个使用 dispatch_group_async 的例子,展示如何同时执行多个异步任务,并在所有任务完成后进行统一处理:

// 创建一个调度组
dispatch_group_t group = dispatch_group_create();

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

// 异步提交第一个任务到组中
dispatch_group_async(group, queue, ^{
    NSLog(@"任务1开始");
    // 模拟耗时操作
    sleep(2);
    NSLog(@"任务1完成");
});

// 异步提交第二个任务到组中
dispatch_group_async(group, queue, ^{
    NSLog(@"任务2开始");
    sleep(3);
    NSLog(@"任务2完成");
});

// 异步提交第三个任务到组中
dispatch_group_async(group, queue, ^{
    NSLog(@"任务3开始");
    sleep(1);
    NSLog(@"任务3完成");
});

// 当组中所有任务执行完毕后,在主线程上执行回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务完成,更新UI");
});

GCD 动态管理线程:
GCD 会根据当前系统资源状况(例如 CPU 核心数量、当前负载、队列中待执行任务数量等)自动创建新线程或复用旧线程。

  • 如果你使用并发队列,并且有大量任务等待执行,GCD 可能会在多个线程上同时执行这些任务,以充分利用多核处理器的能力。
  • 开发者不需要手动管理线程调度,GCD 会自动调整并发执行的程度,使得任务的并行度适应当前系统的资源状况。

调度组的作用:
通过 dispatch_group,你可以把一组任务“打包”,让它们并发执行,并在所有任务完成后收到通知。这样既利用了并发执行的优势,又能在全部任务结束后统一处理后续操作。

dispatch_apply 的使用

  • 功能简介:
    dispatch_apply 是另一种并发任务调度函数,它会对一个给定次数(iterations)重复执行一个 block,并把每次执行时的索引传给该 block。

    • 例如,如果你希望对数组中的每个元素执行相同的操作,可以用 dispatch_apply 替代常规的 for 循环。
  • 使用示例:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t i) {
        // 这里 i 从 0 到 9,每次执行 block 时 i 的值会递增
        NSLog(@"Iteration %zu", i);
        // 执行一些任务...
    });
    

    或者针对数组:

    NSArray *array = @[@"A", @"B", @"C", @"D"];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(array.count, queue, ^(size_t i) {
        id object = array[i];
        [object performTask];
    });
    
  • 与常规 for 循环的比较:

    • 使用简单的 for 循环也可以达到相同效果,但 dispatch_apply 的优势在于它直接利用 GCD 的调度机制,可以自动将任务分发到多个线程上并发执行(前提是使用并发队列)。
    • 不过需要注意,dispatch_apply 是一个阻塞调用,会等待所有任务执行完成后才返回,因此在性能测试中,若每个 block 的执行时间非常短,block 拷贝的开销反而可能使它比简单 for 循环更慢。

使用dispatch_once 来执行只需运行一次的线程安全代码

GCD引入了一项特性,能使单例实现起来更容易。所用的函数是:

void dispatch_once (dispatch_once_t *token,dispatch_block_t block);

此函数接受类型为dispatch_once_t的特殊参数,书中称之为标记,对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行 一次。首次调 用该函数时,必然会执行块中的代码,最重要的 一点在于,此操作完全是线程安全的。请注 意,对于只需执行一次的块来说,每次调用函数时传人的标记都必须完全相同。

我们的单例模式相关方法可以写成以下形式:


+ (id) sharedInstance (
static EOCClass *sharedInstance = nil;
stat icdispatch_once_t onceToken; 
dispatch_once (&onceToken, ^{
	sharedInstance = [[self alloc] init];
};
return sharedInstance;
}

使用dispatch _once可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁 或同步。所有问题都由GCD 在底层处理。由于每次调用时都必须使用完全相同的标记, 所以标记要声明成static。把该变量定义在static作用域中,可以保证编译器在每次执行 sharedInstance 方法时都会复用这个变量,而不会创建新变量。

不要使用dispatch_get_current_ queue

概念不明确
“当前队列”这个概念并不总是明确的。当你调用 dispatch_get_current_queue() 时,它返回的队列可能是系统内部的私有队列,而不一定是你期望的那个队列。这会导致程序行为难以预测。

容易引发死锁
如果你在当前队列中同步调用某个任务,而这个任务又试图获取当前队列,就可能会导致死锁。由于对当前队列的依赖很容易形成循环依赖,使用 dispatch_get_current_queue() 增加了死锁风险。

dispatch_get_current_queue() 返回当前执行任务的队列。如果你使用它来获取队列,并用来进行同步调用,就有可能无意中对同一个队列调用 dispatch_sync。例如:

dispatch_queue_t currentQueue = dispatch_get_current_queue();
// 当前任务在 currentQueue 上执行
dispatch_sync(currentQueue, ^{
    // 这个 block 也要在 currentQueue 执行
    // 但 currentQueue 正在执行外部的任务,无法中断执行
});

即使我们在编程的过程之中可以通过仔细观察从而避免以上的问题,但是书中提到的另一种情况则不是那么容易查出来的,这是书中提出的场景

image-20250216222932249

排在队列B或队列C中的块,稍后会在队列A里依序执行。于是,排在队列A、B、C中 的块总是要彼此错开执行。然而,安排在队列D中的块,则有可能与队列A里的块(也包括队列B与C里的块)并行,因为A 与D的目标队列是个并发队列。若有必要,并发队列可以用多 个线程并行执行多个块,而是否会这样做,则需根据CPU的核心数量等系统资源状况来定。

排在队列C里的块,会认为当前队列就是队列C,而开发者可 能会据此认定:在队列A上能够安全地执行同步派发操作。但实际上,这么做依然会像前面 那样导致死锁。怎么解决这些问题呢?

GCD之中提供了一个队列特定数据(queue-specific data)的功能:

  • 定义:

    队列特定数据允许你将任意数据(以键值对形式)关联到一个特定的 GCD 队列上。这个数据就像“标签”一样,可以让你在执行任务时知道当前任务属于哪个队列。

  • 查找机制:

    当你调用 dispatch_get_specific(key) 时,系统不仅在当前队列中查找与该键关联的数据,还会沿着队列的目标队列(target queue)链进行查找,直到找到该数据或到达根队列为止。

dispatch_queue_t queueA =
    dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB =
    dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);

// 将 queueB 的目标队列设置为 queueA
dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");

// 在 queueA 上设置队列特定数据:键是 &kQueueSpecific,值是 queueSpecificValue
dispatch_queue_set_specific(queueA,
                            &kQueueSpecific,
                            (void*)queueSpecificValue,
                            (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
    dispatch_block_t block = ^{
        NSLog(@"No deadlock!");
    };
    
    // 从当前队列(或沿着目标队列链)获取与键 &kQueueSpecific 关联的数据
    CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
    
    if (retrievedValue) {
        // 如果取到了数据,说明当前执行环境就是在我们想要的queueA之中
        block();
    } else {
        // 否则,说明当前队列不满足条件,就同步将 block 提交到 queueA 执行
        dispatch_sync(queueA, block);
    }
});

这种方式避免了直接使用 dispatch_get_current_queue() 带来的风险,同时保证了任务在正确的队列中执行,从而防止因线程上下文不对而引发的问题。


http://www.kler.cn/a/551565.html

相关文章:

  • 火锅店点餐系统的设计与实现(ssm论文源码调试讲解)
  • 2.18日学习总结
  • Redis 监视器:深入解析与实战指南
  • resnet与yolo
  • 算法【贪心经典题目专题3】
  • 配置Api自动生成
  • 微信小程序通过http通信控制庐山派
  • 分治-归并排序
  • 鸿蒙开发环境准备DevEco Studio下载安装配置
  • 如何使用动画和日期差值来切换和展示任务-计划时钟(微信小程序)
  • C++ 设计模式-责任链模式
  • 数据结构之BST、AVL、红黑树、哈夫曼树与B族树
  • leetcode:942. 增减字符串匹配(python3解法)
  • 23种设计模式 - 装饰器模式
  • 深度解析——Vue与React的核心差异
  • 解锁观察者模式:Java编程中的高效事件管理之道
  • FBD电插锁硬件,如何通过C++ 控制低电压高低电压实现控制开关锁,通过磁吸检查是否开门操作
  • 单纯禁用Cookie能否保证隐私安全?
  • 探秘 DeepSeek R1 模型:跨越多领域的科技奇迹,引领智能应用新浪潮
  • 视觉相关问题总结