1 概念
1.1 线程
线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) – 这就是线程的退让。
线程可以分为:
- **内核线程:**由操作系统内核创建和撤销。
- **用户线程:**不需要内核支持而在用户程序中实现的线程。
1.2 多线程
线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的多个线程共享同一个进程的虚拟空间。线程共享的环境包括进程代码段、进程的公有数据等,利用这些共享的数据,线程之间很容易实现通信。
操作系统在创建进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能要高得多。
总结起来,使用多线程编程具有如下几个优点:
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
- Python 语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了 Python 的多线程编程。
2 线程实现
Python3 线程中常用的两个模块为:
- _thread
- threading(推荐使用)
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 “_thread”。
2.1 threading
threading模块创建线程有两种方式:
- 从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程
- 直接通过threading.Thread创建一个线程
import threading
import time
def run(name):
print("task", name)
time.sleep(1)
print(name, "1s")
class MyThread(threading.Thread):
def __init__(self, name):
super(MyThread, self).__init__()
self.name = name
def run(self):
print("task", self.name)
time.sleep(1)
print(self.name, "1s")
def threading_thread():
"""
普通创建方式
:return:
"""
t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))
t1.start()
t2.start()
def myself_thread():
"""
自定义线程
:return:
"""
t1 = MyThread("t3")
t2 = MyThread("t4")
t1.start()
t2.start()
if __name__ == "__main__":
threading_thread()
myself_thread()
2.2 守护进程
在python中,线程通过setDaemon(True|False)来设置是否为守护线程。如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。即在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志。(thread.setDaemon(True))就表示这个线程“不重要”。默认情况下,新线程通常会生成非守护线程或普通线程,如果新线程在运行,主线程将永远等待,无法正常退出。
守护线程的作用:
守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
import threading
import time
def run(name):
print("task")
time.sleep(1)
print(name)
def daemon_thread():
"""
守护进程——当主线程结束时,子线程也将立即结束,不再执行
:return:
"""
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True) # 把子进程设置为守护线程,必须在start()之前设置
t.start()
def daemon__thread():
"""
守护进程——守护线程执行结束之后,主线程再结束
:return:
"""
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True)
t.start()
t.join()
if __name__ == "__main__":
print("start")
# daemon_thread()
daemon__thread()
2.3 多线程共享全局变量
import threading
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print(f"in work1 g_num is :{g_num}")
def work2():
global g_num
print(f"in work2 g_num is :{g_num}")
if __name__ == '__main__':
t1 = threading.Thread(target=work1)
t1.start()
time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()
2.4 锁
2.4.1 全局解释器锁(GIL)
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己枷锁处理,如下图 :
2.4.2 互斥锁
GIL和Lock是两把锁,保护的数据不一样,前者是解释器级别的(保护的是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,
互斥锁保证同一时间只有一个线程修改数据。加了互斥锁,线程就变成串行了。
import time
from threading import Thread, Lock, RLock
def work(name, lock):
"""
互斥锁
"""
global n
lock.acquire()
temp = n
n = temp - 1
print(name, ":", n)
lock.release()
if __name__ == '__main__':
n = 100
# 互斥锁
lock = Lock()
p_l = []
for i in range(10):
p = Thread(target=work, args=(f'p{i}', lock,))
p_l.append(p)
p.start()
for p in p_l:
p.join()
2.4.3 递归锁
递归锁对象允许多次acquire和多次release。
在Python中为了支持在同一线程中多次请求同一资源,python提供了递归锁RLock。
RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。
直到一个线程所有的acquire都被release,其他的线程才能获得资源。
class Account:
def __init__(self, account, balance):
self.account = account
self._balance = balance
self.lock = RLock()
def get_balance(self):
return self._balance
def draw(self, draw_num):
self.lock.acquire()
try:
if self._balance < draw_num:
print("取钱失败,余额不足")
else:
print(f"成功取出{draw_num}")
self._balance -= draw_num
print(f"余额为{self._balance}\n")
finally:
self.lock.release()
def draw(account: Account, num: int):
account.draw(num)
if __name__ == '__main__':
account = Account("xou", 200)
d1 = Thread(target=draw, args=(account, 100,))
d2 = Thread(target=draw, args=(account, 150,))
d1.start()
d2.start()
2.5 信号量
import threading
import time
def run(n, lock):
lock.acquire() # 加锁
print(f"run the thread:{n}\n")
time.sleep(1)
lock.release() # 释放
if __name__ == '__main__':
num = 0
semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行
for i in range(22):
t = threading.Thread(target=run, args=(f"t-{i}", semaphore))
t.start()
2.6 Event(事件对象)
方法:
is_set()
当且仅当内部标志为True
时返回True
。
set()
将内部标志设置为True
。所有等待它成为True
的线程都被唤醒。当标志保持在True
的状态时,线程调用wait()
是不会阻塞的。
clear()
将内部标志重置为False
。随后,调用wait()
的线程将阻塞,直到另一个线程调用set()
将内部标志重新设置为True
。
wait(timeout=None)
阻塞直到内部标志为真。如果内部标志在wait()
方法调用时为True
,则立即返回。否则,则阻塞,直到另一个线程调用set()
将标志设置为True
,或发生超时。
该方法总是返回True
,除非设置了timeout
并发生超时。
# 利用Event类模拟红绿灯
import threading
import time
event = threading.Event()
def lighter():
count = 1
event.set() # 初始值为绿灯
while True:
if 5 < count <= 10:
event.clear() # 红灯,清除标志位
print(f"{count}now is red")
elif count > 10:
event.set() # 绿灯,设置标志位
count = 0
else:
print(f"{count}now is green")
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set(): # 判断是否设置了标志位
print("[%s] running..." % name)
time.sleep(1)
else:
print("[%s] sees red light,waiting..." % name)
event.wait()
print("[%s] green light is on,start going..." % name)
if __name__ == '__main__':
light = threading.Thread(target=lighter, )
light.start()
car = threading.Thread(target=car, args=("MINI",))
car.start()