需了解

参考:
Piotr Kołaczkowski:How Much Memory Do You Need to Run 1 Million Concurrent Tasks?

线程

超线程:超线程技术(Hyper-Threading Technology)是由英特尔推出的一项技术,它可以在单个物理处理器核心上模拟多个逻辑处理器核心,以提高处理器的并行度和整体性能。一般单核会被模拟出一个线程(共2线程)。

单线程

  • 简单的理解只有一个工人
  • 相当于单核CPU(未使用超线程技术)
  • 人脑就是单线程

多线程

  • 简单的理解同时有多个工人
  • 多核心CPU
  • 使用超线程技术

同步异步任务

无论同步任务还是异步任务都是相对于单线程而言的

同步任务

必须按照顺序的一直执行下去,不能执行其他操作

你在做一件非常复杂的数学题,此时你只能专注这件事,并一步步地解决,不能被打扰

异步任务

在做一件事情的同时,另一件以上的事情同时在进行

一个餐厅里只有一个人(厨师兼服务员),同时要服务多个顾客,点餐的过程手动发给他们每人一个小本本(记录菜单),假设这个人有超能力可以快速移动、记录事件、窃听状态等,就可以一边做饭的同时也在等待所有顾客的点餐、就餐等耗时任务。

此时这个超能力就可以被理解为事件循环(Event Loop)

事件循环

常见于许多编程语言:JavaScript Python Java Rust Go Ruby Swift

  1. 等待阶段(Waiting Phase):

    事件循环开始时,会处于等待状态,等待着异步任务的触发。

  2. 获取事件(Event Acquisition):

    一旦有事件发生,事件循环会获取该事件并进行处理。这些事件可以是用户输入、网络请求、定时器到期等等。

  3. 事件处理(Event Handling):

    事件循环会调用相应的回调函数或者处理器来处理获取到的事件。这些处理器可能包括用户定义的回调函数、系统提供的事件处理函数等。

  4. 执行任务(Task Execution):

    在处理完事件后,事件循环可能会执行与事件相关的任务。这些任务可以是异步的,可能涉及IO操作、计算密集型任务等。

  5. 更新UI(Update UI)(在GUI编程中常见):

    如果事件循环用于GUI编程,它可能会在处理完事件后更新用户界面,以反映最新的状态或者用户交互。

  6. 返回等待阶段(Return to Waiting Phase):

处理完事件后,事件循环会返回到等待状态,等待下一个事件的发生。

可异步任务

  • HTTP请求响应
  • I/O 操作
  • 定时等待操作

语言

Python

我认为Python语言并不是真正意义上的多线程,Python的多线程是在操作系统级别上实现的,因为Python标准库中的threading模块使用了操作系统的原生线程,它可以创建多个线程(一个主线程+多个副线程)并行执行任务,但由于 Python 全局解释器锁(GIL)的存在,在多核 CPU 上它的多线程在任何时刻只有一个线程能够执行 Python 字节码,比如每个子线程的join()就是权给主线程。这就意味着 Python 多线程在 CPU 密集型任务上并不能实现性能的提升,但对于 I/O 密集型任务(如网络请求、文件操作等),多线程依然是有效的。

当然可以使用 multiprocessing 模块,该模块支持在多个进程之间并发执行任务,每个进程都有自己独立的内存空间,从而避免了全局解释器锁(GIL)的限制,并能够充分利用多核 CPU。
参考:
YouTube-码农高天:【python】听说Python的多线程是假的?它真的没有存在的价值么?

IO密集型任务

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
44
words = list(range(6))

class Task(threading.Thread):
def __init_ (self, words):
self.words = words
self.total_word = 0
super()._init_()

def run(self):
for word in self.words:
requests. get(f"https: //en.wikipedia.org/wiki/{words}")

t = Task(words)
t1 = Task(words[: len(words) // 2])
t2 = Task(words[len(words) // 2: ])

start = time.time()
# 启动一个新的线程
t.start()
# join() 用于线程间的协调,相当于主线程让权给新线程,等待线程或进程的完成,主线程继续
t.join()

print(time.time() - start)

start = time.time()
t1.start()
t2.start()
# 主线程会等待两个线程执行完毕。主线程才会继续
t1.join()
t2.join()
print(time.time() - start)

'''
执行结果:
$ python example.py
1.0070123672485352
0.6659753322601318
$ python example.py
1.1642413139343262
0.6097736358642578
$ python example.py
1.044523000717163
0.6331624984741211
'''

以上代码其实更适合使用协程来完成,并且比操作系统级别的线程切换更轻便,没有竞争冒险问题(例:读者-写者问题、哲学家就餐问题、等),可见协程在网络传输的应用里更合适(例:爬虫)。

异步

  • async和await是用于异步编程的关键字,引入了异步/协程(coroutine)的概念。
  • 当函数被async关键字修饰时,该函数会返回一个协程对象(coroutine object)。
  • 当await关键字在函数中使用时,它会暂停(放权)当前协程的执行,直到等待的异步操作完成,此时协程中其他任务可以被继续执行。
  • Python的异步操作需要一个事件循环(event loop),例如asyncio库提供的循环,来管理和调度执行。

Python多线程用途

那么Python的多线程就没有意义了吗?并非如此,可以将协程和Python的多线程结合起来处理 大量计算密集任务 同时处理 低延迟的小任务
但是,如果单纯的使用协程处理以上的问题,就会出现执行大量计算密集型的时候导致无法放权给小的低延迟任务,这时就需要操作系统级别的线程来处理让权问题

例如以下代码中的fib()是一个计算密集型任务,此任务会造成计算过程无法放权导致其他低asyncio.sleep(0)延迟任务无法被执行,这会导致在处理大量密集型任务的时候没有同时处理sleep()操作,从而导致总处理时间相比同时执行两种任务的时间更长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async def long_task():
async def fib(n):
if n <= 1:
return 1
return await fib(n - 1) + \
await fib(n - 2)
while True:
await fib(25)
await asyncio.sleep(0)

async def short_task():
while True:
await asyncio.sleep(0.01)

async def main():
task_list = [short_task() for _ in range(4)]
task_list.append(long_task())
await asyncio.gather(*task_list)

asyncio.run(main())

如果加入多线程,将计算密集型任务扔给一个操作系统级别的线程,此时的情况就会得到改善,虽然依旧同一时刻只能处理一个线程,但操作系统级别的多线程会放权给另一个处理权给小的低延迟任务线程,从而实现执行大量密集型任务的同时也在处理小的低延迟任务(IO、请求响应等…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def long_task():
while True: :
Fib(25)

async def short_task():
while True:
await asyncio.sleep(@.01) :

async def main():
task_list = [short_task() for _ in range(4)] :
t = threading. Thread(target=long_task)
t.start()
await asyncio.gather(*task_list)

asyncio. run(main()) |

Go

Go语言中的多线程是基于goroutines和channel的。Goroutines是Go语言中的轻量级线程,它们是由Go运行时(runtime)进行调度和管理的。使用go关键字可以轻松地创建一个新的goroutine,例如go f(x, y, z)会在一个新的goroutine中执行函数f。

Goroutines在设计上是为了使并发编程更加简单和高效。它们在用户空间内进行调度,这意味着它们的创建和管理不需要操作系统内核的直接干预。创建和切换的开销远低于传统的操作系统级线程。

Channel是Go语言中用于goroutines之间的通信的同步原语。它们提供了一种方式,让goroutines之间可以安全地交换信息,无需担心并发访问的问题。

Go并发模型

Golang 的并发模型基于 CSP(Communicating Sequential Processes)理论,通过通道(channels)来实现 goroutine 之间的通信。它的运行时环境会维护一个由一组操作系统线程组成的线程池,称为M
调度器(M:操作系统线程,N:goroutine)。在这个调度器中,多个 goroutine 可以被调度到少量的操作系统线程上并行执行,不直接依赖于操作系统的线程管理,而是由 Go 运行时环境在操作系统线程上进行调度和管理。

Go与操作系统线程管理

当一个新的 goroutine 被创建时,Golang 运行时会将其放入调度队列中,并决定将其调度到哪一个空闲的操作系统线程上执行。这种调度方式可以根据系统负载和资源利用情况动态地进行调整,从而实现高效的并发执行。

总的来说,Go语言的多线程模型是一种用户级线程模型,但是它通过运行时的智能调度,能够有效地利用多核处理器的并行性能。这种模型在处理并发任务时既高效又易于管理。

真正的多线程

golang 多线程一般主线程无需等待子线程,可以理解成主线程和子线程是同时执行的

这个函数就说明了golang主线程启动一个子线程,当gen中这个子线程开始执行的同时主线程可以打印“执行return!”,无需调用像Python中main函数调用子线程join()这样的函数等待子线程完成。

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
44
45
46
47
48
49
50
51

package main

import (
"context"
"fmt"
)


func main() {

gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
// 当收到context执行cancel()时,ctx.Done()就会传出结束上下文的消息,可防止goroutine 泄漏
case <-ctx.Done():
return
case dst <- n:
n++
}
}
}()
fmt.Println("执行return!")
return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
// 当执行break 意味着接下来defer的执行,cancel()执行使得gen中goroutine接收到上下文结束消息执行return
break
}
}
}

// 执行结果

/*
执行return!
1
2
3
4
5
*/

JavaScript

在浏览器中,JavaScript 是单线程执行的。这意味着在任何给定时间点,JavaScript 代码只能由一个线程执行。这个单线程通常称为主线程或 UI 线程。

NodeJS中JavaScript 也可以实现多线程编程,例如Worker Threads API 允许开发者在 Node.js 中创建独立的线程,这些线程可以执行 CPU 密集型任务、并行处理数据或执行其他需要并发执行的操作。这些线程是由操作系统调度和管理的

但是,需要注意的是,Node.js 是单线程的事件驱动模型,主线程上的事件循环仍然是单线程的,因此在任何给定时刻只有一个事件在主线程上执行。而 Worker 线程的执行是在独立的 JavaScript 执行环境中进行的,它们可以并行执行代码,它们之间通过线程间的消息传递机制进行通信,但不会影响主线程的事件循环。

启动每个 Worker 线程都会消耗一定的系统资源,包括内存和 CPU 资源。每个 Worker 线程都有自己的 JavaScript 执行环境和相关的资源,因此在启动大量的 Worker 线程时需要考虑系统资源的限制。

异步

async和await

  • async和await是处理异步操作的关键字,它们基于Promises。
  • 一个用async关键字声明的函数会返回一个Promise对象。
  • 当await被用于一个异步操作时,它会暂停(放权)该函数的执行,直到Promise解决(resolved)。
  • JavaScript的事件循环(event loop)是其运行时环境的一部分,所有异步行为都是通过这个事件循环来管理的。

Java

Java 中,多线程的实现是依赖于操作系统线程的。Java 的线程是由 Java 虚拟机(JVM)在操作系统上创建和管理的。在大多数情况下,Java 中的多个线程会映射到多个操作系统线程上。

Java 虚拟机会根据底层操作系统的线程调度机制来调度 Java 线程。这意味着在同一时刻,Java 中的多个线程可能会被操作系统调度为在不同的核心上并行执行,因此它们可以同时运行。

然而,需要注意的是,并不是每个 Java 线程都会直接映射到一个操作系统线程上,具体的映射方式取决于 JVM 的实现。有些 JVM 可能会使用一种称为“用户级线程”的技术,将多个 Java 线程映射到较少的操作系统线程上,从而提高线程的创建和销毁效率,但在某些情况下可能会降低并行度。