python网络编程之并发编程

python网络编程之并发编程

1. 什么是进程

  1. 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

  2. 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

  3. 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

  4. 一个车间里,可以有很多工人。他们协同完成一个任务。

  5. 线程就好比车间里的工人。一个进程可以包括多个线程。

  6. 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

  7. 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

  8. 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

  9. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

  10. 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

    不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

  11. 操作系统的设计,因此可以归结为三点:

    (1)以多进程形式,允许多个任务同时运行;

    (2)以多线程形式,允许单个任务分成不同的部分运行;

    (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

以上内容来自阮一峰进程与线程的一个简单解释

简单来说:

程序:一堆代码
进程:正在运行的程序.

每个程序运行只少有一个进程。

1.2 进程调度算法

进程调度算法也称 CPU 调度算法。

当 CPU 空闲时,操作系统就选择内存中的某个「就绪状态」的进程,并给其分配 CPU。

image

什么时候会发生 CPU 调度呢?通常有以下情况:

  1. 当进程从运行状态转到等待状态;
  2. 当进程从运行状态转到就绪状态;
  3. 当进程从等待状态转到就绪状态;
  4. 当进程从运行状态转到终止状态;

其中发生在 1 和 4 两种情况下的调度称为「非抢占式调度」,2 和 3 两种情况下发生的调度称为「抢占式调度」。

状态说明

  • 创建态:对应于进程被创建时的状态,尚未进入就绪队列。

  • 执行(running)态:进程占有处理器正在运行的状态。

  • 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。

  • 阻塞(block)态:又称等待态,指进程不具备运行条件,正在等待某个时间完成的状态。

  • 终止态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。

引起进程状态转换的具体原因如下:

  • 执行态→阻塞态:等待使用资源;如等待外设传输;等待人工干预。

  • 阻塞态→就绪态:资源得到满足;如外设传输结束;人工干预完成。

  • 执行态→就绪态:运行时间片到;出现有更高优先权进程。

  • 就绪态→执行态:CPU 空闲时选择一个就绪进程。

2. 同步异步和阻塞非阻塞

2.1 同步与异步

image

  • 同步:提交完任务之后原地等待任务的返回结果 期间不做任何事
  • 异步:提交完任务之后不原地等待任务的返回结果 直接去做其他事 结果由反馈机制自动提醒

2.2 阻塞和非阻塞

image

  • 阻塞:简单来说就是发出一个请求不能立刻返回响应,要等所有的逻辑全处理完才能返回响应
  • 非阻塞: 发出一个请求立刻返回应答,不用等处理完所有逻辑

阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作。

2.3 四种组合方式

  • 同步阻塞方式:
    发送方发送请求之后一直等待响应。
    接收方处理请求时进行的IO操作如果不能马上等到返回结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。

  • 同步非阻塞方式:
    发送方发送请求之后,一直等待响应。
    接受方处理请求时进行的IO操作如果不能马上的得到结果,就立即返回,取做其他事情。
    但是由于没有得到请求处理结果,不响应发送方,发送方一直等待。
    当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方,发送方才进入下一次请求过程。(实际不应用)

  • 异步阻塞方式:
    发送方向接收方请求后,不等待响应,可以继续其他工作。
    接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。 (实际不应用)

  • 异步非阻塞方式:
    发送方向接收方请求后,不等待响应,可以继续其他工作。
    接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。
    当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。(效率最高)

3. 并发与并行

image

3.1 并发

image

并发:多个程序只要看起来像同时运行。

一个人用微信同时和他的两个女朋友聊天,对他的两个女朋友来说感觉只是和她自己在聊天。

并发时CPU就会在几个任务之间进行切换,并且每次切换的时候都会保存当前任务的状态,这个切换又叫CPU上下切换.

3.2 并行

image

并行:多个程序真正同时执行。

几个人去酒店,每个人都有一个服务员接待。

4. 使用Python创建进程

4.1 方法一

from multiprocessing import  Process
import time
import os

def test():
    print("pid:%s is running" % os.getpid())
    time.sleep(1)
    print("pid:%s is end" % os.getpid())


if __name__ == '__main__':
    p = Process(target=test)
    p.start()
    print("主进程:pid:%s is running" % os.getpid())

# 执行结果:
    主进程:pid:4224 is running
    pid:9296 is running
    pid:9296 is end

在windows中开设进程类似于导入模块

从上往下再次执行代码
一定需要在__main__判断语句内执行开设进程的代码

在linux中是直接将代码完整的复制一份执行

不需要在__main__判断语句内执行

4.2 方法二

from multiprocessing import  Process
import time
import os

class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name = name
    def run(self):
        print("Hi, %s. pid:%s is running" % (self.name, os.getpid()))
        time.sleep(1)
        print("pid:%s is end" % os.getpid())


if __name__ == '__main__':
    p = MyProcess('Hans')
    p.start()
    print("主进程:pid:%s is running" % os.getpid())
    
执行结果:
    主进程:pid:5924 is running
    Hi, Hans. pid:6828 is running
    pid:6828 is end

4.3 等创建的子进程完成后再执行主进程

上面的例子都是主进程先执行,然后再执行子进程。可以使用jion主进程会等子进程全部执行完毕再继续执行

from multiprocessing import  Process
import time
import os

def test():
    print("子进程: pid:%s is running" % os.getpid())
    time.sleep(1)
    print("子进程: pid:%s is end" % os.getpid())


if __name__ == '__main__':
    p = Process(target=test)
    p.start()
    p.join()  # 等子进程全部执行完
    print("主进程:pid:%s is running" % os.getpid())

执行结果:
    子进程: pid:13304 is running
    子进程: pid:13304 is end
    主进程:pid:10652 is running

4.4 常见对象方法

os.getpid() 查看进程号  
os.getppid() 查看父进程进程号
current_process().name 查看进程名
current_process().pid  查看进程pid
p.terminate() 杀死子进程
p.is_alive()  判断进程是否存活
terminate和is_alive结合看不出结果,因为操作系统需要反应时间。效果可以使用time.sleep(0.1)能演示出来

4.5 进程间通信

4.5.1 进程间默认无法通信

进程间数据是相互隔离的

from multiprocessing import  Process
import time
import os


num = 10

def change():
    global  num
    num = 9

if __name__ == '__main__':
    p = Process(target=change)
    p.start()
    p.join()

    print(num)
    
# 执行结果:
10

要想让进程间通信最简单的方法是:管道

常用的方法有三种:消息队列,共享内存和信号量

4.5.2 通过队列使进程间通信

image

队列的使用就可以打破进程间默认无法通信的情况。

队列:先进先出。

from multiprocessing import Queue

q = Queue(5)  # 队列里放五个数据,如果不写则使用默认,默认值为: 2147483647

# 使用put向队列里存数据
q.put("A")   
q.put("B")
q.put("C")
q.put("D")
q.put("E")
q.put("F")   #当队列满了后,再向里面放数据程序就会阻塞,一直到有位置让出来。

# 执行结果:
# 程序会阻塞,表现形式就是卡在那里

# 使用get向队列里取数据
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# 执行结果:
A
B
C
D
E

# 使用Put向队列里存数据的时候,如果超过队列的容量(上面例子是5)则程序会阻塞。
# 使用get从队列里取数据的时候,如果也超过了队列的容量(上面例子是5),例如取6次,则程序也会阻塞

如果在取值时不想让程序阻塞,可以使用Queue里面的一些方法。

get_nowait():不等待

from multiprocessing import Queue

q = Queue(5) 

# 使用put向队列里存数据
q.put("A")   
q.put("B")
q.put("C")
q.put("D")
q.put("E")

# 使用get向队列里取数据
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get_nowait())
# 执行结果:
A
B
C
D
E
Traceback (most recent call last):
  File "D:\pycharm\codestudy\day31\process_queue.py", line 18, in <module>
    print(q.get(timeout=2))
  File "D:\Program Files\python\lib\multiprocessing\queues.py", line 114, in get
    raise Empty
_queue.Empty

# get_nowait()不等待,但会报错,

还可以使用q.get(timeout=2) 如果两秒没有取到值则也会报_queue.Empty错误

使用try捕捉异常并处理:

from multiprocessing import Queue

q = Queue(5) 

# 使用put向队列里存数据
q.put("A")   
q.put("B")
q.put("C")
q.put("D")
q.put("E")

# 使用get向队列里取数据
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
try:
    print(q.get_nowait())
    # print(q.get(timeout=2))
except Exception as e:
    print("已经取完")

#执行结果:
A
B
C
D
E
已经取完

full():判断队列是否满了

from multiprocessing import Queue

q = Queue(5) 

# 使用put向队列里存数据

q.put("A")
q.put("B")
print(q.full())
q.put("C")
q.put("D")
q.put("E")
print(q.full())

# 执行结果:
False   # 没有满返回false
True	# 已经满返回 True

empty() 判断队列是否为空。

from multiprocessing import Queue

q = Queue(5) 

# 使用put向队列里存数据

q.put("A")
q.put("B")
q.put("C")
q.put("D")
q.put("E")

print(q.get())
print(q.get())
print(q.empty())
print(q.get())
print(q.get())
print(q.get())
print(q.empty())

# 执行结果:
A
B
False  # 判断是否已空,没有空为false
C
D
E
True	# 判断是否已空,空为true

使用上面的方法使存取数据不会阻塞和报错:

from multiprocessing import Queue

q = Queue(5)

count = 1

# 把队列填充满
while True:
    if not q.full(): 
        q.put(count)
        count+=1
    else:
        break

# 把队列中的数据全部取走
while True:
    if not q.empty():
        print(q.get())
    else:
        break

注意

在多进程下这三个对象得出来的结果是不精确的:

full()

empty()

get_nowait()

4.5.3 使用IPC进行进程间通信

1. 主进程和子进程通信

from multiprocessing import Queue,Process
import os

def producer(q):
    q.put("我是子进程,进程号是:%s" % os.getpid())

if __name__ == '__main__':
    q = Queue(5)
    p = Process(target=producer, args=(q,))
    p.start()
    print(q.get())  # 在主进程中从队列中取数据
    
# 执行结果:
我是子进程,进程号是:23084

2. 子进程和子进程通信

from multiprocessing import Queue,Process
import os

#子进程向队列中存数据
def producer(q):
    q.put("我是子进程,进程号是:%s" % os.getpid())

#另一个子进程中从队列中取数据
def customer(q):
    print(q.get())

if __name__ == '__main__':
    q = Queue(5)
    p = Process(target=producer, args=(q,))
    p1 = Process(target=customer, args=(q,))
    p.start()
    p1.start()

# 执行结果:
我是子进程,进程号是:25556

4.6 生产者和消费者模型

生产者:生产/制造东西的

消费者:消费/处理东西的

此外除了生产者和消费者之外还需要一个媒介。

from multiprocessing import JoinableQueue,Process
import time
import random

def producer(q,name, food):
    for i in range(1,10):
        q.put("%s做的第%s个%s"%(name,i,food))
        time.sleep(random.random())
        print("%s做的第%s个%s"%(name,i,food))

def customer(q,name):
    while True:
        data = q.get()
        print("%s吃了%s"%(name, data))
        time.sleep(random.random())
        q.task_done()  # 告诉队列已经从里面取出了一个数据,并且处理完毕了。

if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=(q,"A", "Dumpling"))
    p2 = Process(target=producer, args=(q,"B", "Beef"))
    c1 = Process(target=customer, args=(q,"C"))
    c2 = Process(target=customer, args=(q,"D"))
    p1.start()
    p2.start()

    c1.daemon=True  # 消费者设置为守护进程,主进程结束消费者进程也跟着结束了
    c2.daemon=True
    c1.start()
    c2.start()
    p1.join()
    p2.join()
    q.join()  # 等待队列中所有的数据被取完,再向下执行
    
    """
    JoinableQueue() 每当向队列里存入数据的时候,内部会有一个计数器加一。
    task_done()  每调用一次 内部的计数器减一。
    join() 当计数器为0时,才往后运行
    """
    
# 执行结果:
A做的第1个Dumpling
D吃了A做的第2个Dumpling
A做的第2个Dumpling
D吃了A做的第3个Dumpling
B做的第1个Beef
C吃了B做的第2个Beef
A做的第3个Dumpling
C吃了A做的第4个Dumpling
B做的第2个Beef
D吃了B做的第3个Beef
...
A做的第9个Dumpling
D吃了B做的第7个Beef
C吃了B做的第8个Beef
C吃了B做的第9个Beef
D吃了A做的第9个Dumpling

5. 僵尸进程和孤儿进程

  • 僵尸进程
    进程代码运行结束之后并没有直接结束而是需要等待回收子进程资源才能结束

  • 孤儿进程
    即主进程已经死亡(非正常)但是子进程还在运行

6. 守护进程

守护进程:即守护着某个进程,一旦这个进程结束那么也随之结束

from multiprocessing import  Process,current_process
import time
import os


def daemon(name):
    print('PID: %s is running' % os.getpid())
    print("Hi %s, this is a daemon process" % name)
    time.sleep(3)
    print('PID: %s is end' % os.getpid())


if __name__ == '__main__':
    p = Process(target=daemon, args=("Hans",))
    p.daemon=True  # 设置这个为守护进程,并且这个一定要放在start()上面
    p.start()
    print("主进程pid:%s" % os.getpid())
    time.sleep(0.1)

7. 进程互斥锁

并发情况下操作同一份数据,极其容易造成数据错乱

示例模拟买票

# data.txt:
{"train_ticket_num": 1}

# 代码:
import json
from multiprocessing import  Process
import time
import random

#查询还有多少张票
def select(name):
    with open(b"data.txt", 'r',encoding='utf8') as f:
        data = json.load(f)
        print("用户:%s 正在查询,剩于%s张" % (name,data.get('train_ticket_num')))

# 买票:
def buy_ticket(name):
    with open(b"data.txt", 'r',encoding='utf8') as f:
        data = json.load(f)
        ticket_num = data.get('train_ticket_num')

    time.sleep(random.random())  # 随机sleep,模拟买票延迟

    if ticket_num >0:   #判断票数是否大于0
        data['train_ticket_num'] -= 1
        with open(b"data.txt", 'w', encoding='utf8') as f:
            json.dump(data,f)
        print("用户:%s 买票成功,剩于:%s" % (name, data.get('train_ticket_num')))
    else:
        print("余票不足")

def run(name):
    select(name)
    buy_ticket(name)


if __name__ == '__main__':
    for i in range(1,6):
        p = Process(target=run, args=(i,))
        p.start()
        
        
# 执行结果:
用户:1 正在查询,剩于1张
用户:2 正在查询,剩于1张
用户:3 正在查询,剩于1张
用户:5 正在查询,剩于1张
用户:4 正在查询,剩于1张
用户:1 买票成功,剩于:0
用户:5 买票成功,剩于:0
用户:3 买票成功,剩于:0
用户:2 买票成功,剩于:0
用户:4 买票成功,剩于:0
        
# 显示都购买成功,显然是不对,因为只有一张票:{"train_ticket_num": 1}

解决措施:将并发变成串行,虽然降低了效率但是提升了数据的安全。使用互斥锁,把买票的操作变成串行。

import json
from multiprocessing import  Process,Lock
import time
import random

def select(name):
    with open(b"data.txt", 'r',encoding='utf8') as f:
        data = json.load(f)
        print("用户:%s 正在查询,剩于%s张" % (name,data.get('train_ticket_num')))

def buy_ticket(name):
    with open(b"data.txt", 'r',encoding='utf8') as f:
        data = json.load(f)
        ticket_num = data.get('train_ticket_num')

    time.sleep(random.random())

    if ticket_num >0:
        data['train_ticket_num'] -= 1
        with open(b"data.txt", 'w', encoding='utf8') as f:
            json.dump(data,f)
        print("用户:%s 买票成功,剩于:%s" % (name, data.get('train_ticket_num')))
    else:
        print("余票不足")

def run(name, mutex):
    select(name)

    mutex.acquire() # 抢锁
    buy_ticket(name)
    mutex.release() # 释放锁


if __name__ == '__main__':
    mutex = Lock()  # 在主进程中产生,在子进程中使用
    for i in range(1,6):
        p = Process(target=run, args=(i,mutex))
        p.start()

# 执行结果
用户:1 正在查询,剩于1张
用户:2 正在查询,剩于1张
用户:3 正在查询,剩于1张
用户:4 正在查询,剩于1张
用户:5 正在查询,剩于1张
用户:1 买票成功,剩于:0
余票不足
余票不足
余票不足
余票不足

使用锁的注意事项:
在主进程中产生 交由子进程使用
1.一定要在需要的地方加锁 千万不要随意加
2.不要轻易的使用锁(死锁现象)

8. 线程

8.1 什么是线程

进程是资源单位(启动一个进程其实是在内存空间中开辟一块独立的空间)

线程是执行单位(真正被CPU执行的)

一个进程里面一定有一个线程,真正被CPU执行的其实是进程里面的线程。

进程和线程都是虚拟单位。只是为了方便我们描述问题

8.2 为什么要有线程

启动一个进程,要单独开辟内存空间,要拷贝代码,这套流程下来比较消耗资源。

但是启动一个线程的开销要比启动一个进程要小的多,一个进程内可以启动多个线程,在一个进程内启动多个线程不需要再次申请内存空间有拷贝代码的操作,而且同一个进程下的线程之间数据是共享的。

8.3 如何开启线程

方法1

import os
from threading import Thread
import time

def task(pid):
    print("%s is running" % pid)
    time.sleep(1)
    print("%s is end" % pid)


if __name__ == '__main__':
    """
    开启线程不需要在__name__ == '__main__'下执行,但是习惯把启动命令放在main下面
    """
    t = Thread(target=task, args=(os.getpid(),))
    t.start()

    print("\n主进程:%s" % os.getpid())
    
# 执行结果
44632 is running
主进程:44632

44632 is end

方法二

import os
from threading import Thread
import time

class MyThread(Thread):

    def __init__(self,pid):
        super().__init__()
        self.pid = os.getpid()

    def run(self):
        print("%s is running" % self.pid)
        time.sleep(1)
        print("%s is end" % self.pid)

if __name__ == '__main__':
    t = MyThread(os.getpid())
    t.start()
    print("主进程:%s" % os.getpid())
# 执行结果:
43020 is running
主进程:43020
43020 is end

8.4 线程的join方法

上面的写法,子线程在执行的时候主线程已经结束了,现在要主线程等子线程结束后再结束,可以使用join方法

import os
from threading import Thread
import time

def task(pid):
    print("%s is running" % pid)
    time.sleep(2)
    print("%s is stop" % pid)

if __name__ == '__main__':
    t = Thread(target=task, args=(os.getpid(),))
    t.start()
    t.join()
    print('主')
# 执行结果:
31992 is running
31992 is stop
主

8.5 同一进程下的多个线程数据是共享的。

验证:

from threading import Thread

number = 10

def data():
    global number
    number = 20
    print(number)

if __name__ == '__main__':
    t = Thread(target=data, args=())
    t.start()
    t.join()
    print(number)
#执行结果:
20
20

8.6 线程对象的其他方法

active_count统计当前正在活跃的线程数
current_thread

#代码:
import os
from threading import Thread, active_count,current_thread

def data():
    print(os.getpid())
    print(current_thread().name) #当前线程名字


if __name__ == '__main__':
    t = Thread(target=data, args=())
    t.start()
    print('主',os.getpid(),active_count())
    //统计当前正在活跃的线程数

# 执行结果:
22876主 22876 2

Thread-1 (data)

8.7 守护线程

主线程运行结束之后不会立刻结束,会等待所有其他非守护线程结束后才结束。因为主线程结束就意味着所在的进程结束。

示例:

import os
from threading import Thread
import time

def task(pid):
    print("%s is running" % pid)
    time.sleep(2)
    print("%s is stop" % pid)
    
if __name__ == '__main__':
    t = Thread(target=task, args=(os.getpid(),))
    t.start()
    print('主',os.getpid())
# 执行结果:
54704 is running
主 54704
54704 is stop
// 主进程结束后,会等着子进程运行结束。

设置为守护线程

def task(pid):
    print("%s is running" % pid)
    time.sleep(2)
    print("%s is stop" % pid)
    
if __name__ == '__main__':
    t = Thread(target=task, args=(os.getpid(),))
    t.daemon = True //设置为守护线程
    t.start()
    print('主',os.getpid())
# 执行结果:
57764 is running
主 57764

8.8 线程互斥锁

对一个数字开一百个线程进行减一操作。

from threading import Thread
import time

number = 100

def foo():
    global number
    temp = number
    time.sleep(0.1)
    number = temp - 1

if __name__ == '__main__':
    t_list = []
    for i in range(100):
        t = Thread(target=foo)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(number)
# 执行结果
99

多人操作一份数据,造成数据错乱,加锁处理。

from threading import Thread,Lock
import time

number = 100

def foo(mutex):
    global number
    mutex.acquire()
    temp = number
    time.sleep(0.1)
    number = temp - 1
    mutex.release()

if __name__ == '__main__':
    t_list = []
    mutex = Lock()
    for i in range(100):
        t = Thread(target=foo,args=(mutex,))
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(number)
    
# 执行结果
0

也可以使用with加锁和释放锁

rom threading import Thread,Lock
import time

number = 100

def foo(mutex):
    global number
    with mutex:  # 使用with可以自动加锁和释放锁
        temp = number
        time.sleep(0.1)
        number = temp - 1

if __name__ == '__main__':
    t_list = []
    mutex = Lock()
    for i in range(100):
        t = Thread(target=foo,args=(mutex,))
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(number)
    
# 执行结果
0

8.9 使用多进程或多线程实现并发

8.9.1 多进程

# 服务端:
import socket
from multiprocessing import Process

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(('127.0.0.1',8001))
server.listen(5)


def talk(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            print(data.decode('utf-8'))
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break
    conn.close()

while True:
    conn, addr = server.accept()
    p = Process(target=talk, args=(conn,))
    p.start()

#客户端
import socket

client = socket.socket()
client.connect(('127.0.0.1',8001))

while True:
    client.send(b'hello')
    data = client.recv(1024)
    print(data.decode('utf-8'))

8.9.2 多线程

# 多线程
import socket
from threading import Thread

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(('127.0.0.1',8001))
server.listen(5)


def talk(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            print(data.decode('utf-8'))
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break
    conn.close()

while True:
    conn, addr = server.accept()
    t = Thread(target=talk, args=(conn,))
    t.start()

# 客户端
import socket

client = socket.socket()
client.connect(('127.0.0.1',8001))

while True:
    client.send(b'hello')
    data = client.recv(1024)
    print(data.decode('utf-8'))

9. GIL全局解释器锁

Cpython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行。也就是说在同一个进程下多个线程是无法利用多核的。因为Cpython中的内存管理不是线程安全的。

重点

  • GIL不是Python的特点是CPython解释器的特点
  • GIL是保证解释器级别的数据安全
  • GIL会导致同一个进程下的多个线程无法同时运行
  • 针对不同的数据还是需要加不同的锁处理
  • GIL是所有解释型语言的通病

GIL释放有两种方式:

  1. 进入到IO操作前会主动释放GIL
  2. 解释器不间断运行了1000字节码(Python2)或运行15毫秒(Python3)后,该线程也会释放GIL

9.1 GIL导致同一进程下多线程没有多核优势,是否开多进程就没有用?

多线程是否有用要具体问题具体分析,主要看执行的任务是IO密集型还是计算密集型

计算密集型:

不考虑单核情况,因为单核下干活的就那一个CPU,不过在单核下多进程会导致额外的消耗资源,反而多线程节省开销。

多核下利用多进程执行计算密集型的速度要比多线程的要快

IO密集型:

因为有大量的IO操作,使用多进程相对会消耗资源

多线程在IO密集型任务下会节省资源

计算密集型多进程和多进程比较:

# 多进程
import time
from multiprocessing import Process
def foo():
    count = 0
    for i in range(10000000):
        count *= i

if __name__ == '__main__':
    p_list = []
    start_time = time.time()
    for i in range(4):
        p = Process(target=foo)  # 1.9767425060272217
        p.start()
        p_list.append(p)


    for p in p_list:
        p.join()
    print(time.time()-start_time)
# 执行结果:
1.9767425060272217

# 多线程
import time
from threading import Thread
def foo():
    count = 0
    for i in range(10000000):
        count *= i

if __name__ == '__main__':
    t_list = []
    start_time = time.time()
    for i in range(4):
        t = Thread(target=foo)
        t.start()
        t_list.append(t)


    for t in t_list:
        t.join()
    print(time.time()-start_time)
# 执行结果:
3.2588119506835938

# 结论:
在计算密集型任务下多进程效率高

IO密集型多进程和多进程比较

# 多进程
import time
from multiprocessing import Process

def foo():
    time.sleep(2)

if __name__ == '__main__':
    p_list = []
    start_time = time.time()
    for i in range(400):
        p = Process(target=foo)  
        p.start()
        p_list.append(p)


    for p in p_list:
        p.join()
    print(time.time()-start_time)
# 执行结果:
26.391981601715088

# 多线程
import time
from threading import Thread
def foo():
    time.sleep(2)

if __name__ == '__main__':
    t_list = []
    start_time = time.time()
    for i in range(4):
        t = Thread(target=foo)
        t.start()
        t_list.append(t)


    for t in t_list:
        t.join()
    print(time.time()-start_time)
# 执行结果:
2.1058895587921143

#结论:
在IO密集型任务下多线程效率高

总结

  • 计算密集型任务下多进程效率高
  • IO密集型任务下多线程效率高
  • 多进程和多线程在不同的场景下都各有优势。
  • 可以多进程下再开多线程

10. 死锁与递归锁

10.1 死锁

锁在使用的时候要抢锁,使用完后要释放锁。但在使用锁的时候也会造成死锁

from threading import Thread, Lock

mutex1 = Lock()
mutex2 = Lock()

class Mythead(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutex1.acquire()
        print("%s 抢到锁1"% self.name)
        mutex2.acquire()
        print("%s 抢到锁2"% self.name)
        mutex2.release()
        mutex1.release()

    def  func2(self):
        mutex2.acquire()
        print("%s 抢到锁2" % self.name)
        time.sleep(1)
        mutex1.acquire()
        print("%s 抢到锁2" % self.name)
        mutex1.release()
        mutex2.release()

if __name__ == '__main__':
    for i in range(10):
        t = Mythead()
        t.start()
        
# 执行结果:
Thread-1 抢到锁1
Thread-1 抢到锁2
Thread-1 抢到锁2
Thread-2 抢到锁1
...
然后一直卡在这里

10.2 递归锁

递归锁:可以被连续的acquirerelease,但是只能被第一个抢到锁的连接acquirerelease,它的内部有一个计数器,每acquire一次计数加一,每release一次计数减一,只要计数不为0,那么其他人都无法抢到这个锁。

上面产生死锁的例子改成递归锁就不会阻塞。

from threading import Thread, RLock

mutex1 = mutex2 = RLock()

class Mythead(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutex1.acquire()
        print("%s 抢到锁1"% self.name)
        mutex2.acquire()
        print("%s 抢到锁2"% self.name)
        mutex2.release()
        mutex1.release()

    def  func2(self):
        mutex2.acquire()
        print("%s 抢到锁2" % self.name)
        time.sleep(1)
        mutex1.acquire()
        print("%s 抢到锁2" % self.name)
        mutex1.release()
        mutex2.release()

if __name__ == '__main__':
    for i in range(10):
        t = Mythead()
        t.start()
# 执行结果:
Thread-1 抢到锁1
Thread-1 抢到锁2
Thread-1 抢到锁2
Thread-1 抢到锁2
Thread-2 抢到锁1
Thread-2 抢到锁2
Thread-2 抢到锁2
Thread-2 抢到锁2
Thread-4 抢到锁1
Thread-4 抢到锁2
Thread-4 抢到锁2
Thread-4 抢到锁2
Thread-6 抢到锁1
Thread-6 抢到锁2
Thread-6 抢到锁2
Thread-6 抢到锁2
Thread-8 抢到锁1
Thread-8 抢到锁2
Thread-8 抢到锁2
Thread-8 抢到锁2
Thread-10 抢到锁1
Thread-10 抢到锁2
Thread-10 抢到锁2
Thread-10 抢到锁2
Thread-5 抢到锁1
Thread-5 抢到锁2
Thread-5 抢到锁2
Thread-5 抢到锁2
Thread-9 抢到锁1
Thread-9 抢到锁2
Thread-9 抢到锁2
Thread-9 抢到锁2
Thread-7 抢到锁1
Thread-7 抢到锁2
Thread-7 抢到锁2
Thread-7 抢到锁2
Thread-3 抢到锁1
Thread-3 抢到锁2
Thread-3 抢到锁2
Thread-3 抢到锁2

11. 信号量

信号量在不同的阶段可能对应不同的技术。在并发编程中信号量指的是锁。

from threading import Thread, Semaphore
import time
import random

s = Semaphore(5)

def foo(name):
    s.acquire()
    print("%s 正在执行" % name)
    time.sleep(random.randint(1,4))
    s.release()

if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=foo, args=('线程%s' % i,))
        t.start()
        
# 执行结果:
线程0 正在执行
线程1 正在执行
线程2 正在执行
线程3 正在执行
线程4 正在执行
线程5 正在执行
线程6 正在执行
......
线程17 正在执行
线程18 正在执行
线程19 正在执行

12. Event事件

一些进程或线程需要等待另外一些进程/线程运行完毕之后才能运行。

from threading import Thread, Event
import time
event = Event()

def food():
    print("厨师正在做菜")
    time.sleep(2)
    print("菜做好了")
    event.set() # 菜做好了告诉服务员

def waiter(name):
    print("%s号服务员正等着上菜" % name)
    event.wait() # 等厨房发端菜的信号
    print("%s号服务员去上菜" % name)

if __name__ == '__main__':
    t  = Thread(target=food)
    t.start()

    for i in range(10):
        t = Thread(target=waiter, args=('%s' % i,))
        t.start()
        
# 执行结果:
厨师正在做菜
0号服务员正等着上菜
1号服务员正等着上菜
2号服务员正等着上菜
3号服务员正等着上菜
4号服务员正等着上菜
5号服务员正等着上菜
6号服务员正等着上菜
7号服务员正等着上菜
8号服务员正等着上菜
9号服务员正等着上菜
菜做好了
0号服务员去上菜
1号服务员去上菜
4号服务员去上菜
7号服务员去上菜
5号服务员去上菜
3号服务员去上菜
2号服务员去上菜
8号服务员去上菜
6号服务员去上菜9号服务员去上菜

13. 线程队列

13.1 先进先出

import queue
q = queue.Queue(3)
q .put(1)
q .put(2)
q .put(3)
print(q.get())
print(q.get())
print(q.get())
# 执行结果:
1
2
3

13.2 后进先出

import queue
q = queue.LifoQueue()
q .put(1)
q .put(2)
print(q.get())
print(q.get())
# 执行结果:
2
1

13.3优先级队列

import queue
q = queue.PriorityQueue(3)
q.put((10,'A'))
q.put((1,'B'))
q.put((20,'C'))
print(q.get())
print(q.get())
print(q.get())
# 执行结果:
(1, 'B')
(10, 'A')
(20, 'C')

# 数字越小优先级越高

14. 进程池与线程池

开设多进程或多线程处理客户端通信

from threading import Thread
import time
import socket
def communication(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break

    conn.close()


def Server(ip, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        t = Thread(target=communication,args=(conn,))
        t.start()


if __name__ == '__main__':
    ip = '127.0.0.1'
    port = 8000
	s = Thread(target=server,args=(ip,port))
    s.start()

问题:如何开设的进程或线程太多,计算机承受不住。

什么是池:

池是用来保证计算机硬件安全的情况下最大限度 的利用计算机。它降低了程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序能够正常运行。

向池子里提交了任务后,会自动执行任务。

任务提交的方式:

  1. 同步: 提交任务后原地等待结果,期间什么也不做
  2. 异步:提交任务后不等待返回结果,继续向下执行。

14.1 开启线程池

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor(5)
# 括号内可以传数字,不传的话默认会开当前计算机CPU个数乘以5的线程
"""
池子造出来之后,里面会固定存在五个线程
这五个线程不会出现重复创建和销毁的过程
"""

def task(n):
    print(n)
    time.sleep(1)
    return n*2

# 向线程池里提交1个任务
pool.submit(task,1)
print("main")
# 执行结果:
1
main
# pool方法是异步提交

#向线程池里提交20个任务:
for i in range(20):
    pool.submit(task,i)
# 执行结果:
0
1
2
3
4

5
6
7
8
9

10
11
12
13
14

15
16
17
18
19
# 线程池里有只有五个线程,所以打印的时候输出为五个一组

pool.submit()会返回一个Future对象。此对象里有result方法。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor(5)

def task(n):
    print(n)
    time.sleep(1)
    return n*2

for i in range(20):
    res = pool.submit(task,i)
    print(res.result())
    # res.result拿到的是异步提交的任务返回的结果
# 执行结果:
0
0
1
2
2
4
3
6
4
8
......
18
36
19
38
# 然后发现使用result方法后变成了串行。
# 是否可以再变成并行
# 可以使用像join似的方法,把对象放到一个列表中,然后再依次调用result方法

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor(5)

def task(n):
    print(n)
    time.sleep(1)
    return n*2

r_list = []
for i in range(20):
    res = pool.submit(task,i)
    r_list.append(res)

for r in r_list:
    print(">>>:",r.result())
# 执行结果:
0
1
2
3
4
5
6
>>>: 0
>>>: 2
7
8
>>>: 4
>>>: 6
9
>>>:8
10
11
>>>: 10
>>>: 12
12
13
>>>: 14
>>>: 16
14
>>>: 18
15
16
>>>: 20
>>>: 22
17
18
>>>: 24
>>>: 26
19
>>>: 28
>>>: 30
>>>: 32
>>>: 34
>>>: 36
>>>: 38

# 已经变成了并发。
# 是否可以先把任务执行完,然后再打印返回结果
# 只需要pool.shutdown()即可,它就会等待任务执行完,shutdown为关闭线程池。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
pool = ThreadPoolExecutor(5)

def task(n):
    print(n)
    time.sleep(1)
    return n*2

r_list = []
for i in range(20):
    res = pool.submit(task,i)
    r_list.append(res)
pool.shutdown()
for r in r_list:
    print(">>>:",r.result())
# 执行结果:
0
1
2
3
4

5
6
7
8
9

10
11
12
13
14

15
16
17
18
19
>>>: 0
>>>: 2
>>>: 4
>>>: 6
>>>: 8
>>>: 10
>>>: 12
>>>: 14
>>>: 16
>>>: 18
>>>: 20
>>>: 22
>>>: 24
>>>: 26
>>>: 28
>>>: 30
>>>: 32
>>>: 34
>>>: 36
>>>: 38

14.2 开启进程池

开启进程池和开线程池的方法相似

from concurrent.futures import ProcessPoolExecutor
import time
pool = ProcessPoolExecutor(5)
# 括号内可以传数字,不传的话默认会开启当前计算机CPU个数的进程
"""
池子造出来之后,里面会固定存在五个进程
这五个进程不会出现重复创建和销毁的过程
"""
def task(n):
    print(n,os.getpid())
    time.sleep(1)
    return n*2

if __name__ == '__main__':
    for i in range(20):
        pool.submit(task,i)
# 执行结果:
0 63076
1 64556
2 46280
3 51928
4 55008

5 63076
6 64556
7 46280
8 51928
9 55008

10 63076
11 64556
12 46280
13 51928
14 55008

15 63076
16 64556
17 46280
18 51928
19 55008
# 打印结果可以看出,进程号一直是这五个:63076,64556,46280,51928,55008

异步提交任务是不等结果继续执行,那么返回的结果怎么接受,在线程池示例中使用的列表,但异步提交使用回调机制接受返回值。

from concurrent.futures import ProcessPoolExecutor
import time
import os
pool = ProcessPoolExecutor(5)

def task(n):
    print(n,os.getpid())
    time.sleep(1)
    return n*2

def callback(n):
    print("call_back>>>:",n.result())
    
if __name__ == '__main__':
    for i in range(20):
        pool.submit(task,i).add_done_callback(callback)

# 执行结果:
0 66700
1 48560
2 61616
3 66116
4 67092
5 66700
call_back>>>: 0
6 48560
call_back>>>: 2
7 61616
call_back>>>: 4
8 66116
call_back>>>: 6
9 67092
call_back>>>: 8
10 66700
call_back>>>: 10
11 48560
call_back>>>: 12
12 61616
call_back>>>: 14
13 66116
call_back>>>: 16
14 67092
call_back>>>: 18
15 66700
call_back>>>: 20
16 48560
call_back>>>: 22
17 61616
call_back>>>: 24
18 66116
call_back>>>: 26
19 67092
call_back>>>: 28
call_back>>>: 30
call_back>>>: 32
call_back>>>: 34
call_back>>>: 36
call_back>>>: 38

15. 协程

15.1 协程概念

协程这个概念是程序员自己提出的,在计算机中根本不存在。在计算机中只认识进程和线程,进程是资源单位,线程是执行单位。

协程主要是为了在单线程下实现并发。程序员在代码层面上检测到有IO操作时,就在代码级别完成切换,这样就给CPU一种这个程序一直在运行,没有IO操作,从而提升程序的运行效率。

示例:

"""
gevent模块需要单独安装:
pip install gevent

gevent模块本身无法检测常见的一些IO操作,在使用的时候要额外的导入monkey模块,导入后才能检测常见的IO操作。
from gevent import monkey
monkey.patch_all()
上面两行必须要写。然后再导入要用到的spawn模块
"""

from gevent import monkey
monkey.patch_all()

from gevent import spawn
import time

def foo():
    print("foo")
    time.sleep(2)
    print("foo")

def boo():
    print("boo")
    time.sleep(3)
    print("boo")

start_time = time.time()
g1 = spawn(foo)
g2 = spawn(boo)
g1.join()
g2.join()
print(time.time() - start_time)
# 执行结果:
foo
boo
foo
boo
3.0437121391296387


15.2 利用协程实现TCP并发

服务端

from gevent import monkey
monkey.patch_all()
from gevent import spawn
import socket

def communication(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break

    conn.close()


def Server(ip, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        spawn(communication, conn)


if __name__ == '__main__':
    ip = '127.0.0.1'
    port = 8000
    g1 = spawn(Server,ip, port)
    g1.join()

客户端

import socket
from threading import Thread, current_thread


def mut_client(ip, port):
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect((ip,port))

    count = 1
    while True:
        msg = ("%s say: %s" % (current_thread().name,count))
        count +=1
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print(data.decode("utf-8"))

if __name__ == '__main__':
    ip = '127.0.0.1'
    port = 8000
    for i in range(100):
        t = Thread(target=mut_client,args=(ip,port))
        t.start()

总结:

多进程下开多线程

多线程下再开协程

posted on 2022-02-13 21:36  Hans_Wang  阅读(180)  评论(0编辑  收藏  举报

回到顶部