该文章参考了 廖雪峰的教程。
一个进程至少有一个线程。
Python也提供多线程支持,而且Python中的线程并非是模拟出来的多线程,而是系统级别的Posix Thread.Python标准库提供了两个模块thread
和threading
。前者是低级库,后者是高级库。高级库是对低级库的封装。
threading
高级库就可以了。 如何创建Thread
我们只需要创建threading.Thread
类,并传入一个需要线程执行的函数作为target即可。
import threadingimport timedef loop(): count = 1 while count < 4: print "Hello" time.sleep(1) count += 1if __name__ == '__main__': thread = threading.Thread(target=loop, name="LoopThread") thread.start() thread.join() current_thread = threading.current_thread() print current_thread.getName() print "LoopThread Ended"
上述代码主进程默认会创建一个主线程。
上面的代码中,我们用到了多个函数。join()
函数:即让主线程序等待子线程完成后继续。current_thread()
函数:获取当前线程的实例。
Lock 锁
同一个变量,进程会拷贝一份存于各个进程中,互不影响。
同一个变量,线程则会互相共享。这就会带来一些问题。因为cpu对线程是随意切换的。那么如果一个线程对某一个变量进行多步cpu操作的时候,如果不加锁,那么可能会在线程进行到一半的时候,会被切换到其他线程,从而使得操作变乱。一个典型的例子是“一存一取”的场景。例如多个线程对银行中的某一个账户进行操作。一个线程需要对账户进行一存一取某一个金额的操作才会让账户平衡。但是多个线程对该账户进行操作的时候,且它们操作的金额经常是不一样的,那么最终的账户很可能是不平衡的。thread 1: -100thread 1: +100thread 1: -100thread 1: +100thread 2: -200thread 2: +200thread 2: -200thread 2: +200
上述描述了两个线程按顺序互补干扰地对某一个账户进行操作。
但是实际情况是cpu随意切换线程。上述操作是打乱的,例如thread 1: -100thread 2: -200thread 2: +200...
此时,我们发现账户并不是平衡的。
如果解决呢?我们需要对每一个thread的一存一取操作加一把锁。即改账户变量必须是让一个线程先进行完整的一存一取操作后,才能被其他线程所操作。我们可以将要上锁的代码包裹起来,即:
lock.acquire() num = num + n num = num - n print num lock.release()
当然,lock可以对资源的获取和释放,那么我们也可以用with
关键字。
with lock: num = num + n num = num - n print num
死锁 deadlock
死锁的发生往往是两个线程各自占用了对方所需要用到的资源而都在等待对方释放资源而造成的。
遇见死锁后,程序则会一直暂停在那里。直到系统将它们关闭。GIL
说到Python的多线程编程,就会绕不过GIL。那么GIL又是什么鬼呢?
GIL据说是Python中hardest的问题,想要彻底了解GIL,必须要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有着非常彻底的理解。据廖雪峰中的教程中描述:
Python中的线程虽然是真正的线程,但是解释器执行代码的时候,会有一个GIL锁(Global Interpreter Lock). 任何Python线程执行前,必须先获得GIL锁。然后,每执行100条字节码,解释器就会自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁。因此,多线程在Python中只能交替进行,即使100个线程跑在100核CPU上,也只能用到1个核心。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现彻底CPython。
更多关于GIL设计的历史渊源,可以阅读:
那么是不是Python就是不能利用多核处理器任务了呢?我们之前学到过多进程编程,每一个进程都有一个独立的GIL锁,各进程互不影响。