系统编程:多任务编程
1. 线程: 可以理解成执行代码的分支,线程是执行对应的代码的
1.1 线程的工作原理: cpu会调度对应的线程去工作,也可以说线程是cpu调度的基本单位
1.2 主线程会等待所有的子线程执行完成以后程序再退出
2. CPU:对应用程序进行处理执行。
2.1 cpu双核:好比两个人同时干活(两个应用程序执行)只有多核cpu才能做到多个任务真正意义上一起执行。
2.2 cpu单核:好比一个人在干活(一个应用程序运行)
3. 并发: 任务数大于cpu的核数,多个任务轮流执行,由于cpu切换速度特别快,看起来像是一起运行,其实是假象。 交替执行
并行: 任务数小于或者等于cpu的核数,那么多个任务是真正意义一起执行。 同时进行
4. 线程参数的介绍
# group = None 表示线程组,目前这个值必须使用None
# target:执行的目标函数
# name: 线程的名字, 如果不指定名字,那么名字的命名格式Thread-1,....
# args: 以元组方式给函数传参
# kwargs: 以字典的方式给函数传参
# daemon = None:表示以命名参数给函数传参数
例如:sub_thread = threading.Thread(target=show_info, name="mythread", args=("杨钰莹", 18),kwargs={"name":"杨幂", "age":18})
4.1 查看线程的活动列表
4.1 获取当前活动的线程列表 thread_list = threading.enumerate() --->print(thread_list)
4.2 查看线程的数量 threading.active_count()
4.2 获取线程的对象
4.2 current_thread = threading.current_thread()
5. 线程注意点
5.1. 线程之间执行是无序
5.2. 主线程会等待所有的子线程执行完成以后程序再退出
要主线程退出了程序,子线程还在执行代码,执行完毕后才能退出程序
5.3 表示守护主线程,主线程退出子线程直接销毁不再执行对应的代码
5.3.1 work_thread.setDaemon(True) # 守护主线程
5.3.2 daemon=True # 传参数给线程
# -扩展: daemon=True 表示守护主线程,主线程退出子线程直接销毁不再执行对应的代码
6. 自定义线程的类方法,属性。
6.1 介绍:通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法,__init__方法。
6.2 如果子类提供了构造方法(自定义属性),默认不会调用父类的构造方法,需要自己手动调用
建议大家如果以后自己提供了构造方法需要使用super调用父类的构造方法
super(父类名, self).__init__()
6.3 创建自定义线程对象, 提示: 不要这里使用target执行某个任务,因为执行任务同一在run方法里面执行的
custom_thread(类对象) = CustomThread(类名)("show_info1", "show_info2")
重写run类方法,不要直接调用run方法,因为start方法内部已经调用run方法,启动线程统一start方法
7. 全局变量:在一个进程内所有的线程共享全局变量,共享数据。
7.1 多线程对全局变量进行操作时,会有可能出现资源竞争,导致数据错误。
7.2 可变类型(全局变量):内存地址不变,不要加上global全局声明。
7.3 不可变类型(全局变量):内存地址发生变化,需要公用全局变量,就要global声明。
8. 线程同步:按照预先定义好的顺序一个任务执行完成以后另外一个任务才能执行
线程等待:其实主线程等待第一个线程执行完成以后程序在执行下面的代码,让第二个线程再去执行
8.1 解决线程出现资源竞争的关系,等待子线程执行完成以后,在执行其他线程,其实有多任务,变成单任务的线程去执行代码。性能下降。
格式:first_thread.join() —————>线程名.join()
9. 互斥锁:能保证同一时刻只有一个线程去执行代码,具体那个线程抢到锁我们决定不了,但是加上互斥锁多任务瞬间变成单任务。性能下降。
9.1 执行流程:在外部创建互斥锁 lock = threading.Lock()
在线程执行代码方法中上锁:lock.acquire()
有上锁就要释放锁,不释放就会变成死锁,造成代码堵塞:lock.release()
线程执行是无序的,谁先抢到锁,谁先执行。
10. 死锁:程序设计时要尽量避免(银行家算法)
添加超时时间等
2.进程
1. udp发送广播消息:默认情况下是不允许发送广播消息的,想要发送广播消息,要开启广播选项
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)
1.socket.SOL_SOCKET:当前socket
2.socket.SO_BROADCAST:广播选项
3.True:开启广播选项
2. 进程: 通俗理解一个运行起来的程序或者软件叫做进程
2.1 每次启动一个进程都需要向操作系统索要运行资源,让进程中的线程去执行对应的代码,进程是操作系统分配资源的基本单位
2.2 默认情况下一个进程只有一个线程,线程是依附在进程里面的, 没有进程就没有线程, 当在进程里面还可以创建多个线程
2.3 如何理解进程: 把进程想成现实生活中的公司,公司可以给员工提供办公资源(办公桌椅,办公电脑等资源), 真正干活的是员工,所以员工可以想成线程,公司就是进程
3. 进程和线程的对比
2.1 进程是操作系统资源分配的基本单位,每启动一个进程都需要向操作系统索要运行资源,默认一个进程只有一个线程,线程是依附在进程里面的
2.2 线程是cpu调度的基本单位, 通过线程去执行进程中代码, 线程是执行代码的分支
2.3 多进程开发比单进程多线程开发稳定性要强,但是多进程开发比多线程开发资源开销要大
2.4 多进程开发某个进程死了不会影响其它进程的运行,但是多线程开发该进程死了那么这些线程都要进行销毁
4. 获取当期进程活动
main = multiprocessing.current_process()
4.1 获取进程对应的编号 main.pid 根据进程编号强杀指定进程 os.kill(os.getpid(), 9)
5. 进程的参数的介绍
# group: 进程组,目前只能使用None
# target: 执行目标函数
# name: 进程名字
# args: 以元组方式传参
# kwargs: 以字典的方式传参
例如: sub_process = multiprocessing.Process(group=None, target=show_info, name="myprocess", args=("刘恺威", 35))
6. 进程之间:多进程之间不共享全局变量
6.1 子进程是对主进程进行拷贝或者克隆,子进程是主进程的一个副本,只是拷贝了全局变量,进程之间是相互独立的,操作不是一个全局变量
6.2 多进程之间也是无序执行,主进程等待写入进程执行完成以后再启动读取进程执行对应的任务
write_process.join()--->join主进程等待子进程执行完毕以后在执行主进程
sub_process.daemon = True----->子进程守护主进程,主进程结束程序,子进程直接销毁
7. 进程之间的通信queue是:进程之间不共享变量,同一个程序之间不同进程消息的通信。
进程之间的通信socket是:不同设备之间的通信。
8. 消息队列queue:创建队列 queue = multiprocessing.Queue(3)
8.1
# 3:代表队列放入的最大数据个数
# queue.put() 总结:队列里面可以放入任何的数据类型
# 提示: 如果队列满了, 使用put放入数据的时候需要等待队列有空闲位置才能再放入数据,代码阻塞
# queue.put_nowait():表示队列满了不会进行等待,如果放入数据不成功会直接崩溃---->建议: 以后向队列里面放入数据统一put方法
8.2
# 获取队列数据 value = queue.get()
# 如果队列空就等待队列有数据才能进行获取,否则一直等待
# get_nowait: 取值的时候不会等待,如果队列为空那么直接崩溃----->建议: 获取数据的使用同一使用get方法
8.3
# queue.empty()
# 坑点: 使用empty查看队列是否为空的是不会等待数据放入完成以后再进行队列的状态,有可能获取的状态不对是空的
# 解决代码:1. 延时操作 time.sleep(0.001)
9. 进程池Pool:池子里面放的都是进程,进程池可以根据任务自动创建进程,会合理利用进程池中的进程完成多任务,能够节省资源的开销
9.1
# 创建进程池 pool = multiprocessing.Pool(3)
# 3:代表进程池中进程的最大个数
# 提示: 进程池会根据任务的执行情况尽量少创建的进程,最多创建指定个数的进程,减小调用资源开销
9.2
# 执行进程池pool.apply(方法)--->同步执行: 一个任务执行完成另外一个任务才能执行
# pool.apply_async(方法)---->异步执行:方式用进程池执行对应的任务
9.3
异步执行任务时候:
# 子进程是同时进行的,所以要关闭进程池,表示不再接收其它任务 pool.close()
# 主进程不会等待进程池执行完成以后程序再退出,进程池执行完以后在进行主进程。等待进程池 pool.join()
9.4
同步执行的时候:(一般不使用)
# 子进程是交替进程的,不要关闭进程池,子进程执行完毕,就完成接收的任务。
# 主进程会等待子进程池执行完成以后在程序退出。
10. 进程中消息队列和进程池的综合运用
1.创建进程池中的消息队列
queue = multiprocessing.Manager().Queue()
2.创建进程池
pool = multiprocessing.Pool(2)
3.协程
1. 迭代:使用for循环遍历取值的过程 迭代对象-比如:元祖,列表,字典,字符串,集合,range(都是class类)
for循环列表,enumeration()--遍历出列表下标和对应的数据
for循环字典,for key, value in {'name': 'zs', 'age': 18}.items()-- 遍历字典是无序的
1.1 可迭代对象:能够使用for循环遍历取值的对象叫做可迭代对象
2. 判断对象是否为可迭代对象 ----Itertable(可迭代对象类型)int(整数型,python基本数据类型)
from collections import Iterable
isinstance((类型),Itertable)---判断是否为可迭代对象
isinstance还可以判断参数,对象的类型
3. 自定可迭代对象(自定义类对象默认是不可迭代的)
定义:在类的里面提供—__iter__方法,创建的对象就是可迭代对象
可迭代对象之所以能够把数据迭代出来是通过迭代器完成
4. 自定义迭代器(Iterator)
4.1 定义:在类里提供__iter__和__next__的方法创建的对象就是迭代器
__iter__:返回迭代器
__next__:获取迭代器中下一个值
总结:自定义迭代器对象 传给 自定义的可迭代对象,通过__iter__返回return迭代器给迭代对象。
4.2 所以:
可迭代对象本质:通过迭代器依次将数据取出
迭代器的作用:记录数据的位置,以便获取下一个位置的值
5. __iter__和__next__函数
5.1
__iter__函数:获取的是可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法
__next__函数:获取迭代器中下一个值,会调用迭代器身上的__next__方法,取值操作通过迭代器__next__方法
5.2
for循环遍历:
本质:1.如果遍历的对象是可迭代对象,那么会通过__iter__函数获取迭代器,然后通过__next__函数获取迭代器中的下一个值。2.如果遍历的对象是迭代器那么直接通过__next__函数获取迭代器中的下一个值
提示:for循环内部自己捕获停止迭代的异常,while Ture循环没有自己捕获停止迭代异常
6. 生成器(generator)
定义:生成器是一个特殊的迭代器,也就是说生成器依然可以使用__next__函数和for循环取值
6.1 创建生成器
生成器的方式一:把列表推到式中的中括号改成小括号创建的对象就是生成器
(1):可以使用next函数启动生成器的取值
(2):for循环生成器,依次取到生成器的下一个值
6.2 创建生成器
生成器的方式二:在def函数里面看到yield关键字那么就表示就是一个生成器, yield 返回生成一个生成器
(1)yield特点:yield 返回生成一个生成器,代码执行到yield会暂停,然后把结果返回出去,再次启动生成器会在暂停的位置继续往下执行下去,也就是使用next函数启动生成器取值时,依次取值的时候会暂停在yield。
(2)生成器使用return关键字:在yield后使用return。生成器使用return语法上没有问题,但是执行到return关键字的时候会抛出停止迭代异常。--->(了解,return只支持python3)
6.3 总结:
yield:每次启动生成器都会返回一个值,也就是说yield可以返回多次值
return:只会返回一次值,这个返回值还需要在异常捕获中返回这个返回值,还会抛出一个停止迭代异常。(了解)
6.4 next启动生成器和send启动生成器
7. 协程:
概念:又称为微线程,也为用户级线程,在不开辟线程的基础上完成多任务
如何理解协程:在def函数里面只看到一个yield关键字就可以理解为是协程
学习协程的目的:在单线程的基础上可以完成多任务,多个任务按照一定顺序交替执行
本质:函数里面有yield关键子,协程就是一个生成器。启动协程(生成器)也用next
7.1 greenlet的使用:
(1)greenlet:导入greenlet模块(py文件)框架封装的是 yield, greenlet能够让程序员很直观的查看协程的切换/。
(2)创建协程:import greenlet
g1 = greenlet.greenlet(work1)--->返回一个协程
切换任务:g1.switch()
7.2 gevent的使用:
(1)gevent:导入gevent是个包(__init__.py),gevent封装的是greenlet,可以根据耗时操作自动完成协程之间的切换执行
(2)创建协程:import gevent
g1 = gevent.spawn(work1)---->返回一个协程
主线程不会等待协程执行完毕在退出,所以要让主线程等待协程执行完成以后程序在执行
g1.join()
(3)gevent遇到耗时操作会自动识别,进行自动切换协程任务。打补丁:让gevent框架识别耗时操作:time.sleep(),网络请求延时等
from gevent import monkey ---->导入模块monkey识别耗时操作
monkey.patch_all() ---->打补丁
8. 进程~线程~协程对比
1.1 先有进程,然后进程可以创建线程,线程是依附在进程里面的, 线程里面可以包含多个协程
1.2 进程之间不共享全局变量,线程之间共享全局变量,但是要注意资源竞争的问题
1.3 多进程开发比单进程多线程开发稳定性要强,但是多进程开发比多线程开发资源开销要大
1.4 多线程开发线程之间执行是无序的,协程之间执行按照一定顺序交替执行
1.5 协程以后主要用在网络爬虫和网络请求,开辟一个协程大概需要5k空间,开辟一个线程需要512k空间, 开辟一个进程占用资源最多
总结:
1.为什么使用
2.定义,使用方面--注意点
3.使用场景(举例)