Python全局解释器锁和协程

目录

一.进程和线程的比较

二.GIL全局解释器锁

1.介绍

2.背景知识

2.什么是GIL解释器锁

3.GIL与Lock

 4.GIL与多线程

5.需要记住的内容

 三.互斥锁

四.线程队列

1.为什么线程中还要使用队列

2.队列的使用

 五.进程池和线程池的使用

1.什么进程池什么是线程池

2.进程池和线程池有什么好处

六.协程

 1.进程线程和协程的区别

2.协程的优缺点

3.协程实现高并发


一.进程和线程的比较

  1. 进程的开销比线程的开销大很多
  2. 进程之间的数据是隔离的,但是线程之间的数据不隔离
  3. 多个进程之间的线程数据不共享

二.GIL全局解释器锁

1.介绍

首先需要明确的一点是 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。

“Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。”

2.背景知识

(1)Python代码运行在解释器上,由解释器来执行或者解释
(2)Python解释器的种类:CPython   Ipython   PyPy   Jython   IronPython
(3)当前市场使用最多的解释器就是CPython解释器
(4)GIL全局解释器锁是存在于CPython中
(5)结论是GIL锁就是保证同一时刻只有一个线程执行,所有线程必须拿到GIL锁才有执行权限

2.什么是GIL解释器锁

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,依次来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全
对python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一个时刻只有一个线程在运行

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

'''
# 验证python test.py只会产生一个进程
# test.py内容
import os,time
print(os.getpid())
time.sleep(1000)
'''
python3 test.py 
# 在windows下
tasklist |findstr python

# 在linux下
ps aux |grep python

综上,如果

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

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

3.GIL与Lock

GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理

 

 4.GIL与多线程

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

  • 一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等到原材料的到来
  • 如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,反过来讲,如果你的工厂原材料都齐全当然是工人越多效率越高

结论:

对于计算来说,cup越多越好,但是对于I/O来说,再多的cpu也没用,当然,对运行一个程序来说,随着cpu的增多执行效率肯定也会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

  • 我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
    • 方案一:开启四个进程
    • 方案二:一个进程下,开启四个线程
  • 单核情况下:
    • 如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二更好
    • 如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二更好
  • 多核情况下:
    • 如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一更好
    • 如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二更好

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

5.需要记住的内容

1. python有GIL锁的原因,同一个进程下多个线程实际上同一时刻,只有一个线程在执行
2. 只有在python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
3. cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
4. 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%,
5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu
6. cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了---》我们不能有8个核,但我现在只能用1核,----》开启多进程---》每个进程下开启的线程,可以被多个cpu调度执行
7. cpython解释器:io密集型使用多线程,计算密集型使用多进程
    # -io密集型,遇到io操作会切换cpu,假设你开了8个线程,8个线程都有io操作---》io操作不消耗cpu---》一段时间内看上去,其实8个线程都执行了, 选多线程好一些
   
# -计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
'''计算密集型选多进程好一些,在其他语言中,都是选择多线程,而不选择多进程.'''

 三.互斥锁

在多线程的情况下,同时执行一个数据,会发生数据错乱的问题

n = 10
from threading import Lock
import time
def task(lock):
    lock.acquire()
    global n
    temp = n
    time.sleep(0.5)
    n = temp - 1
    lock.release()

拿时间换空间或者拿空间换时间

from threading import Thread

if __name__ == '__main__':
    tt = []
    lock=Lock()
    for i in range(10):
        t = Thread(target=task, args=(lock, ))
        t.start()
        tt.append(t)
    for j in tt:
        j.join()

    print("主", n)

四.线程队列

1.为什么线程中还要使用队列

同一个进程下多个线程数据是共享的,因为队列是管道+锁,所以用队列还是为了保证数据的安全

2.队列的使用

class queue.Queue(maxsize=0) 先进先出

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''

class queue.LifoQueue(maxsize=0) 先进先出 # last in fisrt out 后进先出 

import queue

q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
结果(后进先出):
third
second
first
'''

class queue.PriorityQueue(maxsize=0) # 存储数据时可设置优先级的队列

import queue

q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''

总结:

Constructor for a priority queue. maxsize is an integer that sets the upperbound limit on the number of items that can be placed in the queue. Insertion will block once this size has been reached, until queue items are consumed. If maxsize is less than or equal to zero, the queue size is infinite.

构造一个优先级队列,其中maxsize是一个整数,用于设置可以放入队列的项目数量的上限.一旦达到这个上限,插入就会阻塞,直到队列中有项目被消耗。如果maxsize小于或等于0,则队列长度为无穷大。

The lowest valued entries are retrieved first (the lowest valued entry is the one returned by sorted(list(entries))[0]). A typical pattern for entries is a tuple in the form: (priority_number, data).

首先检索最低值的条目(最低值的条目是指列表经过排序后取到的索引为0的那个元素,一般条目是(优先级数字,数据)这种元组的形式

exception queue.Empty
Exception raised when non-blocking get() (or get_nowait()) is called on a Queue object which is empty.

当表示非阻塞的get()或get_nowait()在一个空的队列对象中被调用时,会抛出异常

exception queue.Full
Exception raised when non-blocking put() (or put_nowait()) is called on a Queue object which is full.

当表示非阻塞的put()或put_nowait()在一个满的队列对象中被调用时,会抛出异常

Queue.qsize()
Queue.empty() #return True if empty  

当队列为空返回True

Queue.full() # return True if full 

当队列为满返回True

Queue.put(item, block=True, timeout=None)
Put item into the queue. If optional args block is true and timeout is None (the default), block if necessary until a free slot is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Full exception if no free slot was available within that time. Otherwise (block is false), put an item on the queue if a free slot is immediately available, else raise the Full exception (timeout is ignored in that case).

将一个项放入队列。如果可选参数block为true并且timeout为None(默认值),则在必要时阻塞,直到有空闲槽可用。如果参数timeout是一个正数,它最多阻塞timeout秒,如果在这段时间内没有可用的空闲槽,则会引发Full异常。否则(block为false),如果有空闲槽可用,则将一个项目放入队列中,否则引发Full异常(在这种情况下,timeout被忽略)。

Queue.put_nowait(item)
Equivalent to put(item, False).

Queue.get(block=True, timeout=None)
Remove and return an item from the queue. If optional args block is true and timeout is None (the default), block if necessary until an item is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Empty exception if no item was available within that time. Otherwise (block is false), return an item if one is immediately available, else raise the Empty exception (timeout is ignored in that case).

从队列中移除并返回一个项。如果可选参数block为true并且timeout为None(默认值),则在必要时阻塞,直到有可用的项。如果timeout为正数,则最多阻塞timeout秒,如果在该时间内没有可用项,则抛出Empty异常。否则(block为false),如果一个项目可用,则返回那个项目,否则引发Empty异常(在这种情况下,timeout被忽略)。

Queue.get_nowait()
Equivalent to get(False).

Two methods are offered to support tracking whether enqueued tasks have been fully processed by daemon consumer threads.

提供了两种方法来支持追踪进入队列的任务是否已被生产者的守护线程完全处理。

Queue.task_done()
Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing on the task is complete.

假定先前进入队列的任务已完成。并且被队列生产者使用。对于每个用于获取任务的get(),后续对task_done()的调用都会告诉队列任务的处理已经完成。

If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue).

如果join()当前正被阻塞,它将在所有项都被处理完时恢复(这意味着对于每个已经put()到队列中的项都接收到task_done()调用)。

Raises a ValueError if called more times than there were items placed in the queue.

如果调用次数超过放入队列的项数,将引发ValueError。

Queue.join() 

阻塞,直到queue被消费完毕

 五.进程池和线程池的使用

1.什么进程池什么是线程池

池:池子,容器类型,可以盛放多个元素
进程池:提前定义好一个池子,然后往这个池子里添加进程,以后,只要往这个池子里放任务就行了,然后右这个进程池里的任意一个进程来执行任务
线程池:提前定义好一个池子,然后往这个池子里添加线程,以后,只要往这个池子里放任务就行了,然后右这个线程池里的任意一个进程来执行任务

2.进程池和线程池有什么好处

进程池和线程池是并发编程中常用的技术,它们都可以用于管理和复用多个并发任务的执行。

  • 提高性能:
    • 进程池和线程池可以并行执行多个任务,从而提高程序的整体性能。通过将任务分配给多个进程或线程,可以利用多核处理器的并行计算能力,加快任务的执行速度
  • 资源管理:

    • 进程池和线程池可以管理和复用系统资源,避免频繁创建和销毁进程或线程的开销。创建和销毁进程或线程需要消耗系统资源,而进程池和线程池可以在程序启动时预先创建一定数量的进程或线程,并在需要执行任务时复用已创建的进程或线程,从而减少资源的浪费

  • 控制并发度:

    • 进程池和线程池可以控制并发任务的数量,避免系统资源被过度占用。通过限制并发任务的数量,可以避免系统资源的竞争和过载,保证程序的稳定性和可靠性

  • 简化编程:

    • 进程池和线程池提供了高级的接口和抽象,简化了并发编程的复杂性。通过使用进程池和线程池,开发者可以将精力集中在任务的实现上,而无需关注底层的并发细节,提高了开发效率和代码的可读性

​​​​​​​总的来说,进程池和线程池可以提高程序的性能、简化并发编程、优化资源管理,是并发编程中常用的技术手段。选择使用进程池还是线程池,需要根据具体的应用场景和需求来进行权衡和选择

def task(n, m):
    return n+m

def task1():
    return {'username':'kevin', 'password':123}
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor


def callback(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # 3

def callback1(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # {'username': 'kevin', 'password': 123}
    print(res.result().get('username'))
if __name__ == '__main__':
    pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
    ## 2. 往池子里面丢任务

    pool.submit(task, m=1, n=2).add_done_callback(callback)
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)

六.协程

 1.进程线程和协程的区别

进程:资源分配
线程:执行的最小单位

协程:单线程下的并发(程序员自己想出来的,并不是在操作系统中实际存在的)

并发:切换+保存状态(以前的并发的切换其实是进程或者线程在切换)

协程是最节省资源的,进程是最消耗资源的,其次是线程

2.协程的优缺点

  • 优点:
    • 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    • 单线程内就可以实现并发的效果,最大限度地利用cpu
  • 缺点:
    • 协程的本质是单线程下无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
    • 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需要加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到I/O操作自动切换到其他协程(如何实现检测I/O,yield,greenlet都无法实现,就用到了gevent模块(select机制))

3.协程实现高并发

服务端:
from gevent import monkey;

monkey.patch_all()
import gevent
from socket import socket
# from multiprocessing import Process
from threading import Thread


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


def server(ip, port):
    server = socket()
    server.bind((ip, port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        # t=Process(target=talk,args=(conn,))
        # t=Thread(target=talk,args=(conn,))
        # t.start()
        gevent.spawn(talk, conn)


if __name__ == '__main__':
    g1 = gevent.spawn(server, '127.0.0.1', 8080)
    g1.join()

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


def socket_client():
    cli = socket.socket()
    cli.connect(('127.0.0.1', 8080))
    while True:
        ss = '%s say hello' % current_thread().getName()
        cli.send(ss.encode('utf-8'))
        data = cli.recv(1024)
        print(data)


for i in range(5000):
    t = Thread(target=socket_client)
    t.start()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值