1多线程介绍
1.1:什么是多进程
- 系统正在运行的一个应用程序,一个应用程序等于一个进程。也就是说在系统中运行的应用程序占用了电脑cpu的一个进程,多进程的意思就是一个电脑中可以同时打开多个应用程序,现在的电脑基本都可以实现多进程。
1.2:多进程的特点
- 一个cpu核心只能执行一个进程,其它进程就处于阻塞状态。多个cpu核心,就可以同时执行多个任务。
1.3:多线程
1.3.1:多线程的特点
- 一个进程可以包含多个线程
- 在python中一次只能进行一个线程,也就是说在python中的多线程不是真正的多线程
- 线程锁(资源竞争问题),由于在python中一次只能执行一个线程,所以才需要使用线程锁
1.4:爬虫使用多线程的好处
- 在一些场景下比较适合多线程,在一些的环境下可以优化程序,会在一个线程阻塞的时候,另一个线程快速的来连接
2:如何创建多线程
2.1:通过函数创建多线程
- 需要使用threading模块当中的一个Threa类,有一个参数target参数,需要我们传递一个函数对象。一般这个参数,是用来传递子线程的逻辑,target这个参数要使用关键字传参,传入的对象是一个函数对象(注意带括号的是一个函数的引用,不带括号的是一个函数的对象)
- 代码:
# 导入模块
import threading # 用来创建多线程的一个类
import time # 包含时间的模块
# 创建多线程的方法
# 1:先创建一个子线程函数
def num():
print('这是一个子线程')
time.sleep(1)
if __name__ == '__main__':
# 2:创建一个子线程,注意需要传递的是一个函数对象而不是一个函数的引用
for i in range(5):
t = threading.Thread(target=num)
t.start() # 创建并执行字线程
- threading.Thread类的参数
- target:需要传递一个函数对象,注意不是引用
- name:线程的名字,方便分辨线程的名字和功能
- agrs:以元组的形式传递给子线程参数
2.2:通过类来创建多线程
- 需要自定义一个类,这个类继承threading.Thread方法
- 在这个类中需要重写run方法
- 在run方法中定义子线程
- 代码:
# 导包
import threading
import time
# 自定义一个类
class T(threading.Thread): # 继承父类的一个类
def run(self) -> None: # 重写run方法
# 创建一个子线程
for i in range(5):
print('这是一个子线程')
time.sleep(1)
if __name__ == '__main__':
t = T() # 实例化类
t.start() # 创建并执行线程
2.3:主线程和子线程的优先级
- 在python中程序会等待子线程结束完的时候在结束,主线程不会等待子程序结束在执行
- 可以使用threading.Thread中的一个方法join方法来使子程序执行完在执行主线程
- 也可以使用setDaemon(True)来守护线程,使线程只执行一个线程
3:多线程的一些方法
3.1:查看多线程的数量
def demo1():
for i in range(5):
print('demo1--%d' % i)
def main():
print(threading.enumerate())
t1 = threading.Thread(target=demo1, name='demo1')
print(threading.enumerate())
t1.start() # 创建并执行线程
print(threading.enumerate())
if __name__ == '__main__':
main()
注意:
- start(),是用来创建并执行线程
- threading.Thread(…),是用来传递线程的时间函数的逻辑,并不创建线程
3.2:线程间的资源竞争
- global:通过函数的内部来修改全局变量
- 多线程的特点
- 解决方法
- 多线程有资源竞争问题,
- 解决方法
- 1:先把第一个线程的工作做完,在把第二个线程的工作做完
- 2:时间等待,join方法
4:线程锁
- 会使代码的效率降低
- 作用:一个线程的数据要修改全局数据的时候就需要使用线程锁,默认情况下是不上锁
- 副作用:会造成代码的效率下降
4.1:上锁代码
t = threading.Lock()
lock.acquire() # 上锁一定要在改变全局变量之前上锁
lock.release() # 同理,解锁也要在改变全局变量之前之后在解锁
注意
- 多上一把锁会阻塞线程,这个时候需要把这个锁给解开,但是若上的锁的的数量多的时候会造成线程的阻塞,这个时候如果解锁也会造成线程的阻塞,这是因为lock为不可重复的锁,只能上一次锁和解一次锁,多上的话就会阻塞线程
- 上多把锁的代码,需要使用Rlock这个代码来上锁
lock = threading.RLock() # 创建一个可以重复的锁的实例对象
4.2:死锁
- 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
- 解决方法
5:线程队列
- 线程队列的特点是先进先出
- 线程队列是一个内置的模块,内部也实现了一些锁的一些特征
- 线程模块:Queue
5.1:线程队列模块常用的方法
- empty():判队列是否为空,队列为空返回True,队列不是空的时候返回False代表队列不是空的
- full():判断队列是否是满的,当队列为满的的时候返回True,队列不是满的时候返回False
- get():从队列中取出数据
- put():向队列中添加数据
- join():等待队列为空在进行别的操作
- qsize():返回队列的长度
5.2:队列的长度
- 在创建队列的实例化对象的时候,是可以指定队列的长度的,如果不指定队列的长度则会指定默认的长度,默认长度的队列大概可以添加2G的数据,一般情况下,我们在使用队列的时候都会指定一下这个队列的长度
- 如果在列表中添加的数据超过了列表的长度就会出现阻塞的情况,在Python中的阻塞情况是最令人无奈的,代码在一直运行,但是不会出现报错
- 出现阻塞的解决方法
- 1:时间等待方法,在添加元素的时候,使用关键字参数(timeout=n)来使代码可以执行n秒,如果出现阻塞了就会报错
- 2:非阻塞方法,使用另一个添加队列数据的方法:put_nowait(),这个添加队列数据的方法,只要队列出现阻塞的情况就会立马报错
- 当取数据的个数超出列表中数据的个数,代码也会阻塞,解决方法和添加数据出现阻塞的处理情况一样
5.3:队列的使用场景
- 在爬虫中,可以使用队列来存放要爬取的url地址,然后一个一个的取出数据,可以代替循环的便利来使用,可以说是一个新的思路
- 注意:在创建线程的时候传递的参数是函数对象,而不是函数的调用
- 如果在使用多线程的时候报出:jsondeccdeerror的错误,是因为抓取的频率太快了,json数据没有及时的返回,造成数据抓取不全