GIL与普通互斥锁区别


验证GIL的存在

from threading import Thread, Lock
import time
money = 100
def task():
  global money
  money -= 1
for i in range(100):  # 创建一百个线程
  t = Thread(target=task)
  t.start()
print(money)  # 0

验证不同数据加不同锁

from threading import Thread, Lock
import time

money = 100
mutex = Lock()

def task():
    global money
    mutex.acquire()
    tmp = money
    time.sleep(0.1)
    money = tmp - 1
    mutex.release()
    """
    抢锁放锁也有简便写法(with上下文管理)
    with mutex:
        pass
    """
    
t_list = []
for i in range(100):  # 创建一百个线程
    t = Thread(target=task)
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
# 为了确保结构正确 应该等待所有的线程运行完毕再打印money
print(money)

结论

​ GIL是一个纯理论知识,在实际工作中根本无需考虑它的存在。

​ GIL作用面很窄,仅限于解释器级别,后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)。

验证多线程作用


验证多线程作用的两大前提:cpu的个数(单个和多个)和任务的类型(IO密集型和计算密集型)。

单个cpu的情况:

多个IO密集型任务
    多进程:浪费资源 无法利用多个CPU
    多线程:节省资源 切换+保存状态
多个计算密集型任务
    多进程:耗时更长 创建进程的消耗+切换消耗
    多线程:耗时较短 切换消耗

多个CPU的情况:

多个IO密集型任务
    多进程:浪费资源 多个CPU无用武之地
    多线程:节省资源 切换+保存状态
多个计算密集型任务
    多进程:利用多核 速度更快
    多线程:速度较慢

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

代码验证

from multiprocessing import Process
import os
import time

def work():
    res = 1
    for i in range(1, 100000):
        res *= i

if __name__ == '__main__':
    print(os.cpu_count())  # 8  查看当前计算机CPU个数
    start_time = time.time()
    # 多进程
    p_list = []
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    # 多线程
    # t_list = []
    # for i in range(12):
    #     t = Thread(target=work)
    #     t.start()
    #     t_list.append(t)
    # for t in t_list:
    #     t.join()
    print('总耗时:%s' % (time.time() - start_time))

"""
计算密集型 
    多进程
        7.588449716567993
    多线程
        27.50218415260315
    两者差了一个数量级(越多差距越大)       
结论
    多进程更好
"""

from multiprocessing import Process
import time

def work():
    time.sleep(2)   # 模拟纯IO操作

if __name__ == '__main__':
    start_time = time.time()
    # 多线程
    # t_list = []
    # for i in range(100):
    #     t = Thread(target=work)
    #     t.start()
    # for t in t_list:
    #     t.join()
    # 多进程
    p_list = []
    for i in range(100):
        p = Process(target=work)
        p.start()
    for p in p_list:
        p.join()
    print('总耗时:%s' % (time.time() - start_time))
"""
IO密集型
    多线程
        总耗时:0.011968135833740234
    多进程
        总耗时:1.0890872478485107
    两者差了两个数量级
结论
    多线程更好
"""

img

死锁现象


锁就算掌握了如何抢,如何放,也会产生死锁现象。所以锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具。

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(f'{self.name}抢到了A锁')
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        mutexB.release()
        mutexA.release()
    def f2(self):
        mutexB.acquire()
        print(f'{self.name}抢到了B锁')
        time.sleep(2)
        mutexA.acquire()
        print(f'{self.name}抢到了A锁')
        mutexA.release()
        mutexB.release()

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

img

信号量(了解)和event事件(了解)


信号量在不同的知识体系中,展示出来的功能是不一样的。在并发编程中信号量意思是多把互斥锁。在django框架中信号量意思是达到某个条件自动触发特定功能。

"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
"""
from threading import Thread, Semaphore
import time
import random

sp = Semaphore(5)  # 创建一个有五个坑位(带门的)的公共厕所

def task(name):
    sp.acquire()  # 抢锁
    print('%s正在蹲坑' % name)
    time.sleep(random.randint(1, 5))
    sp.release()  # 放锁
    print('%s蹲坑完成出去了' % name)

for i in range(1, 31):
    t = Thread(target=task, args=('伞兵%s号' % i, ))
    t.start()

event事件

"""
子线程的运行可以由其他子线程决定!!!
"""
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()
# 这种效果其实也可以通过其他手段实现 比如队列(只不过没有event简便)

img

进程池与线程池(重点)


补充知识:服务端必备的三要素,24小时不间断提供服务,固定的ip和port,支持高并发。

TCP服务端实现并发,多进程:来一个客户端就开一个进程(临时工),多线程:来一个客户端就开一个线程(临时工)。

计算机硬件是有物理极限的,我们不可能无限制的创建进程和线程,所以就产生了池。

池:保证计算机硬件安全的情况下提升程序的运行效率。
进程池:提前创建好固定数量的进程,后续反复使用这些进程(合同工)。
线程池:提前创建好固定数量的线程,后续反复使用这些线程(合同工)。
如果任务超出了池子里面的最大进程或线程数,则原地等待。
进程池和线程池虽然降低了程序的运行效率,但是保证了硬件的安全!!!

代码演示

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
# 线程池
pool = ThreadPoolExecutor(5)  # 线程池线程数默认是CPU个数的五倍 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的线程'''
'''不应该自己主动等待结果 应该让异步提交自动提醒>>>:异步回调机制'''
pool.submit(task, i).add_done_callback(func)
"""add_done_callback只要任务有结果了 就会自动调用括号内的函数处理"""

# 进程池
pool = ProcessPo
olExecutor(5)  # 进程池进程数默认是CPU个数 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的进程'''
pool.submit(task, i).add_done_callback(func)

协程


进程:资源单位
线程:执行单位
协程:单线程下实现并发

并发的概念:切换+保存状态

首先需要强调的是协程完全是程序员自己想出来的词,对于操作系统而言之认识进程和线程。协程就是自己通过代码来检测程序的IO操作并自己处理,让CPU感觉不到IO的存在从而最大幅度的占用CPU。类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换!!!

基本使用

# 保存的功能 我们其实接触过  yield 但是无法做到检测IO切换
from gevent import monkey;monkey.patch_all()  # 固定编写 用于检测所有的IO操作
from gevent import spawn
import time


def play(name):
    print('%s play 1' % name)
    time.sleep(5)
    print('%s play 2' % name)


def eat(name):
    print('%s eat 1' % name)
    time.sleep(3)
    print('%s eat 2' % name)

start_time = time.time()
g1 = spawn(play, 'jason')
g2 = spawn(eat, 'jason')  # 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g1.join()  # 等待检测任务执行完毕
g2.join()  # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time)  # 正常串行肯定是8s+
# 5.00609827041626  代码控制切换

img

基于协程实现TCP服务端并发


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


def communication(sock):
    while True:
        data = sock.recv(1024)  # IO操作
        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)

g1 = spawn(get_server)
g1.join()

#客户端
from threading import Thread, current_thread
import socket

def get_client():
    client = socket.socket()
    client.connect(('127.0.0.1', 8080))
    count = 0
    while True:
        msg = '%s say hello %s'%(current_thread().name, count)
        count += 1
        client.send(msg.encode('utf8'))
        data = client.recv(1024)
        print(data.decode('utf8'))

# 创建诸多线程
for i in range(200):
    t = Thread(target=get_client)
    t.start()

结论:

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

版权声明:本文为早安原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zaoan1207/p/16176619.html