多线程的由来
多线程的由来
计算机的速度一直存在硬件限制——这并不会真正改变。 此外,除非我们发现更先进的计算方法,否则计算机中单个处理器可以完成的操作数量正在达到收益递减规律。
正因为如此,技术已经朝着增加每个处理器的核心数量和每个核心可以同时运行的线程数量的方向发展。这样,您可以在不同线程之间逻辑划分任意数量的任务,并且内核可以通过组织它们来确定它们的工作优先级。 简而言之,多线程极大地改善了计算机系统优化工作的方式以及执行速度。
您可以将相同的想法应用于现代应用程序。 例如,与其在硬件更好的服务器上花费大量资金,不如使用多线程和并发的智能应用来加速整个系统。
对比主线程和工作线程
主线程,或者说 UI 线程,是负责管理 UI 的线程。每个应用程序只能有一个主线程,以避免产生经典的死锁问题。至少需要两个线程以不同的顺序访问相同的资源(在本例中指 UI 组件)时,可能会发生这种情况。不负责渲染 UI 的其他线程称为工作线程或后台线程。允许执行多个控制线程的能力称为多线程,用于控制它们的协作和同步的一组技术称为并发。
基于此,你可以重新思考下 uploadImage
应该如何实现。showLoadingSpinner
启动一个负责进度条动画的新线程,该线程与主线程交互仅仅是通知 UI 刷新。启动一个新线程,函数现在是非阻塞调用,并且可以立即返回,之后允许图片上传启动自己的工作线程。当上传完成后,该后台线程将通知主线程隐藏进度条。
一旦程序启动了一个后台线程,程序可能会忘记它(译者注:类似自生自灭)或期待一些结果。您将在下一节中看到后台线程如何处理结果并与主线程通信。
后台线程与 UI 线程交互
上传图片示例演示了管理线程的重要性。在每一帧刷新时,负责进度条动画的线程需要与主线程通信以便更新 UI。工作线程负责实际的图片上传任务,并且需要在上传任务完成时通知主线程停止进度条动画以及隐藏进度条。所有的这些都必现在非阻塞的情况下发生。了解线程间如何通信是充分发挥并发能力的关键。
共享数据
线程之间通信需要共享数据。例如,负责进度条动画的线程需要通知主线程更新 UI。共享数据并不简单,它需要某种同步,同步是编写良好并发代码的方式之一。
例如,如果主线程收到一个新图像可用的通知,并且在显示它之前,图像被替换了,这时会发生什么呢? 在这种情况下,应用程序会跳过一帧并且会发生竞态条件。接下来你需要一种线程安全的数据结构。这意味着即使被多个线程同时访问,该数据结构也能够正常的工作。
从多个线程访问相同的数据,保持正确的行为和良好的性能,是并发编程的真正挑战。
然而也有特殊情况。 如果该数据结构只被访问而从不更新? 在这种情况下,多个线程可以在没有任何竞态条件的情况下读取相同的数据,那么这种数据结构被称为不可变的。 不可变对象始终是线程安全的。
举个办公室中咖啡机的实际例子。如果有两个人共享它,并且它也不是线程安全的,那么他们很容易煮出糟糕的咖啡或者把咖啡洒出来弄得一团糟。当一个人开始制作摩卡拿铁而另一个人想要一杯黑咖啡时,他们最终会毁掉机器——或者更糟的是咖啡。
为了在线程中安全地共享数据,可以使用哪些数据结构呢? 最重要的数据结构是队列,还有一种特殊情况——管道。
队列
线程通常使用队列进行通信,线程可以作为生产者或消费者对去队列进行操作。线程作为生产者时往队列里放入数据,而作为消费者时从队列里取出并使用数据。你可以认为队列是一个列表,其中生产者把数据放入列表尾部,然后消费者从列表顶部读取数据,这种情况遵循称为 FIFO(先进先出)的逻辑。线程通常将封装了共享数据的消息放入队列。
队列不仅仅是一个容器,因为它还提供同步,以便允许线程仅在消息可用时才读取它。否则,如果消息不可用,它会进入等待。如果队列是一个阻塞队列,消费者会进入阻塞等待新消息到来。
如果队列已满,生产者也会发生同样的情况。队列是线程安全的,所以它可以有多个生产者和多个消费者。
快餐店中的排队是一个很好的现实例子。
假设在快餐店中有一条订单线。对于该订单线可能会发生下面三种情况。
- 第一种情况:订单线中没有订单。虽然快餐店工作人员想要工作,但是没有订单,因此工作人员必须一直等待有客户下单才能开始工作。
- 第二种情况:订单线中有几个订单,随着工作人员消费订单,又没有新订单到来,那么订单就会越来越少。
- 第三种情况:订单线中已经充满了订单,工作人员忙不过来,无法及时处理后续的订单,那么后续的订单就会被阻塞,直到有其他的工作人员来帮忙。
在这个例子中,顾客生成订单,快餐店工作人员消费订单,服务客户。你也可以这样理解:顾客排成一队,等待快餐店工作人员为顾客服务,当食物配齐后,顾客取餐并离开队列。
管道
如果你在思考管道或者水龙头是如何工作的?这是一个比较好理解的概念。当你想要水的时候,你可以拧开水龙头的阀门。在水龙头的另一端有一个控制水流的系统。只要你想要水,它就会一直阻塞,直到水流出来——这就像阻塞调用方法一样。
编程世界中的管道和上面的水流管道类似。编程世界的管道中流动的是数据,通常还有监听器。其中数据通常为字节流,监听器可以将其解析成更有意义的内容。
举个例子,你可以想象一下工厂里的生产线。如果生产线上中的产品太多,那么必须停止生产,直到产品被处理完。同理,如果管道中的数据太多,那么管道将被阻塞,直到你处理一些数据,为后面的数据腾出空间,那么管道中的数据就可以流动。或者,如果生产线上没有产品,那么产线上的工作人员就必须等待产品生产出来。换句话说,如果管道中没有数据,管道也会被阻塞,直到管道中有数据出现。当你尝试将数据发送到溢出的管道或者尝试从空的管道中获取数据时,管道都会阻塞,直到满足条件。
你可以将管道看成阻塞队列,其中不是消息,而是字节。