并发编程之锁问题

并发编程之锁问题

Python GIL锁(Global Interpreter Lock)

GIL锁官方文档介绍

'''
定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly 
because CPython’s memory management is not thread-safe. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)
翻译:在cpython中,全局解释器锁(gil)是一个互斥锁,它可以防止同时执行python字节码的本机线程。这个锁主要是必要的因为cpython的内存管理不是线程安全的。(然而,自从GIL锁的存在,其他特征已经发展到取决于它所实施的保证。)

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。

先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

GIL锁介绍

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

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

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

'''
#验证python test.py只会产生一个进程
#test.py内容
import os,time
print(os.getpid())  # 这个就是Python解释器的进程号
time.sleep(1000)
'''
python3 test.py 
#在windows下
tasklist |findstr python  
#在linux下
ps aux |grep python

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问

1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。

2.所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器码的代码

综上:

如果多个线程的target=work,那么执行流程是

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

GIL锁与Lock

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

GIL与多线程

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

听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了?不是这样的!!!不是这样的!!!不是这样的!!!

要解决这个问题,我们需要在几个点上达成一致:

#1. cpu到底是用来做计算的,还是用来做I/O的?

#2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

#3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

个人理解:在多核情况下,如果是I/O密集型的程序:开启多进程,多个进程并行执行(根据本机CPU核数,一个进程占用一个CPU),每个进程内都会拷贝一份Python解释器代码,同一进程内的多个线程再去抢解释器锁(GIL锁),这时同一进程内的多个线程是并发执行的,每个线程遇到I/O阻塞都会释放掉CPU占用权限(切换+保存状态)。I/O 阻塞时间是常数,多个进程在执行时,遇到I/O阻塞或者分的时间片给用完了,都要释放CPU,进入到下一级队列(多级反馈对列)当中的就绪态,等待再次被调度,至于再次被调度,使用哪个核是不固定的,

一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。

如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,

反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高

结论:

  对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用

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

#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程

#单核情况下,分析结果: 
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
 
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

多线程与多进程性能比较(I/O密集与计算密集)

一下示例均为多核CPU情况下:

# 计算密集型的情况下,开启多进程效率更高

from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i
    print(res)

if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为4核
    start=time.time()
    for i in range(4):
        # p=Process(target=work) #耗时15.843906164169312
        p=Thread(target=work) #耗时26.057490348815918
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)
    print('===>')

if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为4核
    start=time.time()
    for i in range(50):
        # p=Process(target=work) #耗时12.597720623016357,大部分时间耗费在创建进程上
        p=Thread(target=work) #耗时 2.013115167617798
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

应用:

多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析

死锁现象与递归锁(解决死锁)

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

# 这里以开启100个线程为例
from threading import Thread,Lock,RLock
import time

# mutexA=Lock()
# mutexB=Lock()
mutexB=mutexA=RLock()  # 递归锁


class Mythead(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 抢到A锁' %self.name)
        mutexB.acquire()
        print('%s 抢到B锁' %self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 抢到了B锁' %self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 抢到了A锁' %self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(100):
        t=Mythead()
        t.start()

'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能再次抢锁。上面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

信号量(一把锁有多把钥匙)

互斥锁类似多个人(进程/线程)抢一个马桶(一把锁),同一时间仅一人能使用马桶(执行代码),
而信号量类似于公共厕所,一把锁有多把钥匙(同时满足多个线程或进程执行代码),允许有多个线程或进程去抢多把钥匙。

# 线程信号量
from threading import Thread,Semaphore
import time,random
sm=Semaphore(5)  # 设置5个坑位,同时满足5个线程执行,运行完几个线程,待执行的线程补上对应的位置执行。

def task(name):
    sm.acquire()
    print('%s 正在上厕所' %name)
    time.sleep(random.randint(1,3))
    sm.release()

if __name__ == '__main__':
    for i in range(20):
        t=Thread(target=task,args=('路人%s' %i,))
        t.start()

# 进程信号量

from multiprocessing import Process
from multiprocessing import Semaphore
from time import sleep
 
def func(num,lock):
    # 开启锁,信号量内部计数器减1
    lock.acquire()
    print(">>>{0}号进来了".format(num))
    sleep(1)
    # 释放锁,信号量内部计数器加1
    lock.release()
    print("{0}号出去了>>>".format(num))
 
if __name__ == '__main__':
    # 信号量可以一把锁配置多把钥匙
    sema=Semaphore(3) 
    for i in range(10):
        p = Process(target=func,args=(i+1,sema,))
        p.start()

EVENT事件

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

from threading import Thread,Event
import time

event=Event()

def light():
    print('红灯正亮着')
    time.sleep(3)
    event.set() #绿灯亮

def car(name):
    print('车%s正在等绿灯' %name)
    event.wait() #等灯绿
    print('车%s通行' %name)

if __name__ == '__main__':
    # 红绿灯
    t1=Thread(target=light)
    t1.start()
    # 车
    for i in range(10):
        t=Thread(target=car,args=(i,))
        t.start()
        
  '''
  红灯正亮着
车0正在等绿灯
车1正在等绿灯
车2正在等绿灯
车3正在等绿灯
车4正在等绿灯
车5正在等绿灯
车6正在等绿灯
车7正在等绿灯
车8正在等绿灯
车9正在等绿灯
车1通行
车2通行
车4通行
车5通行
车7通行
车8通行
车0通行
车3通行
车9通行
车6通行
  '''

线程queue 的三类

完成分布式,通过中间共享的一个队列,然后完成信息的交互;无论是进程还是线程的通信,只要是网络的通信,都是基于网络的队列的通信方式。若线程间数据不安全,可以使用线程队列。

queue.Queue() # 先进先出
queue.LifoQueue() # 后进先出->堆栈
queue.PriorityQueue() # 优先级
import queue

queue.Queue() #先进先出
q=queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
'''
1
2
3
'''
import queue

queue.LifoQueue() #后进先出->堆栈
q=queue.LifoQueue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
'''
3
2
1
'''

import queue

q=queue.PriorityQueue(3) #优先级,优先级用数字表示,数字越小优先级越高
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())

'''
(-1, 'b')
(10, 'a')
(100, 'c')
'''
原文地址:https://www.cnblogs.com/zhangchaocoming/p/11735912.html