系统中运行的应用程序,打开浏览器、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的错误"""