一、线程和进程
线程和进程的概念:
- 进程:是资源(CPU、内存等)分配的基本单位,是程序执行时的一个实例。进程拥有自己的地址空间、内存、数据栈以及其它用于跟踪执行的辅助数据库,进程间通过IPC(进程间通信)的方式共享信息。
- 线程:是CPU调度和分派的基本单位,是程序执行的最小单位。线程有自己的堆栈和局部变量,线程间共享进程的所有资源。
线程和进程区别和联系:
- 进程是资源分配的最小单位,而线程是程序执行的最小单位
- 进程拥有独立的地址空间,开销较大,而线程共享进程的数据,使用相同地址空间,CPU创建和切换线程比进程开销小得多
- 进程间通信需要通过IPC进行,速度较慢,而线程共享进程所有数据,通信更方便
- 一个进程至少有一个线程。
二、Python多线程编程
先来看看单线程和多线程的区别
采用单线程,程序运行main函数进入进程,一个进程至少有一个线程,示例如下:
from time import sleep,ctime
def loop(t=None):
print 'Start loop %s at:'%t,ctime()
sleep(t)
print 'Stop loop %s at:'%t,ctime()
if __name__ == '__main__':
loop(2)
loop(4)
Start loop 2 at: Thu Apr 04 19:00:43 2019
Stop loop 2 at: Thu Apr 04 19:00:45 2019
Start loop 4 at: Thu Apr 04 19:00:45 2019
Stop loop 4 at: Thu Apr 04 19:00:50 2019
采用多线程,示例代码如下:
from time import sleep,ctime
import threading
def loop(t=None):
print 'Start loop %s at:'%t,ctime()
sleep(t)
print 'Stop loop %s at:'%t,ctime()
if __name__ == '__main__':
t1=threading.Thread(target=loop,args=(2,)) #初始化子线程对象,通过Thread对象创建线程
t2=threading.Thread(target=loop,args=(4,))
t1.start()
t2.start()
Start loop 2 at: Thu Apr 04 19:01:23 2019
Start loop 4 at: Thu Apr 04 19:01:23 2019
Stop loop 2 at: Thu Apr 04 19:01:26 2019
Stop loop 4 at: Thu Apr 04 19:01:28 2019
可以看到代码运行的时间比之前宏观上少了2s,多线程最直观的理解就是“程序可以在同一时间做很多事情”
Python给多线程编程提供了多个模块儿,如thread,threading等。推荐不再使用thread,而使用更高级的threading模块儿。部分原因是:thread模块对于进程退出没有控制,当主线程结束时,所有其他线程强制结束,不会发出警告或作出处理,而threading模块儿可以保证重要的子线程在主进程结束前结束。这里只介绍threading模块儿
threading模块
threading模块中的对象:
- Thread:表示一个执行线程的对象(较常用)
- Lock:锁原语对象
- RLock:可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁)
- Condition:条件变量对象,使得一个线程等待另一个线程满足特定的“条件”,比如改变状态或某个数据值
- Event:条件变量的通用版本,任意数量的线程等待某个事件的发生,在该时间发生后所有线程将被激活
- Semaphore:为线程间共享的有限资源提供了一个“计数器”,如果没有可用资源时会被阻塞
- BoundsSemaphore:与Semaphore相似,不过它不允许超过初始值
- Timer:与Thread相似,不过它在运行前需等待一段时间
- Barrier:创建一个“障碍”,必须达到指定数量的线程后才可以继续(Python 3.2引入)
1、Thread类
- name:线程名
- ident:线程标识符
- daemon:布尔标志,表示线程是否是守护线程
- setDaemon():True设置线程为守护线程,主线程一旦执行结束,则全部线程全部被终止,这时候子线程可能还没有执行完;False为默认情况,主线程执行结束,子线程继续执行自己任务。
- _init_(group=None,target=None,name=None,args=(),kwargs={},verbose=None,daemon=None):实例化对象,可调用的target及其参数args或kwargs,daemon的值会被设定为thread.daemon属性
- start():开始执行该线程
- run():定义线程功能的方法,通常在子类中被应用开发者重写
- join(timeout=None):直至启动的线程终止之前一直挂起,除非出超时时间,否则一直阻塞。即主线程任务结束后,进入阻塞状态,一直等待其他的子线程执行结束后,主线程再终止。
- getName()、setName(name)
Threading创建线程有两种方式,一种是通过初始化Thread对象创建,见上面单线程多线程区别示例,另一种是通过继承Thread类来创建,代码示例如下:在run函数中重写,或者调用外部函数
import threading
from time import ctime, sleep
#继承Thread类,在子类中重写init和run方法
class mythread(threading.Thread):
def __init__(self,name):
super(mythread,self).__init__(name=name)
def run(self):
print self.name
sleep(1)
t1=mythread('thread-1')
t2=mythread('thread-2')
t1.start()
t2.start()
thread-1
thread-2
import threading
from time import ctime, sleep
def loop(t=None):
print 'Start loop %s at:'%t,ctime()
sleep(t)
print 'Stop loop %s at:'%t,ctime()
#继承Thread类,调用外部传入函数
class mythread(threading.Thread):
def __init__(self,name,args,target):
super(mythread,self).__init__(name=name)
self.target=target
self.args=args
def run(self):
print self.name
self.target(*self.args)
t1=mythread(target=loop,args=(2,),name='thread-1')
t2=mythread(target=loop,args=(4,),name='thread-2')
t1.start()
t2.start()
thread-1
Start loop 2 at: Mon Apr 08 19:32:31 2019
thread-2
Start loop 4 at: Mon Apr 08 19:32:31 2019
Stop loop 2 at: Mon Apr 08 19:32:34 2019
Stop loop 4 at: Mon Apr 08 19:32:36 2019
2、Lock/RLock类
多线程和多进程最大的不同之处在于多进程各自有一份独立的数据,互不影响,而多线程,所有变量都有所有线程共享,这样就会造成任意变量会被任意线程操作,出现死锁,数据混乱的现象。锁是所有机制中最简单,最低级的机制。
数据混乱的例子:每次得到的结果不等(多执行几次。。。。)
import threading
#设置全局变量
num=0
def add():
global num
num+=1
num-=1
def test():
for i in range(1000000):
add()
threading.Thread(target=test).start()
threading.Thread(target=test).start()
print num
锁一般通过lock.acquire()+lock.release()组合使用或者with lock来使用,示例代码如下:
import threading
lock = threading.RLock()
#最上层获取锁,释放锁
def locktest():
with lock:
print 'first layer'
layer1()
#第一层获取锁,释放锁
def layer1():
with lock:
print 'second layer'
layer2()
#第二层
def layer2():
print "layer2"
t=threading.Thread(target=locktest)
t.start()
这里使用threading.Lock()会造成阻塞, 因为Lock多次获取锁会发生死锁,RLock不会,因此推荐大家使用RLock,获得锁相对应一定记得释放。上面造成数据混乱的例子,只需要在把add()函数用with lock:“包裹”即可。
注:如果线程不想将变量共享出去,就需要使用局部变量,这样在函数定义局部变量会使得函数之间传递非常麻烦,ThreadLocal就是解决全局变量需要加锁和局部变量传递麻烦这两个问题。local_variable=threading.local(),或者继承类class _DbCtx(threading.local):,这样是一个全局变量,但是对于不同线程又是局部变量,线程之间不会互相影响。应用场景:例如不同数据库连接使用不同线程,local也是一个线程-属性字典。
3、Semaphore类(信号量)
Lock,RLock类只允许一个线程访问共享数据,而信号量可以允许一定数量的线程访问共享数据,示例代码如下:
import threading
from time import sleep,ctime
threadlist=[]
#设置3
semaphore=threading.BoundedSemaphore(3)
def loop(t=None):
semaphore.acquire()
sleep(3)
print "thread %s is running at %s\n" % (t,ctime())
semaphore.release()
for i in range(12):
t=threading.Thread(target=loop,args=(i,))
#获取线程列表
threadlist.append(t)
for threadtemp in threadlist:
threadtemp.start()
4、Condition类
有些线程无法直接运行,需要满足条件才可以允许它运行,Condition对象提供了这样的支持,该对象除RLock()和Lock()方法外,还有notify(),wait()以及notifyAll()方法,这里面锁是可选,默认RLock()。
- wait():条件不满足时,线程释放锁,进入等待阻塞
- notify():条件满足后,通知线程池激活线程
- notifyAll():条件满足后,通知线程池激活所有线程
示例代码如下:
import threading
from time import sleep
num=0
con=threading.Condition()
def wpp():
con.acquire()
while True:
print u"wpp开始训练气球兵"
global num
num+=1
print u"wpp训练气球兵个数:%s"%num
sleep(2)
if num==3:
print u"wpp:够了够了,给你"
sleep(2)
con.notify()
con.wait()
con.release()
def ypp():
con.acquire()
global num
while True:
print u"ypp:可以,可以,我去揍人了"
num-=3
sleep(2)
if num<=0:
print u"ypp:气球兵又没了,救!"
sleep(2)
con.notify()
con.wait()
con.release()
twpp=threading.Thread(target=wpp)
twpp.start()
typp=threading.Thread(target=ypp)
typp.start()
5、Event类
Event和Condition差不多,Event不需要和锁关联,Event应用于不访问线程间共享资源的情景。Event事件处理机制:在全局定义一个Flag,如果Flag值为False,event.wait()方法时会阻塞,为True时不会阻塞。
- set():将标志设为True,并通知所有处于阻塞状态的线程恢复运行状态
- clear():标志设为False
- wait(timeout):标志为True,不阻塞,标志为False,则阻塞
- isSet():获取内置标志状态,返回True,False
示例代码如下:
import threading,time
event=threading.Event()
print event.isSet()
def test():
print "start test and wait"
#等待被唤醒从阻塞状态到运行状态
event.wait()
print event.isSet()
time.sleep(2)
print "waiting is over"
t=threading.Thread(target=test)
t.start()
time.sleep(2)
print "Main func is start running and event is setting True"
#设置为True,wait方法就不再阻塞
event.set()
三、Python多线程的局限性和意义
1、全局解释器锁(Global Interpreter Lock GIL)
对Python虚拟机的访问是由GIL控制的,这个锁保证了同时只有一个线程运行。Python设计之初,GIL是为了数据安全得出的产物,
- 设置GIL
- 切换进一个线程去运行
- 执行下面操作之一
- 指定数量的字节码指令
- 线程主动让出控制权
- 把线程设置回睡眠状态
- 解锁GIL
- 重复上述步骤
2、局限性
由上面的GIL可以看出(感兴趣可以深入了解下,我怕自己理解的不深,就不误导别人了),Python多线程在同一时刻只有一条线程跑CPU里,即使N个线程跑在N核CPU上,也只能用到1个CPU核,这样就导致CPU利用率非常低。所以如果使用多线程,想用Python就无法有效利用多核,还是用C什么的比较好。但是这个东西为什么还留着呢?因为GIL不是bug,是Python设计开发者权衡利弊最终留下的。
2、意义
那既然Python多线程无法有效利用多核,它还有什么存在的必要呢?肯定是有的呀
- IO密集型任务:磁盘IO,网络IO占主要比例,CPU计算占比较小,eg:请求网页,读写文件等
- 计算密集型任务:CPU计算占比很大,eg:复杂的数据计算
对于Python来说,IO密集型任务采用多线程,会有效的利用CPU;计算密集型任务采用多进程会有效利用CPU资源。
对于其它语言,针对不同场景,多线程和多进程的选择也是不同的,具体情况具体分析,所以Python多线程有用,而且用处很大,看开发者要怎么用,怎么写。