进程、线程补充知识

GIL(全局解释器锁)与普通互斥锁

GIL存在的验证

      from threading import Thread, Lock
      money = 100
      def task():
          global money
          money -= 1
      for i in range(100):
          t = Thread(target=task)
          t.start()
      print(money)  # 0

同时创建很多100个线程,并且在每个线程里执行减1的操作,最终结果是0,这里就验证了,对于同一进程的多线程的执行是有先后顺序的,即有锁存在

不加锁情况下对数据进行操作

money = 100
def task():
    global money
    temp = money
    time.sleep(0.1)  # IO操作换线程
    money = temp - 1
t_list = []
for i in range(100):
    t = Thread(target=task)
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
print(money)  # 99

对不同数据加锁

mutex = Lock()
money = 100
def task():
    global money
    mutex.acquire()
    temp = money
    time.sleep(0.1)  # IO操作换锁
    money = temp - 1
    mutex.release()
t_list = []
for i in range(100):
    t = Thread(target=task)
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
print(money)  # 0

多线程的作用与用途

我们考虑该问题时应该了解一些理论前提

  • CPU
     1. 单CPU
     2. 多CPU
  • 任务类型
     1. 计算密集型
     2. IO密集型

不同情况下的情况

单cpu
     多IO密集型任务
         多进程浪费资源
             进程执行中遇到IO操作会直接将cpu切换到下个任务,所以导致多进程也无法利用多核优势
         多线程节省资源
             线程执行过程中只需要在进程中切换执行的线程(切换+保存状态)
      多计算密集型任务
          多进程:耗时较长  (创建进程耗时+切换进程耗时)
          多线程:耗时较短  (切换线程耗时)
多cpu
      多IO密集型任务
          多进程浪费资源
              只能利用某些个cpu,其余的利用不上
          多线程节省资源(切换+保存状态)
      多计算密集型任务
          多进程速度更快(多cpu的计算优势)
          多线程速度较慢
结论: 多线程多进程的应用场景不同。尤其是多线程,在某些场景也比较有用

代码验证

import os
import time
from threading import Thread
from multiprocessing import Process
print(os.cpu_count())
def work():
    res = 1
    for i in range(1, 100000):
        res *= i
if __name__ == '__main__':
    stime = time.time()
    t_list = []
    for i in range(4):
        t = Thread(target=work)
        # t = Process(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(time.time() - stime)

def work():
    time.sleep(0.1)  # 模拟纯 IO操作
if __name__ == '__main__':
    stime = time.time()
    t_list = []
    for i in range(100):
        t = Process(target=work)
        # t = Thread(target=work)
        t.start()
    for t in t_list:
        t.join()
    print(time.time() - stime)

结果

计算密集型:
  多进程:5.426685571670532
  多线程:13.538886785507202

IO密集型
  多进程: 4.05186653137207
  多线程: 0.016954660415649414

结论
 IO密集型多线程更节省资源,节省时间

死锁现象

from threading import Thread, Lock
import time
mutex1 = Lock()
mutex2 = Lock()
class Mythread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        mutex1.acquire()
        print('f1抢到了锁1')
        mutex2.acquire()
        print('f2抢到了锁2')
        mutex2.release()
        mutex1.release()
    def f2(self):
        mutex2.acquire()
        print('f2抢到了锁2')
        time.sleep(0.1)
        mutex1.acquire()
        print('f1抢到了锁1')
        mutex1.release()
        mutex2.release()
for i in range(5):
    t = Mythread()
    t.start()

当存在多把锁的时候,任务内如果采用不同的抢锁放锁的顺序,机会出现死锁现象。所以锁不能轻易使用,并且应该尽可能的少用。

信号量

信号量在不同的知识体系中,所代表的的含义是不同
  并发编程:多把互斥锁
 django框架:达到某个条件自动触发特定功能
就像进入一个房间可以通过多扇门进入
  如果将自定义互斥锁比喻为一扇门
  那么信号量就是指通往房间的多扇门
其实本质意义就是同时提供多把锁,一把锁同时只能被使用一次,放锁以后下一个才可以接着抢锁。但是着了由于存在多吧锁,所以可以支持同时好几个任务抢锁。

import random
import time
from threading import Thread, Semaphore
sp = Semaphore(5)  # 创建一个五个门的房子
def task(name):
    sp.acquire()
    print(f'{name} is peeing')
    time.sleep(random.randint(1, 3))
    print('\n')
    sp.release()
for i in range(50):
    t = Thread(target=task, args=(i,))
    t.start()

event事件

一些子线程需等待另些子线程运行结束才能运行(类似发射信号)
 即某些子线程是否运行取决于其他子线程

from threading import Thread, Event
import time
event = Event()  # 创建event事件
def light():
    print('red')
    time.sleep(5)
    print('green')
    event.set()  # 设置为event事件的参照
def car(i):
    print(i, 'red, wait')
    event.wait()  # 等待light执行后才会执行后面的代码
    print(i, 'green go')
t = Thread(target=light)
t.start()
for i in range(10):
    t = Thread(target=car, args=(i,))
    t.start()

进程池与线程池

知识补充与回顾

  • 补充知识:服务端必备三个特性
     1. 24小时不间断提供服务
     2. 固定的IP和PORT
     3. 支持高并发
  • 知识回顾:TCP服务端实现并发
     1. 多进程:来一个客户端就开一个进程(临时工)
     2. 多线程:来一个客户端就开一个线程(临时工)
  • 问题:物理硬件存在极限,不支持无限创建进程或者线程

问题的解决方案

  1. 池的概念
     其实池的概念就是指CPU同时支持创建的进程或者线程的最大个数
  2. 池的目的
     能够保证物理硬件不被损坏的情况下,提升程序的运行效率
  3. 池的使用
     (1) 进程池:
       提前创建好固定数量的进程 后续反复使用这些进程(合同工)
     (2)线程池:
       提前创建好固定数量的线程 后续反复使用这些线程(合同工)
     (3)如果任务超出了池子里面的最大进程或线程数 则原地等待
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import os
from threading import current_thread
pool1 = ProcessPoolExecutor(3)  # 创建一个最大进程数为3的进程池
pool2 = ThreadPoolExecutor(30)  # 创建一个最大线程数为30的线程池
def task1(n):
    time.sleep(1)
    print(n)
    # print('%s is running' % i)
    # print(current_thread().name)
    print('进程池', os.getpid())
    return '1执行结果%s' % n
def task2(n):
    time.sleep(1)
    print(n)
    # print('%s is running' % i)
    # print(current_thread().name)
    print('线程池', current_thread().name)
    return '2执行结果%s' % n
def run(*args, **kwargs):
    print(args[0].result())
if __name__ == '__main__':
    for i in range(20):
        # pool.submit(task, i)  # 往进程池里提交任务
        # print(res.result())  # 同步提交
        res1 = pool1.submit(task1, i).add_done_callback(run)  # 想进程池内提交任务并返回结果
        '''异步回调机制:不应该主动等待结果,让异步提交自动提醒'''
        # res2 = pool2.submit(task2, i).add_done_callback(run)
        # print(res.result())
if __name__ == '__main__':
    for i in range(100):
        # pool.submit(task, i)  # 往进程池里提交任务
        # print(res.result())  # 同步提交
        # res1 = pool1.submit(task1, i).add_done_callback(run)
        '''异步回调机制:不应该主动等待结果,让异步提交自动提醒'''
        res2 = pool2.submit(task2, i).add_done_callback(run)  # 想线程池内提交任务并返回结果
        # print(res.result())

协程

单线程下实现并发
  切换+保存状态
 协程是程序员意淫出来的,操作系统并不认识它
  携程就是自己检测IO,并对其进行切换操作

协程的实现

from gevent import spawn
from gevent import monkey;monkey.patch_all()  # 检测一切IO操作
import time
def play():
    print('play 1')
    time.sleep(4)
    print('play 2')
def eat():
    print('eat 1')
    time.sleep(3)
    print('eat 2')
stime = time.time()
g1 = spawn(play)  # 检测任务play
g2 = spawn(eat)  # 检测任务eat
g1.join()
g2.join()
print(time.time()-stime)

协程实现TCP并发

# 服务端
  import socket
  from gevent import spawn
  from gevent import monkey;monkey.patch_all()
  def get_server():
      server = socket.socket()
      server.bind(('127.0.0.1', 8080))
      server.listen(5)
      while True:
          sock, addr = server.accept()
          spawn(communicate, sock)


  def communicate(sock):
      data = sock.recv(1024)
      print(data.decode('utf8'))
      sock.send(b'hello')

  s1 = spawn(get_server)
  s1.join()

# 客户端

import socket
from threading import Thread


def get_client():
    client = socket.socket()
    client.connect(('127.0.0.1', 8080))
    count = 0

    msg = 'say hello back'
    count += 1
    client.send(msg.encode('utf8'))
    data = client.recv(1024)
    print(data.decode('utf8'))


for i in range(3000):
    t = Thread(target=get_client)
    t.start()

进程线程理论总结

终极结论
 python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程,从而让程序执行的效率达到极致!!!
 但是实际业务中很少需要如此之高的效率(一直占着CPU不放)。因为大部分程序都是IO密集型的
 所以协程我们知道它的存在即可 几乎不会真正去自己编写

posted @ 2022-04-25 17:28  Oliver-Chance  阅读(25)  评论(0编辑  收藏  举报