赞
踩
目 录
Python 多线程编程目录
Python 多线程编程-01-threading 模块初识
Python 多线程编程-02-threading 模块-锁的使用
Python 多线程编程-03-threading 模块 - Condition
Python 多线程编程-04-threading 模块 - Event
Python 多线程编程-05-threading 模块 - Semaphore 和 BoundedSemaphore
Python 多线程编程-06-threading 模块 - Timer
Python 多线程编程-07-threading 模块 - Barrie
多线程编程有一个非常重要的方面:同步。此处的同步不是指一起行动,而是协同步调,多个线程按预定的先后次序进行运行。
常见的有两种情况:
a)例如某些资源,数据库的某个表格、某个文件,不希望(也不应该)被多个线程同时执行,这样就会产生竞争。那么如何协调这种竞争,可以理解为线程同步。这种内存中的资源可以理解为临界区。
b)需要若干个线程按照特定的顺序完成一组工作,如线程 a 打开文件,输入文字,线程负责 b 修改颜色和字体大小,线程 c 在此基础上插入图片。那么也需要线程同步。
程序员可以选择合适的同步原语,或者线程控制机制来执行同步。最常见的有:锁/互斥,以及信号量。
锁是所有机制中最简单、最低级的机制,而信号量多用于多线程竞争有限资源的情况。
所谓的锁,可以理解为内存中的一个整型数,有两种状态:空闲、上锁。acquire 加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功。如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。
死锁是指两个或两个以上的进程/线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁的原因包括系统资源不足、进程/线程运行推进顺序不合适、资源分配不当等。
比如线程 A 占据了资源 n,在等待临界区资源 m;线程 B 此时占据了 m,但是在等待临界区资源 n,A 只有获得了 m 才能释放 n,而 B 只有获得了 n 才能释放 m,所以彼此之间就造成了死锁,造成了资源的极大浪费。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用,这是对资源的要求。
(2) 请求与保持条件:一个进程/线程因请求资源而阻塞时,对已获得的资源保持不放,这是对进程/线程的要求。
(3) 不剥夺条件:进程/线程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程/线程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立。
对于死锁主要有以下三种处理方式:
(1) 检测与解除策略:检测死锁并且恢复。这是在系统工作中,死锁发生后进行的。
(2) 避免策略:事先对资源进行动态分配,以避免死锁。这是在死锁发生前进行的,主要通过合理的资源分配实现。
(3) 预防策略:通过破除死锁四个必要条件之一,来防止死锁产生。例如设置资源可以被剥夺;请求资源的线程请求失败时候不是阻塞,而是转而去做其他事情;诸如此类等。
threading.Lock 类的属性和方法,已经废弃不再赘述。
序号 | 属性和方法 | 描述 |
1 | 方法:acquire(blocking=True, timeout=-1) | 如果没有参数,且这个锁已经被锁定了,哪怕是被同一个线程锁定,这个线程将会阻塞,需要等待另外一个线程释放获取锁后,返回 True。 如果有参数,只有当参数是 True时候,才会阻塞。 方法的返回值反应释放获取了锁。阻塞操作是可以中断的。 |
2 | 方法:locked() | 返回一个布尔值,指示锁的状态 |
3 | 方法:release() | 释放锁,允许在等待该锁的阻塞队列中的某个线程获得这个锁。这个锁当前应该是被锁定的状态,但是不能是锁定这个锁的线程再次锁定它。 |
先看一段没有使用 Lock 的多线程代码,在多个线程给同一个列表插入数据并且打印数据时候,结果会看起来比较乱。这是我在 jupter 编辑器里面执行的,看起来还不太明显,如果我在 python idle 里面就会更明显。
- import threading
- import time
-
- my_list=[]
- lock=threading.Lock()
-
- def show_list(n):
- my_list.append(n)
- print(my_list)
-
- def show_list_withLock(n):
- lock.acquire()
- my_list.append(n)
- print(my_list)
- lock.release()
-
- numbers=list(range(10))
-
- for num in numbers:
- t= threading.Thread(target=show_list, args=(num, ))
- t.start()
在 idle 可以明显看到,打印的数据顺序和插入的数据顺序是不一致的。
如果使用 threading.Lock 呢,因为每次对列表的操作都必须获得锁,所以就避免了数据紊乱。
如果我对 show_list_withLock 函数做一些修改,原来 lock.acquire() 默认为 lock.acquire(blocking=True),现在我把它设置为 blocking=False。此时,如果获得锁成功还好,如果没有获得锁,但是这个线程并不会阻塞,大家可以看到,可能会出现 "RuntimeError: release unlocked lock ",即释放了并没有获得的锁。但是,并不是一定会出现这种错误。
- import threading
- import time
-
- my_list=[]
- lock=threading.Lock()
-
- def show_list(n):
- my_list.append(n)
- print(my_list)
-
- def show_list_withLock(n):
- lock.acquire(blocking=False)
- my_list.append(n)
- print(my_list)
- lock.release()
-
- numbers=list(range(10))
-
- for num in numbers:
- t= threading.Thread(target=show_list_withLock, args=(num, ))
- t.start()
所以大家写代码时候要注意,对于未阻塞的锁,需要进行判断 acquire 时候是否获得 True。
请注意,一个线程获得的锁,可以由其他线程进行释放!
在上面的代码示意中,每个线程会去 acquire 锁,如果这个锁被其他线程获取了,则 acquire 失败。但是,一个线程获得的锁,可以由其他线程进行释放!
参看下面代码,在主线程中获取了锁但是没有释放,然后子线程中一直无法获得锁,一直在阻塞状态,这时候就进入了死等待状态。一直等我在其他线程敲下了lock.release(),子线程才得以继续。
- import threading
- import time
-
- my_list=[0,1,2]
- lock=threading.Lock()
-
-
- def show_list_withLock(n):
- lock.acquire()
- print("show_list_withLock:Now sleep 5 seconds!")
- print("show_list_withLock1",time.ctime())
- time.sleep(5)
- my_list.append(n)
- print(my_list)
- print("show_list_withLock2",time.ctime())
- lock.release()
-
-
- print("Get the lock and sleep 10 seconds!")
- print("MainThread 1",time.ctime())
- lock.acquire()
- time.sleep(10)
- print("MainThread 2",time.ctime())
-
- t= threading.Thread(target=show_list_withLock, args=(3, ))
- t.start()
另外 acquire() 函数另一个参数是timeout=-1,表示等待获取锁的时间,-1表示一直等待。如果超过timeout设定的时间还没有获取到锁就会有获取锁失败。如果使用上 timeout为正值,也可以有效地减少等待时间,减少死锁的发生。
threading.RLock 和 threading.Lock 的区别在于,同一个线程它可以再次获得已持有的锁(锁递归),而不用阻塞等待。可重入锁只能被所有者释放。使用可重入锁需要注意,有几次acquire,必须对应几次release。
1、threading.Lock 同一时刻只能被上锁一次,而 threading.RLock 可以被同一线程上 N 次锁
2、threading.Lock 可以被非所有者释放,而 threading.RLock 只能被所有者释放
序号 | 属性和方法 | 描述 |
1 | 方法:acquire (blocking=True) | 锁定锁,返回一个布尔值指示是否锁定。blocking 指示我们是否等待这个锁可用。blocking = False 且另外一个线程占据了这个锁,那么立刻返回 False。blocking = True, 且另外一个线程占据了这个锁,那么则等待这个锁释放,拿到这个锁,然后返回 True。 请注意,阻塞操作是可中断的。在所有其他情况下,该方法将立即返回True。 确切地说,如果当前线程已经持有锁,则其内部计数器简单地递增。如果没有人拿着锁,获取锁,其内部计数器初始化为1。 |
2 | 方法:release() | 释放锁,允许在等待该锁的阻塞队列中的某个线程获得这个锁。这个锁当前应该是被锁定的状态,但是必须是锁定这个锁的线程再次锁定它。 |
针对 “threading.RLock 可以被同一线程上 N 次锁 ” 的特性,下面的程序不会出现阻塞。
- import threading
- lock = threading.RLock()
- print(lock.acquire())
- print(lock.acquire())
- lock.release()
- lock.release()
但是下面的代码每个线程依次 acquire 和 release 时候,可能就会报错 “
RuntimeError: cannot release un-acquired lock
”。请注意,是可能,而不是肯定,因为这和机器实时运行状态相关。
之所以会报错,是因为可能没有获得可重入锁。
那么使用 RLock 时候,代码设计就要小心。
- my_list=[]
- lock=threading.RLock()
- threads=[]
-
- def show_list_withLock(n):
- lock.acquire()
- print("Current n is ==>",n)
- my_list.append(n)
- print(my_list)
- time.sleep(n)
- print(n,time.ctime())
- lock.release()
-
- numbers=list(range(10))
-
- for num in numbers:
- t= threading.Thread(target=show_list_withLock, args=(num, ))
- threads.append(t)
-
- for t in threads:
- t.start()
'''
要是大家觉得写得还行,麻烦点个赞或者收藏吧,想个博客涨涨人气,非常感谢!
'''
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。