对于操作系统来说,一个任务就是一个进程(Process),比如打开一个word就是一个进程,打开两个word就是两个进程。打开word之后,可以同时进行编辑,检查,打印的操作,这些子任务就是线程(Thread)。
1,多进程:
from multiprocessing import Process
import os
def run_func(name):
print('run child process %s(%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('父进程是%s' % os.getpid())
p = Process(target=run_func, args=('test',))
print('子进程开始前')
p.start()
p.join() #等待子进程执行结束主进程再往下走
print('子进程结束')
执行结果:
父进程是136
子进程开始前
run child process test(5856)...
子进程结束
2,多线程
import time, threading
#新线程执行的代码
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n += 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='loop')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
3,锁
n = 0
lock = threading.Lock()
#加锁函数
def run_func():
global n
lock.acquire() #加上锁,此时只有一个线程可以获取到锁
try:
n += 1
print('n被修改为%s' % n)
time.sleep(1)
finally:
lock.release() #释放锁,释放之后其他线程才能继续操作
#不加锁的函数
def run_func2():
global n
n += 1
print('n被修改为%s' % n)
time.sleep(1)
threads = []
for i in range(5):
t = threading.Thread(target=run_func2)
t.start()
threads.append(t)
for i in threads:
t.join()
从以上代码的运行结果可以看出
加锁之后的函数在多线程执行时,每个线程需要等锁释放后下一个线程才能执行修改操作,而不加锁的函数在多线程执行时,会很快的并发执行完所有线程。
在python中,启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。