网络编程(五)

网络编程(五)


昨日回顾


创建进程

from multiprocessing import Process

if __name__ == '__main__':
    p = Process(target=需要启动的进程, args=('参数',))  # 创建一个进程对象
    p.start()  # 告诉操作系统创建一个新的进程并启动进程中的所有功能
    print('主进程')

或者重写__init__在创建对象前写入参数


TCP服务端并发

'''服务端'''
import socket
from multiprocessing import Process
def get_server():
  server = socket.socket()  # 创建一个服务器对象
  from socket import SOL_SOCKET, SO_REUSEADDR
  server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 保证重复启动地址号不被占用
  server.bind(('127.0.0.1', 8888))  # 写入地址和端口号
  server.listen(5)  # 最大5人等待
  return server

def talk(sock):  # 接受信息写成一个函数
    while True:
        data = sock.recv(1024)  
        sock.send(data.upper())  

if __name__ == '__main__':
    while True:
        sock, addr = get_server().accept()  # 启动服务器函数
        p = Process(target=talk, args=(sock, ))
        p.start()  # 启动进程接受消息

join方法

类似将并发运行变为串行,主进程代码等待子进程代码运行结束之后再往后执行

进程.join() # 结束一个工作进程


进程间数据隔离

同一台计算机上的多个应用程序,默认情况下,数据相互间隔


进程对象属性和方法

1.获取进程号
	current_process().pid
  os.getpid()
  os.getppid()
2.杀死子进程
	terminate()
3.判断进程是否存活
	is_alive()

僵尸进程与孤儿进程

僵尸进程
	进程结束之后不会立刻清空所有信息,会短时间保留进程号等信息等待父进程回收,所有的进程都会经历僵尸进程
  父进程回收子进程资源的两种方式
  	1.父进程正常结束
    2.调用join方法
 
孤儿进程
	子进程正常运行 父进程意外死亡(主动杀死>>> taskkill\kill)

守护进程

守护进程会跟随主进程一起死亡


互斥锁

在对数据进行操作的时候,并发很容易造成数据混乱

互斥锁的本质就是将并发转换为串行

mutex = Lock()
mutex.acquire()  # 抢锁
mutex.release()  # 放锁


今日学习内容


消息队列


队列:先进先出(使用频率很高)

堆栈:先进后出(特定常见下用)

from multiprocessing import Queue
q = Queue(5)  # 括号内参数表示最大队列数
# 朝队列中存放数据
q.put(111)
q.put(222)
q.put(333)
print(q.full())  # False  判断队列是否满了
q.put(444)
q.put(555)
print(q.full())  # True
# q.put(666)  # 超出最大长度 原地阻塞等待队列中出现空位
print(q.get())
print(q.get())
print(q.empty())  # False  判断队列是否空了
print(q.get())
print(q.get())
print(q.get())
print(q.empty())  # True
# print(q.get())  # 队列中没有值 继续获取则阻塞等待队列中给值
print(q.get_nowait())  # 队列中如果没有值 直接报错

q.full()	判断队列是否满了
q.empty()	判断队列是否空了
q.get_nowait()  队列中没有值就报错

IPC机制(进程间通信)

1.主进程与子进程数据交互
2.两个子进程数据交互
本质:不同内存空间中的进程数据交互

注释:

from multiprocessing import Process, Queue  


def producer(q):  # 生产函数
    print('子进程producer从队列中取值>>>:', q.get())
    q.put('子进程producer往队列中添加值')

def consumer(q):  # 消费者函数
    print('子进程consumer从队列中取值>>>:', q.get())


if __name__ == '__main__':
    q = Queue()  # 启用一个队列
    p = Process(target=producer, args=(q, ))  # 启动一个生产进程
    p1 = Process(target=consumer, args=(q,))  # 启动一个消费函数
    p.start()  # 启动生产函数
    p1.start()  # 启动消费函数
    q.put(0)  # 主进程往队列中存放数据123
    print('主进程')
 
生产函数启动进程后往主进程添加数值,消费函数启动进程后往主进程拿数值


生产者消费者模型

生产者:产生数据,制作数据(类似商店上货)

消费者:拿走数据,消耗数据(类似顾客购物)

在爬虫领域:

​ 通过代码爬取网页数据,获取数据即可称为生产者

​ 针对代码数据做筛选处理被称之为消费者

如果使用进程来演示

​ 需要一个媒介(消息队列)Queue

from multiprocessing import Process, Queue, JoinableQueue
import time
import random

def producer(name, food, q):
    for i in range(5):
        data = f'{name}生产了{food}{i}'
        print(data)
        time.sleep(random.randint(1, 3))  # 模拟产生过程
        q.put(data)

def consumer(name, q):
    while True:
        food = q.get()
        time.sleep(random.random())
        print(f'{name}购买了{food}')
        q.task_done()  # 每次去完数据必须给队列一个反馈

if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('上货员1', '泡面', q))
    p2 = Process(target=producer, args=('上货员2', '挂面', q))
    c1 = Process(target=consumer, args=('顾客1', q))
    c2 = Process(target=consumer, args=('顾客2', q))
    c1.daemon = True
    c2.daemon = True
    p1.start()
    p2.start()
    c1.start()
    c2.start()
    # 生产者生产完所有数据之后 往队列中添加结束的信号
    p1.join()
    p2.join()
    # q.put(None)  # 结束信号的个数要跟消费者个数一致才可以
    """队列中其实已经自己加了锁 所以多进程取值也不会冲突 并且取走了就没了"""
    q.join()  # 等待队列中数据全部被取出(一定要让生产者全部结束才能判断正确)
    """执行完上述的join方法表示消费者也已经消费完数据了"""

线程理论

进程简介:创建一块空间,内部产生代码与数据

线程概念:线程相当于在进程中正在调用运转代码的抽象概念

一个进程中至少有一个线程

进程仅仅是一块内存空间和当中的数据体,真正被运行的其实是线程

启动一个线程的消耗非常的小

启动一个进程:1.申请内存空间 2.拷贝代码

启动一个线程:一个进程的线程都是共用数据的,开启进程后可创建多条线程

多个功能所需求的代码如果相同,那么他们所开设的应该是多线程而不是多进程


开设线程的两种方式

"进程与线程的代码实操几乎是一样的"

from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')

# 创建线程无需在__main__下面编写 但是为了统一 还是习惯在子代码中写
t = Thread(target=task, args=('jason', ))
t.start()  # 创建线程的开销极小 几乎是一瞬间就可以创建
print('主线程')


class MyThread(Thread):
    def __init__(self, username):
        super().__init__()
        self.username = username
    def run(self):
        print(f'{self.username} jason is running')
        time.sleep(3)
        print(f'{self.username} is over')

t = MyThread('jasonNB')
t.start()
print('主线程')

线程实现TCP服务端的并发

仔细体会开设进程和线程的本质区别
import socket
from threading import Thread

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()


def talk(sock):
    while True:
        data = sock.recv(1024)
        print(data.decode('utf8'))
        sock.send(data.upper())


while True:
    sock, addr = server.accept()
    # 每类一个客户端就创建一个线程做数据交互
    t = Thread(target=talk, args=(sock,))
    t.start()

线程的运行资源消耗极小,在客户端需要调用相同的服务端代码时,效果更好。

线程join方法与守护线程

from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')


t = Thread(target=task, args=('jason', ))
t.start()
t.join()  # 主线程代码等待子线程代码运行完毕之后再往下执行
print('主线程')

from threading import Thread
import time


def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')

t1 = Thread(target=task, args=('eason',))
t2 = Thread(target=task, args=('kevin',))
t1.daemon = True
t1.start()
t2.start()
print('主线程')

结果,主线程会等待所有的子线程结束才会结束整个进程,因为子线程所使用的的代码是从相同的进程中取出的,如果主线程结束时直接关闭进程,那么会导致子进程丢失所需要的代码。

而守护进程也是同理,所有线程使用的都是相同的进程代码,只有所有线程都运行完后,进程才会真正的被关闭。


线程对象属性和方法

验证一个进程下多个线程是否确实处于同一进程

验证方式:

统计进程下活跃的线程数
	active_count()  # 主线程同样算
获取线程的名字
	1.current_thread().name
  	MainThread  				 主线程
    Thread-1、Thread-2		子线程
  2.self.name

GIL全局解释器锁

    1. GIL概念:
        GIL,全局解释器锁(global interpreter lock),它不是python语言的特性,而是python默认的解析器cpython的特性cpython要求每个线程必须先获取GIL锁,才能执行线程中的代码
        目的:解决多线程同时竞争解析器程序的全局变量而出现的线程安全问题
        不足:在多线程中不能充分利用多核cpu 原因是一个进程只存在一把gil锁,当在执行多个线程时,内部会争抢gil锁,这会造成当某一个线程没有抢到锁的时候会让cpu等待,进而不能合理利用多核cpu资源

    2. 单线程、多线程、多进程执行分析
       - 单线程: 双核cpu使用率50%
       - 多线程:双核cpu使用率50%
       - 多进程:双核cpu使用率100%               

    3.如何解决GIL问题:
      - 换语言, 在处理多线程代码时,用其他语言代码,比如用
      c或者java
      - 换解析器,比如换jpython
      - 业界常用的方案: 多进程+多协程方法

    4. 面试题:
        描述Python GIL的概念, 以及它对python多线程的影响?一个单线程抓取网页的程序,与一个多线程抓取网页的程序哪个性能更高,并解释原因

        1. GIL,全局解释器锁(global interpreter lock),它是cpython解析器的特性,不是python的特性 ,它要求线程在执行前,需要获取GIL锁,

        2. 由于GIL的存在,会影响多线程不能利用多核CPU资源(原因是一个进程只存在一把gil锁,当在执行多个线程时,内部会争抢gil锁,这会造成当某一个线程没有抢到锁的时候会让cpu等待,进而不能合理利用多核cpu资源),通过多进程方式可利用多个CPU资源

        3. 线程释放GIL锁的情况:
            1.在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL
            2.Python 3x使用计时器(执行时间达到阈值后,当前线程释放GIL)

        4. 多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁,这样在线程阻塞情况下,可以执行其他线程中的代码
          多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁 


今日小结


学习使我充实。

posted @ 2022-04-20 21:15  Eason辰  阅读(37)  评论(0)    收藏  举报