Python的标准库提供了两个模块:_threadthreading_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-1Thread-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 - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8 结果 balance = -8

究其原因,是因为修改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占用率仅有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锁,互不影响。

小结

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

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

python Thread、lock的更多相关文章

  1. C# Thread、lock

    class Program { private static readonly object obj = new object(); static void Main(string[] args) { ...

  2. Python介绍、安装、使用

    Python介绍.安装.使用 搬运工:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Python语言介绍 说到Python语言,就不得不说一下它的创始人Guido van Rossu ...

  3. Python 多线程、多进程 (一)之 源码执行流程、GIL

    Python 多线程.多进程 (一)之 源码执行流程.GIL Python 多线程.多进程 (二)之 多线程.同步.通信 Python 多线程.多进程 (三)之 线程进程对比.多线程 一.python ...

  4. Python 多线程、多进程 (二)之 多线程、同步、通信

    Python 多线程.多进程 (一)之 源码执行流程.GIL Python 多线程.多进程 (二)之 多线程.同步.通信 Python 多线程.多进程 (三)之 线程进程对比.多线程 一.python ...

  5. python 线程、多线程

    复习进程知识: python:主进程,至少有一个主线程 启动一个新的子进程:Process,pool 给每一个进程设定一下执行的任务:传一个函数+函数的参数 如果是进程池:map函数:传入一个任务函数 ...

  6. python队列、线程、进程、协程

    目录: 一.queue 二.线程 基本使用 线程锁 自定义线程池 生产者消费者模型(队列) 三.进程 基本使用 进程锁 进程数据共享 默认数据不共享 queues array Manager.dict ...

  7. python(十一)、线程

    一.基本概念 进程是资源分配的基本单位,而线程则是CPU调度和分派的基本单位.系统需要执行创建进程.撤销进程和进程切换等任务,但创建进程开销大限制了并发的提高.因此,成百上千个进程会造成内存浪费,并且 ...

  8. python队列、线程、进程、协程(转)

    原文地址: http://www.cnblogs.com/wangqiaomei/p/5682669.html 一.queue 二.线程 #基本使用 #线程锁 #自定义线程池 #生产者消费者模型(队列 ...

  9. 这篇文章揭开python进程、线程、协程神秘的面纱

    1.概念 [关注公众号"轻松学编程"了解更多. 回复"协程"获取本文源代码.] 从计算机硬件角度: 计算机的核心是CPU,承担了所有的计算任务. 一个CPU,在 ...

随机推荐

  1. 马哥Python视频

    链接:https://pan.baidu.com/s/1KMXqdXlaIjZ3OaZ-PUwE9A 密码私聊我

  2. python基础学习第一天

    def用法 函数定义的基本格式如下: def function(params):     somthing     return values 说明:return语句可选,出现return语句表示函数 ...

  3. ESP NVS

    简介:NVS的主要功能是:存储键值(存在flash上面): NVS利用spi_flash_{read|write|erase}这些API来操作数据在内存上的删改写,内存上data类型nvs子类型所代表 ...

  4. Android6.0权限大全和权限分类

    本文转载至: https://blog.csdn.net/qq_26440221/article/details/53097868 自从出了Android6.0权限管理之后,再也不能像以前那样粘贴复制 ...

  5. oracle 记录被另一个用户锁住

    第一步:查询处用户,被锁表名,sessionID select b.owner,b.object_name,l.session_id,l.locked_modefrom v$locked_object ...

  6. python游戏开发之俄罗斯方块(一):简版

    编程语言:python(3.6.4) 主要应用的模块:pygame (下面有源码,但是拒绝分享完整的源码,下面的代码整合起来就是完整的源码) 首先列出我的核心思路: 1,图像由"核心变量&q ...

  7. windows平台快速搭建Linux(CentOS)

    VMware简介 VMware Workstation(简称 “虚拟机”)是一款功能强大的桌面虚拟计算机软件,用户可在Windows平台通过VMware软件同时运行不同的操作系统.这对IT开发人员而言 ...

  8. 【转】浅谈JavaScript中forEach与each

    forEach是ES5中操作数组的一种方法,主要功能是遍历数组,例如: var arr = [1,2,3,4]; arr.forEach(alert); 等价于: var arr = [1, 2, 3 ...

  9. Linux常用命令行

    实时查看日志runtime.log最后100行 tail -f -n 100 runtime.log

  10. 案例学Python--案例四:Django实现一个网站的雏形(1)

    第一次用python的Web框架,也是第一次听说Django,参考菜鸡教程和一些博客,倒腾了半天,算是有一个雏形.数据基于昨天爬的豆瓣电影信息,详见案例三. Python版本:3.7.1 Django ...