GCD

dispatch_group

如果想在dispatch_queue中所有的任务执行完成后在做某种操作,在串行队列中,可以把该操作放到最后一个任务执行完成后继续,但是在并行队列中怎么做呢。这就有dispatch_group 成组操作。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t dispatchQueue = dispatch_queue_create("ted.queue.next", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
NSLog(@"dispatch-1");
});
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
NSLog(@"dspatch-2");
});

dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
NSLog(@"end");
});

还有一个类似功能的接口是: long dispatch_group_wait(dispatch_group_t roup, dispatch_time_t timeout), timeout参数是超时时间,如果所需的时间小于timeout,则返回0,否则返回非0值。此参数也可以取常量DISPATCH_TIME_FOREVER。这个接口会阻塞当前线程,而 notify不会。

定时器

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"now is %@",[NSDate date]);
dispatch_source_cancel(timer);
});
dispatch_resume(timer);

自定义队列

1
2
3
4
5
6
7
8
9
10
11
12
13
串行队列
dispatch_queue_t queue = dispatch_queue_create("hehehqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
....
})
};

并行队列:
dispatch_queue_t queue = dispatch_queue_create("hehehqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
....
})
};

dispatch_barrier_async

是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行。
在并行队列中,有的时候我们需要让某个任务单独执行,也就是他执行的时候不允许其他任务执行。这时候dispatch_barrier就派上了用场。

dispatch_barrier最典型的使用场景是读写问题,NSMutableDictionary在多个线程中如果同时写入,或者一个线程写入一个线程读取,会发生无法预料的错误。但是他可以在多个线程中同时读取。如果多个线程同时使用同一个NSMutableDictionary。怎样才能保护NSMutableDictionary不发生意外呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setObject:(id)anObject forKey:(id
)aKey
{
dispatch_barrier_async(self.concurrentQueue, ^{
[self.mutableDictionary setObject:anObject forKey:aKey];
});
}


- (id)objectForKey:(id)aKey
{
__block id object = nil;
dispatch_sync(self.concurrentQueue, ^{
object = [self.mutableDictionary objectForKey:aKey];
}); return object;
}

执行某个代码片段N次

1
2
3
dispatch_apply(5, globalQ, ^(size_t index) {
// 执行5次
});

上面的代码类似于 for(int i = 0, i < 5; i++)。不同的是,dispatch_apply()可以用并发队列,并发的来执行里面的块。

set_specific & get_specific 给队列关联内容

iOS 6之后dispatch_get_current_queue()被废弃,如果我们需要区分不同的queue,可以使用set_specific方法。根据对应的key是否有值来区分。或者有时候我们需要将某些东西关联到队列上,比如我们想在某个队列上存一个东西,或者我们想区分2个队列。GCD提供了dispatch_queue_set_specific方法,通过key,将context关联到queue上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);

- queue:需要关联的queue,不允许传入NULL
- key:唯一的关键字
- context:要关联的内容,可以为NULL
- destructor:释放context的函数,当新的context被设置时,destructor会被调用



有存就有取,将context关联到queue上之后,可以通过dispatch_queue_get_specific或者dispatch_get_specific方法将值取出来。

void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);

void *dispatch_get_specific(const void *key);

dispatch_queue_get_specific: 根据queue和key取出context,queue参数不能传入全局队列

dispatch_get_specific: 根据唯一的key取出当前queue的context。
如果当前queue没有key对应的context,则去queue的target queue取,取不着返回NULL,如果对全局队列取,也会返回NULL

dispatch_block_t 定义不带参数的回调函数

之前不带参数的回调函数这样写的:

在.h文件里

定义类型

1
typedef void(^successBlockAction)();

在.m 文件中

1
2
3
4
5
-(void)onClick:(successBlockAction)successBlock{

successBlock();

}

现在使用这个更加方便的方式:

在.h文件里定义一个属性

1
@property(nonatomic,copy) dispatch_block_t succeedBlock;

在.m文件里

1
2
3
4
5
6
7
UIButton *btn = [[UIButton alloc]init];

btn.successBlockAction= ^() {

NSLog(@" button clicked");

};

dispatch source

上面提到的 定时器其实就是一个事件源,是一个内置事件源。事件源可以分为自定义事件源和内置事件源

https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW1

Reading Data from a Descriptor

To read data from a file or socket, you must open the file or socket and create a dispatch source of type DISPATCH_SOURCE_TYPE_READ. The event handler you specify should be capable of reading and processing the contents of the file descriptor. In the case of a file, this amounts to reading the file data (or a subset of that data) and creating the appropriate data structures for your application. For a network socket, this involves processing newly received network data.

Whenever reading data, you should always configure your descriptor to use non-blocking operations. Although you can use the dispatch_source_get_data function to see how much data is available for reading, the number returned by that function could change between the time you make the call and the time you actually read the data. If the underlying file is truncated or a network error occurs, reading from a descriptor that blocks the current thread could stall your event handler in mid execution and prevent the dispatch queue from dispatching other tasks. For a serial queue, this could deadlock your queue, and even for a concurrent queue this reduces the number of new tasks that can be started.

Listing 4-2 shows an example that configures a dispatch source to read data from a file. In this example, the event handler reads the entire contents of the specified file into a buffer and calls a custom function (that you would define in your own code) to process the data. (The caller of this function would use the returned dispatch source to cancel it once the read operation was completed.) To ensure that the dispatch queue does not block unnecessarily when there is no data to read, this example uses the fcntl function to configure the file descriptor to perform nonblocking operations. The cancellation handler installed on the dispatch source ensures that the file descriptor is closed after the data is read.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
dispatch_source_t ProcessContentsOfFile(const char* filename)
{
// Prepare the file for reading.
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK); // Avoid blocking the read operation

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}

// Install the event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// Read the data into a text buffer.
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual); // Process the data.

// Release the buffer when done.
free(buffer);

// If there is no more data, cancel the source.
if (done)
dispatch_source_cancel(readSource);
}
});

// Install the cancellation handler
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});

// Start reading the file.
dispatch_resume(readSource);
return readSource;
}

NSOperationqueue

一个NSOperation对象可以通过调用start方法来执行任务,默认是同步执行的。也可以将NSOperation添加到一个NSOperationQueue(操作队列)中去执行,而且是异步执行的。

注意:NSOperation添加到queue之后,绝对不要再修改NSOperation对象的状态。因为NSOperation对象可能会在任何时候运行,因此改变NSOperation对象的依赖或数据会产生不利的影响。你只能查看NSOperation对象的状态, 比如是否正在运行、等待运行、已经完成等

设置operation之间的依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSOperationQueue *queue = [[NSOperationQueue alloc] init];  

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第2次操作,线程:%@", [NSThread currentThread]);
}];

// operation1依赖于operation2
[operation1 addDependency:operation2];

[queue addOperation:operation1];
[queue addOperation:operation2];

修改Operations的执行顺序

对于添加到queue中的operations,它们的执行顺序取决于2点:

1.首先看看NSOperation是否已经准备好:是否准备好由对象的依赖关系确定

2.然后再根据所有NSOperation的相对优先级来确定。优先级等级则是operation对象本身的一个属性。默认所有operation都拥有“普通”优先级,不过可以通过setQueuePriority:方法来提升或降低operation对象的优先级。优先级只能应用于相同queue中的operations。如果应用有多个operation queue,每个queue的优先级等级是互相独立的。因此不同queue中的低优先级操作仍然可能比高优先级操作更早执行。

注意:优先级不能替代依赖关系,优先级只是对已经准备好的 operations确定执行顺序。先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。

设置队列的最大并发操作数量

1
2
3
4
// 每次只能执行一个操作  
queue.maxConcurrentOperationCount = 1;
// 或者这样写
[queue setMaxConcurrentOperationCount:1];

取消Operations

一旦添加到operation queue,queue就拥有了这个Operation对象并且不能被删除,唯一能做的事情是取消。你可以调用Operation对象的cancel方法取消单个操作,也可以调用operation queue的cancelAllOperations方法取消当前queue中的所有操作。

1
2
3
4
5
// 取消单个操作  
[operation cancel];

// 取消queue中所有的操作
[queue cancelAllOperations];

等待Options完成

1
2
3
4
5
// 会阻塞当前线程,等到某个operation执行完毕  
[operation waitUntilFinished];

// 阻塞当前线程,等待queue的所有操作执行完毕
[queue waitUntilAllOperationsAreFinished];

暂停和继续线程

1
2
3
4
5
// 暂停queue  
[queue setSuspended:YES];

// 继续queue
[queue setSuspended:NO];

两种方式的对比

GCD仅仅支持FIFO队列,而NSOperationQueue中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。
GCD不支持异步操作之间的依赖关系设置。如果某个操作的依赖另一个操作的数据(生产者-消费者模型是其中之一),使用NSOperationQueue能够按照正确的顺序执行操作。GCD则没有内建的依赖关系支持。
NSOperationQueue支持KVO,意味着我们可以观察任务的执行状态。

  • GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;
  • 在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);
  • NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
  • 我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;
  • 在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;
  • 我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。

总的来说,Operation queue 提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。