一、什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。python中不同的线程实际上并没有同时运行:它们只是看起来像是同时运行的。由于GIL(Global Interpreter Lock),python一次只能运行一个Python线程。
二、调用线程的2中方式
python中使用threading实现多线程,有2中方式可以实现多线程调用
1、直接调用
threading.Thread() 一般接收两个参数:
线程函数名:要放置线程让其后台执行的函数,由我们自已定义,注意不要加();
线程函数的参数:线程函数名所需的参数,以元组的形式传入。若不需要参数,可以不指定。
import threading
import time
from time import ctime,sleep
def f1(n):
print('f1:',n)
time.sleep(n)
print(time.time())
def f2(n):
print('f2:',n)
time.sleep(n)
print(time.time())
#生成2个线程实例
t1=threading.Thread(target=f1,args=(3,))
t2=threading.Thread(target=f2,args=(6,))
#启动线程
t1.start()
t2.start()
2、类继承调用
首先,我们要自定义一个类,对于这个类有两点要求,
1、必须继承 threading.Thread 这个父类;
2、必须覆写 run 方法。
这里的 run 方法,和我们上面线程函数的性质是一样的,可以写我们的业务逻辑程序。在 start() 后将会调用。
class MyThread(threading.Thread):
def __init__(self,num):
#调用父类的初始化函数,注意,super().__init__() 一定要写,而且要写在最前面
super().__init__()
#threading.Thread.__init__(self)
self.num=num
#必须重写run函数,而且想要运行应该调用start方法,run函数定义每个线程要运行的函数
def run(self):
print("running on number:%s" %self.num)
time.sleep(1)
if __name__=='__main__':
t1=MyThread(1)
t2=MyThread(2)
t1.start()
t2.start()
三、守护线程(setDaemon) & join
守护线程:
无论是进程还是线程,都是:守护xxx会等待主xxx完毕后被销毁,
主进程与主线程在什么情况下才算运行完毕
1.主进程在其代码结束后就已经算运行完毕了,(守护进程就在此时被回收)。主进程会一直等非守护的子进程都运行玩不后回收子进程的资源,(否则会产生僵尸进程),才会结束
2.主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。主线程的结束意味着进程的结束,进程整体的资源都被回收,因而主线程必须在其余非守护线程都运行完毕后才能结束
1、setDaemon
setDaemon(True):
1.将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。
2.换句话说:开启,子线程不会挂起,主线程执行了,子线程及时没执行完也会中断
将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
代码实例
import threading, time
"""
设置两个线程
"""
def func1():
print('--非守护线程开始--',ctime())
time.sleep(2)
print('--非守护线程结束--',ctime())
def func2():
print('--守护线程开始--',ctime())
time.sleep(4)
print('--守护线程结束--',ctime())
if __name__ == '__main__':
t1 = threading.Thread(target=func1,args=())
t2 = threading.Thread(target=func2,args=())
t2.setDaemon(True)
t1.start()
t2.start()
print('主线程结束>>>>>>>>')
执行结果
从上面例子知func1为非守护线程,func2设置为守护线程,当主线程结束后,守护线程没执行完也会直接结束
--非守护线程开始-- Mon Jun 17 15:18:40 2019
--守护线程开始-- Mon Jun 17 15:18:40 2019
--非守护线程结束-- Mon Jun 17 15:18:42 2019
2、join():
在子线程完成运行之前,这个子线程的父线程将一直被阻塞
代码实例
import threading
from time import ctime,sleep
import time
def music(func):
for i in range(2):
print ("Begin listening to %s. %s" %(func,ctime()))
sleep(4)
print("end listening %s"%ctime())
def move(func):
for i in range(2):
print ("Begin watching at the %s! %s" %(func,ctime()))
sleep(1)
print('end watching %s'%ctime())
threads = []
t1 = threading.Thread(target=music,args=('七里香',))
threads.append(t1)
t2 = threading.Thread(target=move,args=('阿甘正传',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
#t.setDaemon(True) #把子进程设置为守护线程,必须在start()之前设置
t.start()
t.join()
#t.join()
# t1.join()
#t2.join()########考虑这三种join位置下的结果?
print ("all over %s" %ctime())
执行结果
线程t1,t2均使用了join()方法 ‘all over Mon Jun 17 15:27:28 2019’ 会在t1,t2执行完后才会打印
仅t1使用join方法时,‘all over Mon Jun 17 15:27:28 2019’,会在t1执行完后就打印,然后打印t2内容
在for循环完使用使用t.join()时,此时就是for 循环中最后一个线程使用了join(),即t2.join()
t1、t2均使用join()时
Begin listening to 七里香. Mon Jun 17 15:27:18 2019
end listening Mon Jun 17 15:27:22 2019
Begin listening to 七里香. Mon Jun 17 15:27:22 2019
end listening Mon Jun 17 15:27:26 2019
Begin watching at the 阿甘正传! Mon Jun 17 15:27:26 2019
end watching Mon Jun 17 15:27:27 2019
Begin watching at the 阿甘正传! Mon Jun 17 15:27:27 2019
end watching Mon Jun 17 15:27:28 2019
all over Mon Jun 17 15:27:28 2019
仅t2使用join()时
Begin listening to 七里香. Mon Jun 17 15:49:24 2019
Begin watching at the 阿甘正传! Mon Jun 17 15:49:24 2019
end watching Mon Jun 17 15:49:25 2019
Begin watching at the 阿甘正传! Mon Jun 17 15:49:25 2019
end watching Mon Jun 17 15:49:26 2019
all over Mon Jun 17 15:49:26 2019
end listening Mon Jun 17 15:49:28 2019
Begin listening to 七里香. Mon Jun 17 15:49:28 2019
end listening Mon Jun 17 15:49:32 2019
threading 模块提供的其他方法
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run(): 用以表示线程活动的方法。
start():启动线程活动。
join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
四、同步锁(Lock)
当一个数据有多个线程都可以对其进行修改的时候,任何一个线程改变它都会对其他线程造成影响,如果我们某一个线程在使用完之前,其他线程不能对其修改,就需要对这个线程增加一个线程锁。
有2点需要注意
1.分析Lock的同时一定要说明:线程抢的是GIL锁,拿到执行权限后才能拿到互斥锁Lock
2.使用join与加锁的区别:join是等待所有,即整体串行,而锁只是锁住一部分,即部分串行
python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
不使用锁时
输出的结果可能是89,86,91等等
io密集型:当第一个线程开始start的时候,由于sleep了0.001秒,这0.001秒对于人而言很短,但是对于cpu而言,这0.001秒已经做了很多的事情了,在这里cpu做的事情就是或许已经start了100个线程,所以导致大多数的线程调用的count值还是100,即temp=100,只有少数的线程完成了count=temp-1的操作,所以输出的count结果不确定,可能是89,86,91等。
当不sleep且使用num-=1时结果是正常的是因为因为动作太快(完成这个动作在切换的时间内)
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
#num-=1
temp=num
time.sleep(0.001)
num =temp-1 #对此公共变量进行-1操作
num = 100 #设定一个共享变量
thread_list = []
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
使用锁可以做到线程安全,每次操作共享数据时线程都会先1获取锁然后进行操作,就可以保证同一时刻仅有一个线程在操作共享数据
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
#num-=1
lock.acquire()
temp=num
#print('--get num:',num )
time.sleep(0.001)
num =temp-1 #对此公共变量进行-1操作
lock.release()
num = 100 #设定一个共享变量
thread_list = []
lock=threading.Lock()
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
五、死锁和递归锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)
mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()
mutexA.release()
def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)
mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()
'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
六、条件变量同步(Condition)
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。
lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传入锁,对象自动创建一个RLock()。
wait([timeout]):线程挂起,直到收到一个notify通知或者超时(可选的,浮点数,单位是秒s)才会被唤醒继续运行。wait()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。调用wait()会释放Lock,直至该线程被Notify()、NotifyAll()或者超时线程又重新获得Lock.
notify(n=1):通知其他线程,那些挂起的线程接到这个通知之后会开始运行,默认是通知一个正等待该condition的线程,最多则唤醒n个等待的线程。notify()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。notify()不会主动释放Lock。
notifyAll(): 如果wait状态线程比较多,notifyAll的作用就是通知所有线程(这个一般用得少)
代码实例
import threading,time
from random import randint
'''
条件变量同步(Condition)
除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法,一般线程间存在数据共享时使用
'''
class Producer(threading.Thread):
def run(self):
global L
while True:
val=randint(0,100)
print('生产者',self.name,":Append"+str(val),L)
if lock_con.acquire():
L.append(val)
lock_con.notify()
lock_con.release()
time.sleep(3)
class Consumer(threading.Thread):
def run(self):
global L
while True:
lock_con.acquire()
if len(L)==0:
lock_con.wait() #条件不满足时调用,线程会释放锁并进入等待阻塞 收到notify()的通知时程序从lock_con.acquire()处继续向下执行
print('消费者',self.name,":Delete"+str(L[0]),L)
del L[0]
lock_con.release()
time.sleep(1)
if __name__=="__main__":
L=[]
lock_con=threading.Condition()
threads=[]
for i in range(5):
threads.append(Producer())
threads.append(Consumer())
for t in threads:
t.start()
for t in threads:
t.join()
七、同步条件(Event)
条件同步和条件变量同步差不多意思,只是少了锁功能,因为条件同步设计于不访问共享资源的条件环境。event=threading.Event():条件环境对象,初始值 为False;
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
代码实例
'''
同步条件(Event),和条件变量同步差不多意思,只是少了锁功能,一般线程间不需要共享数据时使用
'''
import threading,time
class Boss(threading.Thread):
def run(self):
print("BOSS:今晚大家都要加班到22:00。")
event.set()
time.sleep(5)
print("BOSS:<22:00>可以下班了。")
event.set()
class Worker(threading.Thread):
def run(self):
event.wait()
print("Worker:哎……命苦啊!")
time.sleep(0.25)
event.clear()
event.wait()
print("Worker:OhYeah!")
if __name__=="__main__":
event=threading.Event()
threads=[]
for i in range(5):
threads.append(Worker())
threads.append(Boss())
for t in threads:
t.start()
for t in threads:
t.join()
八、信号量(Semaphore)
信号量用来控制线程并发数的,semaphore是一个内置的计数器,每当调用acquire()时,内置计数器-1,每当调用release()时,内置计数器+1,计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。一般BoundedSemaphore。
代码实例
'''
信号量Semaphore控制线程并发数
'''
import threading,time
class myThread(threading.Thread):
def __init__(self, num):
#super().__init__() # 调用threading类的构造方法,python3的写法super().__init__()
threading.Thread.__init__(self)
self.num = num
def run(self):
#semaphore.acquire() #和if semaphore.acquire()效果一样
if semaphore.acquire():
print(self.name,'开始执行:',time.ctime())
begin = time.time()
time.sleep(self.num+1)
end = time.time()
t=end-begin
print(self.name,'结束执行:',time.ctime(),'耗时:',t)
semaphore.release()
if __name__=="__main__":
#semaphore=threading.Semaphore(5)
semaphore = threading.BoundedSemaphore(5)
thrs=[]
for i in range(10):
#semaphore.acquire()
t=myThread(i)
thrs.append(t)
t.start()
for i in thrs:
i.join()
print('结束>>>>>>>>')
九、多线程利器(queue)
queue列队类的方法
创建一个“队列”对象
import Queue
q = Queue.Queue(maxsize = 10) Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。
将一个值放入队列中
q.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。
将一个值从队列中取出
q.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。
代码实例
import threading,queue
from time import sleep
from random import randint
class Production(threading.Thread):
def run(self):
while True:
r = randint(0, 100)
q.put(r)
print("生产出来%s号包子" % r)
sleep(1)
class Proces(threading.Thread):
def run(self):
while True:
re=q.get()
print("吃掉%s号包子" % re)
if __name__=="__main__":
q=queue.Queue(10)
threads=[Production(),Production(),Production(),Proces()]
for i in threads: