python 中有 GIL 所以不支持多个线程同时运行,那协程又是什么和线程的区别是什么
3
w568w 1 天前 1
1. python 中有 GIL 所以不支持多个线程同时运行
2. 协程又是什么和线程的区别是什么 我怎么没看懂前后关系? 正经回答:「协程」是一个过度滥用的概念(以及营销术语),你不说清楚具体语境,就有一百种不同的解释。 掰扯这个词本身没什么意思,先说明白你想问的对象是什么。 |
4
pursuer 1 天前
协程的切出点是可以确认的,所以考虑 data race 这类问题时会简单很多,协程的栈(当然我是指把闭包变量当做等价的栈)通常比线程小而且更加灵活
|
5
julyclyde 1 天前
线程是一个操作系统概念,是拥有 pid 的
协程是一种语法结构 |
6
zhangyb123 1 天前
python 在 GIL 的实现下,其多线程的并发可以理解为一个单核设备上不同的线程切换着运行,并不会像多核设备下多个线程并行执行。且 python 会每 5ms 就对占据 GIL 后正在运行的线程进行调度。即停止其运行,释放 GIL ,并重新调度其他线程抢占 GIL 后运行,这种行为是根据操作系统的线程机制来实现的。
而协程,其实是应用层的一个机制,和操作系统无关,就是通过 yield 关键字,将程序逻辑切分,但是无论怎么编写,协程始终是在同一个线程里运行的。 |
7
Abbeyok 1 天前 via iPhone
写爬虫或者 web ,协程真的快很多
|
8
flyqie 1 天前 via Android 1
协程跟线程的区别是一个由编程语言负责调度,一个由操作系统进行调度
本质上协程是线程的包装,协程底层还是走的线程的。 |
9
wangritian 1 天前
cpu 线程:如果你有一块 8 核 cpu 并开启了超线程,那就有 16 个 cpu 线程可用
操作系统线程:每个应用至少有一个进程,每个进程至少有一个线程,操作系统把应用创建的线程动态绑定到任意一个 cpu 线程上 协程:在同一个线程内,应用(编程语言)负责切换上下文,操作系统和 cpu 看不到你在搞事情,最适合 IO 并发任务 |
10
abc612008 1 天前
多线程:
1. 同一时间可以有两个指令在被 CPU 执行。 2. 线程是系统/硬件级的概念。 3. 例子:咖啡店有两个(机器人)店员在同时做咖啡。(你也没法写软件多出一个店员) 协程 1. 同一时间只有一个指令在被执行,但是可以是完全不同地方的指令。 2. 协程是应用级的概念。 3. 一个店员在做两杯咖啡,在等加热的时候去“同时”做另一杯。(因此协程通常只有在 IO bound 的时候才比较有作用) 当然也可以既有多线程也有协程。 我觉得你的问题是,既然 GIL 导致没有办法同时真的执行两个 python 语句,那多线程不就和协程一样了。我的理解是 python 的多线程仍然是多个系统线程,而 GIL 只在 python 代码里会被 lock ,如果是在做 IO bound/syscalls 或者甚至 numpy,pandas 在做计算的时候都会 release GIL 。 |
11
whileFalse 1 天前 1
这玩意你用过 Windows3.1 就知道了。在 3.1 中,多任务的实现方式是程序主动放弃执行,这也导致了一旦有一个应用陷入死循环,整个系统就死了。
协程的好处就是你的代码不会被莫名其妙的打断,不太容易出现多线程中的竞态条件,也不需要锁等东西。坏处是对于那些运行缓慢的代码块,无论是计算密集,还是 IO 密集但你忘了用异步写,都会让程序卡死。 线程的好处是即使跑一个长过程也不会完全 block 住其他 IO 密集型的代码,但要处理多线程的那些问题。 |
12
timerring 1 天前
首先更正一点,只有在 CPython 解释器中才有 GIL 锁,其他解释器例如 JPython 中就没有 GIL 的问题,GIL 本质上是为了保证底层的 C 的存储安全。
我最近写过一篇博客: https://blog.timerring.com/posts/cpu-can-only-see-the-threads 看过能解决你关于 python 中 多线程 多进程 并发 并行 协程 GIL 上下文切换 锁 同步异步 的疑问 核心中的核心就一句话 “CPU 只能看到线程” 如果你能真正理解这句话,你就能理解一切,不信可以读一读,也欢迎进一步斧正或者交流套讨论。 另外我之前也写过在 go 中的实现方式,看过你就能理解 go 的设计到底比 python 好在哪里。具体可以找一找我的博客。 |
15
kaiveyoung 23 小时 48 分钟前 via Android
@xingheng 是有 pid 的,具体哪一章要看你用哪个版本的教材
|
16
w568w 23 小时 33 分钟前 1
@w568w #3 隔了半天回来看,果然大部分回复都按自己的直觉先入为主了(没有说回复不对的意思):
Python / Lua 的非抢占式协同调用(又称生成器), C++20 / Go 的无栈(堆)抢占式微线程, 都能称为「协程」。 还有说「协程一定是单线程的」,等谈到 Java/Kotlin 有 Scheduler 参与的协程时,又要懵逼了。 另外,支持工作分发的 OpenMP 算不算协程?这也不好说。 ---- 至于「线程」和「协程」的区别,品一下它们的目的就知道了: 「线程」:操作系统调度 CPU 资源的最小单位。 「协程」:多程序流协作运作的机制。 「线程」是操作系统的约定,你给操作系统一个程序地址,系统就能为这个程序分配资源。 「协程」是设计上的考虑,是开发者自己设计的、让进行不同逻辑的程序之间协作的机制。 一言蔽之,这俩从概念上就八竿子打不着,根本不是一个 level 的东西。只不过现在很多协程的实现(例如无栈微线程)就是为了解决操作系统线程在协作方面存在的问题(太贵?要考虑并发?写起来麻烦?),所以往往需要涉及线程相关的知识。 看英文也能看出:为什么线程叫 thread 而不是 routine ,协程叫 coroutine 而不是 cothread ?有没有可能它俩根本不是同一种概念? |
17
w568w 23 小时 27 分钟前
|
18
qbqbqbqb 22 小时 53 分钟前
@xingheng “线程有 pid”是 linux 的特色,因为历史遗留问题 Linux 内核里不区分线程进程都统称 task 统一管理。
在 Linux 里 pid 相当于别的系统里的线程 id ,tgid (线程组 id ,等于同一个进程中主线程的 pid )相当于别的系统里的 pid 。 |
19
qbqbqbqb 22 小时 50 分钟前
@kaiveyoung 不是普适的特性,只有 Linux 才是线程有 pid 。像 Windows 这种线程从属于进程的系统就是区分 pid 和 tid 的。
|
20
dearmymy 21 小时 42 分钟前
说实话,能真正理解协程,代表对编程上了一个台阶。
自己真正拿 c 实现下,好处超出想象 |
21
mayli 19 小时 17 分钟前
@kaiveyoung 早期的系统进程和线程分的比较开,但是现在的基本上 thread 也会有 pid, 包括 linux.
|
22
mayli 19 小时 14 分钟前
我觉得主要区别是
线程是系统调度器抢占 cpu, 可以把进程 线程强制踢出 cpu 协程大部分是阻塞时主动让出 cpu, coroutine 的 co 我觉得是 cooperative 。 假如一个协程一直不阻塞,调度器也没法踢他。 |
23
cj323 18 小时 57 分钟前
我的认知里 coroutine 比 thread 和 process 更轻量但是也能共享内存,不过一直没深入理解过。
之前好奇过 python 的 green thread/gevent/asyncio 这些概念有没有区别。以及跟 Erlang 的 process ,Go 的 goroutine ,Node 的 async ,和 Rust 的 tokio 之间有没区别。 |
24
mayli 16 小时 29 分钟前
@cj323 简单说,底层是一样的,或者只有一层上面出来两套
一套是 blocking io ,另一套是 non-blocking io 大部分的 coroutine 都是解决网络 io ( asyncio 默认都不处理本地文件 io ),场景是大部分时间 cpu 都在等网络 io , 比如 webapp 等 db 之类。 python 的话,除了 GIL 部分,gevent 使用的是隐式的方法,相当于所有进到底层 blocking io 的地方,都包( patch )了一遍,强行改成了异步的办法,库用的是 libuv/libev asyncio 用的是显式的写法,你所有碰 io 的地方,都得 asyncio ,然后 asyncio 库再去实现一个 event loop ,然后如果你恰巧用的是 uvloop, 那就跟 gevent+libuv 底层一样了。 对于 go ,由于 goroutine 的 async 是语言级,不是一个库,他实际上可以理解为 gevent 的风格,直接底层把 io 部分包好了。 对于 nodejs ,单线程的部分跟 py 很像,甚至 libuv 本身就是 nodejs 出来的,不过语法上也是要显式的使用 async. tokio 的话,对应的位置应该是 uvloop 。rust 本身 std 有个 async ,tokio 相当于从 0 造了个轮子,包含了 uvloop+libuv 。 语法上要是根据有无显式 async 的话,gevent+go 是一类,其他的都需要显式的写 async await. 底层上除了 tokio/go ,都可以偷懒直接套现有的 event 库,比如 libuv. 综合来看,go 的 async 实现最优雅(原生内置),gevent 对于没有精神洁癖的人来说,性能也过得去,用起来也不难受。 |
25
PTLin 8 小时 42 分钟前
|
26
dearmymy 8 小时 33 分钟前
说下我得理解。
首先进程是有自己一套全部资源。 线程是最小得执行单位,但是绝对大部分资源变量要跟其他线程共享,一个程序运行,至少也有一个主线程进行运行。多跟线程,就是多套执行单元。你起了 10 个线程后,是操作系统在控制执行。 可以简单记住,是操作系统在执行,可以多个线程同时执行 协程可以理解是单个线程下的调度方式。所有的执行顺序是程序主动控制的。 之所以单线程可以做到跟多线程一样,是因为针对很多 io 操作时候,去执行其他代码。所以看起来像是多线程。 假设一个函数是 void request_file() { request_fileA() request_fileB() } 里面分别去请求 A B 两个文件。网络请求是 IO 行为,事实上当 ring3 层请求 A 文件后,处理 io 操作在 ring0 层,ring0 层处理完成后会通知 ring3 ,然后在执行请求 B ,在这个过程中其实是浪费时间,因为请求 A 后等待时间没必要等待。这时候可以直接请求 B ,等 A 完成 io 后在回来继续执行 A 的函数。 这个在单线程里来回乱跳执行,就要在执行 A 需要等待 io 时候,把当前寄存器 栈信息全部保存在一个 context 里,方便恢复环境,强行更改 pc 寄存器去跳转执行 B 函数,在过程中 ring0 通知 io 执行完成,在恢复 A 的 context 把 A 的继续执行。 整个过程就是利用 io 空闲时间,几个函数来回跳,实现在一个线程里看起来像多线程。 这就说明执行过程中 io 操作越多,协程就优势更大。像爬虫或者操作大文件协程就很适合。但是如果都是 cpu 根本抽不出 io 时间间隔,跟执行单线程同步函数一样了。这时候就多线程好。 之前 pc 客户端操作。比如点击一个 button ,去请求一个大数据,并展示在 ui 上。没协程时候,为了不卡界面。只能开线程,或者异步函数,但是异步函数就要有回调。 简单一个功能代码乱的一批。 但是用协程就超级简单。逻辑清晰。 |
27
julyclyde 7 小时 31 分钟前
@xingheng
https://man7.org/linux/man-pages/man7/pthreads.7.html • Threads do not share process IDs. (In effect, LinuxThreads threads are implemented as processes which share more information than usual, but which do not share a common process ID.) LinuxThreads threads (including the manager thread) are visible as separate processes using ps(1). |
28
WorseIsBetter 1 小时 5 分钟前
@julyclyde #27
不过线程不共享 PID 已经是遥远的过去了( LinuxThreads 早在 glibc 2.4 ,也就是 2006 年的时候就不再受支持)。 Linux 2.4 引入了 thread group 的概念,并支持了 CLONE_THREAD 。基于新内核特性的 NPTL (沿用至今的 glibc pthreads 实现)就没有这样的限制。 |