我正在尝试学习python语言及其概念。我写了一些代码来玩多线程。但我注意到多线程和单线程之间没有执行时间差异。
运行脚本的机器有4个核心/线程。
def get_tokens(file_name,map):
print(file_name)
counter = 0
with open(file_name,'r',encoding='utf-8-sig') as f:
for line in f:
item = json.loads(line,encoding='utf-8')
if 'spot' in item and item['sid'] == 4663:
counter+=1
if counter == 500:
break
tokens = nltk.word_tokenize(item['spot'],language='english')
for token in tokens:
if token not in map:
map[token] = 1
else:
map[token] = map[token] + 1;
start_time = time.time()
map = dict();
with ThreadPoolExecutor(max_workers=3) as executor:
for file in FileProcessing.get_files_in_directory('D:\\Raw Data'):
future = executor.submit(FileProcessing.get_tokens, file, map)
end_time = time.time()
print("Elapsed time was %g seconds" % (end_time - start_time))
原始数据中的每个文件的大小都超过25 MB。所以我认为它们之间必须存在差异。但事实并非如此。为什么?我在代码或多线程概念中犯了错误吗?
答案 0 :(得分:5)
原因是臭名昭着的GIL(全球翻译锁定)。 Python的核心不是线程安全的,因为它进行垃圾收集的方式,所以它使用一个锁,这意味着访问python对象的线程一个接一个地运行。
在您的特定情况下,您正在进行一些I / O和一些处理。在python中进行多处理时会产生很大的开销,而I / O的速度增益无法补偿(读取文件的时间与处理文件的时间相比可能很小)。
如果您需要进行真正的多线程处理,请查看Cython(不要与CPython混淆)和“no_gil”,或c-extensions,或多处理模块。
答案 1 :(得分:2)
Python有Global Interpretor Lock (GIL),它可以防止同一个python进程中的两个执行线程同时执行。因此,虽然python线程在单个进程中为您提供了多个控制路径,但多个控制路径无法在多核计算机上同时执行。另一种方法是使用python multiprocessing framework,它将实际创建单独的进程,然后让进程通过进程间通信(IPC)进行通信。您也可以尝试使用ProcessPoolExecutor来生成多个进程,因此您不会遇到GIL问题
答案 2 :(得分:2)
GIL注释是正确的,但是此代码更可能是IO绑定而不是CPU绑定。
即使如果使用C或Java之类的东西,你仍然在通过串行接口读取文件,所以除非你不能处理100-300 MB / s的JSON,否则你赢了看到线程的性能优势。
@DevShark确实说过你会看到IO绑定进程的好处,但它比这复杂得多。对于高延迟的并发网络连接,这往往更多。在这种情况下,您将在磁盘上进行IO绑定,而不是进程(您不等待远程响应),因此并行性将无济于事。
如果你受CPU限制,有真正的线程,并且正在使用旋转磁盘,你仍然需要仔细调整缓冲区大小。 10ms寻道时间可以杀死你,因此你需要使用缓冲区大小的缓冲区读取,如果你想要高磁盘吞吐量的话。使用100MB / s磁盘,寻道时间为10ms,我会使用10MB缓冲区,但这仍然意味着你花费10%的时间在磁盘上寻找。我也会协调我的阅读,所以一次只能读一个读者。
答案 3 :(得分:1)
问题是可以通过线程改进代码。如果代码是串行的,并且有一件事在一条直线上发生,那么无论你有多少线程,代码都将运行相同的代码。但是,如果代码可以分支并同时执行动作a和动作b,那么它将会。仔细观察您的代码,似乎没有分支,至少我不知道。
答案 4 :(得分:1)
您的代码受CPU限制 - 默认情况下,多线程将保留在单个CPU中。您可以使用multiprocessing package来创建子进程,并基本上绕过GIL(全局解释器锁)。当然,这只有在你拥有多个CPU的情况下才有用。我不认为这个用例需要多个进程的开销。