网络编程 并发
目录
网络编程 并发
软件开发架构
# 网络编程:基于网络进行编程
'''
网络编程的目的:就是让我们开发出一款C/S架构或者B/S架构的软件
'''
C/S架构
client(客户端)
server(服务端)
B/S架构
browser(浏览器)
server(服务端)
#客户端
当用户想使用软件必须下载客户端,当用户下载的客户端足够多,电脑运行速度会收到
影响,而且要使用多个就必须下载多个,很不方便
#浏览器
由于客户端存在问题,随着互联网的兴起,开发出了浏览器,浏览器是一个万能客户端或者超级客户端
#服务端
1 24小时不间断提供服务
2 服务端也有一个公共ip
OSI七层协议
1 什么是网络
网络的本质:上传和下载数据的过程
刚刚开始的时候,网络是一个个局域网,世界上的电脑都不互通,后来才吧世界上的电脑互通起来,这就是互联网
'''
网络的组成部分:物理链接设备(网线,交换机,路由器) +互联网通信协议(计算机界的英语)
'''
2 网络有什么好处
'''方便数据传输'''
3 互联网通信协议
#osi七层协议
1 物理层
#负责发送高低电频
'''单纯的发送高低电频没有意义,需要进行分组,来区分不同的内容'''
2 数据链路层
'''数据链路层统一使用以太网协议,一个以太网称为一个数据帧'''
数据帧的组成:head +data
3 网络层
使用ip协议,固定每一个接入互联网的计算机都必须有一个ip地址
IPV4地址==》0.0.0.0---------》255.255.255.255
IPV6地址 使用十六进制表示
'''
公网Ip:是需要购买的,买过之后的ip是固定的
内网ip:以192.168开头的ip地址,也叫局域网的ip
'''
网络的划分:广域网,局域网,城域网
#其实一个ip地址可以定位一个局域网,一个mac地址定位局域网中的一台计算机
ip+mac地址可以定位一个局域网中的唯一一台计算机
#其实根本不需要mac地址,只需要一个ip地址就能确定、
因为有一个arp协议,专门负责把ip地址解析x成一个mac地址,也就有了ip地址,就有了macd地址
'''一个ip可以定位世界上独一无二的一台计算机'''
本地回环地址:127.0.0.1 就是还没有出去网络,找到自己的电脑
4 传输层
使用的是TCP协议和UDP协议,也叫端口协议,0-65535之间的一个数字
#0-1024之间的端口默认是系统使用的
'''在一台计算机中,不能端口重复,因为一个端口就代表这台计算机的一个应用程序'''
#域名本质上也是ip地址,域名要备案,当你再地址栏输入域名的时候,其实内部做了一个
#域名解析,把域名解析成ipd地址,dns解析
#1024-8000Z之间的端口,一般是常用软件的端口
#以后我们自己开发的软件,端口最好是使用8000之后的
ip+port:定位世界上独一无二的一台计算机再运行的一个应用程序
5 应用层
1 如果是客户端程序,协议想使用什么就用什么,因为客户端是我们自己写的
2 如果是浏览器,协议就不能随意使用,就应该按照浏览器规定的协议
'''浏览器使用的协议:hhttp协议,默认端口:80'''
http和https的区别
1 https更安全,密文传输数据
2 http不安全,明文传输数据
3 https协议监听默认端口是:443
TCP协议
TCP协议也叫可靠协议
TCP协议数据传输的时候,是需要建立通道的,并且是双向管道
1 如何建立双向通道
'''TCP协议的三次握手,四川挥手'''
先让客户端先给服务端发送请求,服务端收到数据之后,进行处理,再给客户端返回数据
'''一定是客户端先给服务端发起请求'''
#wesockert协议,它可以实现服务端主动给客户端发送请求
#此外,都是客户端主动给服务端发送数据
面试题:TCP协议的三次握手(建立的双向链接)和四次挥手(断开链接的),四次挥手能不能缩短为三次?
UDP协议
不可靠协议,不建立双向链接
'''如果使用UDP协议,客户端只负责发送数据'''
二者区别:
1 UDP的速度更快
2 TCP数据更安全
SOCKED编程
socket:我们经常把soceket翻译成套接字,socket是再应用层和传输层之间的一个抽象层
它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程再网络中的通信
AF_UNIX:用在局域网中
AF_INET:用在互联网
'''先启动服务端,然后再启动客户端'''
先从服务器端说起。服务器端先初始化Socket,然后与"端口绑定"(bind),
对"端口进行监听"(listen),"调用accept阻塞","等待客户端连接"。
在这时如果有个客户端初始化一个Socket,"然后连接服务器(connect)"",如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,
然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
基于TCP协议的套接字编程
# SOCK_STREAM ====> 代表的是TCP协议
#SOCK_DGRAM # udp协议
server.bind(('127.0.0.1', 8001)) # 服务端绑定一个地址
server.listen(3) # 监听,半连接池允许 等待连接的个数
conn, client_addr = server.accept() # 接收, 程序启动之后,会在accept这里夯住,阻塞
'''
conn:代表的是当次连接对象
client_addr:代表的是客户端的详细信息(ip:port)
'''
基于UDP协议的套接字编程
服务端
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP
data, client_addr = server.recvfrom(1024) # recv
# data接收回来的数据
#client_addr:客户端的信息
server.sendto(data.upper(), client_addr)
客户端
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP
client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
粘包现象
data = conn.recv(1024) # 1024代表的是一次性接收的最大字节数
# 如果客户端发送的数据超过了1024字节,那么,服务端一次性不能接收完整,导致客户端发送的数据丢失
发生粘包的情况
1 发送的数据过小,而且下面还有一个发送数据,这两个数据会一起发送
2 当发送的数据过大,超过最大缓存空间,超出的部分再下一次发送的时候发送
原因:
1 tcp是面向流的,算法自动数据拆分,组合,没有保护边界
udp无粘包现象
表现形式:
发送的数据大小超出最大缓存空间,超出的数据直接丢掉
udp不是面向流,是面向信息的
解决黏包
1 先让服务端知道发送的数据大小
2 使用struct变成固定大小的bytes类型
第一种方法:
为了不产生粘包,每执行一次产生一次网络延迟
第二种的方法:
使用struct====>把数据固定转换为4个字节的bytes类型
注意,unpack时,要使用pack的返回值,unpack的是一个tuple,需要取第一个值
进程概念
# 进程是一个比较抽象的概念
程序:就是一堆代码,没有生命周期
进程:是动态,有生命周期
线程:是实际做事的人
'''进程与线程的关系:先开进程,在进程里面开线程,'''
# 一个进程中可以有多个线程,一个进程中至少要有一个线程
'''进程和线程是操作系统中得一个基本概念'''
# 进程和线程全部都是有操作系统来调度的
单个CPU一次只能运行一个任务
'''如果一个进程中只有一个线程,这个线程就叫主线程,子线程'''
# 协程:单线程下的并发,比线程还要小,如果开协程,那么,消耗的资源更小
进程 >>> 线程 >>> 协程(程序员调度的)
补充:
# CPU的工作机制:
1. 当遇到i/o阻塞的时候,会自动剥夺CPU的执行权限
2. 当CPU遇到占用时间过长的任务时候,也会剥夺执行权限
'''CPU的工作其实是来回切换的!!!!'''
# i/o
# i:input o:output
1. i/o密集型
'''有阻塞,就是不会一直占用CPU资源,就是中间会有等待的状态。比如:sleep状态'''
2. 计算密集型
'''反过来就是没有阻塞,会大量的占用CPU资源这就是计算密集型'''
并行和并发的概念
操作系统调度进程的4中算法
先来先服务调度算法
短作业优先调度算法
时间片轮转法
多级反馈队列
并行:
# 在同一时刻,执行多个任务
并发:
# 在一段时间内,执行多个任务
1. 单核CPU能不能实现并行?
不能实现
# 高并发:极短的时间内,涌进大量的客户
'''面试题:你对如何解决高并发问题有什么见解?'''
同步异步阻塞非阻塞
同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。
异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
'''执行效率:同步阻塞效率最低,异步非阻塞效率最高'''
如何开启进程
1 from multiprocessing import Process中的Process
使用main来开启这一个执行文件,通过Peocess实例化得出一个对象
通过这个对象的start()开启进程
Process类的参数
from multiprocessing import Process
target=none,name=None, args=(), kwargs={}
'''
target指的是这个进程要执行的任务名称
name 是这一个进程的名称,如果name没有给值,name为Process-1
arg=(),位置传参
kwargs(),关键字传参
'''
process中的方法
from multiprocessing import Process
def write_file(a, b, c, name, age):
print('a:', a)
import time
time.sleep(3)
if __name__ == '__main__':
p = Process(target=write_file, name='ly', args=(1, 2, 3), kwargs={'name': 'ly', 'age': 20}) # 实例化得出一个对象
p.start()
# print(p.is_alive()) # 查看进程是不是还存活
# '''通知操作系统去杀死进程,操作系统去杀进程需要有一段时间,不会立马杀掉'''
# p.terminate() # 杀死进程,干掉进程, kill -9
# import time
# time.sleep(0.1)
# print(p.is_alive())
# 子进程先执行完毕,主进程在执行
p.join()
print('end')
查看进程号pid
# 每一个进程都会有一个进程号:pid,name,,,,, kill -9 1001
from multiprocessing import Process
import os
def write_file(a, b, c, name, age):
print('a:', a)
# 在这里查看进程号,
print('子进程的进程号:', os.getpid()) # os.getpid() ----> 写在哪个进程中,就获取的是这个进程的pid
print('父进程的进程号:', os.getppid()) # parent
import time
time.sleep(20)
if __name__ == '__main__':
p = Process(target=write_file, name='ly', args=(1, 2, 3), kwargs={'name': 'ly', 'age': 20}) # 实例化得出一个对象
p.start()
print('子进程的进程号:', p.pid)
print('主进程的进程号:', os.getpid())
import time
time.sleep(10)
如何开启多线程
1 思路:for循环开启多线程,把进程对象保存到列表里,循环这个列表
先让子线程先执行
from multiprocessing import Process
import os
def write_file():
# with open('a.txt', 'a', encoding='utf-8') as f:
# f.write('hello\n')
print('start....')
import time
time.sleep(3)
import time
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(10):
'''通知开进程,但是不一定能立马开起来,开进程肯定会消耗资源的'''
p = Process(target=write_file, name='ly', ) # 实例化得出一个对象
p.start()
# p.join() # 此时的程序变成了串行执行
ll.append(p)
for j in ll:
j.join()
# 需求:我就想让所有的子进程先执行完,最后在执行主进程代码,怎么做?
print('end.......')
print('一共用的时间:', time.time() - ctime)
基于TCP的高并发程序
while True:
conn, client_addr = server.accept() # 接收, 程序启动之后,会在accept这里夯住,阻塞
p = Process(target=task, args=(conn,))
p.start()
思路,使用循环解决一段时间执行多个任务
import socket # python提供的socket模块
def task(conn):
while True:
try:
# 异常了一个bug,粘包现象
data = conn.recv(1024) # 括号里面写的是接收的字节数,最多接收1024个字节
if len(data) == 0:
continue
print(data) # 还是bytes类型
# 服务端开始给客户端也发送一个数据
conn.send(data.upper())
except Exception as e:
print(e)
break
conn.close()
from multiprocessing import Process
if __name__ == '__main__':
# 1. 买手机
# SOCK_STREAM ====> 代表的是TCP协议
# socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议
server = socket.socket() # 默认是TCP协议
# 2. 买手机卡
# '0.0.0.0' =====> 代表允许任何的ip链接
# server.bind(('0.0.0.0', 8000)) # 服务端绑定一个地址
server.bind(('127.0.0.1', 8001)) # 服务端绑定一个地址
# 3. 开机
server.listen(1) # 监听,半连接池
print('服务端正在准备接收客户端消息:')
while True:
conn, client_addr = server.accept() # 接收, 程序启动之后,会在accept这里夯住,阻塞
p = Process(target=task, args=(conn,))
p.start()
进程锁
锁的目的:为了让数据更安全
如何使用
1 使用Lock()类模块 通过实例化得到一个锁对象
2 通过锁对象点acquire()给进程上锁
3 通过锁对象点release()给进程解锁
'''牺牲时间换空间,时间复杂度'''
数据之间的隔离问题
进程之间的数据是相互隔离的,不能相互使用
没有任何交互
队列
队列特点,先进先出
队列的简单过程
1 先创建队列 实例化Queue得到一个队列对象
2 入队 队列对象点put() 实现入队
3 出队 队列对象点get()实现出队
解决进程之间的数据隔离
from multiprocessing import Process,Queue
思路:通过建立一个第三者来,储存进程产生的数据,供使用 eg使用队列
如果使用队列,Queue队列中中的数据再内存值中存储着
# 进程间通信(IPC)
from multiprocessing import Process,Queue
def task(q):
# 在子进程中往队列里面写入数据
q.put('hello')
if __name__ == '__main__':
# 1. 创建队列
q = Queue(10)
p = Process(target=task, args=(q, ))
p.start()
'''在子进程中写入数据,去主进程中读取子进程中写的数据,如果能读到,说明通信了,如果读不到就不能通信'''
# 主进程中读取数据、
print('主进程中读数据:', q.get())
'''
此时, Queue队列中得数据是在内存中存着的
在实际工作中,队列我们会使用专业的消息队列:RabbitMq,Kafka,Rocket Mq...
Redis数据库(缓存)------>作为队列使用
'''
生产者消费者模型
生产者和消费者模式解决大多数的并发问题,
为什么要使用生产者消费者模型
生产者指的是生产数据的任务,消费者指的是处理数据的任务,
在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,
那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,
如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。
为了解决这个问题于是引入了生产者和消费者模式
什么是生产者和消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,
所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,
消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,
平衡了生产者和消费者的处理能力。
线程
# 一个进程下可以开启多个线程,一个进程里面至少要有一个线程,主进程里面的线程称为主线程,其他进程中的线程称为子线程
进程是资源分配的最小单位,线程是CPU调度的最小单位。每一个进程中至少有一个线程。’
# 协程:在单线程下开启协程,程序员来开的。
# 线程才是真正干活的人。
同一个进程下多线程数据是共享的,多进程下的不同线程要通信如何做----->等同于进程间通信
'''进程就是做事的过程,它不会去实际的做事情,是有线程来做事情的,我们开启一个进程,在进程里面开启线程,并且,在进程里面可以开多个线程。'''
# 进程是资源分配的基本单位,线程是CPU执行的最小单位,每一个进程中至少有一个线程
# 当进程中只有一个线程的时候,这个线程叫主线程,子线程
# 操作系统来调度进程和线程,程序员级别是不能调度他们的
# 协程:就是有我们程序员级别来调度的(用户态)
进程 >>> 线程 >>> 协程
进程占用的资源是最多的,其次是线程占用的资源,最后是协程.
'''在python中,我们一般开多进程,而不开多线程,在其他语言中,都是选择开多线程.'''
# 线程才是真正做事情的
进程 线程 协程
# 进程是资源分配的最小单位,线程是CPU执行的最小单位
'''在操作系统中,只有进程和线程的概念,协程在操作系统中没有这个概念'''
进程 >>> 线程
# 先开进程,在进程里面开线程,一个进程中可以有多个线程,但是,一个进程中至少要有一个线程,主线程
开进程或者开线程的目的:为了提高效率.
很多任务我们在单进程或者单线程下都可以完成,只不过效率可能会低一些,如果开了多进程或者多线程,效率就会高.
如何开线程?
# 导入内置模块
from threading import Thread
# 开进程时候需要把实例化类写在__main__里面
# 开线程可以不写在这个里面,但是,我们都推荐也写在这个里面
def task():
pass
if __name__=='__main__':
t = Thread(target=task, name='ly is handsome', args=(1,), kwargs={'a':1})
t.start()
print(123)
print(t.name) # 线程名
print(t.getName()) # 线程名
t.setName('aaa') # 改线程名
t.is_alive() # 查看线程是否存回
'''如何开启多线程?'''
for i in range(10):
t = Thread(target=task, name='ly is handsome', args=(1,), kwargs={'a':1})
t.start()
t.join() # 串行执行了
for j in ll:
j.join()
print('end....')
如何开启线程
from multiprocessing import Process
from threading import Thread
def write():
with open('a.txt', 'w', encoding='utf-8') as f:
f.write('hello')
if __name__ == '__main__':
# p = Process(target=write, )
# p.start()
t = Thread(target=write, )
t.start()
进程和线程中间的比较
1 进程开销远远大于线程的开销
#进程里面可以开多线程
2 进程可以利用多核优势,线程不能利用多核优势,但是,可以开启多个进程,再进程里面开启线程
3 进程之间的数据是隔离的,线程之间的数据是不隔离的,前提,同一个进程
Thread类中的几种方法
1 p.start()
2 p.is_alive()
3 p.getName()
4 p.setName()
def task():
print('123')
print(threading.currentThread())
print(threading.currentThread().getName())
from threading import Thread
import threading
if __name__ == '__main__':
ll = []
# for i in range(10):
# t = Thread(target=task, )
# t.start()
# ll.append(t) # 串行
# for t in ll:
# t.join()
# print('end...')
'''线程的几个方法'''
t = Thread(target=task, )
t.start()
# print(t.isAlive())
# print(t.is_alive())
# print(t.name)
# # print(t.getName())
# t.setName('ly is handsome')
# print(t.getName())
print(threading.currentThread()) # <_MainThread(MainThread, started 5560)>
print(threading.currentThread().getName()) # MainThread
多线程实现socket
import multiprocessing
import threading
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
def action(conn):
while True:
data=conn.recv(1024)
print(data)
conn.send(data.upper())
if __name__ == '__main__':
while True:
conn,addr=s.accept()
p=threading.Thread(target=action,args=(conn,))
p.start()
守护线程
#当主线程运行结束,子线程也跟着结束
# 当主线程运行结束,子线程会跟着结束
import threading
def run(n):
for i in range(n):
print(threading.current_thread().name + " " + str(i))
if __name__ == "__main__":
t = threading.Thread(target=run, args=(100,), name='后台线程')
t.daemon = True # 将此线程设置成后台线程
t.start() # 启动后台线程
# t.join()
for i in range(10):
print(threading.current_thread().name + " " + str(i))
主函数中的for循环和线程t是两个不同的线程,其中for循环是主线程,当把t设置为守护线程时,主线程for循环运行结束后t线程会同时结束。
# 开线程
# 在Windows中,开线程不需要写在if __name__ == '__main__':中,但是我们尽量都写一下
if __name__ == '__main__':
'''
target=None, name=None,
args=(), kwargs=None, *, daemon=None
'''
t = Thread(target=task, name='ly is handsome', args=(1, 2, 3), kwargs={'name': 'a'})
# t.daemon = True # 主线程结束,子线程结束
t.setDaemon(True) # 主线程结束,子线程结束
t.start()
# t.join()
# print(123)
# print(t.name) # Thread-1
# 守护线程
print('主线程')
GIL锁(全局解释器锁)
Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行
# 对Python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
1. 我们写的代码其实是再解释器上执行:
cpython解释器,pypy解释器,ipython解释器...
2. GIL锁只存在cpython解释器中,在其他解释器中不存在
3. 起一个垃圾回收的线程,起一个正常执行代码的线程,当垃圾回收线程还没有把垃圾回收完毕的时候,会出现抢占资源的情况。
4. 如果要想让线程能够正常执行,那么,这个线程需要拿到这把锁(GIL锁)
5. 因为这些原因保证了,同一时刻,只有一个线程在执行。
##########################理解记忆##########################################
1. python有GIL锁的原因,"同一个进程下多个线程实际上同一时刻,只有一个线程在执行"
2. 只有在python上开多进程,其他语言一般不开多进程,只开多线程就够了
3. "cpython解释器开多线程不能利用多核优势",只有开多进程才能利用多核优势,其他语言不存在这个问题
4. 8核cpu电脑,想要充分利用这8个核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%,
5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu
6. cpython解释器中好多代码,"模块都是基于GIL锁机制写起来的,改不了了---》我们不能有8个核,但我现在只能用1核,----》可以开启多进程---》每个进程下开启的线程,可以被多个cpu调度执行
7. cpython解释器:io密集型使用多线程,计算密集型使用多进程
# -io密集型,遇到io操作会自动切换cpu,假设你开了8个线程,8个线程都有io操作---》io操作不消耗cpu---》一段时间内看上去,其实8个线程都执行了
# -计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
##########################理解记忆#######################################
'''
进程:可以利用多核,
线程:没办法利用多核,
i/o密集型:不需要计算,一般不消耗CPU,所以就选择线程比较好
计算密集型:在消耗CPU,所以做好选择进程
'''
for i in range(1000000000000000000000000000000000000000000000):
i+=1
print(i)
import time
for i in range(1000000000000000000000000000000000000000000000):
time.sleep(1)
print(i)
Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行
# 虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
'''对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线
程在运行。'''
1. python代码运行在解释器之上,有解释器来翻译执行
2. python解释器的种类有哪些?
CPython IPython PyPy Jython
3. GIL锁存在于CPython解释器中
4. 市面上目前绝大多数(95%)都使用的是CPython解释器
5. 起一个垃圾回收线程,在起一个正常执行代码的线程,当垃圾回收线程还没回收完毕,其他线程有可能会抢夺资源,这种情况在python设计之处就不允许的
6. python在设计之处,就在python解释器之上加了一把锁(GIL锁),加这个锁的目的是:同一时刻只能有一个线程执行,不能同时有多个线程执行,如果开了多个线程,那么,线程要想有执行权限,必须先拿到这把锁(GIL锁)
"""只需要记住:同一时刻多个线程只能有一个线程在运行,其他线程都处于等待状态"""
#########################理解记忆部分########################################################
1. python有GIL锁的原因,同一个进程下多个线程实际上同一时刻,只有一个线程在执行
2. 只有在python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
3. cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
4. 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%,
5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu
6. cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了---》我们不能有8个核,但我现在只能用1核,----》开启多进程---》每个进程下开启的线程,可以被多个cpu调度执行
7. cpython解释器:io密集型使用多线程,计算密集型使用多进程
# -io密集型,遇到io操作会切换cpu,假设你开了8个线程,8个线程都有io操作---》io操作不消耗cpu---》一段时间内看上去,其实8个线程都执行了, 选多线程好一些
# -计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
'''计算密集型选多进程好一些,在其他语言中,都是选择多线程,而不选择多进程.'''
同步锁(互斥锁)(线程锁)
# 开多进程或者多线程一定好吗?
不一定,虽然可以提高效率,但是,会出现安全问题
'''
多线程同时操作一个数据,数据错乱,并发安全问题--->加锁--》让原本并发的操作,变成串行,牺牲效率,
保证安全---》通过线程queue也可以避免并发安全的问题,所有queue的本质就是锁
'''
from threading import Thread, Lock
import time
n = 10
def task(lock):
lock.acquire() # 上锁
global n
# n -= 1
temp = n
# time.sleep(1)
n = temp - 1 # n=n-1
# 释放锁
lock.release()
if __name__ == '__main__':
lock = Lock()
l = []
for i in range(10):
t = Thread(target=task, args=(lock, ))
t.start()
l.append(t)
for j in l:
j.join()
print('n:', n)
'''多个线程去操作同一个数据,会出现并发安全问题,怎么解决?加锁'''
from threading import Thread, Lock
n = 10
import time
def task(lock):
lock.acquire() # 只要有一个线程进来了,其他线程都要等着
global n
# n -= 1
temp = n
time.sleep(0.1)
n = temp - 1
lock.release()
if __name__ == '__main__':
lock = Lock()
ll = []
for i in range(10):
t = Thread(target=task, args=(lock, ))
t.start()
ll.append(t)
for j in ll:
j.join()
print('n:', n)
'''多个线程之间同时操作一个数据,会出现数据错乱问题,怎么办?加锁'''
from threading import Lock
lock = Lock()
# 1. 加锁
lock.acquire()
# 2. 释放锁
lock.release()
'''加完锁之后,不管有多少个线程进来,同一时刻,只有一个线程进来,等这个线程执行完毕,释放锁之后,其他线程才能在进来'''
问题:
有了GIL锁,还会出现并发安全问题码?会,此时程序变成了串行执行,为什么还会出现并发安全问题呢?
比如:起了2个线程,执行任务:a=a+1,a以开始等于0
1. 第一个线程过来了,拿到了a,计算a=a+1,当计算结果还没有赋值回去
2. 第二个线程来了,拿到了a,此时拿到的a还是0,计算a=a+1,线程切换会第一个线程,a变为1
'''面试题:有了GIL锁,为什么还要互斥锁?'''
死锁现象
死锁:
# 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象
'''单进程和单线程不会出现死锁现象'''
# 哲学家吃面问题
import time
from threading import Thread, Lock, RLock
# noodle_lock = Lock()
# fork_lock = Lock()
noodle_lock = fork_lock = RLock() # 递归锁,也叫可重入锁
# noodle_lock = RLock()
# fork_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条' % name)
fork_lock.acquire()
print('%s 抢到了叉子' % name)
print('%s 吃面' % name)
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()
for name in ['哪吒', 'jack', 'tank']:
t1 = Thread(target=eat1, args=(name,))
t2 = Thread(target=eat2, args=(name,))
t1.start()
t2.start()
线性队列
"""
同一个进程下多个线程数据是共享的
为什么在同一个进程下还会去使用队列呢
因为队列是
管道 + 锁
所以用队列还是为了保证数据的安全
"""
from multiprocessing import Queue
进程队列:Queue
import queue
线程队列:queue
"""
进程Queue用于父进程与子进程(或同一父进程中多个子进程)间数据传递
python自己的多个进程间交换数据或者与其他语言(如Java)进程queue就无能为力
queue.Queue 的缺点是它的实现涉及到多个锁和条件变量,因此可能会影响性能和内存效率。
"""
线程队列有3种:
1. 先进先出
2. 先进后出
3. 优先级队列
'''优先级队列'''
q = queue.PriorityQueue()
# put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((5, 'a')) # 元组里面的第一个数字代表的是优先级,第二个元素是具体的数据
q.put((15, 'b'))
q.put((10, 'c'))
print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
进程池和线程池
池:盛放更多个对象的,盛放更多的进程和线程
'''进程池和线程池都是提供的是异步调用,只需要往池子里丢任务,我们不需要等待结果,
等池子里面的任务完成之后,内部做了一个回调,告诉我们执行后的结果!!!'''
# 进程池:
提前创建一个池子,这个池子里面先创建多个进程,然后,只需要向池子里面丢任务就行.
# 线程池:
提前创建一个池子,这个池子里面先创建多个线程,然后,只需要向池子里面丢任务就行.
创建池的目的:节省资源,防止内存被沾满的情况,另外就是也能提升效率,但是不能无限的开进程.
'''写个进程池来执行任务'''
def task(n, m,):
return n + m
# res=task(1, 2)
# print(res)
def callback(res):
print(res) # <Future at 0x14786aa45e0 state=finished returned int>
print(res.result()) # 3
if __name__ == '__main__':
# 1. 创建进程池
p_pool = ProcessPoolExecutor(5) # 创建一个池子,里面有5个进程
# 2. 执行任务,只需要往池子里面丢任务
# p_pool.submit(task, 1, 2)
p_pool.submit(task, n=1, m=2).add_done_callback(callback)
线程池爬取网页
import requests
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def get_page(url):
# requests.get('http://www.baidu.com')
res = requests.get(url) # 爬取网页
name = url.rsplit('/')[-1] + '.html' # www.baidu.com.html
return {'name': name, 'text': res.content}
def call_back(fut):
print(fut.result()['name'])
with open(fut.result()['name'], 'wb') as f:
f.write(fut.result()['text'])
if __name__ == '__main__':
pool = ThreadPoolExecutor(5)
urls = ['http://www.baidu.com', 'http://www.cnblogs.com', 'http://www.taobao.com']
for url in urls:
pool.submit(get_page, url).add_done_callback(call_back)
协程
进程:资源分配的最小单位
线程:CPU执行的最小单位
协程:
# 协程的概念在操作系统中,压根不存在,操作系统中有的是进程和线程概念,他们两个都是有操作系统调度
# 协程是有我们程序员意淫出来的,它也称作是用户态的轻量级线程. 用户态---->程序员
'''协程它是单线程下的并发.'''
# 并发:
切换,保存状态
# 如何使用协程:
本质:还是来回切换(程序级别切换)
'''要想使用协程,需要借助于第三方模块:gevent模块!!!'''
1. 需要下载和安装gevent模块
pip install gevent
协程实现高并发
#######################################服务端########################################
import socket
'''猴子补丁'''
from gevent import monkey;
monkey.patch_all() # 检测IO
'''协程实现并发:其实是欺骗CPU的行为'''
# 1. 遇到io,2. 切换
'''并发:1, 遇到IO操作,会切换'''
from gevent import spawn
def talk(sock):
while True:
try:
data = sock.recv(1024)
if len(data) == 0: break
print(data)
sock.send(data + b'ly is handsome!')
except ConnectionResetError as e:
print(e)
sock.close()
break
from multiprocessing import Process
def servers():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
while True:
sock, addr = server.accept()
# p = Process(target=talk)
# p.start()
spawn(talk, sock) ### 开销是非常小的
'''协程是单线程下的并发!!!'''
g1 = spawn(servers) # 单线程下开着,,,,,单线程下的并发
g1.join()
客户端
from threading import Thread, current_thread
from socket import *
def client():
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
n = 0
while True:
msg = '%s say hello %s' % (current_thread().name, n)
n += 1
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
if __name__ == '__main__':
for i in range(5000):
t = Thread(target=client)
t.start()