在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 相关文章推荐
在Django的视图中使用form对象的方法
Jul 18 Python
Python实现字符串的逆序 C++字符串逆序算法
May 28 Python
Python实现合并两个列表的方法分析
May 28 Python
python实现定时提取实时日志程序
Jun 22 Python
python 字典中文key处理,读取,比较方法
Jul 06 Python
Python基于聚类算法实现密度聚类(DBSCAN)计算【测试可用】
Dec 26 Python
使用pandas把某一列的字符值转换为数字的实例
Jan 29 Python
Python安装与基本数据类型教程详解
May 29 Python
Python操作word文档插入图片和表格的实例演示
Oct 25 Python
python subprocess pipe 实时输出日志的操作
Dec 05 Python
python 实现逻辑回归
Dec 30 Python
python源码剖析之PyObject详解
May 18 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中OO之静态关键字以及类常量的详解
2013/06/07 PHP
PHP循环输出指定目录下的所有文件和文件夹路径例子(简单实用)
2014/05/10 PHP
PHP中key和current,next的联合运用实例分析
2016/03/29 PHP
ThinkPHP框架分布式数据库连接方法详解
2017/03/14 PHP
加速IE的Javascript document输出的方法
2010/12/02 Javascript
JQuery获取样式中的background-color颜色值的问题
2013/08/20 Javascript
谷歌浏览器调试JavaScript小技巧
2014/12/29 Javascript
jquery通过ajax加载一段文本内容的方法
2015/01/15 Javascript
辨析JavaScript中的Undefined类型与null类型
2016/05/26 Javascript
JS简单实现仿百度控制台输出信息效果
2016/09/04 Javascript
fullpage.js全屏滚动插件使用实例
2016/09/06 Javascript
利用node.js如何搭建一个简易的即时响应服务器
2017/05/28 Javascript
JavaScript中防止微信浏览器被整体拖动的方法
2017/08/25 Javascript
jQuery动态添加.active 实现导航效果代码思路详解
2017/08/29 jQuery
vue+SSM实现验证码功能
2018/12/07 Javascript
JavaScript箭头函数中的this详解
2019/06/19 Javascript
解决使用layui的时候form表单中的select等不能渲染的问题
2019/09/18 Javascript
微信内置开发 iOS修改键盘换行为搜索的解决方案
2019/11/06 Javascript
手机浏览器唤起微信分享(JS)
2020/10/11 Javascript
用Python中的字典来处理索引统计的方法
2015/05/05 Python
在Python中居然可以定义两个同名通参数的函数
2019/01/31 Python
Python检测数据类型的方法总结
2019/05/20 Python
Django 通过JS实现ajax过程详解
2019/07/30 Python
使用html5+css3来实现slider切换效果告别javascript+css
2013/01/08 HTML / CSS
探索HTML5本地存储功能运用技巧
2016/03/02 HTML / CSS
阿里云:Aliyun.com
2017/02/15 全球购物
美国球迷装备的第一来源:FOCO
2020/07/03 全球购物
伊莱克斯阿根廷网上商店:Tienda Electrolux
2021/03/08 全球购物
Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?
2016/08/18 面试题
企业元宵节主持词
2014/03/25 职场文书
团委竞选演讲稿
2014/04/24 职场文书
我的画教学反思
2014/04/28 职场文书
税务干部群众路线教育实践活动对照检查材料
2014/09/20 职场文书
2015年采购部工作总结
2015/04/23 职场文书
大学新生入学感想
2015/08/07 职场文书
Redis的字符串是如何实现的
2021/10/24 Redis