python 进程池 线程异常抛出_Python线程池和进程池

Python3.2之后,标准库里引入了concurrent.futures模块,为异步调用提供了高级的接口。在此记录下我对其中的ThreadPoolExecutor和ProcessPoolExecutor类的学习和理解。

ThreadPoolExecutor

ThreadPoolExecutor是Executor类的子类。它有一个参数是max_workers,指定了线程池中最多同时执行的线程数量。这个是最常用的参数,3.5版本之后又增加了几个参数,但是不常用,可以去文档里查看。

1个线程

开启1个线程访问https://www.baidu.com/

1

2

3

4

5

6

7

8

9

10

11

12from concurrent.futures import ThreadPoolExecutor

import requests

def (url):

return (url, requests.get(url).headers)

with ThreadPoolExecutor(1) as executor:

f = executor.submit(get_headers, 'https://www.baidu.com/')

print(f.result())

3个线程

开启3个线程分别访问https://www.baidu.com/、https://www.163.com/、https://www.qq.com/

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17from concurrent.futures import ThreadPoolExecutor, as_completed

import requests

def (url):

return (url, requests.get(url).headers)

with ThreadPoolExecutor(3) as executor:

fs = [

executor.submit(get_headers, 'https://www.baidu.com/'),

executor.submit(get_headers, 'https://www.163.com/'),

executor.submit(get_headers, 'https://www.qq.com/'),

]

for f in as_completed(fs):

print(f.result())

也可以使用map函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17from concurrent.futures import ThreadPoolExecutor

import requests

def (url):

return (url, requests.get(url).headers)

with ThreadPoolExecutor(3) as executor:

urls = [

'https://www.baidu.com/',

'https://www.163.com/',

'https://www.qq.com/',

]

for result in executor.map(get_headers, urls):

print(result)

需要注意的是,使用map函数时,返回的结果的顺序和Python内置的map函数一样,是按照传递进去的顺序来的,并不是真正的它们完成的顺序。比如在上面的代码里,第一个返回的永远是baidu的请求。通过查看源码也印证了这一问题。

Executor.map的源码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23def map(self, fn, *iterables, timeout=None, chunksize=1):

if timeout is not None:

end_time = timeout + time.monotonic()

fs = [self.submit(fn, *args) for args in zip(*iterables)]

# before the first iterator value is required.

def result_iterator():

try:

# reverse to keep finishing order

fs.reverse()

while fs:

# Careful not to keep a reference to the popped future

if timeout is None:

yield fs.pop().result()

else:

yield fs.pop().result(end_time - time.monotonic())

finally:

for future in fs:

future.cancel()

return result_iterator()

先调用fs.reverse转置列表,然后不断调用pop取最后一个的result。

死锁1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19import time

from concurrent.futures import ThreadPoolExecutor

def a_work():

time.sleep(5)

print(b.result())

return 'a'

def b_work():

time.sleep(5)

print(a.result())

return 'b'

with ThreadPoolExecutor(2) as executor:

a = executor.submit(a_work)

b = executor.submit(b_work)

这个程序永远都不会运行结束,因为线程a在等待b的结果,b在等待a的结果。其中的time.sleep(5)非常关键,它可以保证这两个线程都开始运行了。

去掉它之后,可以发现程序居然可以运行结束了,但是却没有任何的输出。这其实就是第二个常见错误,只有调用在future对象上调用result()函数之后,异常才可以被捕获。

异常1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20from concurrent.futures import ThreadPoolExecutor

def a_work():

print(after_b)

print(b.result())

return 'a'

def b_work():

print(a.result())

return 'b'

after_b = False

with ThreadPoolExecutor(2) as executor:

a = executor.submit(a_work)

b = executor.submit(b_work)

after_b = True

print(a.result(), b.result())

运行上面的代码,可以发现程序输出了一个False,和一个异常:NameError: name 'b' is not defined,这是因为a线程已经开始运行了,但是b还没有开始,即,submit函数还没有返回,那么这个时候after_b的值仍然是False,而且变量b还是未定义的。

我们之前的代码,之所以没有出现异常,就是因为没有调用result函数。下面的代码可以更直观的说明问题。

1

2

3

4

5

6

7

8

9

10from concurrent.futures import ThreadPoolExecutor

def raise_exception():

raise ValueError('123')

with ThreadPoolExecutor(1) as executor:

a = executor.submit(raise_exception)

# a.result()

运行这段代码,不会有任何输出。把a.result()这一行的注释取消,就可以看到异常:ValueError: 123。

ProcessPoolExecutor

和ThreadPoolExecutor的用法基本上类似,不同之处在于,只有可以被pickle库序列化的对象才可以被执行和返回。因为需要在不同的进程之间传递消息。这个类使用了multiprocessing库。

为什么要使用进程池和线程池?

在我的理解看来,线程和进程的创建是需要开销的,而之前创建的线程或进程在执行完了任务之后,可以先不销毁,继续用来执行以后的任务。还有一个优势就是方便对这些创建的进程和线程进行统一的管理。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值