为什么只有一个 worker 的 ThreadPoolExecutor 仍然比正常执行速度快?
Why is a ThreadPoolExecutor with one worker still faster than normal execution?
我正在使用这个库,Tomorrow, that in turn uses the ThreadPoolExecutor 来自标准库,以允许异步函数调用。
调用装饰器 @tomorrow.threads(1)
启动一个带有 1 个 worker 的 ThreadPoolExecutor。
问题
- 为什么使用
1 thread worker
执行一个函数比按原样调用它(例如通常情况下)更快?
- 为什么用
10 thread workers
代替 1 甚至 None 执行相同的代码会更慢?
演示代码
排除进口
def openSync(path: str):
for row in open(path):
for _ in row:
pass
@tomorrow.threads(1)
def openAsync1(path: str):
openSync(path)
@tomorrow.threads(10)
def openAsync10(path: str):
openSync(path)
def openAll(paths: list):
def do(func: callable)->float:
t = time.time()
[func(p) for p in paths]
t = time.time() - t
return t
print(do(openSync))
print(do(openAsync1))
print(do(openAsync10))
openAll(glob.glob("data/*"))
注意:data
文件夹包含18个文件,每700行运行dom文本。
输出
0 名工人: 0.0120 秒
1 名工人: 0.0009 秒
10 个工人: 0.0535 秒
我测试了什么
- 我已经 运行 多次编写代码,后台有不同的程序 运行(运行 昨天有一堆,今天有几个)。数字改变了,ofc,但顺序总是一样的。 (即 1 最快,然后 0 然后 10)。
- 我也尝试过更改执行顺序(例如移动 do 调用)以消除缓存这个因素,但仍然是一样的。
- 事实证明,按
10
、1
、None
的顺序执行会导致不同的顺序 (1 最快,然后是 10,然后是 0) 与所有其他排列相比。结果表明,无论最后执行什么 do
调用,都比先执行或中间执行要慢得多。
结果(从@Dunes 收到解决方案后)
0 名工人: 0.0122 秒
1 名工人: 0.0214 秒
10 名工人: 0.0296 秒
当您调用其中一个异步函数时,它 returns 一个 "futures" 对象(在本例中为 tomorrow.Tomorrow
的实例)。这使您可以提交所有作业,而不必等待它们完成。但是,永远不要真正等待作业完成。所以 do(openAsync1)
所做的只是设置所有作业所需的时间(应该非常快)。为了进行更准确的测试,您需要执行以下操作:
def openAll(paths: list):
def do(func: callable)->float:
t = time.time()
# do all jobs if openSync, else start all jobs if openAsync
results = [func(p) for p in paths]
# if openAsync, the following waits until all jobs are finished
if func is not openSync:
for r in results:
r._wait()
t = time.time() - t
return t
print(do(openSync))
print(do(openAsync1))
print(do(openAsync10))
openAll(glob.glob("data/*"))
在 python 中使用额外的线程通常会减慢速度。这是因为全局解释器锁,这意味着只有 1 个线程可以处于活动状态,无论 CPU 拥有多少个内核。
但是,由于您的工作受 IO 限制,事情变得复杂了。更多的工作线程 可能会 加快速度。这是因为单个线程等待硬盘驱动器响应的时间可能比多线程变体中各个线程之间的上下文切换所浪费的时间更多。
旁注,即使 openAsync1
和 openAsync10
都不等待作业完成,do(openAsync10)
可能更慢,因为它在提交新作业时需要线程之间的更多同步。
我正在使用这个库,Tomorrow, that in turn uses the ThreadPoolExecutor 来自标准库,以允许异步函数调用。
调用装饰器 @tomorrow.threads(1)
启动一个带有 1 个 worker 的 ThreadPoolExecutor。
问题
- 为什么使用
1 thread worker
执行一个函数比按原样调用它(例如通常情况下)更快? - 为什么用
10 thread workers
代替 1 甚至 None 执行相同的代码会更慢?
演示代码
排除进口
def openSync(path: str):
for row in open(path):
for _ in row:
pass
@tomorrow.threads(1)
def openAsync1(path: str):
openSync(path)
@tomorrow.threads(10)
def openAsync10(path: str):
openSync(path)
def openAll(paths: list):
def do(func: callable)->float:
t = time.time()
[func(p) for p in paths]
t = time.time() - t
return t
print(do(openSync))
print(do(openAsync1))
print(do(openAsync10))
openAll(glob.glob("data/*"))
注意:data
文件夹包含18个文件,每700行运行dom文本。
输出
0 名工人: 0.0120 秒
1 名工人: 0.0009 秒
10 个工人: 0.0535 秒
我测试了什么
- 我已经 运行 多次编写代码,后台有不同的程序 运行(运行 昨天有一堆,今天有几个)。数字改变了,ofc,但顺序总是一样的。 (即 1 最快,然后 0 然后 10)。
- 我也尝试过更改执行顺序(例如移动 do 调用)以消除缓存这个因素,但仍然是一样的。
- 事实证明,按
10
、1
、None
的顺序执行会导致不同的顺序 (1 最快,然后是 10,然后是 0) 与所有其他排列相比。结果表明,无论最后执行什么do
调用,都比先执行或中间执行要慢得多。
- 事实证明,按
结果(从@Dunes 收到解决方案后)
0 名工人: 0.0122 秒
1 名工人: 0.0214 秒
10 名工人: 0.0296 秒
当您调用其中一个异步函数时,它 returns 一个 "futures" 对象(在本例中为 tomorrow.Tomorrow
的实例)。这使您可以提交所有作业,而不必等待它们完成。但是,永远不要真正等待作业完成。所以 do(openAsync1)
所做的只是设置所有作业所需的时间(应该非常快)。为了进行更准确的测试,您需要执行以下操作:
def openAll(paths: list):
def do(func: callable)->float:
t = time.time()
# do all jobs if openSync, else start all jobs if openAsync
results = [func(p) for p in paths]
# if openAsync, the following waits until all jobs are finished
if func is not openSync:
for r in results:
r._wait()
t = time.time() - t
return t
print(do(openSync))
print(do(openAsync1))
print(do(openAsync10))
openAll(glob.glob("data/*"))
在 python 中使用额外的线程通常会减慢速度。这是因为全局解释器锁,这意味着只有 1 个线程可以处于活动状态,无论 CPU 拥有多少个内核。
但是,由于您的工作受 IO 限制,事情变得复杂了。更多的工作线程 可能会 加快速度。这是因为单个线程等待硬盘驱动器响应的时间可能比多线程变体中各个线程之间的上下文切换所浪费的时间更多。
旁注,即使 openAsync1
和 openAsync10
都不等待作业完成,do(openAsync10)
可能更慢,因为它在提交新作业时需要线程之间的更多同步。