多线程与多进程
一, 什么是进程, 什么是线程?
进程: 运行中的程序. 每次我们执行一个程序, 咱们的操作系统对自动的为这个程序准备一些必要的资源(例如, 分配内存, 创建一个能够执行的线程. )
线程: 程序内, 可以直接被CPU调度的执行过程. 是操作系统能够进行运算调度的最小单位. 它被包含在进程之中, 是进程中的实际运作单位.
进程与线程之间的关系:
进程是资源单位(公司). 线程是执行单位(员工). 就好比是一家公司. 一家公司的资源就是桌椅板凳, 电脑饮水机这些资源, 但是, 我们如果说一家公司正在运转着, 运行着. 那里面必须要有能为这家公司工作的人. 程序里面也一样, 进程就是为了程序运行而需要的各种资源. 但是程序想要运行, 就必须由线程来被CPU调度执行.
运行的每一个程序默认都会有一个线程. 哪怕是只有helloworld级别的程序. 想要执行. 也会有一个线程产生.
如何提高一家公司的产能效率
二, 多线程
顾名思义, 多线程就是让程序产生多个线程一起去执行. 还拿公司举例子. 一家公司里如果只有一个员工, 工作效率肯定不会高到哪里去. 怎么提高效率? 多招点儿人就OK了.
1. 直接用Thread创建线程
我们先看看单线程的效果
1
2
3
4
5
6
7
8
9
|
def func():
for i in range(1000):
print("func", i)
if __name__ == '__main__':
func()
for i in range(1000):
print("main", i)
|
再看多线程
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from threading import Thread
def func():
for i in range(1000):
print("func", i)
if __name__ == '__main__':
t = Thread(target=func)
t.start()
for i in range(1000):
print("main", i)
|
2. 线程池
python还提供了线程池功能. 可以一次性的创建多个线程, 并且, 不需要我们程序员手动去维护. 一切都交给线程池来自动管理.
1
2
3
4
5
6
7
8
9
10
|
# 线程池
def fn(name):
for i in range(1000):
print(name, i)
if __name__ == '__main__':
with ThreadPoolExecutor(10) as t:
for i in range(100):
t.submit(fn, name=f"线程{i}")
|
如果任务有返回值怎么办?
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
|
def func(name):
time.sleep(2)
return name
def do_callback(res):
print(res.result())
if __name__ == '__main__':
with ThreadPoolExecutor(10) as t:
names = ["线程1", "线程2", "线程3"]
for name in names:
# 方案一, 添加回调
t.submit(func, name).add_done_callback(do_callback)
if __name__ == '__main__':
start = time.time()
with ThreadPoolExecutor(10) as t:
names = [5, 2, 3]
# 方案二, 直接用map进行任务分发. 最后统一返回结果
results = t.map(func, names) # 结果是按照你传递的顺序来执行的, 代价就是如果第一个没结束. 后面就都没结果
for r in results:
print("result", r)
print(time.time() - start)
|
3. 多线程在爬虫中的应用
http://www.boxofficecn.com/boxofficecn
我们抓取从1994年到2021年的电影票房.
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
52
53
54
55
|
import requests
from lxml import etree
from concurrent.futures import ThreadPoolExecutor
def get_page_source(url):
resp = requests.get(url)
resp.encoding = 'utf-8'
return resp.text
def parse_html(html):
try:
tree = etree.HTML(html)
trs = tree.xpath("//table/tbody/tr")[1:]
result = []
for tr in trs:
year = tr.xpath("./td[2]//text()")
year = year[0] if year else ""
name = tr.xpath("./td[3]//text()")
name = name[0] if name else ""
money = tr.xpath("./td[4]//text()")
money = money[0] if money else ""
d = (year, name, money)
if any(d):
result.append(d)
return result
except Exception as e:
print(e) # 调bug专用
def download_one(url, f):
page_source = get_page_source(url)
data = parse_html(page_source)
for item in data:
f.write(",".join(item))
f.write("\n")
def main():
f = open("movie.csv", mode="w", encoding='utf-8')
lst = [str(i) for i in range(1994, 2022)]
with ThreadPoolExecutor(10) as t:
# 方案一
# for year in lst:
# url = f"http://www.boxofficecn.com/boxoffice{year}"
# # download_one(url, f)
# t.submit(download_one, url, f)
# 方案二
t.map(download_one, (f"http://www.boxofficecn.com/boxoffice{year}" for year in lst), (f for i in range(len(lst))))
if __name__ == '__main__':
main()
|
三, 多进程
一个公司能创造的价值毕竟是有限的. 怎么办? 开分公司啊. 此所谓多进程. python实现多进程的方案和多线程几乎一样. 非常的简单
1. 直接用Process创建进程
1
2
3
4
5
6
7
8
9
10
11
|
def func():
for i in range(1000):
print("func", i)
if __name__ == '__main__':
p = Process(target=func)
p.start()
for i in range(1000):
print("main", i)
|
2. 多进程在爬虫中的应用
如果遇到图片抓取的时候, 我们知道图片在一般都在网页的img标签中src属性存放的是图片的下载地址. 此时我们可以采用多进程的方案来实现, 一个负责疯狂扫图片下载地址. 另一个进程只负责下载图片.
综上, 多个任务需要并行执行, 但是任务之间相对独立(不一定完全独立). 可以考虑用多进程.
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
52
|
from multiprocessing import Process,Queue
from concurrent.futures import ThreadPoolExecutor
from lxml import etree
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"
}
def get_img_src(q):
"""
进程1: 负责提取页面中所有的img的下载地址
将图片的下载地址通过队列. 传输给另一个进程进行下载
"""
for i in range(1, 11):
url = f"https://www.pkdoutu.com/photo/list/?page={i}"
resp = requests.get(url, headers=headers)
tree = etree.HTML(resp.text)
srcs = tree.xpath("//li[@class='list-group-item']//img[@referrerpolicy='no-referrer']/@data-original")
for src in srcs:
q.put(src.strip())
resp.close()
q.put("ok")
def download_img(q):
"""
进程2: 将图片的下载地址从队列中提取出来. 进行下载.
"""
with ThreadPoolExecutor(20) as t:
while 1:
s = q.get()
if s == 'ok':
break
t.submit(donwload_one, s)
def donwload_one(s):
# 单纯的下载功能
resp = requests.get(s, headers=headers)
file_name = s.split("/")[-1]
# 请提前创建好img文件夹
with open(f"img/{file_name}", mode="wb") as f:
f.write(resp.content)
print("一张图片下载完毕", file_name)
resp.close()
if __name__ == '__main__':
q = Queue() # 两个进程必须使用同一个队列. 否则数据传输不了
p1 = Process(target=get_img_src, args=(q,))
p2 = Process(target=download_img, args=(q,))
p1.start()
p2.start()
|
记住一个事情就好. 多进程相当于多个程序. 多线程相当于在一个程序里多条任务同时执行.