第四模块 第26章 网络编程进阶

https://www.cnblogs.com/linhaifeng/p/7278389.html

https://www.cnblogs.com/linhaifeng/articles/6817679.html

https://www.cnblogs.com/linhaifeng/articles/7430066.html

 

 

1. 操作系统

双击QQ后,QQ程序会到CPU中去执行。 CPU同一时间只能执行一个任务,如果同时还启动了微信,那么如果QQ运行过程中遇到了IO,则会跳去执行微信, 执行过程中遇到IO后又会跳去执行其他程序。这样,给用户的体验是QQ和微信同时在运行。如果QQ一直运行,没有IO,则不会跳转执行其他程序,那么为了同时运行其他程序,则会在执行一段时间后跳转执行其他程序。至于何时跳转,跳转执行那个程序,则由操作系统决定。

操作系统的两大作用:

  1. 隐藏复杂丑陋的硬件接口,提供良好的抽象接口

  2. 管理调度进程, 并且把多个进程对硬件的竞争变得有序

多道技术:

  单核下实现并发。 并发指的是看起来是同步运行的。 从两个方面实现了看起来是并发的: 1. 空间复用, 即内存复用。2. 时间复用,即来回切换: 一是进程遇到IO阻塞要切换,这种方式可以提升CPU的运行效率;另一种方式是一个进行运行时间过长/有一个比它优先级更高的进行,也会进行切换,这种方式会降低CPU的运行效率。每次切换前会保留当前的状态,方便后面继续运行。

并行:针对多核。真正意义上的同时运行。

2. 并发编程之多进程

2.2 开启进程的两种方式

# 方式一
from multiprocessing import Process
import time
def task(name):
    print('%s is running'%name)
    time.sleep(3)
    print('%s is done'%name)
if __name__ == '__main__':
    # p = Process(target=task,kwargs={'name':'子进程1'})  # 方式一
    p = Process(target=task,args=('子进程1',))   # 方式二
    p.start() # 仅仅只是给操作系统发了个信号
    print('主进程')

'''
结果:
主进程
子进程1 is running
子进程1 is done
'''

# 方式二
from multiprocessing import Process
import time
class MyProcess(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name
    def run(self):  # 此处必须为run
        print('%s is running' % self.name)
        time.sleep(3)
        print('%s is done' % self.name)
if __name__ == '__main__':
    p = MyProcess('子进程1')
    p.start()

2.3 查看进程的pid与ppid

from multiprocessing import Process
import time,os
def task():
    print('%s is running, parent id is <%s>'%(os.getpid(),os.getppid()))
    time.sleep(3)
    print('%s is done, parent id is <%s>'%(os.getpid(),os.getppid()))
if __name__ == '__main__':
    p = Process(target=task,)
    p.start()
    print('主进程', os.getpid(),os.getppid())
    
'''
主进程 388012 375916
384340 is running, parent id is <388012>
384340 is done, parent id is <388012>
其中, 388012是主进程id, 375916是pycharm的id
'''

 

2.4 僵尸进程与孤儿进程

'''
僵尸进程: 子进程结束
孤儿进程: 主进程先结束
僵尸进程:
点击run后运行主进程, 在主进程的运行过程中发了系统调用, 开启了子进程.主进程和子进程共用一个打印终端.
主进程干完自己的活后会等着子进程运行完毕后再结束.目的是为了给子进程收尸.
主进程和子进程的运行是相互独立的, 但是主进程可以查看子进程的相关情况.
子进程运行完毕后会将自身的内存空间等清除感情, 但是依然会保留一些状态信息供主进程查看.
子进程结束后还会保留一些信息, 这种情况称为僵尸进程.
所有的子进程都会经历僵尸进程这么一个状态. 主进程结束后会启用系统调用, 回收所有的僵尸进程.
僵尸进程是有害的, 因为会保留其pid, 但是一个进程会有一个pid, 这就会影响后面进程的创建.
僵尸进程本身没有什么害处, 但是如果主进程一直运行, 就会有害.

孤儿进程:
主进程较子进程先结束, 子进程就称为了孤儿进程. 孤儿进程由init进程托管.
在linux系统中, init是所有进程的爹, 由它回收孤儿进程的僵尸进程.
孤儿进程没有害.

'''

 

2.5 Process对象的其他属性和方法

# 1. join方法
# 让主进程等待子进程运行完毕后再执行.
from multiprocessing import Process
import time,os
def task():
    print('%s is running, parent id is <%s>'%(os.getpid(),os.getppid()))
    time.sleep(3)
    print('%s is done, parent id is <%s>'%(os.getpid(),os.getppid()))
if __name__ == '__main__':
    p = Process(target=task,)
    p.start()
    p.join()  # 主进程等待子进程执行完毕
    print('主进程', os.getpid(),os.getppid())
    print(p.pid)

'''
结果:
200100 is running, parent id is <385428>
200100 is done, parent id is <385428>
主进程 385428 357680
200100
'''


# join下的并发
from multiprocessing import Process
import time,os
def task(name, n):
    print('%s is running'%name)
    time.sleep(n)
if __name__ == '__main__':
    start_time  = time.time()
    p1 = Process(target=task,args=('子进程1',5))
    p2 = Process(target=task,args=('子进程2',3))
    p3 = Process(target=task,args=('子进程3',2))
    p1.start()  # 只是向操作系统发送信号, 执行顺序不一定
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()  # 不是串行, 依然是并发
    print('主进程', (time.time()-start_time))
'''
结果:
子进程1 is running
子进程2 is running
子进程3 is running
主进程 5.940209627151489
'''

# 简写
from multiprocessing import Process
import time,os
def task(name, n):
    print('%s is running'%name)
    time.sleep(n)
if __name__ == '__main__':
    start_time  = time.time()
    p1 = Process(target=task,args=('子进程1',5))
    p2 = Process(target=task,args=('子进程2',3))
    p3 = Process(target=task,args=('子进程3',2))
    p_i = [p1, p2, p3]
    for p in p_i:
        p.start()
    for p in p_i:
        p.join()
    print('主进程', (time.time()-start_time))


# join下的串行
from multiprocessing import Process
import time,os
def task(name, n):
    print('%s is running'%name)
    time.sleep(n)
if __name__ == '__main__':
    start_time  = time.time()
    p1 = Process(target=task,args=('子进程1',5))
    p2 = Process(target=task,args=('子进程2',3))
    p3 = Process(target=task,args=('子进程3',2))
    p1.start()  # 只是向操作系统发送信号
    p1.join()
    p2.start()
    p2.join()
    p3.start()
    p3.join()  # 这种情况是串行
    print('主进程', (time.time()-start_time))
'''
结果:
子进程1 is running
子进程2 is running
子进程3 is running
主进程 11.631278276443481
'''


2. is_alive判断进程是否存活
from multiprocessing import Process
import time,os
def task(name, n):
    print('%s is running'%name)
    time.sleep(n)
if __name__ == '__main__':
    p1 = Process(target=task,args=('子进程1',5))
    p1.start()  # 只是向操作系统发送信号
    p1.join()
    print(p1.is_alive())  # False

from multiprocessing import Process
import time,os
def task(name, n):
    print('%s is running'%name)
    time.sleep(n)
if __name__ == '__main__':
    p1 = Process(target=task,args=('子进程1',1), name='subProcess1')  # 重命名
    p1.start()
    p1.terminate()
    p1.join()  # 如果没有这行代码, 则为True
    print(p1.is_alive())  # False
    print(p1.name)  # 查看进程名, 如果未重命名则为Process-1, 如果重命名则为subProcess1

2.6 练习

1. 验证多进程的内存空间之间是隔离的
from multiprocessing import Process

n=100 #在windows系统中应该把全局变量定义在if __name__ == '__main__'之上就可以了

def work():
    global n
    n=0
    print('子进程内: ',n)


if __name__ == '__main__':
    p=Process(target=work)
    p.start()
    print('主进程内: ',n)

 

2. 基于多进程实现并发的套接字通信
# 服务端
import socket
from multiprocessing import Process
def task(conn):
    while True:
        try:
            data = conn.recv(1024)
            # msg = input('>>>')  # 多进程中不支持input功能
            conn.send(data.upper())
            print(data.decode('utf-8'))
        except ConnectionResetError:
            break
    conn.close()
def server(ip, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((ip,port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        p = Process(target=task, args=(conn,))
        p.start()
    server.close()
if __name__ == '__main__':
    server('127.0.0.1', 8001)


'''
这种方式存在一点的弊端:
如果很多客户端来建立连接, 则会开辟很多内存空间, 导致内存溢出.
'''
# 客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1',8001))
while True:
    msg = input('>>>')
    if not msg:
        continue
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))
client.close()

 2.6 守护进程

主进程创建子进程,然后将该进程设置成守护自己的进程,守护进程就好比崇祯皇帝身边的老太监,崇祯皇帝已死老太监就跟着殉葬了。

关于守护进程需要强调两点:

其一:守护进程会在主进程代码执行结束后就终止

其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

如果我们有两个任务需要并发执行,那么开一个主进程和一个子进程分别去执行就ok了,如果子进程的任务在主进程任务结束后就没有存在的必要了,那么该子进程应该在开启前就被设置成守护进程。主进程代码运行结束,守护进程随即终止

'''
守护进程: 守护着主进程的进程, 主进程结束, 守护进程跟着结束.
必须在start之前设置守护进程
守护进程中不能再设置子进程, 因为这样做可能会产生一堆孤儿进程
'''
from multiprocessing import Process
import time
def task(name):
    print('%s is running'%name)
    time.sleep(2)
if __name__ == '__main__':
    p = Process(target=task,args=('子进程1',))
    p.daemon = True
    p.start()
    print('主进程')
'''
结果: 
主进程
'''
# 练习
from multiprocessing import Process

import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':
    p1=Process(target=foo)
    p2=Process(target=bar)

    p1.daemon=True
    p1.start()
    p2.start()
    print("main-------")
'''
main-------
456
end456
'''

 2.7 互斥锁

确保多个子进程在修改同一个数据时一个一个进行. 由并发变成了串行, 牺牲效率保证了数据安全.

from multiprocessing import Process,Lock
import json
import time
def search(name):
    time.sleep(1)
    dic = json.load(open("db",'r',encoding='utf-8'))
    print('<%s>查到剩余票数[%s]'%(name,dic['count']))
def get(name):
    time.sleep(1)
    dic = json.load(open('db','r',encoding='utf-8'))
    if dic['count'] >0 :
        dic['count'] -= 1
        time.sleep(3)
        json.dump(dic, open('db','w',encoding='utf-8'))
        print('<%s>购票成功'%name)
def task(name,mutex):
    search(name)
    mutex.acquire()
    get(name)
    mutex.release()
if __name__ == '__main__':
    mutex = Lock()   # 在此处创建锁对象, 确保所有子进程共用同一把锁.
    for i in range(3):
        p = Process(target=task,args=('路人%s'%i,mutex))
        p.start()
'''
结果:
<路人0>查到剩余票数[1]
<路人2>查到剩余票数[1]
<路人1>查到剩余票数[1]
<路人0>购票成功
'''

2.8 join与互斥锁的区别

join会把子进程的所有代码变成串行, 如果应用在抢票中, 用户在查票的时候也是串行的.
互斥锁会将子进程的部分代码变成串行, 如果应用在抢票中, 用户在查票的时候依然是并发的.

2.9 队列的使用

2.10 生产者消费者模型

2.11 jionablequeue的使用

2.12 什么是线程

  1. 进程是一个资源单位, 每启动一个进程其中至少启动一个线程.
  2. 一个进程中可以启动多个线程.
  3. 多个线程之间共享数据.
  4. 开进程的开销比开线程的开销大.

2.13 开启线程的方式

# 开启线程的方式1
from threading import Thread
import time
def task(name):
    print('%s is running'%name)
    time.sleep(1)
    print('%s is done'%name)
if __name__ == '__main__':
    t = Thread(target=task,args=('线程1',))
    t.start()
    print('主线程')
# 现在有一个进程, 两个线程
'''
结果:
线程1 is running
主线程
线程1 is done
'''
# 开启线程的方式2
from threading import Thread
import time
class MyThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    def run(self):
        print('%s is running' % self.name)
        time.sleep(3)
        print('%s is done' % self.name)
if __name__ == '__main__':
    t = MyThread('线程1')
    t.start()
    print('主线程')

'''
结果:
线程1 is running
主线程
线程1 is done
'''

2.14 线程与进程的区别

'''
1. 开进程的开销远大于开线程的
2. 同一进程内的多个线程共享该进程的地址空间
3. 看一看pid
'''
# 1、开进程的开销远大于开线程
import time
from threading import Thread
from multiprocessing import Process

def piao(name):
    print('%s piaoing' %name)
    time.sleep(2)
    print('%s piao end' %name)

if __name__ == '__main__':
    # p1=Process(target=piao,args=('egon',))
    # p1.start()
    t1=Thread(target=piao,args=('egon',))
    t1.start()
    print('主线程')
'''
进程运行结果:
主线程
egon piaoing
egon piao end
'''

'''
线程运行结果:
egon piaoing
主线程
egon piao end
'''
# 2、同一进程内的多个线程共享该进程的地址空间
from threading import Thread
from multiprocessing import Process

n=100
def task():
    global n
    n=0

if __name__ == '__main__':
    # p1=Process(target=task,)
    # p1.start()
    # p1.join()

    t1=Thread(target=task,)
    t1.start()
    t1.join()

    print('主线程',n)
'''
进程的运行结果:
主进程 100
'''
'''
线程的运行结果:
主线程 0
'''
# 3、瞅一眼pid
from threading import Thread
from multiprocessing import Process,current_process
import os
# multiprocessing 模块下的current_process用于查看当前进程的pid(process id), 无法查看父进程的pid
# os模块下的getpid()用于查看当前进程的pid, getppid()用于查看当前进程的父进行的pid

def task():
    # print(current_process().pid)
    print('子进程PID:%s  父进程的PID:%s' %(os.getpid(),os.getppid()))

if __name__ == '__main__':
    p1=Process(target=task,)
    p1.start()

    # print('主线程',current_process().pid)
    print('主线程',os.getpid())
'''
进程的运行结果:
主线程 89900
子进程PID:91424  父进程的PID:89900
'''
from threading import Thread
import os

def task():
    print('子线程:%s' %(os.getpid()))

if __name__ == '__main__':
    t1=Thread(target=task,)
    t1.start()

    print('主线程',os.getpid())
'''
线程的运行结果:
子线程:90532
主线程 90532
'''

2.15 Thread对象的其他方法和属性

from threading import Thread,currentThread
import time
def task():
    print('%s is running'%currentThread().getName())
    # currentThread().getName()获取当前线程的名称
    time.sleep(1)
    print('%s is done'% currentThread().getName())
if __name__ == '__main__':
    t = Thread(target=task,name = '子线程1')
    t.start()
    t.setName('儿子线程1')
    print(t.getName())
    currentThread().setName('主线程')
    print(t.is_alive())  # 等同于 t.isAlive()
    print('主线程',currentThread().getName())
'''
结果:
子线程1 is running
儿子线程1
True
主线程 主线程
儿子线程1 is done
'''
from threading import Thread,currentThread
import time
def task():
    print('%s is running'%currentThread().getName())
    # currentThread().getName()获取当前线程的名称
    time.sleep(1)
    print('%s is done'% currentThread().getName())
if __name__ == '__main__':
    t = Thread(target=task,name = '子线程1')
    t.start()
    t.setName('儿子线程1')
    t.join()
    print(t.getName())
    currentThread().setName('主线程')
    print(t.is_alive())  # 等同于 t.isAlive()
    print('主线程',currentThread().getName())
'''
结果:
子线程1 is running
儿子线程1 is done
儿子线程1
False
主线程 主线程
'''
from threading import Thread,currentThread, active_count, enumerate
import time
def task():
    print('%s is running'%currentThread().getName())
    time.sleep(1)
    print('%s is done'% currentThread().getName())
if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    t.join()
    print(active_count())
    print(enumerate())   # 查看当前活跃的线程的对象
'''
结果:
Thread-1 is running
Thread-1 is done
1
[<_MainThread(MainThread, started 101996)>]
'''

2.16 守护线程

无论是进程还是线程, 都遵循: 守护XX会等待主XX运行完毕后被销毁

需要强调的是: 运行完毕并非终止运行

1. 对主进程来说, 运行完毕指的是主进程代码运行完毕

2. 对主线程来说, 运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕, 主线程才算运行完毕

详细解释:

1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收), 然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程), 才会结束.

2. 主线程会在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收). 因为主线程的结束意味着进程的结束, 进程整体的资源都将被回收, 而进程必须保证非守护线程都运行完毕后才结束.

验证

'''
主线程代码运行结束后并不会死, 会等待其他线程运行完毕.
主线程结束就代表主进程结束.
守护线程守护着主线程. 主线程结束, 守护线程随着结束.
'''
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    # t.setDaemon(True) #必须在t.start()之前设置
    t.daemon = True  # 同上一方式
    t.start()
    print('主线程')
    print(t.is_alive())
'''
结果:
主线程
True
'''
from threading import Thread
import time

def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':
    t1=Thread(target=foo)
    t2=Thread(target=bar)

    t1.daemon=True
    t1.start()
    t2.start()
    print("main-------")
'''
结果:
123
456
main-------
end123
end456
'''

2.17 互斥锁

'''
互斥锁: 对部分代码加锁. 只针对共享数据的部分, 使其实现串行.
同一进程的多个线程之间共用同一内存空间, 如果多个线程竞争修改进程中的数据, 则会导致数据不安全问题. 为了解决数据安全问题, 就出现了互斥锁的概念. 互斥锁应该加在线程对进程的数据的修改前后. 不同线程之间可以共同修改一个文件吗? 当然也可以, 这种情况下也可以通过互斥锁来解决. 不同进程之间的内存空间是隔离的, 但是它们可以共同访问和修改同一文件(存在于硬盘上). 不同进程共同修改同一文件会导致数据不安全问题, 为了解决这一问题提出来互斥锁的概念. ''' # mutex(互斥锁) from threading import Thread,Lock import time n = 100 def task(): global n temp = n time.sleep(0.1) n = temp-1 if __name__ == '__main__': mutex = Lock() t_l = [] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for t in t_l: t.join() print('',n) # 主 99

 2.18 GIL的基本概念

一. 引子

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。>有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

二.  GIL介绍

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问
1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。

2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
综上:

如果多个线程的target=work,那么执行流程是

多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

 

 GIL(global interpreter lock): 全局解释器锁

本质就是一把互斥锁. 只有cpython解释器才有GIL.

如果同一时间只启动了一个进程, 一个进程中启动了10个线程, 这10个线程同一时间只能用一个核, 无法使用多个核.

如果同一时间启动了多个进程, 则可以使用多个核.

垃圾回收机制不会一直运行.

运行python程序经历的步骤:

  1. 产生一个进程, 产生一个内存空间

  2. 将python解释器的代码加载到进程的内存空间; 将python.py的代码加载到内存

  3. python代码经过python解释器解释后到CPU执行

2.19 GIL与自定义互斥锁的区别

GIL与自定义的互斥锁本质都是互斥锁, 只不过它们保护的对象不一样. 

GIL保护的是垃圾回收线程与python程序线程之间共用数据的安全性.

自定义互斥锁保护的是python程序中多线程共享数据的安全性.

 

机智的同学可能会问到这个问题:Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?

首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock,如下图

1、100个线程去抢GIL锁,即抢执行权限
2、肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
3、极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
4、直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程

 

2.20 GIL与多线程

CPU是用来计算的.

计算密集型任务应该使用多进程.

IO密集型任务应该使用多线程.

 

有了GIL的存在,同一时刻同一进程中只有一个线程被执行

听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言?
要解决这个问题,我们需要在几个点上达成一致:
1、cpu到底是用来做计算的,还是用来做I/O的?

2、多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

3、每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
结论:
1、对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
2、当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
假设我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程


单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜


多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:

现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
应用:

多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析

 

2.20 死锁与递归锁

互斥锁Lock只能acquire一次, release后才能重新acquire. 如果嵌套使用多把互斥锁, 则可能出现死锁的现象.

如果没有嵌套使用, 则不会出现死锁的现象.

 

同一把互斥锁不能嵌套使用, 如果要嵌套使用, 只能使用多把互斥锁.

递归锁支持嵌套使用同一把锁. 支持多次acquire. acquire一次加1, release一次减1, 累计数为0了其他进程才能抢锁.

 

一 死锁现象

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
二 递归锁

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次

# 死锁
# 互斥锁(Lock)只能acquire一次, release后才能重新acquire.
from threading import Thread,Lock
import time

mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 拿到了A锁' %self.name)
mutexB.acquire()
print('%s 拿到了B锁' %self.name) mutexB.release()
mutexA.release()
def f2(self): mutexB.acquire() print('%s 拿到了B锁' % self.name) time.sleep(0.1) mutexA.acquire() print('%s 拿到了A锁' % self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() # 互斥锁只能acquire一次 from threading import Thread,Lock mutexA=Lock() mutexA.acquire() mutexA.release() # 递归锁:可以连续acquire多次,每acquire一次计数器+1,只有计数为0时,才能被抢到acquire from threading import Thread,RLock import time mutexB=mutexA=RLock() # 其中mutexA与mutexB是同一把锁 class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() # 计数为1 print('%s 拿到了A锁' %self.name) mutexB.acquire() # 计数为2 print('%s 拿到了B锁' %self.name) mutexB.release() # 计数为1 mutexA.release() # 计数为0 def f2(self): mutexB.acquire() print('%s 拿到了B锁' % self.name) time.sleep(7) mutexA.acquire() print('%s 拿到了A锁' % self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start()

 2.21 信号量

信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

from threading import Thread,Semaphore
import threading
import time

def func():
    sm.acquire()
    print('%s get sm' %threading.current_thread().getName())
    time.sleep(3)
    sm.release()

if __name__ == '__main__':
    sm=Semaphore(5)
    for i in range(23):
        t=Thread(target=func)
        t.start()

 

 

 

2.22 事件

 

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

from threading import Event

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

from threading import Thread,Event
import threading
import time,random
def conn_mysql():
    count=1
    while not event.is_set():
        if count > 3:
            raise TimeoutError('链接超时')
        print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))
        event.wait(0.5)
        count+=1
    print('<%s>链接成功' %threading.current_thread().getName())


def check_mysql():
    print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
    time.sleep(random.randint(2,4))
    event.set()
if __name__ == '__main__':
    event=Event()
    conn1=Thread(target=conn_mysql)
    conn2=Thread(target=conn_mysql)
    check=Thread(target=check_mysql)

    conn1.start()
    conn2.start()
    check.start()

2.23 定时器

 定时器指n秒后执行某操作

# from threading import Timer
#
# def task(name):
#     print('hello %s' %name)
#
#
# t=Timer(5,task,args=('egon',))
# t.start()

from threading import Timer
import random

class Code:
    def __init__(self):
        self.make_cache()

    def make_cache(self,interval=5):
        self.cache=self.make_code()
        print(self.cache)
        self.t=Timer(interval,self.make_cache)
        self.t.start()

    def make_code(self,n=4):
        res=''
        for i in range(n):
            s1=str(random.randint(0,9))
            s2=chr(random.randint(65,90))
            res+=random.choice([s1,s2])
        return res

    def check(self):
        while True:
            code=input('请输入你的验证码>>: ').strip()
            if code.upper() == self.cache:
                print('验证码输入正确')
                self.t.cancel()
                break


obj=Code()
obj.check()

2.24 线程queue

# 进程queue, 解决进程之间共享数据的, 且自带加锁功能
# 线程queue, 解决线程之间共享数据的, 但是线程之间本身是可以共享数据的, 但是得自己加锁, queue自带加锁功能.
# 并且, 线程queue还提供很多其他功能.
import queue

q=queue.Queue(3) #先进先出->队列, 吃了拉

q.put('first')
q.put(2)
q.put('third')
# q.put(4)
# q.put(4,block=False) #q.put_nowait(4)    
# 默认有一个参数block=True, 如果队列满了则会阻塞住.
# 如果将block = False, 队列满了则会报错.
# q.put(4,block=True,timeout=3)
# 队列满了会阻塞, 3秒后抛异常


print(q.get())
print(q.get())
print(q.get())
# print(q.get(block=False))  # 等同于 q.get_nowait()
# 默认block = True, 空了会阻塞等待
# print(q.get_nowait())

# print(q.get(block=True,timeout=3))
# 队列空了阻塞等待, 3秒后不再等待, 报错


q=queue.LifoQueue(3) #后进先出->堆栈, 吃了吐
q.put('first')
q.put(2)
q.put('third')

print(q.get())  # third
print(q.get())  # 2
print(q.get())  # first
# 其他用法相同


q=queue.PriorityQueue(3) #优先级队列

q.put((10,'one'))
q.put((40,'two'))
q.put((30,'three'))
# 放元祖或列表形式的数据, 第一个参数是优先级, 第二个参数是数据. 数字越小优先级越高.

print(q.get())  # one
print(q.get())  # three
print(q.get())  # two

2.25 多线程实现并发的套接字通信

# 服务端
import socket
from threading import Thread
def task(conn):
    while True:
        try:
            data = conn.recv(1024)
            print(data.decode('utf-8'))
            # msg = input('>>>')  # 多线程支持input功能
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()
def server(ip, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((ip,port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        t = Thread(target=task, args=(conn,))
        t.start()
    server.close()
if __name__ == '__main__':
    server('127.0.0.1', 8001)
# 客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1',8001))
while True:
    msg = input('>>>')
    if not msg:
        continue
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))
client.close()

注意: 以上方式存在很大的弊端, 即可以根据客户端数目启用线程, 如果客户端数目特别大, 则会导致启用的线程数特别多, 服务端会崩溃. 所以服务端这边要控制开启线程的数量.

2.26 进程池线程池

控制开启的线程数和进程数, 防止机器崩溃.

# 进程池
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import os, time, random
def task(name):
    print('name:%s pid:%s run' %(name,os.getpid()))
    time.sleep(random.randint(1,3))
if __name__ == '__main__':
    pool = ProcessPoolExecutor(4)
    for i in range(10):
        pool.submit(task,'egon%s'%i)
    pool.shutdown()  # 默认wait = True
    # 关闭池子, 防止再往池子中扔任务.
    print('')
'''
结果:
name:egon0 pid:177772 run
name:egon1 pid:171956 run
name:egon2 pid:175496 run
name:egon3 pid:171940 run
name:egon4 pid:171956 run
name:egon5 pid:171956 run
name:egon6 pid:177772 run
name:egon7 pid:175496 run
name:egon8 pid:171940 run
name:egon9 pid:177772 run
主

可以看出, 全程只有4个进程在处理任务
'''
    
# 线程池
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
from threading import currentThread
import os,time,random

def task():
    print('name:%s pid:%s run' %(currentThread().getName(),os.getpid()))
    time.sleep(random.randint(1,3))


if __name__ == '__main__':
    pool=ThreadPoolExecutor(5)

    for i in range(10):
        pool.submit(task,)

    pool.shutdown(wait=True)


    print('')
'''
name:ThreadPoolExecutor-0_0 pid:178624 run
name:ThreadPoolExecutor-0_1 pid:178624 run
name:ThreadPoolExecutor-0_2 pid:178624 run
name:ThreadPoolExecutor-0_3 pid:178624 run
name:ThreadPoolExecutor-0_4 pid:178624 run
name:ThreadPoolExecutor-0_0 pid:178624 run
name:ThreadPoolExecutor-0_4 pid:178624 run
name:ThreadPoolExecutor-0_2 pid:178624 run
name:ThreadPoolExecutor-0_0 pid:178624 run
name:ThreadPoolExecutor-0_1 pid:178624 run
主
'''

2.27 异步调用与回调机制

#提交任务的两种方式
#1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序是串行执行
#
# from concurrent.futures import ThreadPoolExecutor
# import time
# import random
#
# def la(name):
#     print('%s is laing' %name)
#     time.sleep(random.randint(3,5))
#     res=random.randint(7,13)*'#'
#     return {'name':name,'res':res}
#
# def weigh(shit):
#     name=shit['name']
#     size=len(shit['res'])
#     print('%s 拉了 《%s》kg' %(name,size))
#
#
# if __name__ == '__main__':
#     pool=ThreadPoolExecutor(13)
#
#     shit1=pool.submit(la,'alex').result()
#     weigh(shit1)
#
#     shit2=pool.submit(la,'wupeiqi').result()
#     weigh(shit2)
#
#     shit3=pool.submit(la,'yuanhao').result()
#     weigh(shit3)


#2、异步调用:提交完任务后,不地等待任务执行完毕,

from concurrent.futures import ThreadPoolExecutor
import time
import random

def la(name):
    print('%s is laing' %name)
    time.sleep(random.randint(3,5))
    res=random.randint(7,13)*'#'
    return {'name':name,'res':res}


def weigh(shit):
    shit=shit.result()
    name=shit['name']
    size=len(shit['res'])
    print('%s 拉了 《%s》kg' %(name,size))


if __name__ == '__main__':
    pool=ThreadPoolExecutor(13)

    pool.submit(la,'alex').add_done_callback(weigh)

    pool.submit(la,'wupeiqi').add_done_callback(weigh)

    pool.submit(la,'yuanhao').add_done_callback(weigh)

2.28 进程池线程池小练习

 

 

2.29 协程

协程: 单线程中实现并发, 需要我们从应用程序级别找到一种解决方案, 从一个任务切换到另一个任务, 切换之前保留状态.

在遇到计算时间过长或IO时需要进行切换,进程和线程都是由操作系统控制,而协程是由程序员自己控制。在协程中,修改共享数据不需要加锁。

greenlet和yield都无法检测IO然后再进行切换, greenlet比yield好些, 但是两者半斤八两. gevent模块封装了greenlet模块,并能够检测IO,且在遇到IO时自动切换。

# 协程自己理解
from
gevent import monkey,spawn;monkey.patch_all() import time def f1(): while True: print('1') time.sleep(1) print('2') def f(): while True: print('xxx') spawn(f1) time.sleep(2) # 如果上一行代码则会循环打印xxx yyy, 进入了死循环, 并不会执行f1中的代码 print('yyy') # f() # 结果同下 g = spawn(f) g.join() # 主线程中的代码遇到io后会切换执行gevent的任务, gevent的任务遇到io会切换执行主线程代码或gevent任务. ''' 结果: xxx 1 2 1 yyy xxx 1 2 1 2 1 2 1 yyy xxx 2 1 1 '''

 

# 协程自己理解
from gevent import monkey,spawn;monkey.patch_all()
import time
def f1():
    j = 0
    while j<1000:
        print('%s:1'%j)
        time.sleep(0.5)
        print('%s:2'%j)
        j+=1
def f():
    i = 0
    while i<100:
        time.sleep(1)
        spawn(f1)  # 只是提交, 并未执行. 提交后就接着往下执行.
        print('提交了任务%s'%i)
        i+=1
g = spawn(f)
g.join()

'''
提交了任务0         执行f
0:1               执行任务0下的f1 
0:2               执行任务0下的f1
1:1               执行任务0下的f1
提交了任务1         执行f
0:1               执行任务1下的f1
1:2               执行任务0下的f1
2:1               执行任务0下的f1
0:2               执行任务1下的f1
1:1               执行任务1下的f1
2:2               执行任务0下的f1
3:1               执行任务0下的f1
提交了任务2         执行f
0:1               执行任务2下的f1
1:2               执行任务1下的f1
2:1               执行任务1下的f1
3:2               执行任务0下的f1
4:1               执行任务0下的f1
0:2               执行任务2下的f1
1:1               执行任务2下的f1
2:2               执行任务1下的f1
3:1               执行任务1下的f1
4:2               执行任务0下的f1
5:1               执行任务0下的f1
提交了任务3         执行f

综上, 在同一线程的不同任务之间进行切换.
'''

 2.30 基于gevent模块实现套接字通信

#服务端
from gevent import monkey,spawn;monkey.patch_all()
from socket import *

def communicate(conn):
    while True:
        try:
            data=conn.recv(1024)
            if not data:break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

def server(ip,port):
    server = socket(AF_INET, SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)

    while True:
        conn, addr = server.accept()
        spawn(communicate,conn)

    server.close()

if __name__ == '__main__':
    server('127.0.0.1',8090)  # 结果同下
    # g=spawn(server,'127.0.0.1',8090)
    # g.join()
# 客户端
from socket import *
from threading import Thread,currentThread

def client():
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8090))


    while True:
        client.send(('%s hello' %currentThread().getName()).encode('utf-8'))
        data=client.recv(1024)
        print(data.decode('utf-8'))

    client.close()


if __name__ == '__main__':
    for i in range(500):
        t=Thread(target=client)
        t.start()

 2.31 IO模型介绍

协程是单线程下的并发, 并不是对所有的情形都有效. 通过gevent模块实现检测IO, 并自动进行切换.

gevent模型是如何监测IO模型并自动切换的.

两类: 同步, 异步. 提交任务的方式. 异步通常与回调机制连用.

同步不等于阻塞.

IO模型包括以下几种:

* blocking IO
* nonblocking IO
* IO multiplexing
* signal driven IO
* asynchronous IO
由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。
网络通信中的IO套接字有accept, recv, send.

  1)等待数据准备 (Waiting for the data to be ready)
  2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。


2.31.1 阻塞IO模型

  遇到阻塞不处理就等着, 解决方案: 开启多线程.

2.31.2 非阻塞IO模型(了解)

  自己检测IO, 在单线程下实现并发.

  缺点: 1.在操作系统干其他活的时候, 数据来了, 这就造成响应延迟.

    2. 没有任何阻塞, 线程一直处于一个就绪状态, 相当于一个死循环, 大量占用CPU, 不给别人使用CPU.

  综上, 不推荐使用.

2.31.3 多路复用IO模型(了解)

   基于select模块.

  优点: 可以同时监测多个套接字的IO行为. 性能高于阻塞IO和非阻塞IO.

  缺点: 挨着询问, 效率低.

  pol与select半斤八两, 真正效率高的是epol. windows不支持epol, linux支持epol.

  select模块会根据当前平台的不同选择最优的模式.

2.31.4 异步IO模型

  效率高于阻塞IO模型, 非阻塞IO模型和多路复用IO模型.

 

2.32 socketserver

python标准库中一款非常有名的服务器框架.

 

2.31.1 socketserver的使用模式

 

2.31.2 socketserver的源码解析

 

 

Python中多进程、多线程和协程的区别?
进程:一个运行的程序(代码)就是一个进程,没有运行的代码是程序。进程是系统资源分配的最小单位,进程拥有自己独立的内存空间,所以进程间数据不共享,开销大。
线程:调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在的,一个进程至少有一个线程,叫主线程,多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。
协程:一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存在其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈基本没有内核切换的开销。可以不加锁的访问全局变量,所以上下文切换非常快。
什么是多线程竞争?
线程是非独立的,同一进程里线程是数据共享的。当各个线程访问数据资源时会出现竞争状态,造成数据不安全问题。
怎么解决多线程竞争问题?
加锁。
好处:加锁会确保某段关键代码(共享数据资源)只能由一个线程执行,保证数据安全。
坏处:阻止多线程并发执行,降低效率,致命问题:死锁。
总结:
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源很大,效率很低
线程切换需要的资源一般,效率一般
协程切换任务资源很小,效率高
多进行、多线程根据cpu核数不同可能是并行的,但是协程是在一个线程中,是并发的。

 

posted @ 2020-08-04 19:13  自由者妍  阅读(216)  评论(0)    收藏  举报