有许多方法可以调整现有的线程代码以利用Grand Central Dispatch和操作对象。虽然在所有情况下都可能无法远离线程,但在进行切换的地方,性能(以及代码的简单性)可以大大提高。具体来说,使用调度队列和操作队列而不是线程有几个优点:
它减少了应用程序为在应用程序的内存空间中存储线程堆栈而支付的内存损失。
它消除了创建和配置线程所需的代码。
它消除了在线程上管理和调度工作所需的代码。
它简化了您必须编写的代码。
本章提供了有关如何替换现有基于线程的代码以及使用调度队列和操作队列来实现相同类型的行为的一些提示和指南。
要了解如何用调度队列替换线程,首先要考虑一下今天在应用程序中使用线程的一些方法:
单任务线程。创建一个线程来执行单个任务,并在任务完成时释放线程。
工作线程。创建一个或多个具有特定任务的工作线程。定期向每个线程发送任务。
线程池。创建一个通用线程池并为每个线程设置运行循环。当您要执行任务时,从池中获取一个线程并将任务分派给它。如果没有空闲线程,则排队任务并等待线程变为可用。
虽然这些看起来可能是截然不同的技术,但它们实际上只是基于相同原理的变体。在每种情况下,都使用一个线程来运行应用程序必须执行的某个任务。它们之间的唯一区别是用于管理线程和任务排队的代码。使用调度队列和操作队列,您可以消除所有线程和线程通信代码,而只关注您要执行的任务。
如果您使用上述线程模型之一,那么您应该已经非常了解应用程序执行的任务类型。不要将任务提交到其中一个自定义线程,而是尝试将该任务封装在操作对象或块对象中,并将其分派到适当的队列。对于没有特别争议的任务 - 即不采取锁定的任务 - 您应该能够进行以下直接替换:
对于单个任务线程,将任务封装在块或操作对象中,并将其提交到并发队列。
对于工作线程,您需要决定是使用串行队列还是并发队列。如果使用工作线程同步特定任务集的执行,请使用串行队列。如果确实使用工作线程执行没有相互依赖性的任意任务,请使用并发队列。
对于线程池,将任务封装在块或操作对象中,并将它们分派到并发队列以供执行。
当然,像这样的简单替换可能并不适用于所有情况。如果您正在执行的任务争用共享资源,理想的解决方案是尝试首先删除或最小化该争用。如果有一些方法可以重构或重新构建代码以消除对共享资源的相互依赖性,那当然更可取。但是,如果这样做不可能或效率较低,仍然有办法利用队列。队列的一大优势是它们提供了一种更可预测的方式来执行代码。这种可预测性意味着仍然有办法在不使用锁或其他重量级同步机制的情况下同步代码的执行。您可以使用队列执行许多相同的任务,而不是使用锁:
如果您有必须按特定顺序执行的任务,请将它们提交到串行调度队列。如果您更喜欢使用操作队列,请使用操作对象依赖项以确保这些对象按特定顺序执行。
如果您当前正在使用锁来保护共享资源,请创建一个串行队列来执行修改该资源的任何任务。然后,串行队列将现有锁替换为同步机制。有关消除锁定的更多信息,请参阅消除基于锁定的代码。
如果使用线程连接等待后台任务完成,请考虑使用调度组。您还可以使用 NSBlockOperation
对象或操作对象依赖项来实现类似的组完成行为。有关如何跟踪执行任务组的更多信息,请参阅替换线程连接。
如果使用生产者 - 消费者算法来管理有限资源池,请考虑将实现更改为更改生产者 - 消费者实现中显示的实现。
如果使用线程从描述符读取或写入,或监视文件操作,请使用Dispatch Sources中所述的调度源。
重要的是要记住,队列不是替换线程的灵丹妙药。队列提供的异步编程模型适用于延迟不是问题的情况。尽管队列提供了配置队列中任务执行优先级的方法,但更高的执行优先级并不能保证在特定时间执行任务。因此,在需要最小延迟的情况下,例如在音频和视频播放中,线程仍然是更合适的选择。
对于线程代码,锁是同步对线程之间共享的资源的访问的传统方法之一。但是,使用锁是有代价的。即使在无争议的情况下,总是会出现与锁定相关的性能损失。在有争议的情况下,一个或多个线程有可能在等待锁释放时阻塞不确定的时间。
用队列替换基于锁的代码消除了与锁相关的许多消耗,并简化了剩余的代码。您可以改为创建一个队列来序列化访问该资源的任务,而不是使用锁来保护共享资源。队列开销要比锁小很多。例如,排队任务不需要陷入内核以获取互斥锁。
排队任务时,您必须做出的主要决定是同步还是异步。异步提交任务可让当前线程在执行任务时继续运行。同步提交任务会阻止当前线程,直到任务完成。这两个选项都有适当的用途,尽管只要有可能,异步提交任务肯定是有利的。
以下各节介绍如何使用等效的基于队列的代码替换现有的基于锁的代码。
异步锁是一种保护共享资源的方法,而不会阻止任何修改该资源的代码。当您需要修改数据结构作为代码正在执行的其他工作的副作用时,可以使用异步锁定。使用传统线程,通常实现此代码的方式是锁定共享资源,进行必要的更改,释放锁定,并继续执行任务的主要部分。但是,使用调度队列,调用代码可以异步进行修改,而无需等待完成这些更改。
清单5-1显示了异步锁实现的示例。在此示例中,受保护资源定义其自己的串行调度队列。调用代码将块对象提交给此队列,该队列包含需要对资源进行的修改。因为队列本身是按顺序执行块的,所以保证按照接收顺序对资源进行更改; 但是,因为任务是异步执行的,所以调用线程不会阻塞。
清单5-1 异步修改受保护资源
dispatch_async(obj->serial_queue, ^{
// Critical section
});
如果当前代码在给定任务完成之前无法继续,则可以使用该dispatch_sync
功能同步提交任务。此函数将任务添加到调度队列,然后阻止当前线程,直到任务完成执行。调度队列本身可以是串行或并发队列,具体取决于您的需要。但是,因为此函数会阻止当前线程,所以只应在必要时使用它。清单5-2显示了使用包装代码的关键部分的技术dispatch_sync
。
清单5-2 同步运行关键部分
dispatch_sync(my_queue,^ {
//关键部分
});
如果您已使用串行队列来保护共享资源,则同步分派到该队列不会像异步调度那样保护共享资源。同步调度的唯一原因是阻止当前代码继续,直到关键部分完成。例如,如果您想从共享资源中获取一些值并立即使用它,则需要同步调度。如果当前代码不需要等待关键部分完成,或者它只是向同一个串行队列提交其他后续任务,则通常首选提交异步。
如果你的代码有循环,并且每次循环完成的工作都与其他迭代中完成的工作无关,你可以考虑使用dispatch_apply
or dispatch_apply_f
函数重新实现该循环代码。这些函数将循环的每次迭代分别提交给调度队列以进行处理。与并发队列结合使用时,此功能允许您同时执行循环的多次迭代。
dispatch_apply
和 dispatch_apply_f
是同步调用函数,会阻塞当前线程的执行,知道所有的循环迭代完成。当提交到一个并发队列,迭代循环的执行顺序不能得到保证。每个执行迭代的线程可能会阻塞,并引起执行顺序的不一致。因此,用于每个循环迭代的块对象或函数必须是可重入的。
清单5-3显示了如何使用基于调度的等效项替换for
循环。传递给dispatch_apply
或者dispatch_apply_f
的块或函数,必须使用一个整数值来指示当前循环迭代的次数。在此示例中,代码只是将当前循环编号打印到控制台。
清单5-3 替换for循环不使用步幅
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n", i);
});
虽然前面的示例是一个简单的示例,但它演示了使用调度队列替换循环的基本技术。虽然这可能是提高基于循环的代码性能的好方法,但您仍然必须清楚地使用这种技术。尽管调度队列的开销非常低,但仍然需要在线程上调度每个循环迭代的成本。因此,您应确保您的循环代码完成足够的工作以保证成本。确切地说,您需要做多少工作就是必须使用性能工具来衡量。
增加每个循环迭代中的工作量的一种简单方法是使用跨步。通过跨步,您可以重写块代码以执行原始循环的多次迭代。然后dispatch_apply
,按比例减少指定给函数的计数值。清单5-4显示了如何实现对清单5-3中所示的循环代码的跨越。在清单5-4中,块调用printf
语句的次数与stride值相同,在本例中为137.(实际的stride值是你应根据代码所做的工作配置的。)因为当将总迭代次数除以步幅值时剩余剩余部分,任何剩余的迭代都是内联执行的。
文档存档开发人员
搜索
下一个上一个
有许多方法可以调整现有的线程代码以利用Grand Central Dispatch和操作对象。虽然在所有情况下都可能无法远离线程,但在进行切换的地方,性能(以及代码的简单性)可以大大提高。具体来说,使用调度队列和操作队列而不是线程有几个优点:
它减少了应用程序为在应用程序的内存空间中存储线程堆栈而支付的内存损失。
它消除了创建和配置线程所需的代码。
它消除了管理和安排线程工作所需的代码。
它简化了您必须编写的代码。
本章提供了有关如何替换现有基于线程的代码以及使用调度队列和操作队列来实现相同类型的行为的一些提示和指南。
要了解如何用调度队列替换线程,首先要考虑一下今天在应用程序中使用线程的一些方法:
单任务线程。创建一个线程来执行单个任务,并在任务完成时释放线程。
工人线程。创建一个或多个具有特定任务的工作线程。定期向每个线程发送任务。
线程池。创建一个通用线程池并为每个线程设置运行循环。当您要执行任务时,从池中获取一个线程并将任务分派给它。如果没有空闲线程,则排队任务并等待线程变为可用。
虽然这些看起来可能是截然不同的技术,但它们实际上只是基于相同原理的变体。在每种情况下,都使用一个线程来运行应用程序必须执行的某个任务。它们之间的唯一区别是用于管理线程和任务排队的代码。使用调度队列和操作队列,您可以消除所有线程和线程通信代码,而只关注您要执行的任务。
如果您使用上述线程模型之一,那么您应该已经非常了解应用程序执行的任务类型。不要将任务提交到其中一个自定义线程,而是尝试将该任务封装在操作对象或块对象中,并将其分派到适当的队列。对于没有特别争议的任务 - 即不采取锁定的任务 - 您应该能够进行以下直接替换:
对于单个任务线程,将任务封装在块或操作对象中,并将其提交到并发队列。
对于工作线程,您需要决定是使用串行队列还是并发队列。如果使用工作线程同步特定任务集的执行,请使用串行队列。如果确实使用工作线程执行没有相互依赖性的任意任务,请使用并发队列。
对于线程池,将任务封装在块或操作对象中,并将它们分派到并发队列以供执行。
当然,像这样的简单替换可能并不适用于所有情况。如果您正在执行的任务争用共享资源,理想的解决方案是尝试首先删除或最小化该争用。如果有一些方法可以重构或重新构建代码以消除对共享资源的相互依赖性,那当然更可取。但是,如果这样做不可能或效率较低,仍然有办法利用队列。队列的一大优势是它们提供了一种更可预测的方式来执行代码。这种可预测性意味着仍然有办法在不使用锁或其他重量级同步机制的情况下同步代码的执行。您可以使用队列执行许多相同的任务,而不是使用锁:
如果您有必须按特定顺序执行的任务,请将它们提交到串行调度队列。如果您更喜欢使用操作队列,请使用操作对象依赖项以确保这些对象按特定顺序执行。
如果您当前正在使用锁来保护共享资源,请创建一个串行队列来执行修改该资源的任何任务。然后,串行队列将现有锁替换为同步机制。有关消除锁定的更多信息,请参阅消除基于锁定的代码。
如果使用线程连接等待后台任务完成,请考虑使用调度组。您还可以使用 NSBlockOperation
对象或操作对象依赖项来实现类似的组完成行为。有关如何跟踪执行任务组的更多信息,请参阅替换线程连接。
如果使用生产者 - 消费者算法来管理有限资源池,请考虑将实现更改为更改生产者 - 消费者实现中显示的实现。
如果使用线程从描述符读取或写入,或监视文件操作,请使用Dispatch Sources中所述的调度源。
重要的是要记住,队列不是替换线程的灵丹妙药。队列提供的异步编程模型适用于延迟不是问题的情况。尽管队列提供了配置队列中任务执行优先级的方法,但更高的执行优先级并不能保证在特定时间执行任务。因此,在需要最小延迟的情况下,例如在音频和视频播放中,线程仍然是更合适的选择。
对于线程代码,锁是同步对线程之间共享的资源的访问的传统方法之一。但是,使用锁是有代价的。即使在无争议的情况下,总是会出现与锁定相关的性能损失。在有争议的情况下,一个或多个线程有可能在等待锁定释放时阻塞不确定的时间。
用队列替换基于锁的代码消除了与锁相关的许多惩罚,并简化了剩余的代码。您可以改为创建一个队列来序列化访问该资源的任务,而不是使用锁来保护共享资源。队列不会像锁一样处罚。例如,排队任务不需要陷入内核以获取互斥锁。
排队任务时,您必须做出的主要决定是同步还是异步。异步提交任务可让当前线程在执行任务时继续运行。同步提交任务会阻止当前线程,直到任务完成。这两个选项都有适当的用途,尽管只要有可能,异步提交任务肯定是有利的。
以下各节介绍如何使用等效的基于队列的代码替换现有的基于锁的代码。
异步锁是一种保护共享资源的方法,而不会阻止任何修改该资源的代码。当您需要修改数据结构作为代码正在执行的其他工作的副作用时,可以使用异步锁定。使用传统线程,通常实现此代码的方式是锁定共享资源,进行必要的更改,释放锁定,并继续执行任务的主要部分。但是,使用调度队列,调用代码可以异步进行修改,而无需等待完成这些更改。
清单5-1显示了异步锁实现的示例。在此示例中,受保护资源定义其自己的串行调度队列。调用代码将块对象提交给此队列,该队列包含需要对资源进行的修改。因为队列本身是按顺序执行块的,所以保证按照接收顺序对资源进行更改; 但是,因为任务是异步执行的,所以调用线程不会阻塞。
清单5-1 异步修改受保护资源
dispatch_async(obj-> serial_queue,^ {
|
//关键部分 |
}); |
如果当前代码在给定任务完成之前无法继续,则可以使用该dispatch_sync
功能同步提交任务。此函数将任务添加到调度队列,然后阻止当前线程,直到任务完成执行。调度队列本身可以是串行或并发队列,具体取决于您的需要。但是,因为此函数会阻止当前线程,所以只应在必要时使用它。清单5-2显示了使用包装代码的关键部分的技术dispatch_sync
。
清单5-2 同步运行关键部分
dispatch_sync(my_queue,^ {
|
//关键部分 |
}); |
如果您已使用串行队列来保护共享资源,则同步分派到该队列不会像异步调度那样保护共享资源。同步调度的唯一原因是阻止当前代码继续,直到关键部分完成。例如,如果您想从共享资源中获取一些值并立即使用它,则需要同步调度。如果当前代码不需要等待关键部分完成,或者它只是向同一个串行队列提交其他后续任务,则通常首选提交异步。
如果你的代码有循环,并且每次循环完成的工作都与其他迭代中完成的工作无关,你可以考虑使用dispatch_apply
or dispatch_apply_f
函数重新实现该循环代码。这些函数将循环的每次迭代分别提交给调度队列以进行处理。与并发队列结合使用时,此功能允许您同时执行循环的多次迭代。
该dispatch_apply
和dispatch_apply_f
功能是阻止执行的当前线程,直到所有的循环迭代是完全同步的函数调用。当提交到并发队列时,不保证循环迭代的执行顺序。运行每次迭代的线程可能会阻塞并导致给定的迭代在其周围的其他迭代之前或之后完成。因此,用于每个循环迭代的块对象或函数必须是可重入的。
清单5-3显示了如何for
使用基于调度的等效项替换循环。传递给的块或函数,dispatch_apply
或者dispatch_apply_f
必须采用指示当前循环迭代的整数值。在此示例中,代码只是将当前循环编号打印到控制台。
清单5-3for
无需跨步 替换循环
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0); |
dispatch_apply(count,queue,^(size_t i){
|
printf(“%u \ n”,i); |
}); |
虽然前面的示例是一个简单的示例,但它演示了使用调度队列替换循环的基本技术。虽然这可能是提高基于循环的代码性能的好方法,但您仍然必须清楚地使用这种技术。尽管调度队列的开销非常低,但仍然需要在线程上调度每个循环迭代的成本。因此,您应确保您的循环代码完成足够的工作以保证成本。确切地说,您需要做多少工作就是必须使用性能工具来衡量。
增加每个循环迭代中的工作量的一种简单方法是使用跨步。通过跨步,您可以重写块代码以执行原始循环的多次迭代。然后dispatch_apply
,按比例减少指定给函数的计数值。清单5-4显示了如何实现对清单5-3中所示的循环代码的跨越。在清单5-4中,块调用printf
语句的次数与stride值相同,在本例中为137.(实际的stride值是你应根据代码所做的工作配置的。)因为当将总迭代次数除以步幅值时剩余剩余部分,任何剩余的迭代都是内联执行的。
清单5-4 为调度的for循环添加一个步幅
int stride = 137;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count / stride, queue, ^(size_t idx){
size_t j = idx * stride;
size_t j_stop = j + stride;
do {
printf("%u\n", (unsigned int)j++);
}while (j < j_stop);
});
size_t i;
for (i = count - (count % stride); i < count; i++)
printf("%u\n", (unsigned int)i);
使用步幅有一些明显的性能优势。特别地,相对于步幅,当原始循环迭代次数较高时,步幅提供了益处。同时调度更少的块意味着执行这些块的代码花费的时间比调度它们多。与任何性能指标一样,您可能必须使用跨越值来为代码找到最有效的值。
线程连接允许您生成一个或多个线程,然后让当前线程等待,直到这些线程完成。要实现线程连接,父级会将子线程创建为可连接线程。如果父节点在没有子线程结果的情况下无法再进行,则它将与子节点连接。此进程会阻止父线程,直到子进程完成其任务并退出,此时父进程可以从子进程收集结果并继续自己的工作。如果父级需要连接多个子线程,则它一次只能连接一个子线程。
Dispatch组提供的语义与线程连接的语义类似,但具有一些额外的优点。与线程连接一样,调度组是线程阻塞的一种方式,直到一个或多个子任务完成执行。与线程连接不同,调度组同时等待其所有子任务。因为调度组使用调度队列来执行工作,所以它们非常有效。
要使用调度组执行可连接线程执行的相同工作,您将执行以下操作:
使用该dispatch_group_create
功能创建新的调度组。
使用dispatch_group_async
或dispatch_group_async_f
功能向组添加任务。您提交给组的每个任务都代表您通常在可连接线程上执行的工作。
当前线程无法再进行前进时,请调用该dispatch_group_wait
函数以等待该组。此功能会阻止当前线程,直到组中的所有任务完成执行。
如果使用操作对象来实现任务,则还可以使用依赖项实现线程连接。您可以将父代码移动到操作对象,而不是让父线程等待一个或多个任务完成。然后,您将在父操作对象和设置为执行通常由可连接线程执行的工作的任意数量的子操作对象之间设置依赖关系。依赖于其他操作对象会阻止父操作对象执行,直到所有操作都完成为止。
生产者 - 消费者模型允许您管理有限数量的动态生成的资源。当生产者创建新资源(或工作)时,一个或多个消费者等待这些资源(或工作)准备好并在它们消耗时消耗它们。实现生产者 - 消费者模型的典型机制是条件或信号量。
使用条件,生产者线程通常执行以下操作:
锁定与条件关联的互斥锁(使用pthread_mutex_lock
)。
生产资源或工作消费。
发出有消耗的条件变量的信号(使用pthread_cond_signal
)
解锁互斥锁(使用pthread_mutex_unlock
)。
反过来,相应的消费者线程执行以下操作:
锁定与条件关联的互斥锁(使用pthread_mutex_lock
)。
设置while
循环以执行以下操作:
检查是否确实有工作要做。
如果没有工作要做(或没有资源可用),请调用pthread_cond_wait
阻塞当前线程,直到出现相应的信号。
获取生产者提供的工作(或资源)。
解锁互斥锁(使用pthread_mutex_unlock
)。
处理工作。
使用调度队列,您可以将生产者和消费者实现简化为单个调用:
dispatch_async(queue,^ {
|
//处理工作项 |
}); |
当您的生产者要完成工作时,它所要做的就是将该工作添加到队列中并让队列处理该项目。前面代码中唯一改变的部分是队列类型。如果生产者生成的任务需要按特定顺序执行,则使用串行队列。如果生产者生成的任务可以同时执行,则将它们添加到并发队列中,让系统同时执行尽可能多的任务。
如果您当前正在使用信号量来限制对共享资源的访问,则应考虑使用dispatch信号量。传统的信号量总是需要调用内核来测试信号量。相反,调度信号量在用户空间中快速测试信号量状态,并且仅在测试失败并且需要阻塞调用线程时陷入内核。在无争议的情况下,此行为导致调度信号量比传统信号量快得多。但是,在所有其他方面,调度信号量提供与传统信号量相同的行为。
有关如何使用调度信号量的示例,请参阅使用调度信号量来调节有限资源的使用。
如果您使用run loop来管理正在一个或多个线程上执行的工作,您可能会发现队列更容易实现和维护。设置自定义运行循环涉及设置底层线程和run loop本身。运行循环代码包括设置一个或多个运行循环源以及编写回调以处理到达这些源的事件。您可以简单地创建一个串行队列并将任务分派给它,而不是完成所有工作。因此,您可以使用一行代码替换所有线程和运行循环创建代码:
dispatch_queue_t myNewRunLoop = dispatch_queue_create(“com.apple.MyQueue”,NULL); |
由于队列自动执行添加到其中的任何任务,因此不需要额外的代码来管理队列。您不必创建或配置线程,也不必创建或附加任何运行循环源。此外,您只需向其添加任务即可在队列上执行新类型的工作。要使用运行循环执行相同的操作,您需要修改现有的运行循环源或创建一个新的运行循环源来处理新数据。
运行循环的一种常见配置是处理在网络套接字上异步到达的数据。您可以将调度源附加到所需的队列,而不是为此类行为配置运行循环。与传统的运行循环源相比,调度源还提供了更多处理数据的选项。除了处理计时器和网络端口事件外,您还可以使用调度源来读取和写入文件,监视文件系统对象,监视进程和监视信号。您甚至可以定义自定义调度源,并从代码的其他部分异步触发它们。有关设置调度源的更多信息,请参阅调度源。
由于Grand Central Dispatch管理您提供的任务与运行这些任务的线程之间的关系,因此通常应避免从任务代码中调用POSIX线程例程。如果您确实需要出于某种原因调用它们,则应该非常小心您调用的例程。本节向您提供可以安全调用哪些例程以及从排队任务调用哪些例程不安全的说明。此列表不完整,但应该告诉您什么是安全的,哪些不是。
通常,您的应用程序不得删除或改变它未创建的对象或数据结构。因此,使用调度队列执行的块对象不得调用以下函数:
pthread_detach
pthread_cancel
pthread_join
pthread_kill
pthread_exit
虽然在任务运行时修改线程状态是可以的,但必须在任务返回之前将线程返回到其原始状态。因此,只要将线程返回到其原始状态,就可以安全地调用以下函数:
pthread_setcancelstate
pthread_setcanceltype
pthread_setschedparam
pthread_sigmask
pthread_setspecific
用于执行给定块的底层线程可以从调用更改为调用。因此,您的应用程序不应该依赖以下函数在块的调用之间返回可预测的结果:
pthread_self
pthread_getschedparam
pthread_get_stacksize_np
pthread_get_stackaddr_np
pthread_mach_thread_np
pthread_from_mach_thread_np
pthread_getspecific
重要提示: 块必须捕获并抑制其中抛出的任何语言级异常。在执行块期间发生的其他错误应该类似地由块处理或用于通知应用程序的其他部分。
有关POSIX线程和本节中提到的函数的更多信息,请参见pthread
手册页。
有关如何使用调度组的示例,请参阅等待排队任务组。有关在操作对象之间设置依赖关系的信息,请参阅配置互操作依赖关系。
原文https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW1