python线程及多线程实例讲解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python线程及多线程实例讲解相关的知识,希望对你有一定的参考价值。
进程和线程
一、进程
进程是程序的分配资源的最小单元;一个程序可以有多个进程,但只有一个主进程;进程由程序、数据集、控制器三部分组成。
二、线程
线程是程序最小的执行单元;一个进程可以有多个线程,但是只有一个主线程;线程切换分为两种:一种是I/O切换,一种是时间切换(I/O切换:一旦运行I/O任务时便进行线程切换,CPU开始执行其他线程;时间切换:一旦到了一定时间,线程也进行切换,CPU开始执行其他线程)。
三、总结
一个程序至少有一个进程和一个线程;
程序的工作方式:
1.单进程单线程;2.单进程多线程;3.多进程多线程;
考虑到实现的复杂性,一般最多只会采用单进程多线程的工作方式;
四、为什么要使用多线程
我们在实际生活中,希望既能一边浏览网页,一边听歌,一边打游戏。这时,如果只开一个进程,为了满足需求,CPU只能快速切换进程,但是在切换进程时会造成大量资源浪费。所以,如果是多核CPU,可以在同时运行多个进程而不用进行进程之间的切换。
然而,在实际中,比如:你在玩游戏的时候,电脑需要一边显示游戏的动态,一边你还得和同伴进行语音或语言进行沟通。这时,如果是单线程的工作方式,将会造成在操作游戏的时候就无法给同伴沟通,在和同伴沟通的时候就无法操作游戏。为了解决该问题,我们可以开启多线程来共享游戏资源,同时进行游戏操作和沟通。
五、实例
场景一:并发依次执行
如上图所示:有两个简单的函数,一个是听音乐一个是打游戏的函数。
如果按照之前的单线程方式,将会是先运行完听音乐的函数再去运行打游戏的函数,最后打印Ending。如下图所示:
一共的运行时间是6秒。并且是只能单一按照顺序依次去执行。而使用多线时,运行时间是3秒,并且是并行执行。
该情况下的多线程运行方式是,先创建线程1,再创建线程2,然后去启动线程1和线程2,并和主线程同时运行。此种情况下,若子线程先于主线程运行完毕,则子线程先关闭后主线程运行完毕关闭;若主线程先于子线程结束,则主线程要等待所有的子线程运行完毕后再关闭。
该部分代码块:
import threading
import time
def music(name):
print(‘%s begin listen music%s‘%(name,time.ctime()))
time.sleep(3)
print(‘%s stop listen music%s‘ % (name, time.ctime()))
def game(name):
print(‘%s begin play game%s‘%(name,time.ctime()))
time.sleep(3)
print(‘%s stop play game%s‘ % (name,time.ctime()))
if __name__ == ‘__main__‘:
# threadl = []
# t1 = threading.Thread(target=music,args=(‘zhang‘,))
# t2 = threading.Thread(target=game,args=(‘zhang‘,))
# t1.start()
# t2.start()
music(‘zhang‘)
game(‘zhang‘)
print(‘Ending now %s‘%time.ctime())
场景二:主线程等待某子线程结束后才能执行(join()函数的用法)
例如:在实际中,需要子线程在插入数据,主线程需要等待数据插入结束后才能进行查询验证操作(测试验证数据)
该部分代码块为:
import threading
import time
def music(name):
print(‘%s begin listen music%s‘%(name,time.ctime()))
time.sleep(5)
print(‘%s stop listen music%s‘ % (name, time.ctime()))
def game(name):
print(‘%s begin play game%s‘%(name,time.ctime()))
time.sleep(3)
print(‘%s stop play game%s‘ % (name,time.ctime()))
if __name__ == ‘__main__‘:
threadl = [] #线程列表,用例存放线程
#产生线程的实例
t1 = threading.Thread(target=music,args=(‘zhang‘,)) #target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式;
t2 = threading.Thread(target=game,args=(‘zhang‘,))
threadl.append(t1)
threadl.append(t2)
#循环列表,依次执行各个子线程
for x in threadl:
x.start()
#将最后一个子线程阻塞主线程,只有当该子线程完成后主线程才能往下执行
x.join()
print(‘Ending now %s‘%time.ctime())
该部分代码块为:
import threading
import time
def music(name):
print(‘%s begin listen music%s‘%(name,time.ctime()))
time.sleep(2)
print(‘%s stop listen music%s‘ % (name, time.ctime()))
def game(name):
print(‘%s begin play game%s‘%(name,time.ctime()))
time.sleep(5)
print(‘%s stop play game%s‘ % (name,time.ctime()))
if __name__ == ‘__main__‘:
threadl = [] #线程列表,用例存放线程
#产生线程的实例
t1 = threading.Thread(target=music,args=(‘zhang‘,)) #target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式;
t2 = threading.Thread(target=game,args=(‘zhang‘,))
threadl.append(t1)
threadl.append(t2)
#循环列表,依次执行各个子线程
for x in threadl:
x.start()
#将子线程t1阻塞主线程,只有当该子线程完成后主线程才能往下执行
t1.join()
print(‘Ending now %s‘%time.ctime())
六、线程守护(setDaemon()函数)
前面不管是不是用到了join()函数,主线程最后总是要得所有的子线程执行完成后且自己执行完才能关闭(以子线程为主来结束主线程)。下面,我们讲述一种以主线程为主的方法来结束主线程。
图1:无线程守护
图2:t2线程守护
该部分代码块为:
import threading
import time
def music(name):
print(‘%s begin listen music%s‘%(name,time.ctime()))
time.sleep(2)
print(‘%s stop listen music%s‘ % (name, time.ctime()))
def game(name):
print(‘%s begin play game%s‘%(name,time.ctime()))
time.sleep(5)
print(‘%s stop play game%s‘ % (name,time.ctime()))
if __name__ == ‘__main__‘:
threadl = [] #线程列表,用例存放线程
#产生线程的实例
t1 = threading.Thread(target=music,args=(‘zhang‘,)) #target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式;
t2 = threading.Thread(target=game,args=(‘zhang‘,))
threadl.append(t1)
threadl.append(t2)
#循环列表,依次执行各个子线程
t2.setDaemon(True) #t2线程守护
for x in threadl:
x.start()
#将子线程t1阻塞主线程,只有当该子线程完成后主线程才能往下执行
print(‘Ending now %s‘%time.ctime())
所谓’线程守护’,就是主线程不管该线程的执行情况,只要是其他子线程结束且主线程执行完毕,主线程都会关闭。也就是说:主线程不等待该守护线程的执行完再去关闭。
注意:setDaemon方法必须在start之前且要带一个必填的布尔型参数
七、自定义的方式来产生多线程
该部分代码块为:
import threading
import time
class mythread1(threading.Thread):
‘自定义线程‘
def __init__(self,name):
threading.Thread.__init__(self)
self.name=name
def run(self):
‘定义每个线程要运行的函数,此处为music函数‘
print(‘%s begin listen music, %s‘ % (self.name, time.ctime()))
time.sleep(5)
print(‘%s stop listen music, %s‘ % (self.name, time.ctime()))
class mythread2(threading.Thread):
‘自定义线程‘
def __init__(self,name):
threading.Thread.__init__(self)
self.name=name
def run(self):
‘定义每个线程要运行的函数,此处为game函数‘
print(‘%s begin play game, %s‘ % (self.name, time.ctime()))
time.sleep(2)
print(‘%s stop play game, %s‘ % (self.name, time.ctime()))
if __name__ == ‘__main__‘:
threadl = []
t1 = mythread1(‘zhang‘)
t2 = mythread2(‘zhang‘)
threadl.append(t1)
threadl.append(t2)
for x in threadl:
x.start()
print(‘Ending now %s‘ % time.ctime())
八、Threading的其他常用方法
getName() :获取线程名称
setName():设置线程名称
run():用以表示线程活动的方法(见七中自定义线程的run方法)
rtart():启动线程活动
is_alive():表示线程是否处于活动的状态,结果为布尔值;
threading.active_count():返回正在运行线程的数量
Threading.enumerate():返回正在运行线程的列表
该部分代码块为;
import threading
import time
def music(name):
print(‘%s begin listen music%s‘%(name,time.ctime()))
time.sleep(2)
print(‘%s stop listen music%s‘ % (name, time.ctime()))
def game(name):
print(‘%s begin play game%s‘%(name,time.ctime()))
time.sleep(5)
print(‘%s stop play game%s‘ % (name,time.ctime()))
if __name__ == ‘__main__‘:
threadl = [] #线程列表,用例存放线程
#产生线程的实例
t1 = threading.Thread(target=music,args=(‘zhang‘,)) #target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式;
t2 = threading.Thread(target=game,args=(‘zhang‘,))
threadl.append(t1)
threadl.append(t2)
#循环列表,依次执行各个子线程
t2.setDaemon(True) #t2线程守护,setDaemon方法必须在start之前且要带一个必填的布尔型参数
t1.setName(‘线程1‘) #设置线程的名字
for x in threadl:
print(‘线程为:‘,x.getName()) #获取线程的名字
print(‘线程t1是否活动:‘,t1.is_alive()) #判断线t1是否处于活动状态
x.start()
print(‘正在运行线程的数量为:‘,threading.active_count()) #获取正处于活动状态线程的数量
print(‘正在运行线程的数量为:‘,threading.activeCount) #获取正处于活动状态线程的数量
print(‘正在运行线程的list为:‘,threading.enumerate()) #获取正处于活动状态线程的list
print(‘正在运行线程的list为:‘,threading._enumerate()) #获取正处于活动状态线程的list
#将子线程t1阻塞主线程,只有当该子线程完成后主线程才能往下执行
print(‘正在运行的线程为:‘,threading.current_thread().getName()) #获取当前线程的名字
print(‘Ending now %s‘%time.ctime())
九、GIL:cpython解释器的’BUG’
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。在其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
GIL:global interpreter lock,全局解释器锁。原文:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
也就是说:无论有多少个CPU,开启多少线程,每次只能执行一个线程。
基于此设计原理上,我们会觉得python的多线程其实完全没有用,如下图不开多线程执行的时间:
如下图开启多线程执行的时间:
好吧,前者是0.3秒,后者是20秒,这个结果是不是无法接受...........
该部分代码块:
import threading
import time
def add(n):
sum=0
for x in range(1,n+1):
sum+=x
print(‘sum =‘,sum)
def accumulate(n):
mul=1
for x in range(1,n+1):
mul*=x
print(‘mul =‘,mul)
if __name__ == ‘__main__‘:
thread=[]
t1=threading.Thread(target=add,args=(10000001,))
t2=threading.Thread(target=accumulate,args=(100001,))
thread.append(t1)
thread.append(t2)
starttime=time.time()
for i in thread:
i.start()
for i in thread:
i.join()
# add(1000001)
# accumulate(10001)
endtime = time.time()
print(‘spendtime:‘, endtime-starttime)
GIL原理:
前者是单线程,任务串行,执行完add函数后再执行accumulate函数,不用进行线程间的切换。而在后者中,线程add和线程accumulate及主线程三者需要不断的切换来执行,其中切换线程需要消耗大量时间和资源。所以,我们看到是后者的时间是前者的7倍左右。
但是,我们在上面的music和game线程中却发现多线程能大大的节省时间,提高效率,那又是为什么呢?其实,主要要看任务的类型,我们把任务分为I/O密集型和计算密集型,而多线程在切换中又分为I/O切换和时间切换。如果任务属于是I/O密集型,若不采用多线程,我们在进行I/O操作时,势必要等待前面一个I/O任务完成后面的I/O任务才能进行,在这个等待的过程中,CPU处于等待状态,这时如果采用多线程的话,刚好可以切换到进行另一个I/O任务。这样就刚好可以充分利用CPU避免CPU处于闲置状态,提高效率。但是如果多线程任务都是计算型,CPU会一直在进行工作,直到一定的时间后采取多线程时间切换的方式进行切换线程,此时CPU一直处于工作状态,此种情况下并不能提高性能,相反在切换多线程任务时,可能还会造成时间和资源的浪费,导致效能下降。这就是造成上面两种多线程结果不能的解释。
结论:I/O密集型任务,建议采取多线程,还可以采用多进程+协程的方式(例如:爬虫多采用多线程处理爬取的数据);对于计算密集型任务,python此时就不适用了。
十、线程同步锁
1.为什么需要同步锁
看下面例子,我们自定义一个减1的函数,初始赋值100,使用多线程,开启100个线程,那么期望的结果是最终结果为0,看下图:
该部分代码块为:
#进程锁
import threading
import time
def subtraction():
global sum
tmp=sum
time.sleep(0.001)
sum=tmp-1
sum=100
if __name__ == ‘__main__‘:
thread=[]
for x in range(100):
t=threading.Thread(target=subtraction)
thread.append(t)
t.start()
for x in thread:
t.join()
print(‘sum = ‘,sum)
上面现象产生的原因为:我们在开启100个线程的时候,当100个线程在进行subtraction函数操作时,首先要获取各自的sum(漏洞:共同的数据不能共享同时被多线程操作)和tmp,但是此时多线程会按照时间规则来进行切换,如果当前面某些线程在处理sum时未结束,后面的进程已经开始了(上面例子中的代码增加了休眠时间来体现该效果),此时拿到的sum就不再是sum-1的期望结果了,而是拿到了sum的值。这样就会导致,此次的线程进行自减1的操作失效了。So,就会导致上图的现象了,下面就讲述该如何通过加同步锁来解决该问题。
2.增加同步锁进行处理共同数据
如下图:
该部分代码块如下:
#进程锁
import threading
import time
l=threading.Lock()
def subtraction():
global sum
l.acquire()
tmp=sum
time.sleep(0.001)
sum=tmp-1
l.release()
sum=100
if __name__ == ‘__main__‘:
thread=[]
for x in range(100):
t=threading.Thread(target=subtraction)
thread.append(t)
t.start()
for x in thread:
t.join()
print(‘sum = ‘,sum)
难点:2.1何处加锁?何处释放锁?简单的原则就是:需要在引起多线程相互矛盾的共同数据部分枷锁,例如上面例子中的sum多个线程都要使用且后面线程期望使用的应该是前面线程减1的结果;还有在数据库操作时,使用自增主键时,也要对插入的数据进行加锁,否则将可能会导致主键重复。
2.2加锁的部分代码相当于是单线程串行运行了。
3.进程死锁
在线程间共享多个资源的时候,如果分别占有一部分资源并且同时在等待对方的资源,就会造成死锁。例如;数据库操作时A线程需要B线程的结果进行操作,B线程的需要A线程的结果进行操作,当A,B线程同时在进行操作还没有结果出来时,此时A,B线程将会一直处于等待对方结束的状态。
现象如下图:
该部分代码块如下:
#死锁
import threading
lockA = threading.Lock()
lockB = threading.Lock()
class Mythread(threading.Thread):
‘自定义线程类‘
def actionA(self):
‘actionA函数中运行actionB函数,运行actionB函数前加锁,运行actionB函数结束后释放锁‘
lockA.acquire()
print(self.name,‘运行actionA‘)
self.actionB()
lockA.release()
def actionB(self):
‘actionB函数中运行actionA函数,运行actionA函数前加锁,运行actionA函数结束后释放锁‘
lockB.acquire()
print(self.name,‘运行actionB‘)
self.actionA()
lockB.release()
def run(self):
‘运行函数‘
self.actionA()
self.actionB()
if __name__ == ‘__main__‘:
thread = []
for x in range(3):
t = Mythread()
thread.append(t)
print(‘以启动线程:‘, t.getName())
t.start()
for t in thread:
t.join()
print(‘ending......‘)
十一、多线程利器-队列(squeue)
场景:定义一个函数用例删除列表中最后一个元素,使用多线程来删除一个列表中的数据,现象如下图所示:
该部分的代码块如下:
import threading,time
l=[1,3,4,6,8]
def pop(l):
a=l[-1]
print(a)
time.sleep(0.001)
l.remove(a)
if __name__ == ‘__main__‘:
th=[]
for x in range(3):
t = threading.Thread(target=pop, args=(l,))
th.append(t)
print(t.getName())
t.start()
for x in th:
x.join()
# pop(l)
print(‘l = ‘,l)
此处由于多线程在操作时可能拿到相同的最后一个元素值,此时若前者的线程已经删除了该元素,则后面线程的函数则无法删除该元素(remove是按元素来进行删除的)。为了解决此次共享数据导致的多线程问题,我们可以利用前面的进程同步锁来处理,我们可以在获取和删除数据的时候加锁,代码如下:
import threading,time
lock = threading.Lock()
l=[1,3,4,6,8]
def pop(l):
# lock.acquire()
a=l[-1]
print(a)
time.sleep(0.001)
l.remove(a)
# lock.release()
if __name__ == ‘__main__‘:
th=[]
for x in range(3):
t = threading.Thread(target=pop, args=(l,))
th.append(t)
print(t.getName())
t.start()
for x in th:
x.join()
# pop(l)
print(‘l = ‘,l)
在该部分,我们引入新的模块queue(线程队列)来解决该问题,如下图所示:
![](http://i2.51cto.com/images/blog/201809/16/fe0361e391bbe9ac382647cb95834aea.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
该部分代码块如下:
import threading,time
import queue #线程队列
l=[1,3,4,6,8]
def pop(l):
a=l[-1]
print(‘a = ‘,a)
time.sleep(0.001)
l.remove(a)
if __name__ == ‘__main__‘:
q = queue.Queue()
for x in range(3):
t = threading.Thread(target=pop, args=(l,))
q.put(t)
while not q.empty():
data = q.get()
print(‘当前执行的线程:‘, data.getName())
data.run()
print(‘l = ‘,l)
Queue线程队列存放数据的三种方式:
1.1先进先出(FIFO)
q=queue.Queue()
q.put(maxsize)
1.2先进后出(LIFO)
q=queue.LifoQueue()
q.put(maxsize)
1.3按照优先级进出
q = queue.PriorityQueue()
q.put(list) #以长度为2的list存数据,第一个元素表示优先级,第二个元素表示存放的对应的值
代码块如下:
import queue #线程队列
num=5
#num用例限制队列中插入元素的个数,可不填
q1 = queue.Queue(num) #三种存取数据的顺序,1.先进先出(FIFO,不指明方式则默认该方式);2.先进后出(LIFO);3.按优先级进出()
q1.put(123)
q1.put(‘you‘)
q1.put({‘name‘:‘zhangzhou‘})
q2 = queue.LifoQueue(num)
q2.put(123)
q2.put(‘you‘)
q2.put({‘name‘:‘zhangzhou‘})
q3 = queue.PriorityQueue()
q3.put([2,123])
q3.put([3,‘you‘])
q3.put([1,{‘name‘:‘zhangzhou‘}])
if __name__ == ‘__main__‘:
while not q1.empty():
data = q1.get()
print(‘------------‘,data,‘------------‘)
while not q2.empty():
data = q2.get()
print(‘------------‘,data,‘------------‘)
while not q3.empty():
data = q3.get()
print(‘------Priority=‘,data[0],‘value=‘,data[1],‘-------‘)
以上是关于python线程及多线程实例讲解的主要内容,如果未能解决你的问题,请参考以下文章