线程与进程

当我们谈论线程时,通常指的是在一个进程中独立运行的执行单元。一个进程可以包含多个线程,每个线程共享进程的资源,但有自己的独立执行流。线程是操作系统进行调度和执行的基本单位,它使得程序能够同时执行多个任务。

我们要明白线程存在的问题:

  • 线程并不便宜。线程需要进行上下文切换,成本很高。
  • 线程不是无限的。可启动的线程数量受到底层 操作系统 的限制。在服务器端应用程序中,这可能会造成严重瓶颈。
  • 线程并非总是可用。有些平台(如 JavaScript)甚至不支持线程。
  • 线程并非易事。调试线程和避免竞态条件是我们在多线程编程中经常遇到的问题。

多线程的两种模型

  1. 一对一模型(One-to-One Model)
    • 每个用户级(语言)线程(应用线程)都映射到一个内核级(系统)线程。
    • 优点:充分利用多核处理器,可以实现真正的并行执行。
    • 缺点:线程的创建和切换开销较大,特别是在线程数量较多的情况下。
  2. M:N 模型

并发模型

  • OS 线程:

    • 无需改变编程模型,线程间同步困难,性能开销大
    • 线程池可以降低一些成本,但难以支撑大量1O绑定的工作
  • Event-driven 模型:

    • 与回调函数一起用,可能高效
    • 非线性的控制流,数据流和错误传播难以追踪
  • Coroutines:

    • 类似线程,无需改变编程模型
    • 类似 async,支持大量任务
    • 抽象掉了底层细节(这对系统编程、自定义运行时的实现很重要)
  • Actor 模型:

    • 将所有并发计算划分为 actor,消息通信易出错
    • 可以有效的实现 actor 模型,但许多实际问题没解决(例如流控制、重试逻辑)
  • Async/await、Future

Futures, promises, and others

这种方法要求我们在编程方式上做出一系列改变:

不同的编程模型。与回调类似,编程模式也从自上而下的命令式方法转变为链式调用的组合模式。在这种模式下,循环、异常处理等传统程序结构通常不再有效。

不同的应用程序接口。通常需要学习全新的 API,如 thenComposethenAccept,不同平台的 API 也会有所不同。

特定的返回类型。返回类型脱离了我们所需的实际数据,而是返回一个新的 Promise 类型,这就需要进行内省。

错误处理可能很复杂。错误的传播和链式的调用并不总是那么直观、简单。

Reactive extensions 反应式扩展

Reactive Extensions (Rx) 由 Erik Meijer 引入 C#。虽然它在 .NET 平台上得到了广泛应用,但直到 Netflix 将其移植到 Java 并命名为 RxJava 后,它才真正成为主流应用。从那时起,包括 JavaScript (RxJS) 在内的各种平台都有了大量的移植版本。

Rx 背后的理念是转向所谓的可观察流,即我们现在将数据视为流(无限量的数据),而这些流是可以被观察到的。在实际应用中,Rx 就是简单的观察者模式(Observer Pattern),通过一系列扩展,我们可以对数据进行操作。

在方法上,它与 Futures 非常相似,但我们可以认为 Futures 返回的是离散元素,而 Rx 返回的是数据流。不过,与前者类似,它也为我们的编程模型引入了一种全新的思维方式,著名的说法是

“everything is a stream, and it’s observable”。

这意味着处理问题的方式不同了,与我们在编写同步代码时的习惯相比,有了很大的转变。与 Futures 相比,Rx 的一个好处是,由于它已被移植到如此多的平台上,因此无论我们使用 C#、Java、JavaScript 或任何其他可用 Rx 的语言,一般都能找到一致的 API 体验。此外,Rx 在错误处理方面也确实引入了一种更好的方法。

Coroutines

Coroutines:“协程” 是一种可暂停计算的思想,即函数可以在某个时刻暂停执行,稍后再恢复执行。

协程的好处之一是,对于开发人员来说,编写非阻塞代码与编写阻塞代码本质上是一样的。编程模型本身并没有真正改变。

Coroutines 并不是一个新概念,它们已经存在了几十年,在 Go 等其他编程语言中很流行。

以上内容摘自 Kotlin 官网。

多线程可能出现的问题

执行的顺序无法预测、死锁、竞态条件……

在计算机编程中,锁是一种同步机制,用于控制多个线程或进程对共享资源的访问。锁的目的是防止多个线程同时修改共享数据,从而避免数据不一致或其他并发问题。

以下是几种常见的锁类型:

  1. 互斥锁(Mutex):互斥锁是最基本的锁类型之一。在任意时刻,只有一个线程可以持有互斥锁,其他线程必须等待释放锁后才能获取。这确保了同一时刻只有一个线程能够执行被保护的代码段,从而防止数据竞争。

  2. 信号量(Semaphore):信号量是一种更为通用的同步机制,它不仅可以用于互斥访问共享资源,还可以用于控制同时访问某个资源的线程数量。信号量维护一个计数器,线程可以通过执行P(等待)和V(释放)操作来增加或减少计数器的值。

  3. 读写锁(Read-Write Lock):读写锁允许多个线程同时读取共享资源,但只有一个线程能够写入。这种锁适用于读操作频繁而写操作较少的场景,提高了并发性。

  4. 自旋锁(Spin Lock):自旋锁是一种不会使线程进入睡眠状态的锁。当线程尝试获取自旋锁而发现它已经被其他线程持有时,它会一直循环(自旋)等待,直到锁被释放。

  5. 条件变量(Condition Variable):条件变量通常与互斥锁一起使用,用于线程之间的通信。一个线程可以等待某个条件成立,而另一个线程在满足条件时发送信号,唤醒等待的线程。

  6. 轻量级锁(Lightweight Locking):通常用于优化互斥锁的性能。轻量级锁尝试通过使用一些特殊的硬件指令来减小锁的开销,以提高并发性。

  7. 递归锁(Recursive Lock):允许同一线程多次获取同一个锁,而不会造成死锁。线程必须释放相同数量的锁,才能完全释放锁。

锁在多线程和多进程编程中都是非常重要的工具,正确地使用锁可以确保程序在并发环境下的正确性和稳定性。然而,不正确的锁使用可能导致死锁、竞态条件等问题,因此在设计并发程序时需要谨慎使用和管理锁。

除了上述提到的常见锁类型之外,还有一些其他特定场景或特性的锁,这里列举一些:

  1. 分布式锁(Distributed Lock):用于分布式系统中的多个节点之间同步访问共享资源。分布式锁需要考虑网络延迟、节点故障等问题,以确保在整个分布式系统中的一致性。

  2. 公平锁(Fair Lock):公平锁保证等待时间最长的线程最先获得锁,以确保资源的公平分配。相对于非公平锁,公平锁可能导致更大的性能开销。

  3. 写入优先锁(Write-Priority Lock):允许写入操作优先于读取操作,适用于写入操作频繁但读取操作较少的场景。

  4. 悲观锁(Pessimistic Lock):在整个访问过程中保持资源锁定,适用于并发写入较多的情况。常见的悲观锁包括数据库中的排他锁。

  5. 乐观锁(Optimistic Lock):在大部分时间内不加锁,只在更新时检查是否有其他线程对数据进行了修改。如果检测到冲突,再采取相应的处理措施,例如重试或放弃更新。

  6. 时间锁(Timed Lock):允许线程在尝试获取锁时设置最大等待时间,超过该时间仍未获取到锁则放弃。

  7. 自适应自旋锁(Adaptive Spin Lock):根据锁的争用程度动态调整自旋等待时间,以提高性能。

  8. 多粒度锁(Multi-Granularity Lock):允许在不同层次上锁定资源,以提高并发性。

这些锁类型的选择取决于具体的应用场景和性能需求。在设计并发系统时,开发者需要仔细考虑并选择适当的锁机制,以确保系统的正确性、性能和可维护性。

死锁

死锁是多线程或多进程并发编程中一种常见的问题,它发生在两个或多个线程(或进程)相互等待对方释放资源的情况下,导致它们都无法继续执行。死锁通常涉及多个锁,而线程或进程之间的相互等待形成一个闭环。

死锁产生的主要原因包括:

  1. 互斥条件(Mutual Exclusion):资源具有排他性,即一次只能由一个线程或进程占用。如果一个线程已经占用了一个资源,其他线程必须等待。

  2. 占有且等待(Hold and Wait):一个线程在持有资源的同时等待其他资源,而其他线程则可能持有其需要的资源。这样的循环等待可能导致死锁。

  3. 不可抢占(No Preemption):一个线程在没有释放资源的情况下,其他线程无法强制抢占它占用的资源,只能等待。

  4. 环路等待(Circular Wait):存在一个等待循环,使得每个线程都在等待下一个线程释放资源,形成一个环路。

避免死锁的一般方法包括:

  • 按顺序获取锁:规定线程获取锁的顺序,使得所有线程都按照相同的顺序获取锁,从而避免循环等待。

  • 使用超时机制:允许线程在等待一段时间后放弃对资源的请求,以避免死锁。

  • 资源分配策略:确保系统在资源分配时不会导致死锁,可以采用银行家算法等策略。

  • 死锁检测和恢复:定期检测系统中是否存在死锁,并在检测到死锁时采取措施进行恢复,例如强制终止某些线程。

  • 使用锁的层次性:按照资源的层次性进行加锁,确保每个线程按照相同的顺序获取锁。

注意,死锁的发生不仅仅取决于锁的使用,还与程序的设计、资源管理和线程调度等因素有关。因此,在设计并发系统时,需要仔细考虑这些因素,以降低死锁的发生概率。


本站由 江湖浪子 使用 Stellar 1.28.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。