1、线程与进程
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每一个运行中的程序就是一个进程。当一个程序运行时,内部可能包含多个顺序执行流,每一个顺序执行流就是一个线程。所以注意区分多进程与多线程的概念与不同。
一个程序,多个执行流——多线程
- 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个 QQ
- 线程,能够完成多任务,比如 一个 QQ 中的多个聊天窗口
定义的不同
- 进程是系统进行资源分配和调度的一个独立单位.
- 线程是进程的一个实体, 是 CPU 调度和分派的基本单位, 它是比进程更小的能独立运行的基本单位. 线程自己基本上不拥有系统资源, 只拥有一点在运行中必不可少的资源(如程序计数器, 一组寄存器和栈), 但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
- 在硬件层面是没有协程的概念,作业片时间
区别
一个程序至少有一个进程, 一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线程不能够独立执行,必须依存在进程中
关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是 CPU 调度的最小单位”。这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有那么简单了,选的不好,会让你深受其害。所以他也是面试者最喜欢考察的题目之一。
我们按照多个不同的维度,来看看多进程和多线程的对比(注:都是相对的,不是说一个好得不得了,另一个差的无法忍受)
维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据是分开的: 共享复杂,需要用 IPC; 同步简单 | 多线程共享进程数据:共享简单;同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU 利用率低 | 占用内存少,切换简单,CPU 利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度快 | 线程占优 |
编程调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会相互影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布 ;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布 | 进程占优 |
然后我们来看下线程和进程间的比较
多进程 | 多线程 | |
---|---|---|
优点 | 内存隔离,单进程已成不会导致整个应用崩溃。方便调试 | 提高系统的并发性,并且开销小 |
缺点 | 进程间调用,通讯和切换开销均比多线程大 | 没有内存隔离,单线程的崩溃会导致整个应用的推出,发生内存 bug 时,定位及其不方便(回调噩梦) |
使用场景 | 目标子功能交互少,如果资源和性能许可,请设计由多个子应用程序来组合完成。 | 存在大量 IO、网络等耗时操作,或需要与用户交互时,使用多线程有利于提高系统的并行性和用户界面交互的体验。 |
1)需要频繁创建销毁的优先用线程。
实例:web 服务器。来一个建立一个线程,断了就销毁线程。要是用进程,创建和销毁的代价是很难承受的。
2)需要进行大量计算的优先使用线程。
所谓大量计算,当然就是要消耗很多 cpu,切换频繁了,这种情况先线程是最合适的。
实例:图像处理、算法处理
3)强相关的处理用线程,若相关的处理用进程。
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的 server 需要完成如下任务:消息收发和消息处理。消息收发和消息处理就是弱相关的任务,而消息处理里面可能又分为消息解码、业务处理,这两个任务相对来说相关性就要强多了。因此消息收发和消息处理可以分进程设计,消息解码和业务处理可以分线程设计。
4)可能扩展到多机分布的用进程,多核分布的用线程。
5)都满足需求的情况下,用你最熟悉、最拿手的方式。
至于”数据共享、同步“、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,只能说:没有明确的选择方法。一般有一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。
买了一台服务器 2 核 4 线程 部署一个博客项目 2G 内存
python 开发的应用 一个进程一个线程 同一时刻只能处理一个请求 并发数只有 1
并发 项目部署启动 6(2 进程 *(1+2 线程)) 并发数就是 6 线程开的越多 会消耗内存
线程并发有先后顺序,并行同时去做
并行数 最大是 2
并行 同时做多件事情 一起做。
进程
- 不共享任何状态
- 调度由操作系统完成
- 有独立的内存空间(上下文切换的时候需要保存栈、cpu 寄存器、虚拟内存、以及打开的相关句柄等信息,开销大)
- 通讯主要通过信号传递的方式来实现(实现方式有多种,信号量、管道、事件等,通讯都需要过内核,效率低)
线程
- 共享变量(解决了通讯麻烦的问题,但是对于变量的访问需要加锁)
- 调度由操作系统完成
- 一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会小很多)
- 通讯除了可使用进程间通讯的方式,还可以通过共享内存的方式进行通信(通过共享内存通信比通过内核要快很多)
- 线程的使用会给系统带来上下文切换的额外负担。
协程
- 调度完全由用户控制
- 一个线程(进程)可以有多个协程
- 每个线程(进程)循环按照指定的任务清单顺序完成不同的任务(当任务被堵塞时,执行下一个任务;当恢复时,再回来执行这个任务;任务间切换只需要保存任务的上下文,没有内核的开销,可以不加锁的访问全局变量)
- 协程需要保证是非堵塞的且没有相互依赖
- 协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高
2、多线程
2.1 Threading 模块
python 的 thread 模块是底层的模块,python 的 threading 模块是对 thread 做了一些包装的,可以更加方便的被使用
当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在 python 中,默认情况下(其实就是 setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束。
import requests
import re
import threading
import time
def download_one_page(url, name):
print(name,url)
response = requests.get(url)
response.encoding = response.apparent_encoding
html = response.text
text = re.findall('<div id="content" class="showtxt">(.*?)</div>', html, re.S)
print(text)
start_time=time.time()
response = requests.get('http://www.shuquge.com/txt/8659/index.html')
response.encoding = response.apparent_encoding
html = response.text
result = re.findall('<dd><a href="(.*?)">(.*?)</a></dd>', html)
for url, name in result[:5]: // 每一次循环都启动一个新的线程, 不要同时启动太多了
host_url='http://www.shuquge.com/txt/8659/'
t = threading.Thread(target=download_one_page,args=(host_url + url, name))
t.start()
while len(threading.enumerate()) > 1: // 判断子线程是否结束
pass
print(time.time()-start_time()) // 计算子线程全部走完所花的时间
2.2 线程池
为了合理运用内存,我们一般设置每次最多只运行若干个子线程,比如同时要运行 5 个,已经完成了 3 个,为了避免资源浪费,这时候就可以通过线程池进行补调,保证每时每刻都有 5 个线程在同时运行。
线程池模块:concurrent.futures
创建它的最简单方法是作为上下文管理器,使用该 with
语句来管理池的创建和销毁。
import concurrent.futures
if __name__ == "__main__":
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(download_one_page, host_url + url, name)
还可以通过如下方式创建:
import concurrent.futures
thread_pool=concurrent.futures.ThreadPoolExecutor(max_workers=5) // 设置最多子任务为 5
for url, name in result: # 每一次循环都启动一个新的线程
host_url = 'http://www.shuquge.com/txt/8659/'
thread_pool.submit(download_one_page, host_url + url, name)
thread_pool.shutdown()
3、多进程
python GIL 限制了一个程序只能使用一个进程。
3.1 multiprocessing 模块
multiprocessing 是 Python 的标准模块,它既可以用来编写多进程,也可以用来编写多线程。multiprocessing 提供了一个 Process 类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。
import multiprocessing
import time
def upload():
print(" 开始上传文件...")
time.sleep(1)
print(" 完成上传文件...")
def download():
print(" 开始下载文件...")
time.sleep(1)
print(" 完成下载文件...")
def main():
multiprocessing.Process(target=upload).start()
multiprocessing.Process(target=download).start()
if __name__ == '__main__': // 必须有个主程序
main()
还可以这样:
import multiprocessing
if __name__ == '__main__': // 必须有个主程序
for url in url_list:
mp=multiprocessing.Process(target=download_one_page,agrs=(url,))
mp.start()
3.2 进程池
也是采用 concurrent.futures 模块。
import concurrent.futures
process_pool=concurrent.futures.ProcessPoolExecutor(max_workers=5) // 设置最多子任务为 5
for url, name in result: # 每一次循环都启动一个新的线程
host_url = 'http://www.shuquge.com/txt/8659/'
process_pool.submit(download_one_page, host_url+url, name)
process_pool.shutdown()
4、综合案例
采用多进程,多线程的方式爬取小说:
import requests
import re
import time
import concurrent.futures
def get_index():
response = requests.get('http://www.shuquge.com/txt/8659/index.html')
response.encoding = response.apparent_encoding
html = response.text
result = re.findall('<dd><a href="(.*?)">(.*?)</a></dd>', html)
return result
# 多线程
def thread_download_ebook(url, name):
print(name, url)
response = requests.get(url)
response.encoding = response.apparent_encoding
html = response.text
result = re.findall('<div id="content" class="showtxt">(.*?)</div>', html, re.S)
with open(name + '.txt', mode='w', encoding='utf-8') as f:
f.write(result[0].replace('<br/> ', "").replace('<br/>', ""))
# 多进程
def process_download_ebook(urls):
# 每一个进程 启动五个线程 25
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=5)
for url, name in urls:
# 往线程池里面放任务
thread_pool.submit(thread_download_ebook, 'http://www.shuquge.com/txt/8659/' + url, name)
# 等待线程池关闭
thread_pool.shutdown()
if __name__ == '__main__':
content_list = get_index()[:20]
length = int(len(content_list)/5) # 把所有任务分成五份
start_time = time.time()
# 启动五个进程
# 进程之间的相互通信 默认情况下 进程之间的变量不共享数据
process_pool = concurrent.futures.ProcessPoolExecutor(max_workers=length)
for i in range(length):
if i == length:
i += 1
process_pool.submit(process_download_ebook, content_list[i * length:(i + 1) * length])
process_pool.shutdown()
print(time.time() - start_time)
欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net