GIL全局解释器锁、互斥锁和协程

目录

一、进程和线程的比较

二、GIL全局解释器锁

2.1 背景信息

2.2  总结

三、互斥锁

四、线程队列

4.1 为什么线程中还有使用队列?

4.2 先进先出

4.3 后进先出

4.4 优先级队列

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

5.1 基本方法

5.2 使用

5.3 多线程爬取网页

六、协程

6.1 协程基础

6.2 协程之gevent模块

6.2.1 猴子补丁的功能(一切皆对象)

6.2.2 monkey patch的应用场景

6.2.3 示例1(遇到io自动切)

6.2.4 示例2

6.3 协程实现高并发

服务端

客户端


一、进程和线程的比较

1.  进程的开销比线程大很多

2 . 进程之间的数据是隔离的,但是,线程之间的数据不隔离

3 .多个进程之间的线程数据不共享----->进程下的线程通信(IPC)------->队列 

二、GIL全局解释器锁

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

对Python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线

程在运行。

2.1 背景信息

1. Python代码运行在解释器上,由解释器来执行或者解释

2. Python解释器的种类:
    1、CPython  2、IPython 3、PyPy  4、Jython  5、IronPython

3. 当前市场使用的最多(95%)的解释器就是CPython解释器

4. GIL全局解释器锁是存在于CPython中

5. 结论是同一时刻只有一个线程在执行? 想避免的问题是,出现多个线程抢夺资源的情况

        比如:现在起一个线程,来回收垃圾数据,回收a=1这个变量,另外一个线程也要使用这个变量a,当垃圾回收线程还没没有把变量a回收完毕,另一个线程就来抢夺这个变量a使用。
        怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限。 

结论:GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限 

2.2  总结

  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 = 20
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)  # 主 10

补充:

面试题:既然有了GIL锁,为什么还要互斥锁? (多线程下)

比如:我起了2个线程,来执行a=a+1,a一开始是0

        1. 第一个线程来了,拿到a=0,开始执行a=a+1,这个时候结果a就是1了
        2. 第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到的a是0,继续执行
          a=a+1结果还是1
        3. 加了互斥锁,就能够解决多线程下操作同一个数据,发生错乱的问题 

四、线程队列

4.1 为什么线程中还有使用队列?

同一个进程下多个线程数据是共享的
为什么先同一个进程下还会去使用队列呢
因为队列是管道 + 锁
所以用队列还是为了
保证数据的安全

4.2 先进先出

'''
queue.Queue 的缺点是它的实现涉及到多个锁和条件变量,
因此可能会影响性能和内存效率。
'''
import queue
queue.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
'''

4.3 后进先出

import queue

'Lifo:last in first out'
q = queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

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

4.4 优先级队列

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')
'''

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

池:池子、容器类型,可以盛放多个元素

进程池:提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,由这个进程池里面的任意一个进程来执行任务

线程池:提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,由这个线程池里面的任意一个线程来执行任务

5.1 基本方法

submit(fn, *args, **kwargs):异步提交任务

map(func, *iterables, timeout=None, chunksize=1):取代for循环submit的操作

shutdown(wait=True):相当于进程池的pool.close()+pool.join()操作

  • wait=True,等待池内所有任务执行完毕回收完资源后才继续
  • wait=False,立即返回,并不会等待池内的任务执行完毕
  • 但不管wait参数为何值,整个程序都会等到所有任务执行完毕
  • submit和map必须在shutdown之前

result(timeout=None):取得结果

add_done_callback(fn):回调函数

done():判断某一个线程是否完成

cancle():取消某个任务

5.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 0x2ad4ae50bb0 state=finished returned dict>
    print(res.result())  # {'username': 'kevin', 'password': 123}
    print(res.result().get('username'))  # kevin


if __name__ == '__main__':
    pool = ThreadPoolExecutor(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)  # 123

5.3 多线程爬取网页

import requests

def get_page(url):
    res = requests.get(url)
    name = url.rsplit('/')[-1]+'.html'
    return {'name':name,'text':res.content}

def call_back(fut):
    print(fut.result()['name'])
    with open(fut.result()['name'],'wb') as f:
        f.write(fut.result()['text'])


if __name__ == '__main__':
    pool = ThreadPoolExecutor(2)
    urls = ['http://www.baidu.com','http://www.cnblogs.com',
'http://www.taobao.com']
    for url in urls:
        pool.submit(get_page,url).add_done_callback(call_back)

六、协程

6.1 协程基础

之前我们学习了线程、进程的概念,了解了在操作系统中进程是资源分配的基本单位,线程是CPU调度的最小单位。按道理来说我们已经算是把CPU的利用率提高很多了。但是我们知道无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的CPU只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。

为此我们需要先回顾下并发的本质:切换+保存状态。以前的并发的切换其实是进程或者线程在切换。CPU正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由我们自己控制调度的,在操作系统中实际不存在的。

对比操作系统控制线程的切换,用户在单线程内控制协程的切换。 

优点如下: 

  1. 协程是最节省资源的,进程是最消耗资源的,其次是线程
  2. 单线程内就可以实现并发的效果,最大限度地利用CPU 

缺点如下:

  1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

6.2 协程之gevent模块

6.2.1 猴子补丁的功能(一切皆对象)

拥有在模块运行时替换的功能,例如: 一个函数对象赋值给另外一个函数对象(把函数原本的执行的功能给替换了)。

class Monkey():
    def play(self):
        print('猴子在玩')

class Dog():
    def play(self):
        print('狗子在玩')
m=Monkey()
m.play()
m.play = Dog().play
m.play()

6.2.2 monkey patch的应用场景

这里有一个比较实用的例子,很多用到import json, 后来发现ujson性能更高,如果觉得把每个文件的import json改成import ujson as json成本较高, 或者说想测试一下ujson替换是否符合预期, 只需要在入口加上:

import json
import ujson

def monkey_patch_json():
    json.__name__ = 'ujson'
    json.dumps = ujson.dumps
    json.loads = ujson.loads
monkey_patch_json()
aa = json.dumps({'name':'lqz','age':19})
print(aa)

6.2.3 示例1(遇到io自动切)

import gevent
import time


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


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


# eat('kevin')
# play('jerry')
start_time = time.time()
g1 = gevent.spawn(eat, 'lqz')
g2 = gevent.spawn(play, name='lqz')
g1.join()
g2.join()  # 相当于执行这些任务完毕
# 或者gevent.joinall([g1,g2])
print('主', time.time() - start_time)

结果如下:

'''
lqz eat 1
lqz play 1
lqz play 2
lqz eat 2
主 2.0305111408233643
'''

6.2.4 示例2

'''
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
'''
from gevent import monkey; monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

start_time = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)

g1.join()
g2.join()
# gevent.joinall([g1,g2])
print('主', time.time() - start_time)

结果如下:

'''
eat food 1
play 1
play 2
eat food 2
主 2.0193047523498535
'''

6.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(50000):
    t = Thread(target=socket_client)
    t.start()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值