[Effective Objective] 块与大中枢派发

为了解决多线程问题,苹果公司以全新的方式设计了多线程。核心就是“块”(block)与“大中枢派发”(Grand Central Dispatch, GCD)。“块”是一种可在C、C++及Objective-C代码中使用的“词法闭包”,借由此机制,开发者可将代码向对象一样传递,令其在不同环境下运行。GCD是一种与块有关的技术,它提供了对线程的抽象,这种抽象则基于“派发队列”。开发者可将块排入队列中,由GCD负责所有调度事宜。GCD会根据系统资源情况,创建、复用、摧毁后台线程,以便处理每个队列。
摘要由CSDN通过智能技术生成

为了解决多线程问题,苹果公司以全新的方式设计了多线程。核心就是“块”(block)与“大中枢派发”(Grand Central Dispatch, GCD)。

“块”是一种可在C、C++及Objective-C代码中使用的“词法闭包”,借由此机制,开发者可将代码向对象一样传递,令其在不同环境下运行。

GCD是一种与块有关的技术,它提供了对线程的抽象,这种抽象则基于“派发队列”。开发者可将块排入队列中,由GCD负责所有调度事宜。GCD会根据系统资源情况,创建、复用、摧毁后台线程,以便处理每个队列。

理解“块”这一概念

块可以实现闭包。这项语言特性是作为“扩展”而加入GCD编译器中的。

块的基础知识

块类似于直接定义在另一个函数里的函数,和定义它的函数共享一个范围内的东西。

定义简单块的例子:

^{
	//Block implementation here
}

块其实是一个值,有其相关类型。也可以把块赋给变量,然后像使用其他变量那样使用它。

例,没有参数,不返回值的块:

void (^someBlock) () = ^{
	//Block implementation here
};

这段代码定义了一个名为someBlock的变量。块类型的语法结构如下:

return_type (^block_name)(parameters)

下面这种写法所定义的块,返回int值,并且接受两个int做参数:

int (^addBlock)(int a, int b) = ^(int a, int b) {
        return a + b;
    };

使用:

int add = addBlock(2, 3);
NSLog(@"%d", add); // 5

块的强大之处是:在声明它的范围里,所有的变量都可以为其所捕获。比如,下面这段代码所定义的块,就使用了块以外的变量:

	int additional = 5;
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        return a + b + additional;
    };
    
    int add = addBlock(2, 5);
    NSLog(@"%d", add); // 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。如果想在块里修改变量,可以在声明变量时加上__block修饰符。

例如本例,在块内修改additional:

__block int additional = 5;
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        additional++;
        return a + b + additional;
    };
    
    int add = addBlock(2, 5);
    NSLog(@"%d %d", add, additional); // 13, 6

内联块

不把块赋给局部变量,而是内联到函数调用里。这样可以把逻辑调用都放在一处。

例,用块来枚举数组中的元素,判断其中有多少个小于2的数:

NSArray* array = @[@0, @1, @2, @3, @4, @5];
    __block NSInteger count = 0;
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSNumber* number = (NSNumber*)obj;
        if ([number compare:@2] == NSOrderedAscending) {
            count++;
        }
    }];

块与对象类型

如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候也会将其一并释放。

实际上,块本身可视为对象,很多其他Objectivec-C可响应的选择子中,有很多,块也可以响应。而最重要之处在于块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量。

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

例如,下面这个块声明在EOCClass类的方法中:

@implementation EOCClass

- (void) anInstanceMethod {
    void (^someBlock)() = ^{
        _value = @"Something";
        NSLog(@"%@", _value);
    };
}

@end

如果某个EOCClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。直接访问实例变量和通过self来访问是等效的:

self->_value = @"Something";

之所以要捕获self变量,原因正在于此。我们经常通过属性访问实例变量,在这种情况下,就要指明self了:

self.value = @"Something";

块的内部结构

块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。
在这里插入图片描述

invoke

在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。

invoke函数需要把块对象作为参数传进来,以便于在执行块时,把捕获到的变量从内存中读出来。

descriptor

descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块时运行,其中会执行一些操作。

块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。

全局块、栈块及堆块

栈块

定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围生效。例如,下面这段代码就很危险:

void (^block)();
if (...) {
	block = ^{
		NSLog(@"Block A");
	};
} else {
	block = ^{
		NSLog(@"Block B");
	};
}
block();

定义在if及else语句中的两个块都分配在栈内存中,等离开了相应的范围,编译器很可能吧分配给栈的内存覆写掉。于是,这两个块只能保证在对应的if或else语句内有效。

堆块

未解决此问题,可给块对象发生copy消息,以拷贝。这样的话,就可以吧块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且一旦复制到堆上,块就成了带了引用计数的对象了。后续的复制操作都只是递增块对象的引用计数,

例:

void (^block)();
if (...) {
	block = [^{
		NSLog(@"Block A");
	} copy];
} else {
	block = [^{
		NSLog(@"Block B");
	} copy];
}
block();

现在代码就安全了。如果手动管理引用计数,那么在用完块之后还需要将其释放。

全局块

这种块不会捕捉任何状态,运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要每次用的时候于栈中创建。

另外,全局块的拷贝是个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。

例:

void (^block)() = &{
	NSLog(@"This is a block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。

要点

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。

为常用的块类型创建typedef

每个块都具备其“固有类型”,因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。
例:

^(BOOL flag, int value) {
	// Implementation
	return someInt;
}

如果想要将其赋给变量,则需注意其类型。变量类型及相关赋值语句如下:

int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value) {
	// Implementation
	return someInt;
}

与其他类型的变量不同,在定义块变量时,要把变量名称放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型其个别名。

为例隐藏复杂的块类型,需要用的C语言中名为“类型定义”的特性。typedef关键字用于给类型起个易懂的别名。

例:

typedef int (^EOCSomeBlock)(BOOL flag, int vlaue);

上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,创建变量直接使用新类型:

EOCSomeBlock block = ^(BOOL flag, int value) {
	// Implementation
};

块作方法参数

类里面有些方法可能需要使用块来做参数,遇到这种情况,可以通过定义别名使代码变得更为易读。

比方说,类里面又个方法,它接受一个块作为处理程序,在完成任务后执行这个块。若不定义别名,则方法签名会像下面这样:

- (void) startWithCompletHandler:(void (^)(NSData* data, NSError* error))completion;

这种情况,我们可以给参数类型起个别名,然后使用此名称来定义:

typedef void (^EOCCompletionHandler)(NSData *data, NSError *error);

- (void) startWithCompletHandler:(EOCCompletionHandler)completion;

当前,优秀的集成开发环境都可以自动吧类型定义展开,所以typedef这个功能变得很实用。

使用类型定义还有一个好处,就是当你打算重构块的类型签名时,只需要修改类型定义语句即可。

如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么每个类就应该有自己的completion handler类型。这几个completion handler的签名也许完全相同,但最好还是在每个类里都各自定义一个别名。若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,以供个字类使用。

要点

  • 以typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef。

用handler块降低代码分散程度

为用户界面变吗时,一种常见范式就是“异步执行任务”。这种范式的好处在于:处理用户界面的显示及触摸所用的线程,不会因为执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。

异步方法在执行完任务之后,需要以某种手段通知相关代码。实现此功能有很多方法

委托模式

设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时得到通知了。

比方说,要写一个从URL中获取数据的类。使用委托模式设计出来的类会是这个样子:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class EOCNetwork;
@protocol EOCNetworkDelegate <NSObject>

- (void)network:(EOCNetwork*)network didFinishWithData:(NSData*)data;

@end

@interface EOCNetwork : NSObject

@property (nonatomic, weak) id<EOCNetworkDelegate> delegate;

- (id) initWithURL:(NSString*)string;
- (void) start;

@end

NS_ASSUME_NONNULL_END

其他类可像下面这样使用此类所提供的API:

- (void) network:(EOCNetwork *)network didFinishWithData:(NSData *)data {
    NSLog(@"cvdsvdfs");
}

- (void) fechFooData {
    NSString* url = @"vcdsvdsfvb";
    EOCNetwork* net = [[EOCNetwork alloc] initWithURL:url];
    net.delegate = self;
    [net start];
}

completion handler

把completion handler定义为块类型,将其直接传给start方法。


typedef void (^EOCCompletionHandler)(NSData *data, NSError *error);

@interface EGClass : NSObject <EOCNetworkDelegate>

- (void) startWithCompletHandler:(EOCCompletionHandler)completion;

@end

这样可以在调用start方法时直接以内联形式定义completion handler,以此方式来使用“网络数据获取器”,可以令代码比原先易懂很多。
例:

EGClass* egClass = [[EGClass alloc] init];
        [egClass startWithCompletHandler:^(NSData * _Nonnull data, NSError * _Nonnull error) {
            NSLog(@"%@", data);
        }];

由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

如果类使用多个获取器下载不同数据

委托模式

此时使用委托模式,就得在delegate的会调方法里根据传入的获取器参数来切换。例:

- (void) fechFooData {
    NSString* url = @"vcdsvdsfvb";
    _foo = [[EOCNetwork alloc] initWithURL:url];
    _foo.delegate = self;
    [_foo start];
}

- (void) fechBarData {
    NSString* url = @"vcdsvdsfvb";
    _bar = [[EOCNetwork alloc] initWithURL:url];
    _bar.delegate = self;
    [_bar start];
}

- (void) network:(EOCNetwork *)network didFinishWithData:(NSData *)data {
    if (_foo == network) {
        NSLog(@"_foo");
    } else if (_bar == network) {
        NSLog(@"_bar");
    }
    // etc.
}

这么写代码,会令delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量。(这种写法可能有其他原因,比如稍后要根据情况解除监听等。)然而这种写法通常很快就会使类的代码激增。

改用块来写的好处是:无须保存获取器,也无须在回调方法里切换。每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的:


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

- (void)fetchBarData {
	NSURL *url = [[NSURL alloc] initWithString:@"..."];
	EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
	[fetcher startWithCompletionHandler:^(NSData *data){
		_fetchedBarData = data;
	}];
}

使用块来处理错误

采用两个独立的处理程序

例:

typedef void (^UserEnrollModelBlock)(UserEnrollModel * _Nonnull userEnrollModel);
typedef void (^ErrorBlock)(NSError * _Nonnull error);

@interface LNManager (UserLoginModel)

- (void) postEmail:(NSString*)emailString getVerCodeSucceedBlock:(UserEnrollModelBlock)succeedBlock errorBlock:(ErrorBlock)errorBlock;

@end

调用方式:

- (void) getVerCode {
    [[LNManager shareLNManager] postEmail:_userLoginView.emailString getVerCodeSucceedBlock:^(UserEnrollModel * _Nonnull userEnrollModel) {
        
        self.userEnrollModel ==  userEnrollModel;
        
    } errorBlock:^(NSError * _Nonnull error) {
        NSLog(@"get VerCode error");
    }];
}

把处理成功情况和失败情况所用代码全放在一个块里

typedef void (^UserEnrollModelBlock)(UserEnrollModel * _Nonnull userEnrollModel, NSError * _Nonnull error);

@interface LNManager (UserLoginModel)

- (void) postEmail:(NSString*)emailString getVerCodeHandler:(UserEnrollModelBlock)handler;

@end

调用:

[[LNManager shareLNManager] postEmail:(NSString*)emailString getVerCodeHandler:^(UserEnrollModel * _Nonnull userEnrollModel, NSError * _Nonnull error){
	if (error) {
		NSLog(@"get VerCode error");
	} else {
		self.userEnrollModel ==  userEnrollModel;
	}
}];

这种代码需要在代码中检测传入的error变量,并且要把所有逻辑代码都放到一处。

缺点:由于全部逻辑都写在一起,所以块变得比较长,且比较复杂。

优点:处理更灵活。比方说,在传入错误信息时,可以把数据也传进来。

这种方法还有个优点:调用API的代码可能会在处理成功响应的过程中发现错误。比方说,返回的数据可能太短了。这种情况需要和网络数据获取所认定的失败情况按同一方式处理。使用本方法就可以处理这种情况。

在相关时间点执行回调操作

这种情况也可以使用handler块。比方说,调用网络数据获取器的代码,也许想在每次有下载进度时都得到通知。
把处理下载进度的handler定义成块类型,并新增一个此类型的属性:

typedef void(^EOCNetworkFetcherProgressHandler)(float progress);

@property (nonatomic, copy) EOCNetworkFetcherProgressHandler progressHandler;

这种写法很好,因为它还是能把所有业务逻辑都放在一起:也就是把创建网络数据获取器和定义progress handler所用的代码写在一处。

线程

基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上。因此最好能由调用API的人来决定handler应该运行在哪个线程上。NSNotificationCenter就属于这种API,它提供了一个方法,调用者可以经由此方法来注册想要接受的通知,等到相关事件发生时,通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列。若没有指定队列,则按默认当时执行,也就是说,将由投递通知的那个线程来执行。

下列方法可用来新增观察者:

- (id)addObserverEorName:(NSSting*)name object:(id)object queue:(NSOperationQueue*)queue usingBlock:(void(^)(NSNotification*))block;

此处传入的NSOperationQueue参数表示触发通知时用来执行块代码的那个队列。这个是操作队列,而非“底层GCD队列”,不过两者语义相同。

要点

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象切换,而若改用handler来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

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

使用块时,若不仔细思量,则很容易导致“保留环”。比方说,下面这个类就提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时可设置completion handler,这个块在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted方法执行调用者所指定的块,这段代码需要把completion handler保存到实例变量里面。

// .h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData* data);
 
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL* url;
- (id) initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

NS_ASSUME_NONNULL_END

// .m
#import "EOCNetworkFetcher.h"

@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL* url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData* downloadedData;
@end

@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL *)url {
    if ((self = [super init])) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
    self.completionHandler = completion;
    // ...
    // 任务完成, 调用p_requestCompleted
}

- (void)p_requestCompleted {
    if (_completionHandler) {
        _completionHandler(_downloadedData);
    }
}
@end

某个类可能会创建这种网络数据获取器对象,并且其从URL中下载数据:

@implementation EOCClass {
    EOCNetworkFetcher* _networkFetcher;
    NSData* _fetchedData;
}

- (void) downloadData {
    NSURL* url = [[NSURL alloc] initWithString:@"..."];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData * _Nonnull data) {
        NSLog(@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
    }];
}

这里,因为completion handler要设置_fetchedData实例变量,所以它必须捕获self变量。就是说,handler块保留创建网络数据获取器的那个EOCClass实例。而EOCClass实例通过strong实例变量保留了获取器,最后,获取器对象有保留了handler块。这就是一个保留环。

如图:
在这里插入图片描述

解决方法:令_networkFetcher实例变量不在引用获取器,或令获取器的completionHandler属性不再持有handler块。

在本例中,应该等completion handler块执行完毕后,再去打破保留环,以便使获取器对象在handler块执行期间保持存活状态。比方说,completion handler块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data){
	NSLog(@"Request for URL %@ finished", _networkFercher.url);
	_fetchedData = data;
	_networkFetcher = nil;
}];

一般来说,只要适时清理掉环中的某个引用,即可解决此问题。

特例:在本例中,只有completion handler运行过后,方能解除保留环。若是completion handler一直不运行,那么保留环就无法打破,于是内存就会泄露。

这种写法,还可能引入另外一种形式的保留环。如果completion handler块所引用的对象最终又引用了这个块本身,那么就会出现保留环。比方说,我们修改一下前面那个例子,使调用API的那段代码无须在执行期保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。

想要保持存活,获取器对象可以在启动任务时把自己加到全局的collection中,待任务完成后,再移除。而调用方将其代码修改如下:

- (void) downloadData {
    NSURL* url = [[NSURL alloc] initWithString:@"..."];
    EOCNetworkFetcher* networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData * _Nonnull data) {
        NSLog(@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
}

在这里,completion handler块其实要通过获取器对象来引用其中的URL。于是,块就要保留获取器,而获取器反过来又经由其completionHandler属性保留了这个块。

修改,将P_requestCompleted方法按如下方式修改即可:

- (void) (_completionHandler) {
	if (_completionHandler) {
		_completionHandler(_downloadedData);
	}
	self.completionHandler = nil;
}

这样一来,只要下载请求指向完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。

要点

  • 如果块所捕获的对象直接或间接地保留块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。

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

在Objective-C中,如果有多个线程要执行同一份代码,通常要使用锁来实现某种同步机制。

在GCD出现之前,有两种办法。

内置“同步块”

例:

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

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就解放了。

在本例中,同步行为所针对的对象是self。

缺点:滥用@synchronized(self)则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是在self对象上频繁加锁,那么程序可能等另一段于此无关的代码执行完毕,才能继续执行当前代码,这样做其实没有必要。

NSLock与NSRecursiveLock对象:

NSLock例:

_lock = [[NSLock] init];

- (void)synchronizedMethod {
	[_lock lock];
	// Safe
	[_lock unlock];
}

也可以使用NSRecursiveLock这种“递归锁”,线程能够多次持有该锁,而不会出现死锁现象。但是它的效率不高。

GCD

GCD能以更简单、更高效的形式为代码加锁。

例,使用@synchronized为某个属性的存取方法加锁:

- (NSString*)someString {
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

但是如果很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行,这也许并不是开发者想要的效果。我们只是想令每个属性各自独立地同步。

顺便,这么做只能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对是线程安全的。

串行同步队列

使用串行同步队列可以简单而高效的代替同步块或锁对象。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。
例:

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

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

此模式的思路是:把设置操作与获取都安排在队列里执行,这样的话,所有针对属性的访问操作就都同步了。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的,于是能够做许多优化。因此,开发者无须担心那些事,只要专心把访问方法写好就行。

然而还可以进一步优化。设置方法并不一定非得是同步的。设置实例变量所用的块,并不需要向设置方法返回什么值。
改之后:

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

这只是把同步派发改成了异步派发。

优点:可以提升设置方法的执行速度,而读取操作与写入操作依然会按顺序执行。

缺点:程序性能会降低,因为执行异步派发需要拷贝块。

适用情节:派发给队列的块要执行更为繁重的任务,此时可以考虑。

并发队列

多个获取方法并发执行。

static dispatch_queue_t _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, o);

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

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

栅栏

上例并不能正确实现同步。所有读取操作于写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写入操作可以随时执行。使用栅栏可以让这些操作随意执行。

下列函数可以向队列中派发块,将其作为栅栏使用:

void dispatch_barrrier_async(dispatch_queue_t queue, dispatch_block_t block);

void dispatch_barrrier_sync(dispatch_queue_t queue, dispatch_block_t block);

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

在本例中,可以用栅栏来实现属性的设置方法。

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

在这里插入图片描述

要点

  • 派发队列可用来表述同步语义,这种做法要比使用@synchronized块或NSLock对象更简单
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
  • 使用同步队列及栅栏块,可以令同步行为更加高效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值