多线程
多进程
系统中运行的应用程序,打开浏览器、pycham等都是一个个的应用程序,可以同时运行。一个应用程序就是一个进程,多个就是多进程。比如电脑卡住了,但是可以打开任务管理器去关掉占用资源多的应用程序。最开始电脑只有1个CPU,只能执行一个进程,其他进程就会处于堵塞的状态,之所以感觉到多个进程同时运行,是CPU进行高速的切换处理,出现了多核,多个CPU就可以同时执行多个任务。
多线程
CPU 是通过线程去执行进程的,进程中的执行单位是线程,进程中包含的执行单元就是线程,一个进程可以包含多个线程。一个微信就是一个进程,每一个聊天窗口就是一个线程,在python中一次只能执行一个线程,打开多个线程之后,会有线程锁的存在,来解决资源竞争的问题。python中的多线程是伪多线程,并不是纯粹意义上的多线程,同一时间只有一个线程处于执行的状态。
充分利用等待的时间,比如线程1对url1发送请求,在等待相应的时间内,线程2就可以对url2发送请求,在等待的时间内,线程3就可以对url3发送请求,这时url1获取到响应的内容,就可以进行下一步的操作,把时间充分利用,在等待的时间去做其他的事情,最大限度的提高爬取的效率。
多线程的创建
通过函数创建
通过threading模块中的Thread类,里面有个参数target,通过参数去传递函数对象,传递时函数不用加括号,来实现多线程的逻辑,把需要做的事情都写到函数里,通过target传递进去,要用start给一个启动的状态。
注意:要先创建一个函数,函数里存放多线程的实现功能,把通过threading模块中的Thread类,把函数传递进去。
通过类来创建
自定义一个类,若想实现多线程,就要继承父类,threading.Thread,还要重写run()方法。
import threading
import time
# 1.通过函数创建多线程
def demo1():
#线程的函数事件
print("子线程!")
if __name__ == '__main__':
for i in range(7):
# 创建多线程t,通过threading里的Thread类,把demo1传入到target中
t = threading.Thread(target=demo1) #仅仅是传递函数事件,并未创建线程
t.start() # 创建并启动多线程(是一个启动的状态),告诉CPU可以调用多线程了
# 如果需要启动多次,用for循环实现
"""def __init__(self, group=None(对线程进行分组), target=None(接收函数事件), name=None(线程分组的名字),
args=()(传入的元组), kwargs=None, *, daemon=None):
"""
# 2. 通过类创建多线程
# 创建MyThread类,继承父类threading.Thread的功能
class MyThread(threading.Thread):
# 重写run方法
def run(self):
for i in range(5):
print("这是一个子线程!")
if __name__ == '__main__':
# 实例化类
m = MyThread() # 实例化对象
# start 启动子线程
m.start() # 通过start执行多线程
# 小案例
def test():
for i in range(4):
print("子线程")
# time.sleep(1) # 如果没有强制等待,主线程111运行的时间不固定
if __name__ == '__main__':
t = threading.Thread(target=test)
t.start()
print("111")
# 函数运行的时候111是最后才执行的,执行完函数运行结束。
# 而用了多线程之后,有点同步的意思,先打印1个“子线程”,
# 然后打印111,之后又继续执行多线程中的剩余打印“子线程”
总结:正常函数运行的时候,在主函数中运行最后一行代码就结束了,而在多线程中,会继续运行子线程,不管主线程有没有运行完成(例题中的print(111)),都会等待子线程(print(“子线程”))运行完毕再退出。主线程会等待子线程运行结束后才结束运行。
如果非要子线程运行结束后再执行主线程,就需要在主线程前加上time.sleep,强制等待,或者 用 t.join(),不管前面的子线程运行多久,都要等待子线程运行完之后,才运行主线程。
# 小案例,先运行子线程,后运行主线程
def test():
for i in range(4):
print("子线程")
# time.sleep(1) # 如果没有强制等待,主线程111运行的时间不固定
if __name__ == '__main__':
t = threading.Thread(target=test)
t.start()
# 第一种方法,用time强制等待
time.sleep(3)
# 第二种方法,用join
t.join()
print("111")
查看线程的数量
主线程是本来就有的,除了主线程之外,我们会创建多个子线程,如何查看子线程的数量?先回顾一下enumerate。
# enumerate()回顾
test_lst = ['xxx', 'yyy', 'zzz']
# 取出列表数据的方法,1是下标,2是索引
for i in test_lst:
print(i)
for i in enumerate(test_lst):
print(type(i), i) # 元组类型的数据,包含索引和列表元素值
for index, i in enumerate(test_lst):
print(index, i) # 返回两个值,一个是索引,一个是元素值
threading.enumerate(),返回存活的线程的列表,没有消亡的,存活的线程都会以列表的形式返回。主线程一般会等到子线程运行结束,才退出。
import threading
import time
# threading.enumerate()
# 返回存活的线程的列表,没有消亡的,存活的线程都会以列表的形式返回。
def test1():
for i in range(5):
print("demo1--%d" % i)
def test2():
for i in range(5):
print("demo2--%d" % i)
if __name__ == '__main__':
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
print(threading.enumerate())
# 会根据子线程执行的速度快慢,主线程执行的先后顺序,返回1个或2个存活线程
import threading
import time
def test1():
for i in range(5):
time.sleep(1)
print("demo1--%d" % i)
def test2():
for i in range(5):
time.sleep(1)
print("demo2--%d" % i)
if __name__ == '__main__':
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
print(threading.enumerate())
# 返回3个当前存活的线程,在t1,t2强制等待的时候,先运行了主线程
对程序稍微做一下修改,当只剩一个线程的时候,退出。
import threading
import time
def test1():
for i in range(8):
time.sleep(1)
print("demo1--%d" % i)
def test2():
for i in range(5):
time.sleep(1)
print("demo2--%d" % i)
if __name__ == '__main__':
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
while True:
print(threading.enumerate())
time.sleep(1)
if len(threading.enumerate()) <= 1:
break
# 让程序一直运行,如果只剩下一个线程的时候,退出,demo1运行8次,demo2运行5次
# 之前运行的是3个线程,demo2 运行到4的时候,该线程就已经消亡了,剩下的线程就只有2个
# 在demo1 运行到7的时候,该线程也消亡了,就只剩下主线程
threading.Thread()只是把函数事件传递进去,start才是真正意义上的创建并启动线程。
import threading
import time
def test1():
for i in range(3):
time.sleep(1)
print("demo1--%d" % i)
def test2():
for i in range(3):
time.sleep(1)
print("demo2--%d" % i)
if __name__ == '__main__':
print('前', threading.enumerate()) # 1个线程
t1 = threading.Thread(target=test1) # 该方法只是把函数时间传递进去
print('中', threading.enumerate()) # 1个线程
t2 = threading.Thread(target=test2)
t2.start() # 创建并启动线程
print('后', threading.enumerate()) # 2个线程
# 只有经过start之后,才算是真正的创建并启动线程
多线程的工作原理
在没有创建,启动多线程之前,只有主线程在运行。增加了一个子线程,好比公司招了一个人,给他分配任务,子线程往下做事情,同时主线程也会继续做自己该做的事情,两个线程同时在运行,两者并不冲突。再增加的话,三者同时运行,各司其职。
线程间的资源竞争(线程锁)
用线程锁解决之间资源竞争的问题。
复习一下局部变量和全局变量
a = 10 # 全局变量
def fn():
# global a # 声明为全局后,外部1为10,内部、外部打印的都是99
a = 99 # 局部变量
print("函数内部的a为%d" % a) #99
print("函数外部1的a为%d" % a) # 10
fn()
print("函数外部的a为%d" % a) # 10
没有资源竞争时的多线程。
num = 100
def demo1():
global num
num += 1
print("demo1--%d" % num)
def demo2():
print("demo1--%d" % num)
def main():
t1 = threading.Thread(target=demo1) # 把函数传递进去
t2 = threading.Thread(target=demo2)
t1.start() # 创建并启动线程
t2.start()
print("main--%d" % num)
if __name__ == '__main__':
main()
""""运行结果三者都为101,程序启动的时候,t1运行,num为全部变量,执行+1操作,返回101
t2运行时,使用+1过的num。也不排除t2抢到资源优先启动的情况,几率很小。主线程只进行了资源访问"""
通过传参的形式,往函数中传入参数,当传入的参数足够大的时候,会出现资源竞争的问题。
import threading
import time
num = 0
def demo1(nums):
global num
for i in range(nums):
num += 1
print("demo1--%d" % num)
def demo2(nums):
global num
for i in range(nums):
num += 1
print("demo1--%d" % num)
def main():
# 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
t1 = threading.Thread(target=demo1, args=(1000000,)) # 把函数传递进去
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start() # 创建并启动线程
t2.start()
time.sleep(3)
print("main--%d" % num)
if __name__ == '__main__':
main()
"""当传入的10000时,demo1--10000,demo1--20000,main--20000;
当传入1000000时候,三者的值就发生了变化,没有直接相加的那么多,出现了资源竞争的问题
CPU运行的时候,要看哪个先抢到资源,当传入的数值比较小的时候,循环次数少,
循环的次数多了,就有可能被抢走资源。"""
在上述例题中,num初始值为0,有可能demo1先抢到资源,对num进行+1的操作,重新赋值给num,再重新获取num的变量,此时num为1,再进行+1的运算,有可能还没来得及重新赋值给num,就被demo2抢走了资源,此时demo2拿到的num就为1,做完+1操作后重新赋值给num,此时做了3次+1的操作,但有效的只有2次,丢了一次;在运行过程中就会出现相互抢的情况,造成了最后的结果没有那么多。
此时,就要用线程锁解决资源竞争的问题,可以在t1.start之后,用time强制等待3秒,让demo1先做完运算,再执行demo2。这时如果运算用不了3秒,就会造成程序资源的浪费,如果强制等待1秒,demo1运行不完,又解决不了实际问题。可以用一把锁来解决问题,threading.Lock(),Lock对应的是一个类,有很多方法,acquire是加锁,release是解锁。
import threading
import time
num = 0
# Lock只能上一把锁
lock = threading.Lock()
def demo1(nums):
global num
# 上锁
lock.acquire()
for i in range(nums):
num += 1
# 解锁
lock.release()
print("demo1--%d" % num)
def demo2(nums):
global num
# 上锁
lock.acquire()
for i in range(nums):
num += 1
# 解锁
lock.release()
print("demo1--%d" % num)
def main():
# 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
t1 = threading.Thread(target=demo1, args=(1000000,)) # 把函数传递进去
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start() # 创建并启动线程
t2.start()
time.sleep(3)
print("main--%d" % num)
if __name__ == '__main__':
main()
我们要把锁放在有可能产生资源竞争的地方,上锁和解锁是一一对应的,不能在同一个地方上2把锁,好比门已经锁上了,再去锁的话就没有意义,程序会停到上锁的地方,因为找不到地方可以上锁。
可以用rlock完成同一个地方多次上锁和多次解锁,上锁和解锁的次数也是一一对应的。
import threading
import time
num = 0
# RLock能上多把锁,上多少锁就要解多少锁
rlock = threading.RLock()
def demo1(nums):
global num
# 上锁
rlock.acquire()
rlock.acquire()
for i in range(nums):
num += 1
# 解锁
rlock.release()
rlock.release()
print("demo1--%d" % num)
def demo2(nums):
global num
# 上锁
rlock.acquire()
rlock.acquire()
rlock.acquire()
for i in range(nums):
num += 1
# 解锁
rlock.release()
rlock.release()
rlock.release()
print("demo1--%d" % num)
def main():
# 注意,传参的时候是以元组的形式传入的,只传一个内容,要加个逗号
t1 = threading.Thread(target=demo1, args=(1000000,)) # 把函数传递进去
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start() # 创建并启动线程
t2.start()
time.sleep(3)
print("main--%d" % num)
if __name__ == '__main__':
main()
不管是Lock还是Rlock,上锁和解锁次数不一致都会出现问题,锁要上在可能会出现资源竞争的位置。
线程队列
特点:先进先出
from queue import Queue
"""
empty():判断队列是否为空
full():判断队列是否满了
get():从队列中取数据
put():把一个数据放到队列中
"""
# 实例化对象,然后可以使用里面的方法
q = Queue()
# 返回布尔型,队列为空,返回为True;队列不为空,返回False
print(q.empty()) # True
# 如果不为空,就可以用get从队列中取值
print(q.full()) # False
# 判断队列是不是满了,True表示满的,False表示不是满的
# 如果队列不是满的,可以用put往里面加数据
现在往里面加数据进行测试
from queue import Queue
# 在初始化的时候,没有设定容量,可以存放大概2G的数据
# q = Queue()
# 可以在初始化的时候指定容量大小
q = Queue(3) # 初始化容量为3
print(q.empty())
print(q.full())
q.put(1)
q.put(2)
q.put(3)
print('*'*50)
print(q.empty())
print(q.full())
"""没有指定容量,在添加1之前,队列是空的,不是满的;添加了1之后,队列不是空的,但也不是满的,
得到的结果是True,False,False,False
如果指定了队列长度为3,添加了1,2,3,之后,队列容量达到了上限,得到的结果是True,False,False,True
如果队列添加满了后,再往队列里加内容,程序就会卡到那,q.put(4,timeout=2),在2秒后就会提示queue.full的错误
跟q.put_nowait(4)一样的效果"""
print(q.get())
print(q.get())
print(q.get())
print(q.get(timeout=2))
"""先进先出,先传进去的最先取出来,如果多取一个程序也会卡到那,q.get(timeout=2),2秒后提示queue.Empty的错误"""