网络编程其他篇
目录:
- 1.1 进程与线程简介
- 1.2 多线程
- 1.3 进程
- 1.4 协程
- 1.5 Python进程池和线程池(ThreadPoolExecutor&ProcessPoolExecutor)
- 1.6 使用线程池、进程池、协程向多个url并发获取页面数据比较
1.1 进程与线程简介 返回顶部
1、什么是进程(process)?(进程是资源集合)
定义:1)进程是资源分配最小单位
2)当一个可执行程序被系统执行(分配内存资源)就变成了一个进程
1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
5. 进程之间有自己独立的内存,各进程之间不能相互访问
6. 创建一个新线程很简单,创建新进程需要对父进程进行复制
多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
单道编程: 计算机内存中只允许一个的程序运行
进程并发性:
1)在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
2)对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念
他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
3)由于操作系统分时,让每个进程都觉得自己独占CPU等资源
注:如果是多核CPU(处理器)实际上是可以实现正在意义的同一时间点有多个线程同时运行
线程并发性:
1)操作系统将时间划分为很多时间段,尽可能的均匀分配给每一个线程。
2)获取到时间片的线程被CPU执行,其他则一直在等待,所以微观上是走走停停,宏观上都在运行。
多核CPU情况:
如果你的程序的线程数少于CPU的核心数,且系统此时没有其他进程同时运行,那么这个程序的每个线程会享有一个CPU,
当同时运行的线程数多于CPU核心数时,CPU会采用一定的调度算法每隔一段时间就将这些线程调入或调出CPU
以确保每个线程都能分享一部分CPU时间,实现多线程并发。
2、有了进程为什么还要线程?
1. 进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
2. 进程的两个重要缺点
a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
3、什么是线程(thread)(线程是操作系统最小的调度单位)
定义:1)线程是操作系统调度的最小单位
2)它被包含在进程之中,是进程中的实际运作单位
3)进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
1. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
2. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
3. 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
4. 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
5. 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
6. 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
7. 两个进程想通信,必须要通过一个中间代理
8. 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是
同一进程下的线程共享内存
4、进程和线程的区别
启动一个线程比启动一个进程快,运行速度没有可比性。
先有一个进程然后才能有线程。
1、进程包含线程
2、线程共享内存空间
3、进程内存是独立的(不可互相访问)
4、进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
5、在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
6、创建新线程很简单,创建新进程需要对其父进程进行克隆。
7、一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
8、父进程可以修改不影响子进程,但不能修改。
9、线程可以帮助应用程序同时做几件事
5、进程和程序的区别
1. 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
2. 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资
源或事件而被处于等待状态,因完成任务而被撤消
3. 进程是系统进行资源分配和调度的一个独立单位
4.一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)
5. 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
1.2 多线程 返回顶部
Python多线程编程中常用方法:
1、join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,
那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout:可选参数,线程运行的最长时间
2、isAlive()方法:查看线程是否还在运行
3、getName()方法:获得线程名
4、setDaemon()方法:主线程退出时,需要子线程随主线程退出,则设置子线程的setDaemon()
1、线程2种调用方式:直接调用, 继承式调用
import threading import time def sayhi(num): # 定义每个线程要运行的函数 print("running on number:%s" % num) time.sleep(3) #1、target=sayhi :sayhi是定义的一个函数的名字 #2、args=(1,) : 括号内写的是函数的参数 t1 = threading.Thread(target=sayhi, args=(1,)) # 生成一个线程实例 t2 = threading.Thread(target=sayhi, args=(2,)) # 生成另一个线程实例 t1.start() # 启动线程 t2.start() # 启动另一个线程 print(t1.getName()) # 获取线程名 print(t2.getName())
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
2、for循环同时启动多个线程
说明:下面利用for循环同时启动50个线程并行执行,执行时间是3秒而不是所有线程执行时间的总和
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start()
3、t.join(): 实现所有线程都执行结束后再执行主线程
说明:在4中虽然可以实现50个线程同时并发执行,但是主线程不会等待子线程结束在这里我们可以使用t.join()指定等待某个线程结束的结果
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) t_objs = [] #将进程实例对象存储在这个列表中 for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start() #启动一个线程,程序不会阻塞 t_objs.append(t) print(threading.active_count()) #打印当前活跃进程数量 for t in t_objs: #利用for循环等待上面50个进程全部结束 t.join() #阻塞某个程序 print(threading.current_thread()) #打印执行这个命令进程 print("----------------all threads has finished.....") print(threading.active_count()) print('cost time:',time.time() - start_time)
4、setDaemon(): 守护线程,主线程退出时,需要子线程随主线程退出
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置 t.start() #启动一个线程,程序不会阻塞 print('cost time:',time.time() - start_time)
注:因为刚刚创建的线程是守护线程,所以主线程结束后子线程就结束了,运行时间不是3秒而是0.01秒
5、GIL锁和用户锁(Global Interpreter Lock 全局解释器锁
)
1.全局解释器锁:保证同一时间仅有一个线程对资源有操作权限
作用:在一个进程内,同一时刻只能有一个线程通过GIL锁 被CUP调用,切换条件:I/O操作、固定时间(系统决定)
说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多
1)为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
2)GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
3)为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
4)python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
5)但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷
2. 用户锁:线程锁(互斥锁Mutex) :当前线程还未操作完成前其他所有线程都无法对其操作,即使已经释放了GIL锁
1. 在有GIL锁时为何还需要用户锁
1)GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
2. 线程锁的原理
1)当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
2)这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题
3)释放锁的时间点
GIL全局解释器锁:只要CPU分时用完时就会强制释放锁,不管你对这个数据执行是否完成
线程锁:只有当执行结果完成才会释放,不管CPU分时是否到期
import time import threading lock = threading.Lock() #1 生成全局锁 def addNum(): global num #2 在每个线程中都获取这个全局变量 print('--get num:',num ) time.sleep(1) lock.acquire() #3 修改数据前加锁 num -= 1 #4 对此公共变量进行-1操作 lock.release() #5 修改后释放
3. 在有GIL的情况下执行 count = count + 1 会出错原因解析,用线程锁解决方法
# 1)第一步:count = 0 count初始值为0 # 2)第二步:线程1要执行对count加1的操作首先申请GIL全局解释器锁 # 3)第三步:调用操作系统原生线程在操作系统中执行 # 4)第四步:count加1还未执行完毕,时间到了被要求释放GIL # 5)第五步:线程1释放了GIL后线程2此时也要对count进行操作,此时线程1还未执行完,所以count还是0 # 6)第六步:线程2此时拿到count = 0后也要对count进行加1操作,假如线程2执行很快,一次就完成了 # count加1的操作,那么count此时就从0变成了1 # 7)第七步:线程2执行完加1后就赋值count=1并释放GIL # 8)第八步:线程2执行完后cpu又交给了线程1,线程1根据上下文继续执行count加1操作,先拿到GIL # 锁,完成加1操作,由于线程1先拿到的数据count=0,执行完加1后结果还是1 # 9)第九步:线程1将count=1在次赋值给count并释放GIL锁,此时连个线程都对数据加1,但是值最终是1
1、使用线程锁解决上面问题的原理
1) 在GIL锁中再加一个线程锁,线程锁是用户层面的锁
2) 线程锁就是一个线程在对数据操作前加一把锁,防止其他线程复制或者操作这个数据
3) 只有这个线程对数据操作完毕后才会释放这个锁,其他线程才能操作这个数据
2、定义一个线程锁非常简单只用三步:
第一步: lock = threading.Lock() #定义一把锁
第二步: lock.acquire() #对数据操作前加锁防止数据被另一线程操作
第三步: lock.release() #对数据操作完成后释放锁
6、死锁
1. 死锁定义
两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
2. 死锁举例
1. 启动5个线程,执行run方法,假如thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁,
在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待
2. thread1执行完func1函数,然后执行func2函数,此时thread1拿到B锁,然后执行time.sleep(2),此时不会释放B锁
3. 在thread1执行func2的同时thread2开始执行func1获取到了A锁,然后继续要获取B锁
4. 不幸的是B锁还被thread1占用,thread1占用B锁时还需要同时获取A锁才能向下执行,但是此时发现A锁已经被thread2暂用,这样就死锁了
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('