线程的互斥、死锁

线程的互斥

多个线程共享全局变量,线程之间抢占资源,就会出现线程的互斥

我们举例说明:

import threading

num = 0  # 多个线程共享全局变量,会出现线程的互斥!

def taskA(times):
    global num
    for i in range(times):
        num += 1  # 此时互斥
    print(threading.currentThread(), "num:", num)


def taskB(times):
    global num
    for i in range(times):
        num += 1  # 此时互斥
    print(threading.currentThread(), "num:", num)


t1 = threading.Thread(target=taskA, args=(200000,))
t2 = threading.Thread(target=taskB, args=(200000,))

t1.start()
t2.start()

t1.join()
t2.join()
print(threading.currentThread(), "end:", num)

在以上代码中,我使用了两个线程对 num 各加 200000 次,理论上最后 num 的值应该为 400000。

但是实际的运行结果如下:

<Thread(Thread-1, started 12892)> num: 200000
<Thread(Thread-2, started 12940)> num: 311008
<_MainThread(MainThread, started 4728)> end: 311008

这是因为两个线程同时对变量 num 进行操作,他们会竞争 CPU 的资源,在线程2进行运算时,可能此时线程1抢占到了 CPU 的资源,所以就会造成最终的运行结果要少于你需要的结果。

怎么解决互斥问题?

引入了对象互斥锁的概念,来保证共享数据操作的完整性。
每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

对以上案例,我们引入互斥锁再次运行:

import threading

num = 0  # 多个线程共享全局变量,会出现线程的互斥!

def taskA(times):
    global num
    for i in range(times):
        temp = lockA.acquire()  # 上锁
        if temp:
            num += 1
            lockA.release()  # 释放锁
    print(threading.currentThread(), "num:", num)


def taskB(times):
    global num
    for i in range(times):
        temp = lockA.acquire()  # 上锁
        if temp:
            num += 1
            lockA.release()  # 释放锁
    print(threading.currentThread(), "num:", num)


lockA = threading.Lock()  # 声明一个锁

t1 = threading.Thread(target=taskA, args=(200000,))
t2 = threading.Thread(target=taskB, args=(200000,))

t1.start()
t2.start()

t1.join()
t2.join()

print(threading.currentThread(), "end:", num)

这里我们用到了3个方法:

  • lock = threading.Lock() :创建一把线程锁
  • lock.acquire() :上锁
  • lock.release() :开锁

运行结果为:

<Thread(Thread-1, started 1680)> num: 342427
<Thread(Thread-2, started 9468)> num: 400000
<_MainThread(MainThread, started 13368)> end: 400000

我们可以看到,在引入互斥锁之后,一个线程在运行时,不会受到其它线程的影响,保证了数据操作的完整性。

作用
  • 保证共享数据操作的完整性
  • 可以解决资源分配问题

死锁

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

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁。

我们举一个有趣的小例子:

import threading
import time


def boy():
    if boyLock.acquire():
        print('男孩生气了,等待女孩道歉才理她!')
        time.sleep(1)
        if girlLock.acquire():  # 此时出现了死锁
            print('男孩原谅女孩了!')
            girlLock.release()
        boyLock.release()


def girl():
    if girlLock.acquire():
        print('女孩生气了,等待男孩道歉才理他!')
        time.sleep(1)
        if boyLock.acquire():  # 此时出现了死锁
            print('女孩原谅男孩了!')
            boyLock.release()
        girlLock.release()


boyLock = threading.Lock()
girlLock = threading.Lock()

t1 = threading.Thread(target=boy)
t2 = threading.Thread(target=girl)

t1.start()
t2.start()
t1.join()
t2.join()

print('End')

上方代码运行结果为:

男孩生气了,等待女孩道歉才理她!
女孩生气了,等待男孩道歉才理他!
...

在以上代码中,我写了男孩女孩两个方法,并且创建了两个互斥锁,两个方法都需要对方的锁解开之后,才能解开自己的锁。否则两个人就会一直等待对方道歉,程序也就不会中止。

这种各自占有对方一部分资源,并且同时等待对方的资源,并且没有外力介入,就会一直等待的状态,就是死锁。

如何避免死锁?我在这里添加超时时间 timeout=3 来解决:

import threading
import time


def boy():
    if boyLock.acquire():
        print('男孩生气了,等待女孩道歉才理她!')
        time.sleep(1)
        if girlLock.acquire(timeout=1):  # 此时出现了死锁
            print('男孩原谅女孩了!')
            girlLock.release()
        boyLock.release()


def girl():
    if girlLock.acquire():
        print('女孩生气了,等待男孩道歉才理他!')
        time.sleep(1)
        if boyLock.acquire(timeout=3):  # 此时出现了死锁
            print('女孩原谅男孩了!')
            boyLock.release()
        girlLock.release()


boyLock = threading.Lock()
girlLock = threading.Lock()

t1 = threading.Thread(target=boy)
t2 = threading.Thread(target=girl)

t1.start()
t2.start()
t1.join()
t2.join()

print('End')

运行结果:

男孩生气了,等待女孩道歉才理她!
女孩生气了,等待男孩道歉才理他!
女孩原谅男孩了!
End

最终女孩原谅了男孩,死锁问题解决!

产生死锁的原因

1.竞争资源

系统中的资源可以分为两类:

  • 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU 和主存均属于可剥夺性资源;
  • 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,假定 P1 已占用了打印机,若 P2 继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、
消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁。

2. 进程间推进顺序非法

若 P1 保持了资源 R1,P2 保持了资源 R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。
例如:当P1运行到 P1: Request (R2) 时,将因 R2 已被 P2 占用而阻塞;当P2运行到 P2: Request (R1) 时,也将因 R1 已被 P1 占用而阻塞,于是发生进程死锁。

死锁的条件

  • 互斥条件 (Mutual exclusion) : 资源不能被共享,只能由一一个进程使用。
  • 请求与保持条件 (Hold and wait) : 进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
  • 不可抢占条件 (No pre-emption) : 有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
  • 循环等待条件 (Circular wait) : 若干个进程形成环形链,每个都占用对方申请的下一个资源。

如何避免死锁?

  • 程序设计时尽量避免(银行家算法)
  • 添加超时时间
银行家算法

银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。

在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。在这样的描述中,银行家就好比操作系统,资金就是资源,客户就相当于要申请资源的进程。

我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。

为保证资金的安全,银行家规定:

  1. 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
  2. 顾客可以分期贷款,但贷款的总数不能超过最大需求量;
  3. 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
  4. 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。

操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值