Python3中的线程,GIL,线程安全(上)

举报
xenia 发表于 2019/09/03 23:06:46 2019/09/03
【摘要】 假期看了一篇关于 Python3 线程的文章,感觉非常棒,特意分享给大家。说它好在于完完全全解答了我的很多疑问,以一种更高阶的方式解读 Python3 中的线程,尤其现在大家都在说异步编程,很容易在线程和异步之间做比较,而这篇文章无疑解释的很好。另外读完这篇文章也在想,为什么大家有这么多错误的理解?难道深入一门语言一定要研究语言的源码吗?我们如何才能更加高效的掌握一个技能?这篇文章从三部分解...

假期看了一篇关于 Python3 线程的文章,感觉非常棒,特意分享给大家。说它好在于完完全全解答了我的很多疑问,以一种更高阶的方式解读 Python3 中的线程,尤其现在大家都在说异步编程,很容易在线程和异步之间做比较,而这篇文章无疑解释的很好。

另外读完这篇文章也在想,为什么大家有这么多错误的理解?难道深入一门语言一定要研究语言的源码吗?我们如何才能更加高效的掌握一个技能?

这篇文章从三部分解释了线程,分别是原理、实践、解疑,为了把事情说清楚,我分两篇文章说明,关于代码实践部分在下一篇介绍。


什么是线程?

在过去,线程认为是一种轻量级的任务,比如主程序在后台启动五个线程,在需要的时候可以将五个线程运行的结果汇总在一起。

当启动python程序的时候,会产生一个包含主线程的新进程,如果你需要,可以并行运行多个任务(即线程)。

一个常见的例子就是主线程等待网络连接,然后将接收到的连接交给其他线程去处理。

线程由操作系统管理,在大多数情况下,程序调用 pthreads C 库让操作系统来创建新进程,CPython 也是如此工作的。

可怕的线程

线程非常强大但也很难使用:因为它们共享相同的内存空间。

正因为共享,所以线程非常的轻量,产生一个线程仅仅额外需要一点点内存,内核有足够的内存处理线程栈。

这也意味着同个进程的每个线程能够互相访问其他线程。比如一个线程处理一个网络套接字,并不能保证仅仅由它来处理,在同一时刻,其他线程也能处理该套接字,比如修改、关闭、销毁。

很多和线程打交道的程序员都有一个共识:使用线程编写代码很难。

线程安全

线程编码很困难,但已经存在很长时间了,很多程序员倾向使用它,所以必须尽可能找到一种方式和它共存。

一些优秀的开发者发明了很多可以和线程良好共存的可重用的工具和库,这些 API 可以避免线程陷阱,这些 API 是线程安全的。

在 Python 中,线程安全通常通过避免共享可变状态来实现,重点就是避免修改线程共享的数据。

当然完全避免共享可变状态是不可能的,线程在协作的时候必须:

  • 所有的线程可以读,完全没有线程在写入数据。

  • 一个单独的线程在写,没有其他线程在读。

如果要理解线程,这两点必须要记住。

Rust在编译代码的时候能够保证代码是线程安全的,如果你经常编写与线程有关的代码,可以采用它。

使用 Python 编写线程代码的难点在于你无法证明代码是线程安全的,就算有单元测试、静态分析器也没有用,唯一要做的就是小心编写代码。

CPython,GIL,原子操作

网络上有很多对 Python 线程的误解,最糟糕也是最流行的错误观点是“Python不能使用线程”,如果你不使用线程,那么你就不会犯错,这是你不使用 Python 线程的原因吗?

另外一种没有危险的说法:由于 GIL 的存在,能够保证 Python 线程编码是线程安全的。

GIL 是由 CPython 实现的,GIL 能够避免 Python 内部出现和线程安全有关的一些错误(并不是为了线程安全而产生的)。

1:Python 绝对能使用线程,不要有丝毫疑问

CPython 使用操作系统原生的线程,GIL 能够确保同一时刻仅有一个线程执行 Python 字节码,潜台词是 CPython 无法有效使用多核。

这个特性让 Python 很适合有大量 IO 操作的任务(这些任务不依赖于多核 CPU)。

如果是 CPU 密集操作,即使你有多核,使用 CPython 多线程编码是很糟糕的,如果要发挥多核的能力,为规避 CPython GIL 带来的问题,有以下两种解决方案:

  • 用原生 C 代码编写,某些库(比如 Numpy)不会有 GIL 带来的问题。

  • 用多进程代替多线程。

2:GIL 能让 Python 代码线程安全吗?

不会,GIL 不会让编写的 Python 代码线程安全,但会保证某些 Python 基本操作原子性。其他一些 Python 实现,比如 Pypy 不会有相同的保证。

重要的是,GIL 并不完全保证 Python 的基本操作是原子性的,带来的影响就是很难校验代码是线程安全的。

一些常见的问题

1:线程安全的代价

如果你编写多线程代码,额外的付出就是内存的消耗,具体消耗依赖很多因素,每个线程至少消耗 32KB。

当操作系统决定切换线程的时候,上下文切换也是要付出 CPU 时间的,和其他 Python 操作相比,这些消耗可以忽略不计。一个相对较新的操作系统很容易同时处理1万个线程。

线程需要有很多的开发成本,多线程程序开发比单线程程序开发有更多的复杂性。

2:多线程还是异步IO

关于这个问题很容易迷失,有很多异步IO库,但 Python 生态系统仍然在寻找社区都满意的异步 IO 库,这些第三方库多多少少存在一些问题。

换句话说,第三方库视同步的线程代码为一等公民,如果你忘记异步 IO,会从这种开发模型中获益。

对于开发人员来说也是如此,写同步代码是相对容易的,每个开发者都知道这一点,而想找到在有经验的异步编码的人则相对较难。

3:多线程有没有落伍?

现在如果你关心一些流行的开发语言,很容易会这么想(比如我),比如 Nodejs 仅使用单线程运行时间循环;Go 语言对开发者隐藏了多线程,提供一种更简单的接口进行并发编程。

但多线程仍然是很多任务首选的解决方案,比如文件系统的操作;Linux 并没有提供异步 API;甚至很多重度依赖异步 IO 的软件也会使用多线程,比如 Nginx 也会使用线程池避免异步操作变慢。

值得一提的是 CPU 的频率增速已经逐步停止了,现在是多核的时代,要么使用多线程,要么使用多进程。


本文转载自异步社区

原文链接:https://www.epubit.com/articleDetails?id=N9ec67b27-1ff2-4dff-a0f4-b3fa2bc65f5b


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。