1.udp聊天室
udp的网络程序流程.jpg# 定义udp聊天室方法
def udp_communication():
# 1.创建套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2.指定本机Ip及端口(可选)
local_addr = ("", 8080)
udp_socket.bind(local_addr)
# 3.循环发收
while True:
print("----udp聊天器----")
print("\n 0:发送消息\n1:接收消息\n2:退出")
op = input("请输入选项: ")
if op == "0":
# 发消息
send_msg(udp_socket)
elif op == "1":
# 收消息
receive_msg(udp_socket)
elif op == "2":
udp_socket.close()
break
else:
print("输入有误!")
def send_msg(udp_socket):
"""发送消息"""
# 发消息
dest_ip = input("请输入对方ip: ")
try:
dest_port = int(input("请输入port: "))
except Exception as e:
print("输入的端口有误, 请重新输入", e)
send_data = input("请输入要发送的消息: ")
# 使用套接字发送数据
udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
def receive_msg(udp_socket):
"""接收消息"""
msg = udp_socket.recvfrom(1024)
print("收到ip为%s的信息为%s" % (msg[1], msg[0].decode("utf-8")))
# 调用udp聊天室方法
udp_communication()
2.tcp
2.1tcp介绍
2.1.1tcp简介
TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话""
2.1.2TCP特点
1. 面向连接
通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。
双方间的数据传输都可以通过这一个连接进行。
完成数据交换后,双方必须断开此连接,以释放系统资源。
这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。
2. 可靠传输
1)TCP采用发送应答机制
TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
2)超时重传
发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。
TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
3)错误校验
TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
4) 流量控制和阻塞管理
流量控制用来避免主机发送得过快而使接收方来不及完全收下。
2.1.3tcp通信模型
tcp通信模型.png2.1.4TCP与UDP的不同点
面向连接(确认有创建三方交握,连接已创建才作传输。)
有序数据传输
重发丢失的数据包
舍弃重复的数据包
无差错的数据传输
阻塞/流量控制
2.1.5tcp注意点
tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
关闭accept返回的套接字意味着这个客户端已经服务完毕
当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
2.2tcp的demo
客户端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
def main():
# 1. 创建tcp套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 链接服务器
dest_ip = input("请输入要链接的服务器ip: ")
dest_port = input("请输入要链接的服务器port: ")
try:
dest_port = int(dest_port)
except Exception as e:
print("输入的端口号有误", e)
tcp_socket.connect((dest_ip, dest_port))
# 3. 发送/接收数据
send_data = input("请输入要发送的数据: ")
tcp_socket.send(send_data.encode("utf-8"))
# 4. 关闭套接字
tcp_socket.close()
if __name__ == '__main__':
main()
服务端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
def main():
# 1.创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.绑定本地信息: 传入一个元组类型
tcp_server_socket.bind(("", 8081))
# 3.让默认的套接字由主动变为被动(listen)
tcp_server_socket.listen(128)
print("----------1----------")
while True:
# 4.等待客户端的连接(accept)
new_client_socket, client_addr = tcp_server_socket.accept()
print("----------2----------")
print("----------client_addr----------", client_addr)
while True:
# 5.接收客户端发送过来的请求
receive_data = new_client_socket.recv(1024)
print(">>>>>>>>>>>", receive_data.decode("utf-8"), "<<<<<<<<<<")
if receive_data: # 如果接收到的数据不为空, 即客户端未关闭, 则循环, 否则关闭客户端连接
# 6.回送一部分信息给客户端
new_client_socket.send("ok".encode("utf-8"))
else:
break
# 7.关闭套接字
new_client_socket.close()
tcp_server_socket.close()
if __name__ == '__main__':
main()
3.线程--进程--协程(并发,并行)
效率快慢:
cpython的解释器(存在GIL)
epoll > 进程 > 协程 > 线程
jpython的解释器
epoll > 协程 > 线程 > 进程
多线程或者多协程效率比单线程或单协程快的原因是:
将其他线程等待的时间利用起来, 做了其他事情, 一旦获取了执行任务的锁, 便可以立刻执行
一个处理器(CPU)在某一个时间点上永远都只能是一个线程!
双核[cpu]你可以理解为两块[cpu],4核、8核等以此类推,
就单个cpu而言某个时间点只能是一个线程在运行,
所谓的多线程是通过调度获取cpu的时间片实现的,
其实就相当于 cpu一个人,多线程是几件事,cpu一下子干这件事,
干一会儿时间片到了就干另一件,
由于cpu计算速度很快很快,所以看起来就像几件事情在同时做着,
不过现在cpu都是双核四核八核的 这些是真的一起干的
应用场景选择:
计算密集型(CPU): 多进程
IO密集型(硬盘): 多线程, 多协程
进程能够充分利用多核CPU
而线程或协程仅仅利用了单核CPU
程序:
指的是存储在计算机上的一个一个程序, 如各种.exe文件
进程:
指的是程序运行起来后的一系列资源的总称, 如各种.exe文件运行起来后, 拥有各种资源, 如占用CPU, 调用摄像头等功能
进程是系统进行资源分配和调度的一个独立单位
线程:
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),
但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
线程:
占用资源少, 共享资源, 能够完成多任务,比如 一个QQ中的多个聊天窗口
进程:
占用资源多, 不共享资源, 能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线线程不能够独立执行,必须依存在进程中
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
先有进程, 才有线程!!!
线程和进程在使用上各有优缺点:
线程执行开销小,但不利于资源的管理和保护;而进程正相反。
图片.png
图片.png
#并发:
指的是任务数多于cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行
(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
#并行:
指的是任务数小于等于cpu核数,即任务真的是一起执行的
3.1线程
3.1.1创建线程
方法1:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
def main():
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start() # 创建线程, 并开始执行线程
t2.start()
while True:
ts = threading.enumerate()
print(ts)
if len(ts) <= 1:
break
time.sleep(1)
def sing():
for i in range(4):
print("第%d次唱歌-------" % i)
time.sleep(1)
def dance():
for i in range(4):
print("第%d次跳舞<<<<<<<" % i)
time.sleep(1)
if __name__ == '__main__':
main()
方法2: 集成threading.Thread类
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
class ThreadSelfDefine(threading.Thread):
"""通过集成threading.Thread类, 来自定义线程类, 从而提升代码的封装性"""
def run(self):
for i in range(5):
print(i)
time.sleep(1)
if __name__ == '__main__':
t = ThreadSelfDefine()
t.start()
3.1.2多线程共享全局变量
#同步:
就是协同步调,按预定的先后次序进行运行
#互斥锁:
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;
直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
锁的好与坏
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
gl_count = [1, 2, 3]
def main():
"""
定义线程的方法1:
target指明线程调用的方法
args指明调用方法时传入的参数(参数必须是元组类型, 末尾逗号不能少)
:return:
"""
t1 = threading.Thread(target=sing, args=(gl_count,))
t2 = threading.Thread(target=dance, args=(gl_count,))
t1.start() # 创建线程, 并开始执行线程
time.sleep(1)
t2.start()
time.sleep(1)
def sing(count):
count.append(5)
print("in sing gl_count = %s-------" % str(count))
def dance(count):
print("in dance gl_count = %s<<<<<<<" % str(count))
if __name__ == '__main__':
main()
print("gl_count=", gl_count)
3.1.3线程同步及互斥锁-->共享变量的问题
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
g_num = 0
mutex = threading.Lock()
def fn01(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 释放锁
def fn02(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 释放锁
def main():
t1 = threading.Thread(target=fn01, args=(1000000,))
t2 = threading.Thread(target=fn02, args=(1000000,))
t1.start()
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("最后计算结果g_num为: ", g_num)
if __name__ == '__main__':
main()
3.1.4死锁
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time
mutexA = threading.Lock()
mutexB = threading.Lock()
class MyThread01(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name, "MyThread01--up-")
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name, "MyThread01--down-")
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread02(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name + '----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
if __name__ == '__main__':
t1 = MyThread01()
t2 = MyThread02()
t1.start()
t2.start()
3.1.5死锁解决方案
方案1: 程序设计时要尽量避免(银行家算法, 即合理设计锁, 合理释放)
方案2: 添加超时时间等
3.1.6udp多线程聊天室
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import threading
def send_msg(udp_socket):
"""获取键盘数据,并将其发送给对方"""
while True:
ip = input("请输入对方的ip: ")
port = input("请输入对方的port: ")
try:
port = int(port)
except Exception as e:
print("port输入有误")
send_data = input("请输入要发送的数据: ")
# 发送数据
udp_socket.sendto(send_data.encode("utf-8"), (ip, port))
def recv_msg(udp_socket):
"""接收数据并显示"""
while True:
recv_data = udp_socket.recvfrom(1024)
recv_ip = recv_data[1]
recv_data = recv_data[0].decode("utf-8")
print("接收到来自%s的数据是%s" % (recv_ip, recv_data))
def main():
# 创建套接字
socket_ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定本地信息
socket_.bind(("", 8088))
# 创建一个子线程用来接收数据
t = threading.Thread(target=recv_msg, args=(socket_,))
t.start()
# 让主线程用来检查键盘数据并且发送
send_msg(socket_)
if __name__ == '__main__':
main()
3.2进程
进程状态
3.2.1创建一个进程
给子进程指定的函数传递参数
进程间不同享全局变量
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import time
from multiprocessing import Process
# 全局变量: 多进程间不共享全局变量
nums = [1, 2]
def fn01(*args, **kwargs):
print(args)
print(kwargs)
nums.append(4)
print("进程的pid是%s, 全局变量nums=%s" % (os.getpid(), str(nums))) # [1, 2, 4]
def fn02():
print("进程的pid是%s, 全局变量nums=%s" % (os.getpid(), str(nums))) # [1, 2]
if __name__ == '__main__':
"""
multiprocessing模块就是跨平台版本的多进程模块,
提供了一个Process类来代表一个进程对象,
这个对象可以理解为是一个独立的进程,可以执行另外的事情
"""
p = Process(target=fn01, args=("test", 12), kwargs={"name": "tom"})
p.start()
time.sleep(1)
p1 = Process(target=fn02)
p1.start()
p.terminate()
p.join()
Process([group [, target [, name [, args [, kwargs]]]]])
target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
args:给target指定的函数传递的参数,以元组的方式传递
kwargs:给target指定的函数传递命名参数
name:给进程设定一个名字,可以不设定
group:指定进程组,大多数情况下用不到
Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程)
is_alive():判断进程子进程是否还在活着
join([timeout]):是否等待子进程执行结束,或等待多少秒
terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
pid:当前进程的pid(进程号)
3.2.2进程间通信-Queue
Process之间有时需要通信,操作系统提供了很多机制来实现进程间的通信。
初始化Queue()对象时(例如:q=Queue()),
若括号中没有指定最大可接收的消息数量,或数量为负值,
那么就代表可接受的消息数量没有上限(直到内存的尽头);
常用方法:
Queue.qsize():返回当前队列包含的消息数量;
Queue.empty():如果队列为空,返回True,反之False ;
Queue.full():如果队列满了,返回True,反之False;
Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
Queue.get_nowait():相当Queue.get(False);
Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常;
2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常;
Queue.put_nowait(item):相当Queue.put(item, False);
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Queue, Process
def write(queue):
for i in range(5):
if not queue.full():
queue.put(i)
print("数据已经写入队列")
def read(queue):
while True:
if not queue.empty():
print(queue.get(True))
else:
break
print("数据读取完毕")
if __name__ == '__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue(3)
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 等待pw结束:
pw.join()
# 启动子进程pr,读取:
pr.start()
pr.join()
3.2.3进程池Pool及其中的Queue
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import time
from multiprocessing import Manager
from multiprocessing.pool import Pool
def write(q):
print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in "hello_world":
q.put(i)
print("写的进程执行完毕")
def read(q):
print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print("reader从Queue获取到消息:%s" % q.get(True))
if __name__ == '__main__':
# 使用Manager中的Queue
q = Manager().Queue()
pool = Pool(3)
pool.apply_async(write, (q,))
# 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据
time.sleep(1)
pool.apply_async(read, (q,))
pool.close()
pool.join()
3.3协程
协程,又称微线程,纤程。英文名Coroutine。
协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。
为啥说它是一个执行单元,因为它自带CPU上下文。
这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。
只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:
在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,
然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,
并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。
操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。
所以线程的切换非常耗性能。
但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
3.3.1使用yield实现多任务(不推荐)
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import time
def fn01():
while True:
print("1")
time.sleep(1)
yield
def fn02():
while True:
print("2")
time.sleep(1)
yield
def main():
t1 = fn01()
t2 = fn02()
while True:
next(t1)
next(t2)
if __name__ == '__main__':
main()
3.3.2使用greenlet实现多任务(不推荐)
使用如下命令安装greenlet模块:
sudo pip3 install greenlet
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import time
from greenlet import greenlet
def fn01():
while True:
print("------1-------")
gr2.switch()
time.sleep(1)
def fn02():
while True:
print("------2--------")
gr1.switch()
time.sleep(1)
gr1 = greenlet(fn01)
gr2 = greenlet(fn02)
# 切换到gr2中运行
gr2.switch()
3.3.3使用gevent实现多任务(>>>>>>>>推荐<<<<<<<)
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
安装
pip3 install gevent
用法一: 直接用gevent中的各种内容
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import gevent as gevent
def fn01(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 用来模拟一个耗时操作,注意不是time模块中的sleep
gevent.sleep(1)
def fn02(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 用来模拟一个耗时操作,注意不是time模块中的sleep
gevent.sleep(1)
g1 = gevent.spawn(fn01, 3)
g2 = gevent.spawn(fn02, 3)
g1.join()
g2.join()
用法二: 若是原始代码, 已经用了大量的非gevent包中的内容, 如下即可
即: 给程序打补丁
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import time
import gevent as gevent
from gevent import monkey
def fn01(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 用来模拟一个耗时操作,注意不是time模块中的sleep
time.sleep(1)
def fn02(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 用来模拟一个耗时操作,注意不是time模块中的sleep
time.sleep(1)
# 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
monkey.patch_all()
# 用列表来处理所有任务
gevent.joinall([
gevent.spawn(fn01, 3),
gevent.spawn(fn02, 3)
])
资源下载器
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import urllib.request
import gevent
from gevent import monkey
def download(file_name, url):
resp = urllib.request.urlopen(url)
data = resp.read()
print(len(data))
with open(file_name, "wb") as f:
f.write(data)
# 有IO才做时需要这一句
monkey.patch_all()
gevent.joinall([
gevent.spawn(download, "abc.png", "https://imgsa.baidu.com/news/q%3D100/sign=dbbbf8cbf903918fd1d139ca613c264b/3b87e950352ac65ca8a4c958f5f2b21193138a7c.jpg")
])
3.4进程、线程、协程对比
有一个老板想要开个工厂进行生产某件商品(例如剪子)
他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
这个老板为了提高生产率,想到3种办法:
在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即"单进程 多线程"方式
老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即"多进程 多线程"方式
老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:"协程"方式
简单总结:
1.进程是资源分配的单位
2.线程是操作系统调度的单位
3.进程切换需要的资源很最大,效率很低
4.线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
5.协程切换任务资源很小,效率高
6.多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
4.正则表达式
4.1匹配单个字符
字符 | 功能 |
---|---|
. | 匹配任意1个字符(除了\n) |
[ ] | 匹配[ ]中列举的字符 |
\d | 匹配数字,即0-9 |
\D | 匹配非数字,即不是数字 |
\s | 匹配空白,即 空格,tab键 |
\S | 匹配非空白 |
\w | 匹配单词字符,即a-z、A-Z、0-9、_ |
\W | 匹配非单词字符 |
4.2匹配多个字符
字符 | 功能 |
---|---|
* | 匹配前一个字符出现0次或者无限次,即可有可无 |
+ | 匹配前一个字符出现1次或者无限次,即至少有1次 |
? | 匹配前一个字符出现1次或者0次,即要么有1次,要么没有 |
{m} | 匹配前一个字符出现m次 |
{m,n} | 匹配前一个字符出现从m到n次 |
4.3匹配开头结尾
字符 | 功能 |
---|---|
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
4.4匹配分组
字符 | 功能 |
---|---|
竖线 | 匹配左右任意一个表达式 |
(ab) | 将括号中字符作为一个分组 |
\num | 引用分组num匹配到的字符串 |
(?P<name>) | 分组起别名 |
(?P=name) | 引用别名为name分组匹配到的字符串 |
4.5简单示例
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
"""
re模块的使用过程
# 1.导入re模块
import re
# 2.使用match方法进行匹配操作
result = re.match(正则表达式,要匹配的字符串)
# 3.如果上一步匹配到数据的话,可以使用group方法来提取数据
result.group()
"""
# match() 能够匹配出以xxx开头的字符串
result = re.match(r"hello", "hello world")
# 如果上一步匹配到数据的话,可以使用group()方法来提取数据
print(result, result.group())
# 大小写h都可以的情况
ret = re.match(r"[hH]", "hello world")
print(ret.group())
# 下面这个正则不能够匹配到数字4,因此ret为None
ret = re.match(r"[0-35-9]", "4hello world")
print(ret is None)
# 使用\d进行匹配
ret = re.match(r"嫦娥\d号", "嫦娥6号发射成功")
print(ret.group())
# 需求:匹配出,一个字符串第一个字母为大小字符,后面都是小写字母并且这些小写字母可有可无
ret = re.match(r"[A-Z][a-z]*", "Hello") # Hello
print(ret.group())
ret = re.match(r"[A-Z][a-z]*", "Helloworld Yeah") # Helloworld
print(ret.group())
# 需求:匹配出,变量名是否有效
ret = re.match(r"[A-Za-z_]+\w*", "2a")
print(ret)
ret = re.match(r"[A-Za-z_]+\w*", "hello")
print(ret.group())
# 需求:匹配163.com的邮箱地址
email_list = ["tom@163.com", "jerry@163.comheihei", ".hello@qq.com"]
for email in email_list:
regex = re.match(r"[\w]{2,20}@163\.com$", email)
if regex:
print(regex.group())
# 需求:匹配出0-100之间的数字
ret = re.match(r"[1-9]?\d$|100", "08")
print(ret)
# 需求:匹配出163、126、qq邮箱
ret = re.match(r"\w{4,20}@(126|163|qq)\.com$", "hello@126.com")
print(ret.group())
# 需求:匹配出163、126、qq邮箱的@符号签名的部分
ret = re.match(r"(\w{4,20})@(126|163|qq)(\.com)", "world@qq.com")
print(ret.group(3)) # 获取()中的第三个元素
# 需求: 匹配成对的html标签
ret = re.match(r"<([a-zA-Z]*)>.*</\1>", "<div>hi</div>")
print(ret.group())
4.6高级用法
"""
search方法
findall方法
sub 将匹配到的数据进行替换
split 根据匹配进行切割字符串,并返回一个列表
"""
# 需求:匹配出文章阅读的次数
ret = re.search(r"\d+", "文章的阅读次数是10000")
print(ret.group())
# 需求:统计出python、java相应文章阅读的次数
ret = re.findall(r"\d+", "python=1000, java=1299")
print(ret)
# 需求:将匹配到的阅读次数加1
ret = re.sub(r"\d+", "100", "python=19")
print(ret)
# 需求:切割字符串“info:xiaoZhang 33 shandong”
ret = re.split(r":| ", "info:xiaoZhang 33 shandong")
print(ret)
4.7python贪婪和非贪婪
Python里数量词默认是贪婪的
(在少数语言里也可能是默认非贪婪),总是尝试匹配尽可能多的字符;
非贪婪则相反,总是尝试匹配尽可能少的字符。
在"*","?","+","{m,n}"后面加上?,使贪婪变成非贪婪。
正则表达式模式中使用到通配字,那它在从左到右的顺序求值时,会尽量“抓取”满足匹配最长字符串,
在我们上面的例子里面,“.+”会从字符串的启始处抓取满足模式的最长字符,
其中包括我们想得到的第一个整型字段的中的大部分,“\d+”只需一位字符就可以匹配,所以它匹配了数字“4”,
而“.+”则匹配了从字符串起始到这个第一位数字4之前的所有字符。
解决方式:
非贪婪操作符“?”,这个操作符可以用在"*","+","?"的后面,要求正则匹配的越少越好。
4.8Python的正则前面的r的作用
Python中字符串前面加上 r 表示原生字符串,
与大多数编程语言相同,正则表达式里使用"\"作为转义字符,这就可能造成反斜杠困扰。
假如你需要匹配文本中的字符"\",
那么使用编程语言表示的正则表达式里将需要4个反斜杠"\\":
前两个和后两个分别用于在编程语言里转义成反斜杠,
转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。
Python里的原生字符串很好地解决了这个问题,有了原生字符串,
你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。
>>> ret = re.match(r"c:\\a",mm).group()
>>> print(ret)
c:\a
5.迭代器, 生成器
迭代器, 生成器 保存的是生成结果的代码, 而非结果
5.1迭代器
#可迭代对象:
一个class要想能够被迭代(使用for循环), 需要实现__iter__(self)方法,
并且返回一个具有迭代器的对象的引用
#迭代器:
具有迭代器功能的对象是指实现了__iter__(self)方法和__next__(self)方法
的类
demo01
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from collections import Iterator, Iterable
class Stu(object):
"""自定义的一个可迭代对象"""
def __init__(self):
self.names = list()
def add(self, name):
self.names.append(name)
def __iter__(self):
return ClassIterator(self)
class ClassIterator(object):
"""自定义的供上面可迭代对象使用的一个迭代器"""
def __init__(self, my_list):
self.my_list = my_list
# current用来记录当前访问到的位置
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current < len(self.my_list.names):
name = self.my_list.names[self.current]
self.current += 1
return name
else:
raise StopIteration
stus = Stu()
stus.add("tom")
stus.add("jerry")
stus.add("john")
stus.add("mary")
# 判断Stu的实例是否是可迭代对象
print("Stu的实例是否是可迭代对象: ", isinstance(stus, Iterable))
# 如何判断一个对象是否是迭代器
print("判断ClassIterator是否是迭代器: ", isinstance(ClassIterator([]), Iterator))
for stu in stus:
print(stu)
demo02
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Stu(object):
"""自定义的一个可迭代对象"""
def __init__(self):
self.names = list()
# current用来记录当前访问到的位置
self.current = 0
def add(self, name):
self.names.append(name)
def __iter__(self):
return self
def __next__(self):
if self.current < len(self.names):
name = self.names[self.current]
self.current += 1
return name
else:
raise StopIteration
stus = Stu()
stus.add("tom")
stus.add("jerry")
stus.add("john")
stus.add("mary")
for stu in stus:
print(stu)
demo03斐波那契数列
class Fibonacci(object):
"""斐波那契数列"""
def __init__(self, n):
self.n = n
self.current = 0
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
if self.current < self.n:
num = self.a
self.a, self.b = self.b, self.a + self.b
self.current += 1
return num
else:
raise StopIteration
for number in Fibonacci(8):
print(number)
除了for循环能接收可迭代对象,list、tuple等也能接收
li = list(Fibonacci(8))
print('-----', li, '-------')
tupleA = tuple(Fibonacci(8))
print(tupleA)
5.2生成器
生成器是一类特殊的迭代器
使用了yield关键字的函数不再是函数,而是生成器。
(使用了yield的函数就是生成器)
yield关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
Python3中的生成器可以使用return返回最终运行的返回值,
而Python2中的生成器不允许使用return返回一个返回值
(即可以使用return从生成器中退出,但return后不能有任何表达式)。
demo01
def generator_method(n):
"""
如果一个函数中有yield语句, 那么这个函数就不再是函数, 而是一个生成器模板
在调用该模板(函数)时, 会创建一个生成器对象
:param n:
:return:
"""
a, b = 0, 1
current = 0
while current < n:
ret = yield a # 这里的a赋值给了next方法调用的结果, 这里ret接收send传入的值
if ret is not None:
current = ret
a, b = b, a + b
current += 1
return "over...." # 可用异常的value属性获取该返回值
generator01 = generator_method(8)
# 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
print('->', next(generator01), ',', next(generator01))
#
generator01.send(5) # 可用通过send传入的值来修改生成器的指针位置
print('-->', next(generator01))
generator02 = generator_method(3)
# 遍历生成器中的值, 并用异常对象的value属性, 获取该返回值
while True:
try:
print(next(generator02))
except Exception as e:
print(e.value)
break
6.GIL(全局解释器锁)
GIL面试题如下
描述Python GIL的概念, 以及它对python多线程的影响?
编写一个多线程抓取网页的程序,
并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。
1.Python语言和GIL没有半毛钱关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL。(java版本的jpython解释器, 不存在GIL问题)
2.GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
3.线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100
4.Python使用多进程是可以利用多核的CPU资源的。
5.多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁
7.深拷贝、浅拷贝
7.1浅拷贝
浅拷贝是对于一个对象的顶层拷贝
通俗的理解是:拷贝了引用,并没有拷贝内容
如:
a = [1, 2]
b = a
copy.copy(obj)
copy.copy对于可变类型,会进行浅拷贝
copy.copy对于不可变类型,不会拷贝,仅仅是指向
7.2深拷贝
深拷贝是对于一个对象所有层次的拷贝(递归)
copy.deepcopy(obj)
a = [1, 2]
b = a
c = copy.deepcopy(a)
d = copy.copy(a)
a.append(3)
print(b, c, d) # b=[1, 2, 3] c=[1, 2] d=[1, 2]
print(id(b[0]), id(c[0]), id(d[0])) # 三者相等
e = [a, b]
f = copy.copy(e)
g = copy.deepcopy(e)
a.append(4)
print(f, g) # f=[[1, 2, 3, 4], [1, 2, 3, 4]] g=[[1, 2, 3], [1, 2, 3]]
print(id(f[0]), id(g[0])) # 二者不相等
网友评论