前言
我们在日常开发中,不可避免要处理并发的情况。常用并发手段有多进程和多线程。这篇文章主要讲多线程,后面会专门出一篇多进程的文章。
线程
线程(Thread)也叫轻量级进程,是程序执行流的最小单元。它被包涵在进程之中,是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
在Python3中引入了threading模块,同时thread模块在Python3中改名为_thread模块,threading模块相较于thread模块,对于线程的操作更加的丰富,而且threading模块本身也是相当于对thread模块的进一步封装而成,thread模块有的功能threading模块也都有,所以涉及到多线程的操作,一般都是使用threading模块。
多线程的优点
使用多线程编程具有如下几个优点:
-
提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
-
进程之间不能共享内存,但线程之间共享内存非常容易。
-
开销少,操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
-
Python 语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了 Python 的多线程编程。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
threading函数
Python程序启动时,Python解释器会启动一个继承自threading.Thread的threading._MainThread线程对象作为主线程,所以涉及到threading.Thread的方法和函数时通常都算上了这个主线程的,比如在启动程序时打印threading.active_count()的结果就已经是1了。
- threading.active_count():返回当前存活的threading.Thread线程对象数量,等同于len(threading.enumerate())。
- threading.current_thread():返回此函数的调用者控制的threading.Thread线程对象。如果当前调用者控制的线程不是通过threading.Thread创建的,则返回一个功能受限的虚拟线程对象。
- threading.get_ident():返回当前线程的线程标识符。注意当一个线程退出时,它的线程标识符可能会被之后新创建的线程复用。
- threading.enumerate():返回当前存活的threading.Thread线程对象列表。
- threading.main_thread():返回主线程对象,通常情况下,就是程序启动时Python解释器创建的threading._MainThread线程对象。
- threading.stack_size([size]):返回创建线程时使用的堆栈大小。也可以使用可选参数size指定之后创建线程时的堆栈大小,size可以是0或者一个不小于32KiB的正整数。如果参数没有指定,则默认为0。如果系统或者其他原因不支持改变堆栈大小,则会报RuntimeError错误;如果指定的堆栈大小不合法,则会报ValueError,但并不会修改这个堆栈的大小。32KiB是保证能解释器运行的最小堆栈大小,当然这个值会因为系统或者其他原因有限制,比如它要求的值是大于32KiB的某个值,只需根据要求修改即可。
threading常量
threading.TIMEOUT_MAX:指定阻塞函数(如Lock.acquire()、RLock.acquire()、Condition.wait()等)中参数timeout的最大值,在给这些阻塞函数传参时如果超过了这个指定的最大值会抛出OverflowError错误。
实现线程
首先创建 Thread 对象,然后让它们运行,每个 Thread 对象代表一个线程,在每个线程中我们可以让程序处理不同的任务。程序运行时默认就是在主线程上。
线程特点
- 每个线程都有一个唯一标示符,来区分线程中的主次关系
- 线程数量:主线程数 + 子线程数
- 主线程:mainThread,Main函数或者程序主入口,都可以称为主线程。子线程:Thread-x 使用 threading.Thread() 创建出来的都是子线程
- threading.Thread()中daemon 属性默认是 False,这使得 MainThread 需要等待它的结束,自身才结束。所以一般情况下,主线程会等所有的子线程结束之后才会结束。
传递参数的方法
- 使用args 传递参数 threading.Thread(target=sing, args=(10, 100, 100))
- 使用kwargs传递参数 threading.Thread(target=sing, kwargs={“a”: 10, “b”:100, “c”: 100})
- 同时使用 args 和 kwargs 传递参数 threading.Thread(target=sing, args=(10, ), kwargs={“b”: 100,“c”: 100})
创建线程的两种方式:
-
普通方式:直接创建 Thread ,将一个 callable 对象从类的构造器传递进去,这个 callable 就是回调函数,用来处理任务。
-
自定义线程:继承 threading.Thread,然后复写 run() 方法,在 run() 方法中编写任务处理代码,然后创建这个 Thread 的子类。
普通方式
import threading
import time
def run(n):
print("task", n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))
t1.start() #当调用start()时,才会真正的创建线程,并且开始执行
t2.start()
----------------------------------
>>> task t1
>>> task t2
>>> 2s
>>> 2s
>>> 1s
>>> 1s
>>> 0s
>>> 0s
自定义线程
import threading
import time
class MyThread(threading.Thread):
def __init__(self, n):
super(MyThread, self).__init__() # 重构run函数必须要写
self.n = n
def run(self):
print("task", self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == "__main__":
t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t2.start()
----------------------------------
>>> task t1
>>> task t2
>>> 2s
>>> 2s
>>> 1s
>>> 1s
>>> 0s
>>> 0s
守护线程
我们可以使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。
import threading
import time
def run(n):
print("task", n)
time.sleep(1) #此时子线程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')
if __name__ == '__main__':
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True) #把子进程设置为守护线程,必须在start()之前设置
t.start()
print("end") #此处主线程结束,子线程也随之结束。
----------------------------------
>>> task t1
>>> end
使用join()进行线程阻塞,让主线程等待子线程结束
默认的情况是,join() 会一直等待对应线程的结束。但可以通过参数赋值,等待规定的时间就好了。
thread.join(1.0)
看下面的例子:
import threading
import time
def run(n):
print("task", n)
time.sleep(1) #此时子线程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')
if __name__ == '__main__':
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True) #把子进程设置为守护线程,必须在start()之前设置
t.start()
t.join(1.0) # 设置主线程等待子线程结束
print("end")
----------------------------------
>>> task t1
>>> 3
>>> 2
>>> 1
>>> end
多线程之间共享数据
线程是进程的执行单元,进程是系统分配资源的最小单位,所以在同一个进程中的多线程是共享资源的。
import threading
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print("in work1 g_num is : %d" % g_num)
def work2():
global g_num
print("in work2 g_num is : %d" % g_num)
if __name__ == '__main__':
t1 = threading.Thread(target=work1)
t1.start()
time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()
----------------------------------
>>> in work1 g_num is : 103
>>> in work2 g_num is : 103
Lock
由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 像下面的代码, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。为了方式上面情况的发生,就出现了互斥锁(Lock),是一种多线程同步的手段。
Lock 有 locked 和 unlocked 两种状态,而这两中状态之间是可以转换的.
- 当 Lock 是 unlocked 状态时候,某个线程调用 acquire() 可以获取这个 Lock,并且将 Lock将状态转换成 locked 状态,并且线程不会阻塞。
- 但当 Lock 是 locked 状态时,某个线程调用 acquire() 会阻塞自己,直到其他的线程将 Lock 的状态变成 unlocked。
- 当 Lock 是 locked 状态时,调用 release() 方法,可以释放一个 Lock,这样其它线程就可以获取这个 Lock 了。
- 但当 Lock 是 unlocked 状态时,某个线程调用 release(),程序会抛出 RuntimeError 异常。
一般来说,acquire() 和 release() 方法在单个线程当中都是成对使用的。利用lock机制,就可以避免多个线程同时修改同一份数据。
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire() #加锁
temp=n
time.sleep(0.1)
n=temp-1
lock.release() #解锁
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
上面加了lock之后,即便有多个线程同时调用work(),在同一时间也只能有一个线程可以对n值进行修改。
递归锁RLock
递归锁和普通锁的差别在于加入了“所属线程”和“递归等级”的概念,释放锁必须有获取锁的线程来进行释放,同时,同一个线程在释放锁之前再次获取锁将不会阻塞当前线程,只是在锁的递归等级上加了1(获得锁时的初始递归等级为1)。
使用普通锁时,对于一些可能造成死锁的情况,可以考虑使用递归锁来解决。
- acquire(blocking=True, timeout=-1):与普通锁的不同之处在于:当使用默认值时,如果这个线程已经拥有锁,那么锁的递归等级加1。线程获得锁时,该锁的递归等级被初始化为1。当多个线程被阻塞时,只有一个线程能在锁被解时获得锁,这种情况下,acquire()是没有返回值的。
- release():没有返回值,调用一次则递归等级减1,递归等级为零时表示这个线程的锁已经被释放掉,其他线程可以获取锁了。可能在一个线程中调用了多次acquire(),导致锁的递归等级大于了1,那么就需要调用对应次数的release()来完全释放锁,并将它的递归等级减到零,其他的线程才能获取锁,不然就会一直被阻塞着。
"""
在普通锁中可能造成死锁的情况,可以考虑使用递归锁解决
"""
import time
import threading
# 如果是使用的两个普通锁,那么就会造成死锁的情况,程序一直阻塞而不会退出
# rlock_hi = threading.Lock()
# rlock_hello = threading.Lock()
# 使用成一个递归锁就可以解决当前这种死锁情况
rlock_hi = rlock_hello = threading.RLock()
def test_thread_hi():
# 初始时锁内部的递归等级为1
rlock_hi.acquire()
print('线程test_thread_hi获得了锁rlock_hi')
time.sleep(2)
# 如果再次获取同样一把锁,则不会阻塞,只是内部的递归等级加1
rlock_hello.acquire()
print('线程test_thread_hi获得了锁rlock_hello')
# 释放一次锁,内部递归等级减1
rlock_hello.release()
# 这里再次减,当递归等级为0时,其他线程才可获取到此锁
rlock_hi.release()
def test_thread_hello():
rlock_hello.acquire()
print('线程test_thread_hello获得了锁rlock_hello')
time.sleep(2)
rlock_hi.acquire()
print('线程test_thread_hello获得了锁rlock_hi')
rlock_hi.release()
rlock_hello.release()
def main():
thread_hi = threading.Thread(target=test_thread_hi)
thread_hello = threading.Thread(target=test_thread_hello)
thread_hi.start()
thread_hello.start()
if __name__ == '__main__':
main()
>>> 线程test_thread_hi获得了锁rlock_hi
>>> 线程test_thread_hi获得了锁rlock_hello
>>> 线程test_thread_hello获得了锁rlock_hello
>>> 线程test_thread_hello获得了锁rlock_hi
信号量对象:threading.Semaphore
一个信号量管理一个内部计数器,acquire()方法会减少计数器,release()方法则增加计数器,计数器的值永远不会小于零,当调用acquire()时,如果发现该计数器为零,则阻塞线程,直到调用release()方法使计数器增加。
threading.Semaphore(value=1):value参数默认值为1,如果指定的值小于0,则会报ValueError错误。一个信号量对象管理一个原子性的计数器,代表release()方法调用的次数减去acquire()方法的调用次数,再加上一个初始值。
-
acquire(blocking=True, timeout=None):默认情况下,在进入时,如果计数器大于0,则减1并返回True,如果等于0,则阻塞直到使用release()方法唤醒,然后减1并返回True。被唤醒的线程顺序是不确定的。如果blocking设置为False,调用这个方法将不会发生阻塞。timeout用于设置超时的时间,在timeout秒的时间内没有获取到信号量,则返回False,否则返回True。
-
release():释放一个信号量,将内部计数器增加1。当计数器的值为0,且有其他线程正在等待它大于0时,唤醒这个线程。
"""
通过信号量对象管理一次性运行的线程数量
"""
import time
import threading
# 创建信号量对象,初始化计数器值为3
semaphore3 = threading.Semaphore(3)
def thread_semaphore(index):
# 信号量计数器减1
semaphore3.acquire()
time.sleep(2)
print('thread_%s is running...' % index)
# 信号量计数器加1
semaphore3.release()
def main():
# 虽然会有9个线程运行,但是通过信号量控制同时只能有3个线程运行
# 第4个线程启动时,调用acquire发现计数器为0了,所以就会阻塞等待计数器大于0的时候
for index in range(9):
threading.Thread(target=thread_semaphore, args=(index, )).start()
if __name__ == '__main__':
main()
事件对象:threading.Event
一个事件对象管理一个内部标志,初始状态默认为False,set()方法可将它设置为True,clear()方法可将它设置为False,wait()方法将线程阻塞直到内部标志的值为True。
如果一个或多个线程需要知道另一个线程的某个状态才能进行下一步的操作,就可以使用线程的event事件对象来处理。
- is_set():当内部标志为True时返回True。
- set():设置内部标志为True。此时所有等待中的线程将被唤醒,调用wait()方法的线程将不会被阻塞。
- clear():将内部标志设置为False。所有调用wait()方法的线程将被阻塞,直到调用set()方法将内部标志设置为True。
- wait(timeout=None):阻塞线程直到内部标志为True,或者发生超时事件。如果调用时内部标志就是True,那么不会被阻塞,否则将被阻塞。timeout为浮点类型的秒数。
"""
事件对象使用实例
"""
import time
import threading
# 创建事件对象,内部标志默认为False
event = threading.Event()
def student_exam(student_id):
print('学生%s等监考老师发卷。。。' % student_id)
event.wait()
print('开始考试了!')
def invigilate_teacher():
time.sleep(5)
print('考试时间到,学生们可以开始考试了!')
# 设置内部标志为True,并唤醒所有等待的线程
event.set()
def main():
for student_id in range(3):
threading.Thread(target=student_exam, args=(student_id, )).start()
threading.Thread(target=invigilate_teacher).start()
if __name__ == '__main__':
main()
输出为
学生0等监考老师发卷。。。
学生1等监考老师发卷。。。
学生2等监考老师发卷。。。
考试时间到,学生们可以开始考试了!
开始考试了!
开始考试了!
开始考试了!
附录:with语法
这个模块中所有带有acquire()和release()方法的对象,都可以使用with语句。当进入with语句块时,acquire()方法被自动调用,当离开with语句块时,release()语句块被自动调用。包括Lock、RLock、Condition、Semaphore。
以下语句:
with some_lock:
# do something
pass
相当于:
some_lock.acquire()
try:
# do something
pass
finally:
some_lock.release()
参考
1.https://docs.python.org/zh-cn/3/library/threading.html?highlight=threading#module-threading
2.https://www.jb51.net/article/176573.htm
3.https://frank909.blog.csdn.net/article/details/85101144
4.https://www.cnblogs.com/luyuze95/p/11289143.html
5.https://www.cnblogs.com/guyuyun/p/11185832.html#:~:text=Python%E7%9A%84%E7%BA%BF,ading%E6%A8%A1%E5%9D%97%E3%80%82
6.https://zhuanlan.zhihu.com/p/43495190
长按关注我的公众号哦!