线程相关锁

线程相关锁

一、线程同步锁(互斥锁)

多线程抢占资源的数据安全问题

# 案例一
from threading import Thread
import time

n = 10
def func():
    global n
    temp = n
    time.sleep(0.2)
    n = temp - 1

if __name__ == '__main__':
    lis = []
    for i in range(10):
        t = Thread(target=func)
        t.start()

        lis.append(t)
    for i in lis:
        i.join()
    print(n)  # 9

'''
因为将n的递减操作拆分为两个阶段,一个赋值,一个减值,中间休眠0.2s
此时,当10个线程去修改n的时候,就会出现在0.2s之前10个线程拿到的n都是10
最后每个线程返回的修改值都是9,这样就产生了数据不同步的安全问题
所以,当延迟时间过长或者数据量过大时,使用多线程同时访问同一个数据时,不安全
'''

# 案例二
from threading import Thread

n = 0
def task():
    global n
    for i in range(100000):
        n += 1

if __name__ == '__main__':
    lis = []
    for i in range(3):
        t = Thread(target=task)
        t.start()
        lis.append(t)
    for i in lis:
        i.join()
    print(n)  # 274155

'''
当启动三个线程来执行task时
发现结果永远小于300000
这也验证了多个线程修改同一个数据时会引发数据安全问题
**********所以,有了进程锁的概念***********
'''

使用线程同步锁解决数据安全问题

# 案例一解决方法
from threading import Thread,Lock
import time

n = 10
lock = Lock()
def func():
    global n
    lock.acquire()
    temp = n
    time.sleep(0.2)
    n = temp - 1
    lock.release()

if __name__ == '__main__':
    lis = []
    for i in range(10):
        t = Thread(target=func)
        t.start()

        lis.append(t)
    for i in lis:
        i.join()
    print(n)  # 0

# 案例二解决方
from threading import Thread,Lock
n = 0
lock = Lock()
def task():
    global n
    lock.acquire()
    for i in range(100000):
        n += 1
    lock.release()
if __name__ == '__main__':
    lis = []
    for i in range(3):
        t = Thread(target=task)
        t.start()
        lis.append(t)
    for i in lis:
        i.join()
    print(n)  # 300000
    
'''
给要处理的数据加一把锁,限定同一时间只能有一个线程来访问这段代码
同样,在保证了数据安全的同时,牺牲了效率
'''

二、死锁

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

# 著名的科学家吃面问题
from threading import Thread,Lock
import time

noodle_lock = Lock()
fork_lock = Lock()

def eat1(name):
    noodle_lock.acquire()
    print(f'{name}拿到面条了')
    fork_lock.acquire()
    time.sleep(1)
    print(f'{name}拿到叉子了')
    print(f'{name}吃面')
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print(f'{name}拿到叉子了')
    time.sleep(1)
    noodle_lock.acquire()

    print(f'{name}拿到面条了')
    print(f'{name}吃面')
    noodle_lock.release()
    fork_lock.release()

Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('nick',)).start()
Thread(target=eat1,args=('tank',)).start()
Thread(target=eat2,args=('egon',)).start()
'''
alex拿到面条了
alex拿到叉子了
alex吃面
nick拿到叉子了
tank拿到面条了
'''

三、递归锁

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

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

from threading import Thread,RLock
import time

fork_lock = noodle_lock = RLock()


def eat1(name):
    noodle_lock.acquire()
    print(f'{name}拿到面条了')
    fork_lock.acquire()
    print(f'{name}拿到叉子了')
    print(f'{name}吃面')
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print(f'{name}拿到叉子了')
    time.sleep(1)
    noodle_lock.acquire()

    print(f'{name}拿到面条了')
    print(f'{name}吃面')
    noodle_lock.release()
    fork_lock.release()

Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('nick',)).start()
Thread(target=eat1,args=('tank',)).start()
Thread(target=eat2,args=('egon',)).start()

'''
alex拿到面条了
alex拿到叉子了
alex吃面
nick拿到叉子了
nick拿到面条了
nick吃面
tank拿到面条了
tank拿到叉子了
tank吃面
egon拿到叉子了
egon拿到面条了
egon吃面
'''

四、全局解释器锁GIL

​ Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
  对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

  在多线程环境中,Python 虚拟机按以下方式执行:

  a、设置 GIL;

  b、切换到一个线程去运行;

  c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));

  d、把线程设置为睡眠状态;

  e、解锁 GIL;

  d、再次重复以上所有步骤。
  在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

GIL 就是当线程访问cpu对数据进行修改时介于两者之间的一把锁,其实就是Cpython的解释器起作用。
	他控制了同一时刻只能有一个线程来访问数据,将此线程设置为睡眠状态
	当线程对数据修改完毕了,GIL就会释放该线程
    之后才允许下一个线程来访问数据

GIL锁的是线程
GIL导致的不能并发问题是Cpython的特性,目前还不能得以解决!

posted @ 2019-09-18 21:38  SweetJack  阅读(130)  评论(0编辑  收藏  举报