在Python下尝试多线程编程


Posted in Python onApril 28, 2015

多任务可以由多进程完成,也可以由一个进程内的多线程完成。

我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。

由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。

Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

import time, threading

# 新线程执行的代码:
def loop():
  print 'thread %s is running...' % threading.current_thread().name
  n = 0
  while n < 5:
    n = 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='LoopThread')
t.start()
t.join()
print 'thread %s ended.' % threading.current_thread().name

执行结果如下:

thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
Lock

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

来看看多个线程同时操作一个变量怎么把内容给改乱了:

import time, threading

 
# 假定这是你的银行存款:
balance = 0

def change_it(n):
  # 先存后取,结果应该为0:
  global balance
  balance = balance + n
  balance = balance - n

def run_thread(n):
  for i in range(100000):
    change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print balance

我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。

原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:

balance = balance + n

也分两步:

  1.     计算balance + n,存入临时变量中;
  2.     将临时变量的值赋给balance。

也就是可以看成:

x = balance + n
balance = x

由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1   # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1   # balance = 0

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2   # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2   # balance = 0

结果 balance = 0

但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2   # balance = 8

t1: balance = x1   # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1   # balance = 0

t2: x2 = balance - 5 # x2 = 0 - 5 = -5
t2: balance = x2   # balance = -5

结果 balance = -5

究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

balance = 0
lock = threading.Lock()

def run_thread(n):
  for i in range(100000):
    # 先要获取锁:
    lock.acquire()
    try:
      # 放心地改吧:
      change_it(n)
    finally:
      # 改完了一定要释放锁:
      lock.release()

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
多核CPU

如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。

如果写一个死循环的话,会出现什么情况呢?

打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。

我们可以监控到一个死循环线程会100%占用一个CPU。

如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

试试用Python写个死循环:

import threading, multiprocessing

def loop():
  x = 0
  while True:
    x = x ^ 1

for i in range(multiprocessing.cpu_count()):
  t = threading.Thread(target=loop)
  t.start()

启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有160%,也就是使用不到两核。

即使启动100个线程,使用率也就170%左右,仍然不到两核。

但是用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锁,互不影响。
小结

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。

 

Python 相关文章推荐
使用python获取CPU和内存信息的思路与实现(linux系统)
Jan 03 Python
举例讲解Python程序与系统shell交互的方式
Apr 09 Python
Python在Windows和在Linux下调用动态链接库的教程
Aug 18 Python
PyCharm设置每行最大长度限制的方法
Jan 16 Python
python3.7简单的爬虫实例详解
Jul 08 Python
python爬取盘搜的有效链接实现代码
Jul 20 Python
Django 通过JS实现ajax过程详解
Jul 30 Python
Python 中的 global 标识对变量作用域的影响
Aug 12 Python
python内置函数sorted()用法深入分析
Oct 08 Python
使用jupyter notebook将文件保存为Markdown,HTML等文件格式
Apr 14 Python
GitHub上值得推荐的8个python 项目
Oct 30 Python
只用50行Python代码爬取网络美女高清图片
Jun 02 Python
Python输出PowerPoint(ppt)文件中全部文字信息的方法
Apr 28 #Python
python使用append合并两个数组的方法
Apr 28 #Python
python实现的简单文本类游戏实例
Apr 28 #Python
初步解析Python下的多进程编程
Apr 28 #Python
python实现将pvr格式转换成pvr.ccz的方法
Apr 28 #Python
简单介绍Python中的JSON使用
Apr 28 #Python
浅析Python中的序列化存储的方法
Apr 28 #Python
You might like
PHP 检查扩展库或函数是否可用的代码
2010/04/06 PHP
PHP中数据库单例模式的实现代码分享
2014/08/21 PHP
Win7下手动安装apache2.2、php5.4笔记
2015/04/03 PHP
PHP QRCODE生成彩色二维码的方法
2016/05/19 PHP
PHP rsa加密解密算法原理解析
2020/12/09 PHP
基于JQuery的6个Tab选项卡插件
2010/09/03 Javascript
IE8提示Invalid procedure call or argument 异常的解决方法
2012/09/30 Javascript
jQuery中实现动画效果的基本操作介绍
2013/04/16 Javascript
Jquery方式获取iframe页面中的 Dom元素
2014/05/07 Javascript
如何正确使用Nodejs 的 c++ module 链接到 OpenSSL
2014/08/03 NodeJs
javascript省市级联功能实现方法实例详解
2015/10/20 Javascript
jQuery插件开发精品教程让你的jQuery提升一个台阶
2016/01/27 Javascript
js正则表达式验证表单【完整版】
2017/03/06 Javascript
vue-music 使用better-scroll遇到轮播图不能自动轮播问题
2018/12/03 Javascript
vue移动端城市三级联动组件使用详解
2019/07/26 Javascript
浅析Python中的多重继承
2015/04/28 Python
python简单判断序列是否为空的方法
2015/06/30 Python
Python基于scapy实现修改IP发送请求的方法示例
2017/07/08 Python
浅谈flask截获所有访问及before/after_request修饰器
2018/01/18 Python
浅谈关于Python3中venv虚拟环境
2018/08/01 Python
numpy下的flatten()函数用法详解
2019/05/27 Python
使用WingPro 7 设置Python路径的方法
2019/07/24 Python
Python爬虫解析网页的4种方式实例及原理解析
2019/12/30 Python
Python递归及尾递归优化操作实例分析
2020/02/01 Python
Python+unittest+DDT实现数据驱动测试
2020/11/30 Python
selenium+python实现基本自动化测试的示例代码
2021/01/27 Python
Chain Reaction Cycles俄罗斯:世界上最大的在线自行车商店
2019/08/27 全球购物
办理退休介绍信
2014/01/09 职场文书
教师通用专业自荐书范文
2014/02/11 职场文书
学校社会实践活动总结
2014/07/03 职场文书
2014年乡镇卫生院工作总结
2014/11/24 职场文书
史上最牛的辞职信
2015/02/28 职场文书
幼儿园中班个人总结
2015/02/28 职场文书
导游词之大雁塔景区
2019/09/17 职场文书
Nginx虚拟主机的搭建的实现步骤
2022/01/18 Servers
javascript的var与let,const之间的区别详解
2022/02/18 Javascript