本篇概要
1. 线程与多线程
2. 进程与多进程
3. 多线程并发下载图片
4. 多进程并发提高数字运算
号: 960410445
群里有志同道合的小伙伴, 互帮互助,
群里有不错的视频学习教程和 PDF!
关于并发
在计算机编程领域, 并发编程是一个很常见的名词和功能了, 其实并发这个理念, 最初是源于铁路和电报的早期工作. 比如在同一个铁路系统上如何安排多列火车, 保证每列火车的运行都不会发生冲突.
后来在 20 世纪 60 年代, 学术界对计算机的并行计算开始进行研究, 再后来, 操作系统能够进行并发的处理任务, 编程语言能够为程序实现并发的功能.
线程与多线程
什么是线程
一个线程可以看成是一个有序的指令流(完成特定任务的指令), 并且可以通过操作系统来调度这些指令流.
线程通常位于进程程里面, 由一个程序计数器, 一个堆栈和一组寄存器以及一个标识符组成. 这些线程是处理器可以分配时间的最小执行单元.
线程之间是可以共享内存并且互相通信的. 但是当两个线程之间开始共享内存, 就无法保证线程执行的顺序, 这可能导致程序错误, 或者产生错误的结果. 这个问题我们日后会专门提及.
下面这个图片展示了多个线程在多个 CPU 中的存在方式:
image
线程的类型
在一个典型的操作系统里面, 一般会有两种类型的线程:
1. 用户级线程: 我们能够创建, 运行和杀死的线程;
2. 内核级线程: 操作系统运行的低级别线程;
Python 工作在用户级线程上, 我们介绍的内容也主要是在用户级的线程上运行的.
什么是多线程
现在的 CPU 基本上都是多线程的 CPU, 比如我们随意从京东上找一个 Inter 的酷睿 i5 处理器, 看看它的产品规格:
image
这些 CPU 能够同时运行多个线程来处理任务, 其实从本质上来说, 这些 CPU 是利用一个能够在多个线程之间快速切换的单个内核来完成多线程的运行的, 切换线程的速度足够快, 所以我们并不会感觉到. 但实质上, 它们并不是同时运行的.
为了形象的理解多线程, 我们来回忆一个场景.
在大学时代, 期末的时候, 有些科目的老师为了不为难大家, 把考试设为开卷考试, 不知道大家面对开卷考试的时候, 做题的顺序是怎样的?
在单线程的工作模式下, 我们从选择题到填空题到简答题再到分析题, 一个一个按顺序的写.
遇到一个特别难的题目, 我们就要翻书翻资料了, 当然既然是开卷考试, 有些题目的答案就不可能直接出现在教科书中, 那么我们就要花费更多的时间来找答案, 直到考试结束, 因为某个难题耗费的翻书时间太多, 导致后面一些简单的题目也没用做, 嗯, 开卷都写不完试卷, 挂科名额就给你了.
而在多线程的工作模式下, 我们也是按顺序写, 但是遇到难题时, 我们会稍微从书中找找答案, 如果没找到, 就先做下面的题目, 把会做的题目做好, 做好了容易的题目, 再回到那个难题上, 仔细从书中的蛛丝马迹中找答案.
在这个例子里面, 我们只是一个人来完成, 如果想要更快地完成考试, 就得跟其他同学通力合作和分工了.
让我们看看线程的一些优点:
1. 多线程能够有效提升 I/O 阻塞型程序的效率;
2. 与进程相比, 占用的系统资源少;
3. 线程间能够共享资源, 方便进行通信;
线程还有一些缺点:
1.Python 中有全局解释器锁 (GIL) 的限制;
2. 虽然线程之间能够进行通信, 但是容易导致程序结果出错, 使用的时候必须小心;
3. 在多线程之间切换的计算代价高, 会导致程序的整体性能下降.
进程与多进程
进程在本质上与线程非常相似, 进程几乎可以完成线程能够完成的任何事情.
按照上面开卷考试的例子, 如果我们和室友组成一个小团伙, 那么我们就有四个 CPU(4 个人), 四个人分别写和找不同的答案, 这样考试的效率会提高很多.
一个进程里面, 包含一个主线程, 还可以生成很多子线程, 每个线程都包含自己的寄存器组合堆栈. 如果有需要的话, 可以将它们组成多线程.
下面是单线程单进程和多线程单进程的示例:
image
进程的特性
一个进程通常包含以下的内容:
1. 进程 ID, 进程组 ID, 用户 ID, 组 ID
2. 环境
3. 工作目录
4. 程序指令
5. 寄存器
6. 堆栈
7. 文件描述
8. 进程间通信工具
9. 等等......
进程有以下优点:
1. 更好地利用多核处理器;
2. 在处理 CPU 密集型任务时比多线程要好;
3. 可以通过多进程来避免全局解释器锁 (GIL) 的局限;
4. 崩溃的进程不会导致整个程序的崩溃;
同时, 还有以下缺点:
1. 进程之间没有共享资源;
2. 进程需要消耗更多的内存;
多进程
在 Python 中我们可以使用多线程或者多进程的方式来运行我们的代码以改进传统的单线程方式的性能.
在单核的 CPU 上可以使用多线程提高处理能力, 但是在现在的计算机 CPU 中, 多核处理器早已普及, 为了有效的利用机器的资源, 我们有必要使用多进程来发挥机器的价值.
一个 CPU 内核将任务分配给其他 CPU:
image
通过 Python 的进程处理模块 multiprocessing, 我们可以有效的利用机器上所有的处理器, 这有助于我们在处理 CPU 密集型任务时获得更高的性能.
使用 multiprocessing 模块, 查看我们机器上的 CPU 核心数量:
image
结果返回一个数字, 为 CPU 核心数.
多进程不仅能够提高我们的计算机的利用率, 还能够避免全局解释器锁的限制, 一个潜在的缺点是多进程间不能进行共享和通信(可以通过其他手段实现), 但是这个缺点同时也使多进程更加容易使用和避免出现崩溃.
Python 的局限性
在文章的前面, 我们谈到了在 Python 中存在的全局解释器锁 GIL 的局限性. 那 GIL 到底是个什么东西?
GIL 本质上是一个互斥锁, 它可以防止多个线程同时执行 Python 代码. 它是一个只能由一个线程保持的锁, 如果你想要一个线程去执行代码, 那么在它执行代码之前, 首先必须获得这个锁. 这样做的一个好处是, 当它被锁定的时候, 没有别的进程可以同时运行代码, 一定程度上避免了线程间的冲突:
image
上面这个图说明了多个线程如何被 GIL 阻塞. 每个线程必须等待获取到 GIL 才能进行下一步的运行, 然后再释放 GIL. 线程之间使用随机循环的方式, 所以并不能控制和保证哪个线程会先得到 GIL.
这样的设计似乎很反人类, 而这也是很多人诟病 Python 的地方. 但是, 这个设计确实是保证的多线程之间的内存安全.
现在我们已经了解了线程和进程, 以及 Python 的一些限制, 现在是时候了解一下我们如何在应用程序中使用多线程多进程, 以提高程序的速度.
并发文件下载
毫无疑问的, 展现多线程优点的一个例子就是使用多线程来下载多个图片或者文件, 由于 I/O 的阻塞性质, 下载任务可能是多线程最佳的运用场景了.
http://tool.bitefu.net/jiari/data/2017.txt 是一个提供 2017 年所有节假日的文本文件:
image
我们访问 10 次, 获得 10 次文本文件, 然后保存在本地.
先看看一个普通的爬取:
image
我们引入了模块 urllib.request, 然后创建了一个函数 downloadImage()用于下载文件, 创建了一个函数 main()用于对下载函数进行遍历 20 次.
image
耗时 4 秒多.
下面看看使用多线程的:
image
程序的前部分大同小异, 后面我们创建了一个 threads 列表,, 然后遍历 10 次, 创建一个新的线程对象, 将其添加到 threads 列表中, 然后启动该线程.
最后, 我们通过遍历我们的 threads 列表来调用我们的线程, 然后调用 join()方法在每个线程上, 这确保我们在下载完文件之前, 不会执行剩下的代码.
image
运行代码, 可以发现程序几乎同时启动了 10 个下载任务, 然后在图片下载完成后, 再打印出来.
耗时 0.1 秒, 效率提高很多.
但是需要注意的是, 在网络中进行文件 IO, 还需要考虑网络状况和自身机器的影响, 不同的网络状况下, 完成的效率也不一样.
并发数字运算
I/O 密集型的任务适合于多线程, 而 CPU 密集型的任务则适合用多进程.
在下面的例子里, 我们将找出 100 万个 20000 到 100000000 之间随机数的质数.
顺序运算:
image
image
耗时 18 秒.
多进程运算:
image
image
耗时 11 秒.
我们分别按顺序循环 100 万遍和使用多进程的进程池循环 100 万次, 多进程模式下速度提升了近 7 秒.
来源: http://www.jianshu.com/p/696dc77d8f7c