GIL的存在,死锁,进程池与线程池,协程

验证GIL的存在

from threading import Thread

money = 100


def task():
    global money
    money -= 1


for i in range(100):  # 创建100个线程
    t = Thread(target=task)
    t.start()


print(money)

'''
输出结果:
0
'''

GIL的特点

当存在IO机制的时候,可能会出现数据错乱的现象

from threading import Thread
import time

money = 100


def task():
    global money
    tmp = money
    time.sleep(0.1)  # 创造出IO机制的效果
    money = tmp - 1


t_list = []
for i in range(100):
    t = Thread(target=task)  # 有IO机制的时候,会自动跳走
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(money)

'''
输出结果:
99
'''

解决措施:

GIL不会影响程序层面的数据也不会保证它的修改是安全的要想保证得自己加锁

from threading import Thread, Lock
import time

money = 100
lock = Lock()  # 制造锁


def task():
    lock.acquire()  # 枪锁
    global money
    tmp = money
    time.sleep(0.1)
    money = tmp - 1
    lock.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)

总结:

  • GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
  • GIL作用面很窄 仅限于解释器级别,平时我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)

多线程与多进程

  • 讨论两大前提

    • CPU个数(单个,多个)
    • 任务类型(IO密集型,计算密集型)
  • 单个CPU

    • IO密集型

      多进程:空间消耗多,资源消耗大
      多线程:资源消耗较少,通多多道技术
      相对而言,多线程更好!!!

    • 计算密集型

      多进程:空间消耗多,资源消耗大,执行总时间较长
      多线程:资源消耗相对较少,执行总时间较短
      相对而言,还是多线程更好!!!

  • 多个CPU

    • IO密集型

      多进程:浪费资源,多个CPU无法发挥作用,总耗时更长
      多线程:节省资源,总耗时更短,切换+保存状态
      相对而言,多线程还是更好!!!

    • 计算密集型

      多进程:总耗时是单个进程的时间,多个CPU发挥了作用
      多线程:总耗时是所有线程之和,耗时更久
      相对而言,多进程更好!!!

    结论:

    多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用

  • 代码实操

    多进程:

    import os
    from multiprocessing import Process
    import time
    
    
    def task():
        res = 1
        for i in range(1, 10000):
            res *= i
    
    
    if __name__ == '__main__':
        print(os.cpu_count())  # 8 查看当前计算机CPU个数
        start_time = time.time()
        t_list = []
        for i in range(100):
            t = Process(target=task)
            t.start()
            t_list.append(t)
    
        for t in t_list:
            t.join()
    
        print('总耗时:%s' % (time.time() - start_time))  # 总耗时:3.4357264041900635
    

    多线程:

    import os
    from multiprocessing import Process
    from threading import Thread
    import time
    
    
    def task():
        res = 1
        for i in range(1, 10000):
            res *= i
    
    
    print(os.cpu_count())
    start_time = time.time()
    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('总耗时:%s' % (time.time() - start_time))  # 总耗时:4.525548219680786
    

    结论:

    多核,计算密集型:从总耗时时间来看,多进程更好

死锁现象

死锁就是一个线程需要的锁在另外一个线程哪里,而自己手里拿着他需要的锁,一次死锁

  • 代码实操
from threading import Thread, Lock
import time

mutexA = Lock()  # 类名加括号每执行一次就会产生一个新的对象
mutexB = Lock()  # 类名加括号每执行一次就会产生一个新的对象


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print(f'{self.name}抢到了A锁')
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        mutexB.release()
        print(f'{self.name}释放了B锁')
        mutexA.release()
        print(f'{self.name}释放了A锁')

    def func2(self):
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        time.sleep(1)
        mutexA.acquire()
        print(f'{self.name}抢到了A锁')
        mutexA.release()
        print(f'{self.name}释放了A锁')
        mutexB.release()
        print(f'{self.name}释放了B锁')


for i in range(10):
    t = MyThread()
    t.start()

结果分析:

  • 死锁产生原因 首先线程1 抢到A锁 其他抢不到等着,接着抢到B锁 其他任然抢A
  • 接着释放B锁,别的进程任然抢A接着释放A锁,别的进程抢到A锁,线程1抢到B锁,然后睡了2秒
  • 其他要抢B锁,B锁在线程1上,然后线程1要抢A锁,A锁在线程2上,索要之锁都在别的线程中,所以死锁

信号量

信号量可以理解为多把锁,同时允许多个线程来更改数据。而互斥锁同时只允许一个线程更改数据。

强调:

信号量在不同的知识体系中 意思可能有区别
在并发编程中 信号量就是多把互斥锁
在django中 信号量指的是达到某个条件自动触发(中间件)

理解

我们之前使用Lock产生的是单把锁
	类似于单间厕所
信号量相当于一次性创建多间厕所
	类似于公共厕所

代码实操

from threading import Thread, Lock, Semaphore
import time
import random


sp = Semaphore(5)  # 一次性产生五把锁


class MyThread(Thread):
    def run(self):
        sp.acquire()  # 抢锁
        print(self.name)
        time.sleep(random.randint(1, 3))
        sp.release()  # 放锁


for i in range(20):
    t = MyThread()
    t.start()

event事件

Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。

理解:

子进程和子线程之间可以彼此等待彼此
eg:
  子A运行到某一个代码位置后发信号告诉子B开始运行

方法

  • is_set()
    当且仅当内部标志为True时返回True。

  • set()
    将内部标志设置为True。所有等待它成为True的线程都被唤醒。当标志保持在True的状态时,线程调用wait()是不会阻塞的。

  • clear()
    将内部标志重置为False。随后,调用wait()的线程将阻塞,直到另一个线程调用set()将内部标志重新设置为True。

  • wait(timeout=None)
    阻塞直到内部标志为真。如果内部标志在wait()方法调用时为True,则立即返回。否则,则阻塞,直到另一个线程调用set()将标志设置为True,或发生超时。
    该方法总是返回True,除非设置了timeout并发生超时。

代码实操

from threading import Thread, Event
import time

event = Event()  # 类似于造了一个红绿灯


def light():
    print('红灯亮着的 所有人都不能动')
    time.sleep(3)
    print('绿灯亮了 油门踩到底 给我冲!!!')
    event.set()


def car(name):
    print('%s正在等红灯' % name)
    event.wait()
    print('%s加油门 飙车了' % name)


t = Thread(target=light)
t.start()
for i in range(20):
    t = Thread(target=car, args=('熊猫PRO%s' % i,))
    t.start()

进程池和线程池

引入

多进程 多线程
在实际应用中是不是可以无限制的开进程和线程
肯定不可以!!! 会造成内存溢出受限于硬件水平
我们在开设多进程或者多线程的时候 还需要考虑硬件的承受范围

提交任务的两种方式

  • 同步:提交完任务后,就在原地等待,直到该任务运行完拿到返回值后,才执行下一行代码—>导致任务的运行方式为串行
  • 异步:提交完任务后,就立即执行下一行代码,当任务有返回值后,自动触发回调函数—>导致任务的运行方式为并发

实现并发的两种方式:多进程和多线程

什么是池?

可以将其理解为一种容器,有其固定的大小
池的功能就是限制启动的进程数或线程数
降低程序的执行效率 保证计算机硬件的安全

什么时候用进程池和线程池?

  1. 什么时候用池:当程序中的任务并发数远远大于计算机的承受能力时,就应该用池的概念将开启的进程数或者线程数限制在计算机的承受范围之内
  2. 用什么样的池:用进程池还是线程池取决于程序的类型,对于IO密集型>>>线程,对于计算密集型>>>进程

线程池

from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(5)

concurent.future模块:

concurrent.futures模块提供了高度封装的异步调用接口

ThreadPoolExecutor:线程池,提供异步调用

p = ThreadPoolExecutor(max_works)对于线程池如果不写max_works:默认的是cpu的数目*5

import time,random
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
 
 
def task():
    
    sum = 0
    print('%s正在产生数据...' % current_thread().name)
    time.sleep(random.random()) # 模拟IO行为
    return sum 
 
 
def dispose(sum):
    print('%s正在处理数据...' % current_thread().name)
    time.sleep(1)  # 模拟处理数据
 
 
if __name__ == '__main__':
    p = ThreadPoolExecutor(4)
    for i in range(10):  
        feture = p.submit(task)  
        feture.add_done_callback(dispose)  
    p.shutdown()  
 
    print('主', current_thread().name)

分析:

1.只开启了四个线程实现并发,且剩余的任务仍然是由这四个线程完成(谁有空,谁执行)

2.与进程池不同,回调函数是由开启的子线程完成(谁有空,谁执行)

进程池

ProcessPoolExecutor: 进程池,提供异步调用.

p = ProcessPoolExecutor(max_works)对于进程池如果不写max_works:默认的是cpu的数目,默认是4个
import os,time
from concurrent.futures import ProcessPoolExecutor
 
 
def task():
    
    sum = 0
    print('%s正在产生数据...'%os.getpid())
    for i in range(1000000):
        sum +=i
    return sum
 
 
def dispose(sum):
    print('%s正在处理数据...'%os.getpid())
    time.sleep(1) # 模拟处理数据
 
 
if __name__ == '__main__':
    p = ProcessPoolExecutor(4)
    for i in range(10):   # 有十个任务需要并发处理,但是假设计算机无法承受开启10个进程
        feture=p.submit(task) # 提交任务,并得到一个feture对象
        feture.add_done_callback(dispose) # 为其绑定回调函数,并将feture对象作为参数传入,会在任务执行完后自动触发
    p.shutdown() # 关闭进程池的入口,并等待任务执行结束
 
    print('主',os.getpid())

分析:

1.只开启了4个子进程,且执行结束后,仍然是这四个子进程干剩下的任务(谁空闲,谁就执行下一个)

2.是主进程在执行回调函数

add_done_callback():解析异步回调机制

协程

定义

协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。

理解

进程:资源单位
线程:执行单位
协程:单线程下实现并发(效率极高)

在代码层面欺骗CPU 让CPU觉得我们的代码里面没有IO操作
实际上IO操作被我们自己写的代码检测 一旦有 立刻让代码执行别的
(该技术完全是程序员自己弄出来的 名字也是程序员自己起的)
核心:自己写代码完成切换+保存状态

代码实操

import time
from gevent import monkey;

monkey.patch_all()  # 固定编写 用于检测所有的IO操作(猴子补丁)
from gevent import spawn


def func1():
    print('func1 running')
    time.sleep(3)
    print('func1 over')


def func2():
    print('func2 running')
    time.sleep(5)
    print('func2 over')


if __name__ == '__main__':
    start_time = time.time()
    # func1()
    # func2()
    s1 = spawn(func1)  # 检测代码 一旦有IO自动切换(执行没有io的操作 变向的等待io结束)
    s2 = spawn(func2)
    s1.join()
    s2.join()
    print(time.time() - start_time)  # 8.01237154006958   协程 5.015487432479858

协程实现TCP服务端并发

服务端:

import socket
from gevent import monkey;monkey.patch_all()  # 固定编写 用于检测所有的IO操作(猴子补丁)
from gevent import spawn


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


def get_server():
    server = socket.socket()
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:
        sock, addr = server.accept()  # IO操作
        spawn(communication, sock)

s1 = spawn(get_server)
s1.join()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值