Libdispatch出了什么问题

2020-11-24 06:56:48

早在2000年代中期,处理器性能就开始达到平稳状态,像英特尔这样的芯片制造商告诉世界,不断提高CPU时钟速度的时间还不够。他们将无法再以这种方式满足摩尔定律,但他们有另一种方式:将更多的内核封装到同一芯片上。当然有很多问题,如果开发人员希望能够利用这些许多内核,就需要更新其软件。当时有人认为,具有80核(甚至更多核)的消费类计算机将在10年内普及。快进到今天的2020年,大多数消费类计算机具有约4个内核,而专业计算机具有约8至12个内核。在此过程中一定出了点问题。剧透:多线程很难。

苹果公司在2008年做出了回应,宣布推出Mac OS X 10.6 Snow Leopard(有人认为这是有史以来最好的Mac OS版本),其中包括libdispatch(又名Grand Central Dispatch)。当我宣布WWDC 2008时,我在那儿,我们都很欣喜若狂,这可能是我参加过的最激动人心的WWDC(此后我将再参加5次)。 libdispatch和新的内联块语法令人惊叹,它们有望最终轻松地访问多核计算机的功能。在此之前,多核计算机已经使用了很长时间(实际上是双处理器),但是它主要由Photoshop等专业应用程序使用。在2000-2008时代,开发人员通常只在需要时才开始对应用程序进行多线程处理,例如,因为一项工作需要长时间运行,并且可能会阻塞应用程序的用户事件运行循环太长时间(导致臭名昭著)旋转沙滩球出现)。

苹果公司展示了libdispatch,它的承诺似乎很棒,他们介绍了串行队列的概念,并告诉我们应该停止考虑线程,而开始考虑队列。我们将提交各种程序任务以串行或并发执行,而libdispatch将完成其余工作,并根据可用硬件自动缩放。队列很便宜,我们可以有很多。实际上,我非常清楚地记得在WWDC的一次会议结束时进行的问答,一位开发人员来到麦克风旁,问我们程序中可以有多少个队列,它们的真正价格是多少?舞台上的苹果工程师回答说,大多数队列大小基本上是开发人员在创建时传递给它的调试标签。我们可以毫无问题地拥有成千上万个。

串行队列将如何帮助我们进行并发?那么,各种程序组件将具有自己的专用队列,该专用队列将用于确保线程安全(甚至不再需要锁定),并且这些组件之间将是并发的。他们告诉我们,这是“并发海洋中的序列化之岛”。

未来是多线程,我们不得不使用libdispatch到达那里。我们做到了。

然后问题开始了。我们遇到了线程爆炸问题,这真是令人惊讶,因为我们被告知libdispatch将根据可用硬件自动扩展,因此我们期望线程数或多或少地与计算机中的内核数相匹配。 2010年,一个年纪较小的我在libdispatch邮件列表上寻求帮助,当时Apple的回应是删除同步点并一直保持异步。

当我们钻进那个兔子洞时,情况逐渐恶化。异步函数有污染其他函数的坏习惯:因为一个函数不能调用另一个异步函数并在不自身异步的情况下返回结果,因此整个链调用必须变为异步。我们开始拥有许多异步功能,这些功能实际上对异步没有任何意义,它们没有执行长时间运行的后台任务,并且它们本身并不是异步的(例如网络请求)。我们不得不处理繁重的回调设计的复杂性,这使得所有内容都难以阅读。更为令人担忧的是,异步使我们的程序变得更加不可预测且难以推理:因为每次我们分派异步时,我们都会释放执行上下文,直到工作项完成为止,现在该程序可以以交错方式执行新的调用了,我们方法的中间这导致了各种非常微妙且难以调试的订购错误。更糟糕的是,它们确实也很难修复,并导致了数天的调试,实施技巧和拖延。更糟糕的是,我们最终意识到我们遇到了可怕的性能问题,事实证明异步小型任务并不断地在许多队列上调度确实是浪费。这有点疯狂,因为我们一开始就做所有这些事情的全部原因是为了从内核中获得更好的性能,但是实际上我们的情况更糟。尽管我们付出了最大的努力,并且实际上拥有一个非常异步的程序,但在正常运行期间,我们仍然可以轻松地看到4核计算机上正在运行30至60个线程。

事实证明,苹果工程师是像我们一样的开发人员,并且遇到了与我们完全相同的问题。在Mac OS X 10.7 Lion中,他们引入了安全转换(Security Transforms),这是一种用于执行安全操作(哈希,加密等)的全新异步API。我们使用它解密文件,并在程序中引起线程爆炸。事实证明,每个安全转换都由其自己的专用队列支持,从而导致产生太多线程。现在已放弃了该API,而倾向于使用诸如CommonCrypto和新的CryptoKit之类的同步库。

一位苹果工程师还透露,iOS 12的许多性能优势都来自单线程守护进程。这意味着多线程代码已在OS本身中编写,维护和交付了很多年,直到工程师最终意识到它不能很好地工作。该工程师还建议“强烈考虑不编写异步/并发代码”。是的,我知道这种感觉。

我在推特上开玩笑说,当我们不得不使用+ [NSThread detachNewThreadSelector ...]进行多线程处理时,一切都会变得更好。当然,这不是真的,libdispatch具有有用的功能,我确实小心使用了它。我之所以这么说,是因为那时候程序员进行多线程的前期成本较高(诸如启动新线程,跨线程通信等)。结果是,开发人员将停止并认真思考创建线程是否有意义,他们将仔细考虑其程序设计。 libdispatch的实现太简单了,开发人员开始左右调度,而不必再真正考虑他们的软件和硬件中实际发生了什么,这使开发人员远离精心设计。

直到2017年之前,我终于偶然发现了Swift邮件列表上的讨论(第1页,第2页)。当时苹果公司的libdispatch维护者Pierre Habouzit试图向Swift编译器工程师解释我希望苹果多年以前告诉我们的事情(公平地说,苹果公司开始回溯并在最近的WWDC会议上进行了解释,但它没有像这次讨论那样打我。)我收集了所有可以找到的信息并将其发布在这里(如果您尚未阅读,请立即阅读)。事实证明,解决方案是仔细考虑队列,就像它们是线程一样,并谨慎使用异步。由于某种原因,Apple从未更新过libdispatch API以使其更易于滥用,也从未更新过文档来解释所有这一切。

我应用了建议,花了一些时间,但是很棒。许多本来不需要真正异步的代码又回到了同步,这一切都与众不同。事情变得简单得多,我可以删除大量的代码,以防止前面提到的无序交错调用。现在,只有在真正有意义的情况下,事情才是异步的(例如,将长期运行的任务置入后台或执行网络请求)。该程序更具可预测性,更易于阅读和推理。线程数下降到合理的数量。它也更快,更快(该程序从内核扩展接收并处理了各种系统事件,因此很多事情都在进行)。

现在很明显,libdispatch的原始意图失败了。开发人员确实需要认真考虑多线程,并且需要仔细考虑他们的程序设计。其他所有操作系统和语言都尝试了自己的libdispatch变体,从我阅读的内容来看,它们都在某种程度上失败了。毕竟,多线程是一个很难解决的难题。

现在我有点担心,因为我看到Apple计划将所有闪亮的新事物添加到Swift语言中,我不知道这次会发生什么。

“ actor”是一种新型的类,它具有自己的内部专用队列,在其内部执行其函数以确保线程安全,而actor的公开函数只能是异步的。您可能会问这如何帮助并发?好了,您看到了,您的程序中可以有许多参与者,它们之间可以同时执行,它们是并发海洋中的序列化孤岛。现在我不认识你,但对我来说,这似乎像我们几年前所做的那样,真是太惨了。我什至不能确定参与者是否正确,因为在各处使用异步来保护共享状态的想法非常有问题。如果整个故事都告诉我一件事,那就是应该非常谨慎地使用异步,并且仅在真正有意义的情况下使用(并且保护共享状态不是其中之一)。如果您看到一个异步接口并且不明白为什么它需要异步,那么可能就不需要。我知道有人在讨论如何在等待时暂停参与者的内部队列,以避免出现交错呼叫的问题,但是对我来说,这清楚地表明,整个想法都被误导了(我已经去过那里,我们已经这样做了,是使死锁成为可能的不幸技巧。至于其余的(异步/等待,结构化并发),可能还可以,但是可以进一步降低编写异步代码的成本,如果我们希望人们编写更多的异步代码,那么这很好。

对我来说,最令人担忧的是libdispatch对我们整个软件生态系统的长期影响。 libdispatch引入12年后,我仍然看到它几乎在我所看到的任何地方都被滥用。我仍然看到人们在推特上写博客文章,推荐目前已知有问题的做法。我仍然看到不应该的异步代码,程序产生太多线程。我认为这不会消失。现在可能是时候格外小心了,因为这些API对我们的软件(包括操作系统)产生了长期影响。一旦像演员这样的事情出现了,它们将被使用,并且将不会停止。我们当然应该对长期影响和后果感到奇怪。