一些基本概念
线程:利用cpu和io可以同时执行的原理,让cpu不会傻傻的等待io完成。
进程:利用多核cpu的能力,真正的并行执行任务。
cpu密集型:cpu密集型也叫计算密集型,io在很短的时间内就可以完成,cpu需要大量的计算和处理,特点就是cpu占用率相当高。
io密集型:io密集型指的是系统运作大部分状况都是cpu在等待io操作,cpu占用率很低。
串行:单进程中任务是按时间轴一个一个按照顺序执行的,其中可能会阻塞但是不会出现前一个任务还没结束,就开始执行下一个任务(任务的交叉执行)当cpu执行任务A时,在时间点 cpu调度执行任务B时,当 时刻任务A获取到cpu时间片后,仍然会从 时执行的代码开始往下执行,这样并不会造成资源的抢占,也就是并发问题。
这就像一群小朋友在排队等老师发糖果,排到最前面的小朋友先拿到糖果,老师回个微信停止发糖果或者老师暂时离开教室(此时教室的环境和小朋友的排队状态都没变)过了10分钟后回来继续发糖果,都是被允许的。
并行:多个进程同时执行任务,在多核cpu下,同时一时刻每一个核都在执行任务,且任务的总数等于cpu核心数,也就是说同一时刻任务在执行的时候没有发生cpu上下文切换的问题。这是一种理想状态。但在实际场景中,处于运行状态的任务是非常多的,很难做到真正意义上的并行。
还是那一群小朋友在排队领糖果,此时教室里又来了3个老师队伍被分成了4个,由4个老师在一个一个发糖果,发糖果结束的时间由队伍的长度(任务数)或者老师发的速度有关(cpu性能),老师在发糖果的过程中可以回微信(io操作)但是不能离开房间了(cpu上下文切换)。有的老师先发完糖果后也不能帮其他老师发糖果。这是理想的并发。
并发:指的是多个任务可以“同时"运行的现象,它是一种现象。是多任务"同时"运行的现象。对于单核心CPU来说,同一时刻只能运行一个任务。由于cpu上下文快速切换,使得看上去有多个任务同时都在运行,这种同时运行是一种假象。当任务数超过cpu核心数时,必然会导致cpu的上下文切换。此时需要考虑并发的问题。
并发的百度百科解释:
Python语言慢的主要原因
动态类型语言解释边执行
GIL 无法利用多核cpu并发执行
单/多线程在io密集型和cpu密集型的测试
lscpu 指令可以看到系统处理器架构为x86_64 32核服务器。
实验设计:
场景一 单/多线程大量请求数据库读取数据查看耗时,io密集型测试。
场景二 单/多线程/多进程计算cal_test函数查看耗时,cpu密集型测试。
加载网络请求读取数据属于io操作,不占用cpu执行时间;计算log求和属于cpu密集型操作占用cpu。
实验目的:
场景一验证Python多线程在io密集型操作的业务中可以提升性能。
场景二验证Pyhton多线程在cpu密集型业务没有性能提升,多进程可以提升性能。
场景一 单线程读取数据库数据
def query_db():
with app.app_context():
for i in range(10000):
id = 1
fraud_words = Fraud.query.get_or_404(id)
label = fraud_words.fraud_type_word
label_json = json.loads(label)
if __name__ == "__main__":
t = time.time()
th = threading.Thread(target=query_db)
th.start()
th.join()
print(f"消耗时间:{time.time() - t} s")
time.sleep(60)
程序启动可以在服务器上看到cpu累计执行时间远小于程序执行时间,可以简单说明程序在读取数据是不占用cpu时间片的,10000条数据的查询大概需要20s。
场景一 多线程读取数据库数据
def query_db():
with app.app_context():
for i in range(1000):
id = 1
fraud_words = Fraud.query.get_or_404(id)
label = fraud_words.fraud_type_word
label_json = json.loads(label)
name = threading.current_thread().name
print(f"线程:{name},消耗时间:{time.time() - t}")
if __name__ == "__main__":
t = time.time()
thread_list = []
for i in range(10):
thread = threading.Thread(target=query_db, name=f"threading_{i}")
thread_list.append(thread)
for th in thread_list:
th.start()
for th in thread_list:
th.join()
print(f"消耗时间:{time.time() - t} s")
将10000次数据库读取的任务分给10个线程去做,从实验结果中可以看到在10s左右就可以完成10000次的数据读取,在大量的io操作场景中我们可以适当的增加线程来提升效率。
场景二:单线程计算cal_test函数
def cal_test():
t = time.time()
sum = 0
for i in range(10000000):
x = random.randint(1, 3)
sum = sum + math.log(x - 0.5)
# print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
# print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")
if __name__ == "__main__":
t = time.time()
th = threading.Thread(target=cal_test)
th.start()
th.join()
print(f"消耗时间:{time.time() - t} s")
1000万次的log计算并求和程序执行大概16s,任务进程cpu一直处于近100%的状态
场景二:多线程计算cal_test函数
def cal_test():
t = time.time()
sum = 0
for i in range(10000000):
x = random.randint(1, 3)
sum = sum + math.log(x - 0.5)
# print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")
if __name__ == "__main__":
t = time.time()
thread_list = []
for i in range(10):
thread = threading.Thread(target=cal_test, name=f"threading_{i}")
thread_list.append(thread)
for th in thread_list:
th.start()
for th in thread_list:
th.join()
print(f"消耗时间:{time.time() - t} s")
time.sleep(60)
从结果上来看这可能和我们的预期不符合,10个线程在计算cal_test每个线程应该在16s左右,但是每个线程执行的时间大致上是单个线程执行cal_test函数时间的10倍左右。由于GIL的存在同一时刻cpu只能执行一个任务,其他任务处于阻塞状态,导致10个cal_test函数的计算时间是1个cal_test函数的10倍左右。通过上例可以看到在cpu密集型的任务中,多线程可能并不适用。(此例中相当于10个计算任务在排队获取cpu资源,某个时间点只能有一个任务在执行,并没有充分利用多核cpu的计算能力)
场景二:多进程计算cal_test函数
多线程并没有像我们想象的那样充分利用多核cpu的计算能力,python使用多进程可以解决这个问题。
def cal_test():
t = time.time()
sum = 0
for i in range(10000000):
x = random.randint(1, 3)
sum = sum + math.log(x - 0.5)
print(f"{multiprocessing.current_process().name}执行时间:{time.time() - t} s")
# print(f"{threading.current_thread().name}执行时间:{time.time() - t} s")
if __name__ == "__main__":
t = time.time()
proc_list = []
for i in range(10):
proc = multiprocessing.Process(target=cal_test, name=f"processing_{i}")
proc_list.append(proc)
for proc in proc_list:
proc.start()
for proc in proc_list:
proc.join()
print(f"消耗时间:{time.time() - t} s")
time.sleep(60)
- top 指令可以看出有10个正在处于运行的任务且进程cpu是跑满的,每个进程执行的时间都在单个计算函数cal_test的时间上下浮动,说明系统充分利用到了多核cpu的计算能力。
- 在16s的时间内我们可以执行10个类似cal_test函数的任务,适当增加进程数可以提高程序执行的效率。此外我们可以看到cpu累计使用时间和任务执行的时间基本保持一致。
总结
- Python多线程适用于io密集型场景。
- Python多进程适用于cpu密集型场景。
- 串行:一次只能执行一个任务,该任务阻塞时其他任务只能等待。
- 并行:通过多进程/多线程的方式取得多个任务,同时执行这些任务。
- 并发:一种cpu在多任务之间来回切换执行的现象,看起来像是多个任务同时在执行。这些任务可能是并行执行的,也可能是串行执行的,和cpu核心数无关,是操作系统进程调度和cpu上下文切换达到的结果。