effective-Objective-C第六章阅读笔记

块与大中枢派发

文章目录

理解块这一概念

块这个概念和函数类似,只不过是直接定义在另一个函数内的,和定义他的那个函数共享同一个范围内的东西

objc 复制代码
^{
  //Block implementation
}

块的语法和函数指针类似:

objc 复制代码
void (^someBlock)() = ^{};
returnType (^bolckName)(parameters);//语法格式

块的一个强大之处在与在他的范围内,所有变量都可以为其所捕获。这也就是说,范围内部的全部变量,在块内仍然可以使用:

objc 复制代码
int add = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){
  return a + b + additional;
}
int add = addBlock (2, 5); //< add = 12

在默认情况下,为块所捕获的变量是不可以在块中被修改。不过如果在声明变量的时候加上一个__block这个修饰符,这样就可以在块里被修改了。

内联块的用法:

objc 复制代码
NSArray *array = @[@0, @1, @2, @3, @4, @5];
block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
^ (NSNumber *number, NSUInteger idx, BOOL *stop) ( 
  if ([number compare: @2] == NSOrderedAscending) {
			count++;
	}
};
// count = 2

如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题。块本身可视为对象。实际上,在其他 Objective-C 对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。

如果将块定义在 Objective-C类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把 self变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。

objc 复制代码
@interface EOCClass
-(void) anInstanceMethod {
	void (someBlock) () = ^(
		_anInstanceVariable = @"Something";
			NSLog (@ "anInstanceVariable = 80", _anInstanceVariable);
    };
// ...
}
@end

如果保留self所指代的那个对象的同时也保留了块,那么这种情况通常会导致保留环。

内部结构

先认识一下我们的块的一个结构

invoke这个变量是指向块的实现代码,是一个函数指针,函数原型表示我们表示至少接受一个void类型的参数。这个参数代表块。

descriptor这个变量是指向结构体的一个指针,每个块内都包含此结构体,这个声明了块的一个整体大小,还声明了copy和dispose这两个辅助函数所对应的函数指针,

块会把他的所捕获的变量全部都拷贝一份,这些拷贝放在descriptor后面,捕获了多少个变量,就要占据多撒后的空间。执行块时,要从内存中 把这些变量读出来。

全局块,堆块,栈块

定义了块的时候,其所占的内存区域时分配在栈中的,下面这段代码其实就有风险:

objc 复制代码
void (^block)();
            if (a == 1) {
                block = ^{
                NSLog (@"Block A") ;
                };
            } else {
                block = ^{
                    NSLog (@"Block B") ;
                };
          }
        block () ;

定义在if及 else 语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else 语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

为了解决这个问题,可以给块对象发生copy消息以拷贝。这样的话,就可以把块从栈复制到堆上,拷贝之后的块,可以在定义他的范围之外使用,一但复制到堆上,就成为带有引用计数的对象了,后续的复制操作都不会进行复制,而是递增引用计数。在ARC环境下会自动释放,在MRC环境下需要自己手动release。

objc 复制代码
void (^block)();
            if (a == 1) {
                block = [^{
                NSLog (@"Block A") ;
                } copy];
            } else {
                block = [^{
                    NSLog (@"Block B") ;
                } copy];
          }
        block () ;

除了这两个块,我们还有一类块叫做全句块。另外,全局块的拷贝操作是一个空操作,因为全局块不可能被系统回收。这种块实际上相当于单例。

要点

  • 快是c,c++,OC的语法闭包
  • 块可以接受参数,也可以返回值
  • 块可以分配在栈或堆上,也可以是全局的,分配在栈上的块可以拷贝到堆上。

为常用的块创建typedef

objc 复制代码
returnType (^bolckName)(parameters);//语法格式

这里我们定义块的名字不想别的类型变量,是放在类型之中。

所以我们会采用typedef

objc 复制代码
typedef int (EOCSomeBlock) (BOOL flag, int value) ;

这样创建就更加方便了,也更加符合我门的一个创建的习惯

objc 复制代码
EOCSomeBlock block = ^ (BOOL flag, int value) {
// Implementation
};

这样就和定义其他变量一样变量类型在左边,变量名在右边。

使用类型定义语句还有一个好处就是我们,重构类型的名字的时候会方便狠多:

objc 复制代码
typedef void (^EOCCompletionHandler)
(NSData *data, NSTimeInterval duration, NSError*error) ;

要点

  • 以 typedef 重新定义块类型,可令块变量用起来更加简单。

  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。

  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应 typedef 中的块签名即可,无须改动其他 typedef。

用handler来降低代码分散度

假如把执行异步任务的方法做成同步的,那么在执行任务的时候,用户界面就会变得无法响应用户输入,所以我们一般采用block处理。

objc 复制代码
typedef void (^EOCNetworkFetcherCompletionHandler) (NSData *data) ;
@interface EOCNetworkFetcher : NSObject
- (id) initwithURL: (NSURL*) url;
- (void) startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler) handler;
@end

- (void) fetchFooData {
    NSURL *url = [[NSURL alloc] initWithString:
                  @ "http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher =
    [ [EOCNetworkFetcher alloc] initWithURL:url];
    [fetcher startWithCompletionHandler:^ (NSData *data) {
        _fetchedFooData = data;
    };
}

这种API的设计很不错,他可以根据不同的情况获取不同的handle块来获取不同的内容,比方说我们有成功的返回和失败的返回,那么会进入不同的一个handler块。

要点

  • 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。

  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。

  • 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

用块引用所属对象时不要出现保留环

这里直接看这段代码:

objc 复制代码
@implementation EOCClass {
EOCNetworkFetcher * _networkFetcher;
NSData * fetchedData;
}
- (void) downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
    @ "http://www.example.com/something.dat"];
    _networkFetcher =
    [ [EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^ (NSData *data) {
        NSLog (@"Request URL 8@ finished",         _networkFetcher.url) ;
    		_fetchedData = data;
    }];
}

@end

这段代码看上去似乎没有什么问题,但是这段代码他捕获了实例变量,所以他必须捕获self变量。所以就会出现下面这种情况:

这里我们其实只用添加一条,也就是使用完EOCClass之后将NetworkFetcher直接释放掉就好了。也就是把他置为nil就可以了。

我们设置API的时候才用了这种handle的方式就会很经常性的出现保留环的一个问题。

如果 completionhandler 块所引用的对象最终又引用了这个块本身,那么就会出现保留环。比方说,我们修改一下前面那个例子,使调用API 的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以在启动任务时把自己加到全局的 collection 中(比如用 set 来实现这个 collection),待任务完成后,再移除。

这样的话我们就不需要找之前的方式样子自己手动释放newworkFetcher了。因为这里他会自己吧获取器对象给移除掉。

要点

  • 如果块所捕获的对象直接或间接的保留了块本身,那么就要当心保留环问题
  • 一定要找一个合适的机会解除保留环,

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

同步锁就是:

objc 复制代码
@synchronized(self) {
  
}

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。但是这样子频繁枷锁会降低很多效率。

大部分情况我们都是采用GCD来代替🔒来进行操作。

有一种简单而且高效的方法可以代替同步块或锁对象,那就是串行同步队列。:

objc 复制代码
- (NSString*) someString {
	__block NSString *localSomeString;
	dispatch_sync (_syncQueue, ^{//这里使用的是一个串行对列
		localSomeString =_someString;
	});
	return localSomeString;
}

这里我们看一下两段代码的一个差异:

objc 复制代码
- (void) setSomeString: (NSString*) someString {
  dispatch_sync (_syncQueue, ^{
		_someString = someString;
	});
}

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

这次只是把同步派发改成了异步派发,从调用者的角度来看,这个小改动可以提升设置方法的执行速度,而读取操作与写入操作依然会按顺序执行。但这么改有个坏处:如果你测一下程序性能,那么可能会发现这种写法比原来慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间,则这种做法将比原来更慢。由于本书所举的这个例子很简单,所以改完之后很可能会变慢。然而,若是派发给队列的块要执行更为繁重的任务,那么仍然可以考虑这种备选方案。

如果采用的是并发队列的话,我们就需要采用栅栏函数来处理这部分代码:

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块(barrier block),那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。

这里我们看修改后的代码:

objc 复制代码
- (NSString*) someString {
	__block NSString *localSomeString;
	dispatch_sync (_syncQueue, ^{//这里使用的是一个串行对列
		localSomeString =_someString;
	});
	return localSomeString;
}
- (void) setSomeString: (NSString*) someString {
  dispatch_barrier_async (_syncQueue, ^{
		_someString = someString;
	});
}

上面这张图就展示出了一个我们这里并发队列的样式,可以保证读写安全。

要点

  • 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized 块或 NSLock对象更简单。

  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。

  • 使用同步队列及栅栏块,可以令同步行为更加高效。

多使用GCD,少用performSelector方法

这里我们看一下这段代码:

objc 复制代码
SEL selector;
if (/* some condition */ ) {
	selector = @selector (foo) ;
} else if (/* some other condition */ ) {
	selector = @selector (bar) ;
} else {
	selector = @selector (baz) ;
}
[object performSelector:selector];

你可能没料到会出现这种警告。要是早就料到了,那么你也许已经知道使用这些方法时为何要小心了。这条消息看上去可能比较奇怪,而且令人纳闷:为什么其中会提到内存泄漏问题呢?只不过是用 "performSelector:"调用了一个方法。原因在于,编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用 ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

这里我们看另一段代码:

objc 复制代码
SEL selector;
if (/* some condition */ ) {
  selector = (selector (newObject) ;
} else if (/* some other condition */ ) {
	selector = @selector (copy) ;
} else {
	selector = @selector (someProperty) ;
}
id ret = [object performSelector: selector];

这里如果调用的是前两个选择子中的任意一个,我们都会发现这里的ret会由这单代码来释放,但是如果是第三个选择子则意味着我们不用释放,这样子就很容易出现一个内存泄漏的问题。这里还有不好的地方在于,返回值只能是void和对象类型,不能返回整形和浮点型的内容。

尽管有其他方法来处理这部分内容,但是还是局限很多:

objc 复制代码
- (id) performSelector: (SEL) selector
withObject: (id) object
- (id) performSelector: (SEL) selector
withObject: (id) objectA withObject: (id) objectB

但是还是只能传输我们的参数类型还都是id类型,依旧不可以传输我们的一个整数类型。

要点

  • performSelector 系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而 ARC编译器也就无法插人适当的内存管理方法。
  • performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
  • 如果想把任务放在另一个线程上执行,那么最好不要用 performSelector 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。

掌握GCD及操作队列的使用时机

我们得先认识一下两者的一个区别:

在两者的诸多差别中,首先要注意:GCD是纯C的API,而操作队列则是 Objective-C的对象。在GCD 中,任务用块来表示,而块是个轻量级数据结构(参见第37条)。与之相反,"操作"(operation)则是个更为重量级的Objective-C对象。虽说如此,但GCD并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

接下来简单介绍一下这个操作队列的内容,他有一些优点:

  • 取消某一个操作。如果使用操作队列,那么想取消操作是很容易的。在运行任务之前,可以在NSOperation上面调用cancel方法,这个方法会设置对象内的标志位,用来表明该任务不用执行。但是如果安排到GCD中,就无法取消任务了。
  • 指定操作之间的依赖关系,开发者能够制定操作之间的一个依赖体系,使特定的操作必须在另外一个操作顺利完成之后才可以执行
  • 通过简直观测机制来监控NSOperation对象的属性。NSOperation中很多属性哦度可以KVO来进行监听
  • 指定操作之间的一个优先级。操作之间的优先级表示这个操作和其他操作之间的优先级关系
  • 可以冲用NSoperation对象。系统内置了一些NSOperation的子类共开发者来调用,也可以自己创建一些子类。

要点

  • 解决多线程与任务管理问题是,派发队列并非唯一方案
  • 操作队列提供了一套高层的OCAPI,能实现GCD所具备的绝大部分功能而且还能完成一些更加复杂的操作,哪些操作如果该用GCD来实现,则需要另外编写代码。

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

Dispatch Group这个是GCD的一个特性,能够将任务分组。调用者可以等待这组任务执行完毕之后,也可以提供回调函数之后继续往下执行,这组任务完成之后,调用者会得到通知。

objc 复制代码
dispatch_group_t dispatch_group_create();

我们有下面这两个函数来指定任务所属的一个group

objc 复制代码
dispatch_group_enter(<#dispatch_group_t  _Nonnull group#>)
dispatch_group_leave(<#dispatch_group_t  _Nonnull group#>)

前者能够给任务添加到对应的组中。后者是移除任务,每一个enter都要有对应的一个leave操作。

下面这个函数用于等待group执行结束:

objc 复制代码
dispatch_group_wait(<#dispatch_group_t  _Nonnull group#>, <#dispatch_time_t timeout#>)

一个表示要等待的group,另一个则是代表等待时间的timeout值,后面表示如果执行完毕之后应该阻塞多久,这里也可以采用DISPATCH_TIME_FOREVER这个表示知道dispatch_group执行完,不会超时。

除了上面那个函数之外我们还可以看一下下面这个函数:

objc 复制代码
dispatch_group_notify(<#dispatch_group_t  _Nonnull group#>, <#dispatch_queue_t  _Nonnull queue#>, <#^(void)block#>)

这里是开发者想这个函数传入块,块就会在group执行完之后在特定一个线程上执行。假如当前线程不应阻塞,而开发者有希望在哪些任务完成之后得到通知。就可以采用这个函数来处理

如果想让数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用GCD这个特性来实现。

objc 复制代码
dispatch_queue_t queue =
dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ;
dispatch_group_t dispatchGroup = dispatch_group_create () ;
for (id object in collection) {
	dispatch_group_async (dispatchGroup, queue,^{
    [object performTask]; });
	dispatch_group_wait (dispatchGroup, DISPATCH_TIME_FOREVER);
}
// Continue processing after completing tasks

这个例子是所有任务都派发到同一队列中。但十几岁未必一定要这么做,也可以把任务放在优先级高的线程上去执行,同时仍然吧所有任务都归入同一个group中:

除了像这样把任务给并发队列外,也可以提交到各个穿行队列中,并且用group来跟踪其执行情况。但是这样的话group存在的意义就不打了,这样和提交完所有任务后一个块就可以了。

所以有些时候我们没必要使用dispatch group。有些时候采用单个队列搭配标准的异步并发,也可以实现同样的效果。

通过 dispatch group 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。由于GCD有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务。而开发者则可以专注干业务罗辑代码,无须再为了外理并发任务而编写复杂的调度器。

在前面的范例中完,我们遍历collection的时候我们也可以采用另外一个gcd来实现。

objc 复制代码
dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t  _Nullable queue#>, <#^(size_t iteration)block#>)

这个函数会将块反复执行一定的次数,每次穿给块的参数值会递增,从0开始一直到iterations - 1。

objc 复制代码
dispatch_queue_t queue =
dispatch_queue_create ("com. effectiveobjectivec. queue", NULL) ;
dispatch_apply(10, queue, ^ (size_t i) {
// Perform task
}) ;

apply所用的队列可以是并发队列。如果是并发队列的话,那么系统就要根据资源状况来处理这些块了,这与group那段代码就一样了。

这里要注意apply会持续堵塞,知道所有任务都执行完毕位置,由此可见,假如把块派给了当前队列,那么就讲导致思所。如果想在后台执行任务就应该使用dispatch group。

dispatch_apply

  • dispatch_apply 是一个同步的函数,用于并行执行多个任务。它会等待所有的任务都执行完毕才会继续往下执行代码。
  • 它会阻塞当前线程,直到所有的任务执行完毕。如果你将任务分配到当前线程(或者是主线程)上,就会导致阻塞,使得程序无法继续执行后续的代码,进而影响性能或造成死锁。

dispatch_group

  • dispatch_group 则是一种异步方式,适合用于并行执行多个任务并在所有任务完成后统一处理(比如通知或回调)。它不会像 dispatch_apply 那样阻塞当前线程,可以避免卡住线程的问题。
  • 使用 dispatch_group,你可以将多个任务添加到组中,等所有任务完成后再执行后续操作,而这个过程是异步的,不会卡住线程。(引自chatgpt)

总结:

  • 如果需要并行执行任务并等待结果,但不希望阻塞当前线程(比如主线程),使用 dispatch_groupdispatch_apply 更加合适。
  • dispatch_apply 阻塞当前线程,可能在当前队列上执行时造成死锁,因此应避免在主队列上调用它。

要点

  • 一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
  • 通过 dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。

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

单例模式这里就会用到一个全类共用的一个单例实例:

objc 复制代码
+ (id) sharedInstance i
	static EOCClass *sharedInstance = nil;
	@synchronized (self) {
		if (!sharedInstance) {
			sharedInstance = [[self alloc] initl;
		}
	}
	return sharedInstance;
}

然后我们可以用GCD来实现我们的一个单例模式:

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

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

此外,dispatch_once 更高效。它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,相反,此函数采用"原子访问"(atomic access)来查询标记,以判断其所对应的代码原来是否已经执行过。笔者在自己装有64位 Mac OS X 10.8.2系统的电脑上简单测试了性能,分别用 @synchronized 方式及 dispatch L_once 万式来买现 sharedInstance方法,结果显示,后者的速度几乎是前者的两倍。

要点

  • 经常需要编写"只需执行一次的线程安全代码"(thread-safe single-code execution)。通过GCD 所提供的 dispatch_once函数,很容易就能实现此功能。
  • 标记应该声明在 static 或global 作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的。

不要使用dispatch_get_current_queue

使用GCD的时候,经常需要判断当前代码在那个队列上执行,向多个队列派发任务的时候,有些时候进场判断代码在那个线程执行。

objc 复制代码
dispatch_get_current_queue()

该函数有种典型的错误用法(antipattern,"反模式"),就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题。

objc 复制代码
- (NSString*) someString {
	__block NSString *localSomeString;
	dispatch_sync (_syncQueue, ^{
  	localSomeString =_someString;
	});
	return localSomeString;
}
- (void) setSomeString: (NSString*) someString {
  dispatch_async (_syncQueue, ^(
		_someString = someString;
   }) ;
}

这里其实存在一个死锁的问题,如果是同步操作所针对的队列就会出现一个死锁的问题(在本例中是_syncQueue)_

_这种写法的问题在于,获取方法(getter)可能会死锁,假如调用获取方法的队列恰好是同步操作所针对的队列(本例中是_syncQueue),那么 dispatch_sync 就一直不会返回,直到块执行完毕为止。可是,应该执行块的那个目标队列却是当前队列,而当前队列的dispatch_sync 又一直阻塞着,它在等待目标队列把这个块执行完,这样一来,块就永远没机会执行了。像someString这种方法,就是"不可重入的"。

知道前面那个方法后,我们可能会出现下面的代码:

objc 复制代码
- (NString*) someString i
	__block NSString *localSomeString;
	dispatch_block_t accessorBlock = ^{
		localSomeString =_someString;
	} ;
	if (dispatch_get_current_queue() == _syncQueue) {
		accessorBlock () ;
	} else {
		dispatch_sync (_syncQueue, accessorBlock);
	}
	return localSomeString;
}

但是仍然会有死锁的情况。

这里假设有两个穿行派发队列:

objc 复制代码
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        dispatch_sync(queueA, ^{
            // Deadlock
        });
    });
});

这段代码执行到最内层的派发操作的时候,总会死锁,因为此操作是针对A队列的,所以要等到最外的dispatch_sync执行完才行,但是这里外部的有需要等待内部的sync执行完之后才能执行,所以就死锁。现在按照之前的做法用这个函数来做检测:

objc 复制代码
dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ /* ... */ };
        if (dispatch_get_current_queue() == queueA) {
            block();
        } else {
            dispatch_sync(queueA, block);
        }
    });
});

但是这样做仍然是死锁,因为他返回的是queueB永远会进入第二个,然后还是死锁。

在这种情况下,正确做法是:不要把存取方法做成可重人的,而是应该确保同步操作所用的队列绝不会访问属性,也就是绝对不会调用 someString 方法。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以,为了确保每项属性都有专用的同步队列,我们不妨创建多个队列。

下面展示一下一个队列之间的一个层级体系,在某一条队列中的块,会在他的上级队列中执行

由于队列间有层级关系,所以"检查当前队列是否为执行同步派发所用的队列"这种办法,并不总是奏效。比方说,排在队列C里的块,会认为当前队列就是队列C,而开发者可能会据此认定:在队列A上能够安全地执行同步派发操作。但实际上,这么做依然会像前面那样导致死锁。

有的API 可令开发者指定运行回调块时所用的队列,但实际上却会把回调块安排在内部的串行同步队列上,而内部队列的目标队列又是开发者所提供的那个队列,在此情况下,也许就要出现刚才说的那种问题了。使用这种 API 的开发者可能误以为:在回调块里调用dispatch_get_current_queue所返回的"当前队列",总是其调用API 时指定的那个。但实际上dispatch_get_current返回的却是API 内部的那个同步队列。

要解决这个问题,最好的办法就是通过GCD 所提供的功能来设定"队列特有数据"(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列止。笔者这么说,大家也许还不太明白其用法,所以来看下面这个例子:

objc 复制代码
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,
                            &kQueueSpecific,
                            (void*)queueSpecificValue,
                            (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
    dispatch_block_t block = ^{
        NSLog(@"No deadlock!");
    };

    CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
    if (retrievedValue) {
        block();
    } else {
        dispatch_sync(queueA, block);
    }
});

队列A的目标队列是默认优先级的全局并发队列,队列B德目标队列是队列A,而队列A的目标队列仍然是默认优先级的全局并发队列,然后给A队列设置队列特定值:

objc 复制代码
void dispatch_queue_set_specific(dispatch_queue_t queue,
                                  const void *key,
                                  void *context,
                                  dispatch_function_t destructor);

参数说明:

  1. queue :
    要为其设置特定值的队列。可以是任何 dispatch_queue_t 类型的队列。
  2. key :
    一个指向常量 void* 的指针,它是用于标识队列中特定值的键。通常是一个唯一的标识符,用来关联队列和它所包含的特定值。多个值可以使用不同的键来存储。
  3. context :
    一个指向 void* 的指针,表示你想要存储在队列中的特定值。可以是任何类型的数据,它将在指定的队列中与 key 相关联。
  4. destructor :
    这是一个函数指针,当队列被销毁时,会调用这个函数来清理 context(特定值)。该函数的签名是 dispatch_function_t 类型,通常是用于释放 context 或做其他清理工作。

key和context是键与值。这两个都是不透明的void指针,这里注意函数是通过函数的指针值来比对建的,而不是按照他的内容。所以,队列特定数据和NSDictonary对象不同,后者是比较对象等同性,这里的值更类似于关联引用。值也是不透明的void指针,于是可以在其中存放任意数据。然而必须管理这个对象的内存,这使得在ARC环境xAI很难用OC对象作为值,范例中使用Core Foundation的值,因为ARC不会处理这个Core Foundation的值,所以它适合充当对应的队列特定数据。

最有一个函数是作为析构函数的,用来回收context的内容,有新的值和建关联的时候,原来的值就会被移除,这时候就会调用这个析构函数,所以这里采用CFRelease。

于是,"队列特定数据"所提供的这套简单易用的机制,就避免了使用 dispatch_getcurrent_queue 时经常遭遇的一个陷阱。此外,调试程序时也许会经常用到 dispatch_getcurrent_queue。在此情况下,可以放心使用这个已经废弃的方法,只是别把它编译到发行版的程序里就行。如果对"访问当前队列"这项操作有特需求,而现有函数又无法满足,那么最好还是联系苹果公司,请求其加入此功能。

要点

  • dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
  • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念。
  • dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用"队列特定数据"来解决。
相关推荐
小吴先生66621 分钟前
Groovy 规则执行器,加载到缓存
java·开发语言·缓存·groovy
秋风&萧瑟37 分钟前
【QT】QT的多界面跳转以及界面之间传递参数
开发语言·qt
骑牛小道士40 分钟前
JAVA- 锁机制介绍 进程锁
java·开发语言
郭涤生43 分钟前
Chapter 1: Historical Context_《C++20Get the details》_notes
开发语言·c++20
独好紫罗兰1 小时前
洛谷题单2-P5712 【深基3.例4】Apples-python-流程图重构
开发语言·python·算法
安建资小栗子1 小时前
2025年汽车加气站操作工备考题库
笔记
东方佑1 小时前
深度解析Python-PPTX库:逐层解析PPT内容与实战技巧
开发语言·python·powerpoint
水w1 小时前
【Android Studio】如何卸载干净(详细步骤)
android·开发语言·android studio·activity
weixin_307779131 小时前
判断HiveQL语句为建表语句的识别函数
开发语言·数据仓库·hive·c#
一顿操作猛如虎,啥也不是!1 小时前
JAVA-Spring Boot多线程
开发语言·python