Python 网络与并发编程
并发编程
并发编程介绍
串行-并行-并发的区别
- 串行(serial)):一个CPU上,按顺序完成多个任务
- 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
- 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
进程-线程-协程的区别
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低
线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GL的情况下)
协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高
进程是什么?
**进程(Process)**是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
线程是什么?
**线程(Thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
并发编程解决方案:
多任务的实现有3种方式:
- 多进程模式
- 多线程模式
- 多进程+多线程模式
协程是什么?
协程,Coroutines,也叫作纤程(Fiber),是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。
当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充
分利用了IO等待的时间,提高了效率。
同步和异步通信机制的区别
同步和异步强调的是消息通信机制 (synchronous communication/asynchronous communication)。
同步(synchronous):A调用B,等待B返回结果后,A继续执行
异步(asynchronous ):A调用B,A继续执行,不等待B返回结果;B有结果了,通知A,A再做处理。
同步方式通信:
1 高淇买一本书《Python实战笔记》。
2 书店老板说:等三分钟啊,我帮你查查。
3 高淇等一小时
4 老板说,找到书了,发给你
异步方式通信:
1 高淇买一本电子书《Python实战笔记》。
2 书店老板说:我查一下,有结果了告诉你。
3 高淇刷抖音一小时
4 老板说,找到书了,发给你
线程
线程(Thread)特点:
1 **线程(Thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
2 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
3 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
4 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
5 调度和切换:线程上下文切换比进程上下文切换要快得多。
线程的创建方式
Python的标准库提供了两个模块: _thread
和 threading
, _thread
是低级模块, threading
是高级模块,对 _thread
进行了封装。绝大多数情况下,我们只需要使用 threading
这个高级模块。
线程的创建可以通过分为两种方式:
- 方法包装
- 类包装
线程的执行统一通过 start()
方法
方法包装创建线程
#coding=utf-8
""""
方法包装建立线程
"""
from threading import Thread
from time import sleep
def function1(name):
print(f"线程{
name}, start") # format
for i in range(3):
print(f"线程:{
name}, {
i}")
sleep(1)
print(f"线程{
name}, end") # format
if __name__ == "__main__":
print("主线程,start")
# 创建线程
t1 = Thread(target=function1, args=("t1",))
t2 = Thread(target=function1, args=("t2",))
# 启动线程
t1.start()
t2.start()
print("主线程,end")
"""
运行结果可能会出现换行问题,是因为多个线程抢夺控制台输出的IO流。
主线程,start
线程t1, start
线程:t1, 0
线程t2, start
线程:t2, 0
主线程,end
线程:t2, 1线程:t1, 1
线程:t2, 2
线程:t1, 2
线程t1, end线程t2, end
"""
类包装创建线程
from time import sleep
from threading import Thread
"""
类包装创建线程
"""
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
# 重写run方法
def run(self):
print(f"线程{
self.name}, start") # format
for i in range(3):
print(f"线程:{
self.name}, {
i}")
sleep(1)
print(f"线程{
self.name}, end") # format
if __name__ == '__main__':
print("主线程,start")
# 创建线程
t1 = MyThread("t1")
t2 = MyThread("t2")
# 启动线程
t1.start()
t2.start()
print("主线程,end")
join()和守护线程
join()
之前的代码,主线程不会等待子线程结束。
如果需要等待子线程结束后,再结束主线程,可使用join()方法。
from threading import Thread
from time import sleep
def function1(name):
print(f"线程{
name}, start") # format
for i in range(3):
print(f"线程:{
name}, {
i}")
sleep(1)
print(f"线程{
name}, end") # format
if __name__ == "__main__":
print("主线程,start")
# 创建线程
t1 = Thread(target=function1, args=("t1",))
t2 = Thread(target=function1, args=("t2",))
# 启动线程
t1.start()
t2.start()
# 主线程会等待t1,t2结束后,再往下执行
t1.join()
t2.join()
print("主线程,end")
守护线程
在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过 setDaemon(True|False)
来设置是否为守护线程。
守护线程的作用:
守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
from time import sleep
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
# 重写run方法
def run(self):
print(f"线程{
self.name}, start") # format
for i in range(3):
print(f"线程:{
self.name}, {
i}")
sleep(1)
print(f"线程{
self.name}, end") # format
if __name__ == '__main__':
print("主线程,start")
# 创建线程(类的方式)
t1 = MyThread("t1")
# t1设置为守护线程
t1.setDaemon(True)
# 启动线程
t1.start()
print("主线程,end")
全局解释器锁GIL问题
在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核…不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是“含有水分的线程”。
Python GIL(Global Interpreter Lock)
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
⚠️GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。
线程同步和互斥锁资源冲突案例
线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
# coding=utf-8
"""
未使用线程同步和互斥锁的情况
"""
from threading import Thread
from time import sleep
class Account:
def __init__(self, money, name):
self.money = money
self.name = name
# 模拟提款操作
class Drawing(Thread):
def __init__(self, drawingNum, account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
if self.account.money < self.drawingNum:
return
sleep(1) # 判断完后阻塞
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
print(f"账户{
self.account.name},余额{
self.account.money}")
print(f"账户{
self.account.name},总共取了{
self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100, "gaoqi")
draw1 = Drawing(80, a1) # 定义取钱线程对象;
draw2 = Drawing(80, a1) # 定义取钱线程对象;
draw1.start() # 你取钱
draw2.start() # 你老婆取钱
"""
账户gaoqi,余额20账户gaoqi,余额-60
账户gaoqi,总共取了80
账户gaoqi,总共取了80
"""
互斥锁典型案例
我们可以通过“锁机制”来实现线程同步问题,锁机制有如下几个要点:
- 必须使用同一个锁对象
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
- 使用互斥锁会影响代码的执行效率
- 同时持有多把锁,容易出现死锁的情况
互斥锁是什么?
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意: 互斥锁是**多个线程一起去抢**,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁
threading
模块中定义了 Lock
变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
# coding=utf-8
"""
互斥锁典型案例
"""
from threading import Thread, Lock
from time import sleep
class Account:
def