目录
python多线程通过threading模块实现,支持并发执行任务,但受GIL限制,适合I/O密集型场景。
(0)重要名词
线程冲突 → 锁 → 锁与with → 死锁 → Rlock
线程信号量 vs 锁定线程数量
线程同步与线程通信
Lock、Event 、Condition
线程调度
生产者消费者模式
线程池
定时线程
后台线程
线程独立空间(TLS)
(1)如何创建线程
用_thread模块
# 普通运行的程序都是主线程,按代码顺序运行
# 多线程,就是使用特定模块(此处用的_thread模块)创建了几个次线程(小弟线程),并发运行
# 使用_thread创建的次线程,与后面的主线程同时运行,当主线程结束时,若次线程还未结束,会被强制结束
import win32api # 引用系统函数
import _thread # 创建多线程的一种模块
def show(num):
win32api.MessageBox(0, "你的账户很危险"+str(num), "来自支付宝的问候", num)
# 顺序执行
# for i in range(5):
# show(i)
# 伪并发
for i in range(5):
_thread.start_new_thread(show, (i,))
# 第二参数为元组,用于传递show函数的参数,单个参数时仍以元组形式,比如(3,)
show(5)
while True: # 让主线程不死 # 主线程不死,子线程就不会死
pass
用threading模块
①直接使用线程类接口函数创建多线程
import threading
import win32api
def show(i):
win32api.MessageBox(0, "你的账户很危险" + str(i), "来自支付宝的问候", 6)
print(threading.current_thread().name) # threading.current_thread().name返回当前线程的名称
threading.Thread(target = show, args = (2,)).start() # 匿名线程对象
for i in range(5):
threading.Thread(target = show, args = (i,)).start()
②主线程会等待子线程全部运行完才会结束
以下示例代码会演示,主线程并不会结束,而是要等子线程结束后才会结束
import time
import threading
def write_data(num):
time.sleep(10)
file = open('./data.txt', 'a')
file.write(f"{num}\n")
file.close()
th = threading.Thread(target=write_data, args=(100,))
th.start()
③继承线程类重写run函数来创建多线程
import threading
import win32api
class Mythread(threading.Thread): #继承threading.Thread实现多线程
def __init__(self, num):
threading.Thread.__init__(self) # 父类初始化
self.num = num
def run(self): # run重写
win32api.MessageBox(0, "你的账户很危险" + str(self.num), "来自支付宝的问候", 6)
for i in range(5):
t = Mythread(i) # 初始化
t.start() # 开启
④获取线程名字
线程对象self.name或者self.getName(),返回当前线程名字
import threading
import win32api
class Mythread(threading.Thread): #继承threading.Thread实现多线程
def __init__(self, num):
threading.Thread.__init__(self) # 父类初始化
self.num = num
def run(self): # run重写
win32api.MessageBox(0, "你的账户很危险" + str(self.num), "来自支付宝的问候"+self.name, 6)
for i in range(5):
t = Mythread(i) # 初始化
t.start() # 开启
(2)线程对象的join方法
- 若某子线程使用了join方法,则主线程要等到该线程执行完毕后才能继续执行
- 若子线程未使用join方法,即不阻塞,则主线程和子线程同时运行,同时若主线程先运行还会等待子线程运行完,整个程序才会结束
①顺序执行
import threading
import win32api
class Mythread(threading.Thread):
def run(self):
win32api.MessageBox(0, "你的账户很危险", "来自支付宝的问候", 6)
for i in range(5):
t = Mythread() # 创建线程对象
t.start() # 线程开启
t.join() # 主线程等待线程t执行完成
print("当无join时,这句话在对话框结束前执行;当有join时,这句话在对话框结束后执行")
②并发执行
import threading
import win32api
class Mythread(threading.Thread):
def run(self):
win32api.MessageBox(0, "你的账户很危险", "来自支付宝的问候", 6)
# 同时运行5个次线程
threads = [] # 集合list
for i in range(5):
t = Mythread() # 初始化
t.start()
threads.append(t) #加入线程集合
# 使用线程类的join函数,实现“执行完所有同时运行的次线程后再运行主线程”
for mythd in threads: # mythd是一个线程
mythd.join() # 主线程等待线程t执行完成,不需要阻塞
print("game over")
# 这里有join,子线程全部运行完毕后,才会打印game over
# 如果没有join,则会立即打印game over,同时子线程也继续运行,
# 子线程运行完,整个程序才会结束
(3)多线程共享全局变量
对于全局变量x,每个线程访问和修改x时,操作的是同一个x
import threading
data = 10
def func(num):
global data
data += num
for i in range(5):
t = threading.Thread(target=func, args=(i,))
t.start()
t.join()
print(data)
(4)线程冲突
# 什么是线程冲突
# 线程冲突,多个线程同时访问同一个资源使得结果在逻辑上出现错误,尽管没有弹出异常
import threading
import time
num = 0
def add():
global num
for i in range(1000000):
num += 1
print(num)
# 目标:执行五次add函数使得num的值变为5000000
for i in range(5):
add()
# 如果使用多线程会产生错误
num = 0
print("-"*20)
for i in range(5):
t = threading.Thread(target = add, args = ())
t.start()
(5)用锁解决线程冲突
# mutex = threading.Lock() # 创建锁,默认是开锁状态
# mutex.acquire(True) # 对锁进行锁住尝试,若锁住成功继续干活,并且返回True,没有锁住成功就一直请求
# 当锁是锁住状态时,锁住尝试将会不成功
# mutex.acquire的参数中的blocking布尔变量,是否锁住;timeout浮点数,锁住几秒,默认一直锁住
# mutex.release() # 释放锁 # 当释放锁后,其他人就可以锁住了
# 锁住的好处,比如写1000个线程爬虫,爬虫可以同步,但在往文件里写东西时不能同时写,必须要锁住(向同一个文件写东西时经常用锁)
import threading
import time
num = 0
mutex = threading.Lock()
class Mythread(threading.Thread):
def run(self):
global num
if mutex.acquire(True):
for i in range(1000000):
num += 1
mutex.release()
print(num)
print("game start")
threads = []
for i in range(5):
t = Mythread()
t.start()
threads.append(t)
for i in range(len(threads)):
threads[i].join()
print("game over")
(6)用with简化锁操作
# with就是锁“请求”和“释放”的结合,更简洁
import threading
mutex = threading.Lock()
num = 0 # 全局变量多个线程可以读写,传递数据
class addthread(threading.Thread):
# def __init__(self):
# threading.Thread.__init__(self)
def run(self):
global num
with mutex:
for i in range(1000000):
num += 1
print(num)
threads = []
for i in range(5):
t = addthread()
t.start()
threads.append(t)
for t in threads:
t.join()
print("game over")
(7)死锁
# 死锁就是锁本身处于锁定状态,但是却请求锁定操作,无法成功的情况,因为根据锁的锁定原理,只有锁处于释放状态时,才能锁定成功
# 例子,男生要等到女方道歉才肯道歉,女生要等到男生道歉才肯道歉,于是产生死锁
# 解决办法,比如女方先道歉,则在锁住后执行相关命令后,就释放锁,而不是要等到男方释放锁以后才释放锁
import threading
import time
boymutex = threading.Lock() # 创建男孩的锁
girlmutex = threading.Lock() # 创建女孩的锁
# 男孩线程类
class boythread(threading.Thread):
def run(self):
if boymutex.acquire(1):
print(self.name + "男方尝试道歉,但想等女方道歉后再道歉")
time.sleep(3)
if girlmutex.acquire(1):
print(self.name + "girl say I am sorry")
girlmutex.release()
boymutex.release()
# 女孩线程类
class girlthread(threading.Thread):
def run(self):
if girlmutex.acquire(1):
print(self.name + "女方尝试道歉,但想等男方道歉后再道歉") # self.name线程名称
time.sleep(3)
#girlmutex.release() # 让女孩先道歉,可以解决死锁问题
if boymutex.acquire(1):
print(self.name+"boy say I am sorry")
boymutex.release()
girlmutex.release()
#开启两个线程
bt = boythread()
bt.start()
gt = girlthread()
gt.start()
(8)用Rlock创建锁避免死锁
# 使用Rlock创建的锁可以避免单线程死锁
# 单线程死锁:对一个线程加锁再加锁
import threading
num = 0
mutex = threading.RLock()
class Mythread(threading.Thread):
def run(self):
global num
if mutex.acquire(1):
print(self.name, num)
if mutex.acquire(1):
num += 1000
mutex.release()
mutex.release()
for i in range(5): # 开启五个线程
t = Mythread()
t.start()
(9)线程信号量(限制最大线程数量)
①线程信号量的本质和原理
多线程同时运行,能提高程序的运行效率,但是并非线程越多越好,而semaphore信号量可以通过内置计数器来控制同时运行线程的数量,启动线程(消耗信号量)内置计数器会自动减一,线程结束(释放信号量)内置计数器会自动加一;内置计数器为零,启动线程会阻塞,直到有本线程结束或者其他线程结束为止;
信号量本质上还是锁,只不过这个锁有很多把钥匙,同一时间只允许有限个线程进程操作(实现有限个数据的并发),但是线程数开了很多个的(信号量的参数不是开的线程数。只是代表同一时间允许并发的线程数)
②semaphore信号量相关函数介绍
- acquire() — 消耗信号量,内置计数器减一;
- release() — 释放信号量,内置计数器加一;
在semaphore信号量有一个内置计数器,控制线程的数量,acquire()会消耗信号量,计数器会自动减一;release()会释放信号量,计数器会自动加一;当计数器为零时,acquire()调用被阻塞,直到release()释放信号量为止。
示例1
# 以爬虫为例,不可能每个网页都创建一个线程,因为内存有限,所以要限定线程数量
import threading
import time
sem2 = threading.Semaphore(2) # 限制线程的最大数量为2个
sem4 = threading.Semaphore(4) # 限制线程的最大数量为4个
def gothread():
with sem4: # 锁定数量
for i in range(10):
print(threading.current_thread().name, i)
# threading.current_thread().name返回当前线程的名称
time.sleep(1)
for i in range(5):
threading.Thread(target = gothread).start()
示例2
创建多个线程,限制同一时间最多运行5个线程,示例代码如下:
# 导入线程模块
import threading
# 导入时间模块
import time
# 添加一个计数器,最大并发线程数量5(最多同时运行5个线程)
semaphore = threading.Semaphore(5)
def foo():
semaphore.acquire() # 计数器获得锁
time.sleep(2) # 程序休眠2秒
print("当前时间:", time.ctime()) # 打印当前系统时间
semaphore.release() # 计数器释放锁
if __name__ == "__main__":
thread_list = list()
for i in range(20):
t = threading.Thread(target=foo, args=()) # 创建线程
thread_list.append(t)
t.start() # 启动线程
for t in thread_list:
t.join()
print("程序结束!")
输出结果:
当前时间: Wed Oct 23 22:21:59 2019
当前时间: Wed Oct 23 22:21:59 2019
当前时间: Wed Oct 23 22:21:59 2019
当前时间: Wed Oct 23 22:21:59 2019
当前时间: Wed Oct 23 22:21:59 2019
当前时间: Wed Oct 23 22:22:01 2019
当前时间: Wed Oct 23 22:22:01 2019
当前时间: Wed Oct 23 22:22:01 2019
当前时间: Wed Oct 23 22:22:01 2019
当前时间: Wed Oct 23 22:22:01 2019
当前时间: Wed Oct 23 22:22:03 2019
当前时间: Wed Oct 23 22:22:03 2019
当前时间: Wed Oct 23 22:22:03 2019
当前时间: Wed Oct 23 22:22:03 2019
当前时间: Wed Oct 23 22:22:03 2019
当前时间: Wed Oct 23 22:22:05 2019
当前时间: Wed Oct 23 22:22:05 2019
当前时间: Wed Oct 23 22:22:05 2019
当前时间: Wed Oct 23 22:22:05 2019
当前时间: Wed Oct 23 22:22:05 2019
程序结束!
根据打印的日志可以看出,同一时间只有5个线程运行,间隔两秒之后,再次启动5个线程,直到20个线程全部运行结束为止;如果没有设置信号量Semapaore,创建线程直接start(),输出的时间全部都是一样的,这个问题比较简单,可以自己去实验一下!
(10)线程屏障(所需最低线程数量)
达到指定线程数量才能执行某项任务
# 比如开启1000个线程,但必须每次要有3个线程的数据时才能写到一个文件,缺一不可
# 比如由1000个线程,先找3个线程运行,再找3个运行,当最后不够3个时,就等着
# 只有线程数量为3的整数倍时程序能正常运行完
import time
import threading
bar = threading.Barrier(3) # 必须凑到3个才能一起执行
# help(bar) → abort(self) reset(self) wait(self, timeout=None)
# help(bar)
def sever():
print(threading.current_thread().name, "start")
# 打印当前线程的名称
time.sleep(5)
# 5秒的缓冲时间足以打印完所有线程的名称
bar.wait() # 锁定线程匹配数量,只有达到指定线程数量时才可继续运行
print(threading.current_thread().name, "end")
for i in range(8):
threading.Thread(target = sever).start()
(11)线程事件
# 用threading.Event()来创建一个事件标志,默认值为False,可以让子线程来调用该标志具有的方法来是否阻塞该子线程
Event和Lock的区别:锁在同一时间只放一个线程通行,而事件的作用要么阻塞全部线程全部放全部线程通行。
# event对象主要有四种方法可以调用:
event.wait(timeout=None) | 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行 |
event.set() | 将event的标志设置为True,调用wait方法的所有线程将被唤醒 |
event.clear() | 将event的标志设置为False,调用wait方法的所有线程将被阻塞(不用此方法会使得wait方法只生效一次,因此在循环中使用wait方法时,记得还要加上此方法) |
event.isSet() | 判断event的标志是否为True |
例子
import threading
import time
e = threading.Event() # 标志
def go(event):
event.wait() # 阻塞线程,后续代码不再执行,等待命令set后再执行
print("go")
threading.Thread(target = go, args = (e,)).start() # 开启一个线程
time.sleep(5)
e.set() # 唤醒线程
(12)线程同步和线程通信
# 线程冲突 → 解决办法:锁
# 使用锁后,这种一个线程“等待”另一个线程的过程,就是线程同步
# 线程同步和线程通信的区别:线程同步谁先抢到就是谁的,线程通信是指定下一个线程
# 通常使用lock产生线程同步,用event产生线程通信
例子:线程通信
# 线程通信例子
import threading
import time
class mythread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.event = threading.Event()
def run(self):
global mystr
print(self.name + "start")
self.event.wait() # 也可以将此阻塞语句放在主线程里面设计
mystr += " " + self.name
print(self.name, mystr)
# 用mystr变量来记录子线程的唤醒顺序
mystr = "order:"
threads = []
for i in range(5):
t = mythread()
threads.append(t)
t.start()
# 按指定顺序唤醒子线程
orderList = [2, 1, 4, 0, 3]
for i in orderList:
time.sleep(3)
threads[i].event.set()
for thd in threads:
thd.join()
print(mystr)
(13)condition
# condition:像lock和event的结合体,兼顾线程同步和线程通信
①像锁一样
import threading
import time
cond = threading.Condition() # 线程条件变量
def go1():
with cond: #锁定cond期间,线程2的go2不能使用cond,这里就像锁一样
for i in range(10):
time.sleep(1)
print(threading.current_thread().name, i)
def go2():
with cond:
for i in range(10):
time.sleep(1)
print(threading.current_thread().name, i)
threading.Thread(target = go1).start()
threading.Thread(target = go2).start()
②结合lock和event
import threading
import time
cond = threading.Condition() # 线程条件变量
def go1():
with cond: #锁定cond期间,线程2的go2不能使用cond,这里就像锁一样
for i in range(10):
time.sleep(1)
print(threading.current_thread().name, i)
if i == 5:
cond.wait() #像event的wait方法一样阻塞调用该方法的线程
#使用wait方法后线程2的go2可以使用cond
def go2():
with cond:
for i in range(10):
time.sleep(1)
print(threading.current_thread().name, i)
cond.notify() #像event的set方法一样唤醒因wait方法被阻塞的线程
threading.Thread(target = go1).start()
threading.Thread(target = go2).start()
(14)线程调度
①两个线程交替执行
# 面试经常问到的问题:两个线程如何交替执行
# 例子:
# 线程1:打印0 2 4 6 8
# 线程2:打印1 3 5 7 9
# 实现:线程1打印0 → 线程2打印1 → 线程1打印2 → 线程2打印3 ..... 线程1打印8 → 线程2打印9
# A线程阻塞在wait方法时,只有B线程执行了notify方法A线程才能继续执行
# 反过来,B线程同理
import threading
import time
def go1():
with cond:
for i in range(0, 10, 2):
time.sleep(1)
print(threading.current_thread().name, i)
cond.wait() # 当前线程被阻塞,要等到被释放后才能继续运行
# 使用wait方法后其他线程可以使用cond
cond.notify()
def go2():
with cond:
for i in range(1, 10, 2):
time.sleep(1)
print(threading.current_thread().name, i)
cond.notify() # 释放被wait方法阻塞的线程(释放了go1所在的线程)
cond.wait() # 当前线程被阻塞
cond = threading.Condition() # 线程条件变量
threading.Thread(target = go1).start()
threading.Thread(target = go2).start()
②多个线程交替执行
思路就是给每个线程都配一把锁,执行某个线程前就开启该线程的锁,同时其它线程的锁全部锁住。
import threading
import time
class printThread(threading.Thread):
def __init__(self, index):
super().__init__()
self.index = index
def run(self):
while True:
while threads_locks[self.index][1].acquire():
time.sleep(0.5)
# 工作内容
print(threading.current_thread().name, self.index+1)
if self.index < 4:
threads_locks[self.index+1][1].release()
elif self.index == 4:
threads_locks[0][1].release()
# 创建线程和锁
threads_locks = []
for i in range(5):
t = printThread(i)
lock = threading.Lock()
threads_locks.append((t, lock))
# 初始只开启第一个锁,其余锁全部锁住
for i in range(1, len(threads_locks)):
threads_locks[i][1].acquire()
for t_lock in threads_locks:
t_lock[0].start()
for t_lock in threads_locks:
t_lock[0].join()
print("game over")
(15)生产者消费者模式
# 队列一端的压入为生产,另一端取出为消费
# 队列的创建方法queue,压入方法put,取出方法get
生产者消费者模式的意思就是,创建两种线程类,一种线程类负责生产,并将生产的结果压入队列,而另一种线程负责从队列中取出结果,执行对应的操作。
import threading
import queue
import time
import random
#生产
class creatorThread(threading.Thread):
def __init__(self, index, myqueue):
threading.Thread.__init__(self)
self.index = index #生产者代号
self.myqueue = myqueue #队列
def run(self):
while True:
time.sleep(3) #三秒生产一个
num = random.randint(1, 1000000) #随机数
self.myqueue.put("生产者" + str(self.index) + "硅胶娃娃" + str(num))
print("生产者" + str(self.index) + "硅胶娃娃" + str(num))
#self.myqueue.task_done() #完成任务
#消费
class buyerThread(threading.Thread):
def __init__(self,index, myqueue):
threading.Thread.__init__(self)
self.index = index #消费者代号
self.myqueue = myqueue #队列
def run(self):
while True:
time.sleep(1)
item = self.myqueue.get() #抓取数据
if item is None:
break
print("客户", self.index, "买到物品", item)
#self.myqueue.task_done() # 完成任务
myqueue = queue.Queue(10) #0代表无限,10最大容量
for i in range(3): #3个生产者线程
creatorThread(i, myqueue).start()
for i in range(8): #8个消费者线程
buyerThread(i, myqueue).start()
(16)线程池
①线程池的作用
Python 常常使用线程池来加速代码的执行。线程池是一种预先创建的线程集合,可以用来运行大量的并发任务。Python 中的 GIL(全局解释器锁)使得 Python 解释器在任何时候只能执行一个线程,这在一些情况下会令 Python 代码性能下降。但是,使用线程池可以缓解这种问题,尤其在 CPU 密集任务下会更为明显。
线程池可以提供以下几个好处:
- 减少线程创建的开销:线程创建和销毁期间的操作需要占用额外的系统资源,使用线程池可以避免这种操作。
- 管理线程执行:线程池可以更好地控制线程的数量和优先级,更好地管理运行的线程。
- 将并发性和并行性分离:线程池利用并行性和并发性,而将它们分离会使代码更容易理解和维护。
- 避免竞争条件:线程池可以显式地控制共享资源,从而避免竞争条件。
总的来说,线程池的使用可以提高 Python 代码的性能,并帮助 Python 解释器更好地管理运行的线程。
②安装和基本使用
安装
pip install threadpool
基本使用
import threadpool
pool = threadpool.ThreadPool(poolsize)
定义了一个线程池,表示最多可以创建poolsize这么多线程
requests = threadpool.makeRequests(some_callable, list_of_args, callback)
创建了要开启多线程的函数,以及函数相关参数和回调函数,其中回调函数可以不写,default是无
[pool.putRequest(req) for req in requests]
将所有要运行多线程的请求扔进线程池
pool.wait()
等待所有的线程完成工作后退出,类似join的作用
例子1:函数只有一个参数时
import time
import threadpool
def show(str):
time.sleep(1)
print("hello", str)
namelist = ["高清华", "hefengcheng", "sunyu", "gogogo"]
start_time = time.time() #开始时间
pool = threadpool.ThreadPool(10) #最大容量10个
requests=threadpool.makeRequests(show, namelist) #任务函数,参数列表
for req in requests:
pool.putRequest(req) #将每一个请求压入线程池开始执行
pool.wait()
end_time = time.time() #结束时间
print(end_time - start_time)
例子2:函数有多个参数时
import time
import threadpool
def hello(x, y, z):
time.sleep(2)
print("x = {}, y = {}, z = {}".format(x, y, z))
if __name__ == '__main__':
#方法1:将参数解析为列表
list_vars_1 = ['1', '2', '3']
list_vars_2 = ['4', '5', '6']
fun_var = [(list_vars_1, None), (list_vars_2, None)]
# 方法2:将参数解析为字典
# dict_vars_1 = {"x": "1", "y": "2", "z": "3"}
# dict_vars_2 = {"x": "4", "y": "5", "z": "6"}
# fun_var = [(dict_vars_1, None), (dict_vars_2, None)]
start = time.time()
pool = threadpool.ThreadPool(3)
requests = threadpool.makeRequests(hello, fun_var)
[pool.putRequest(req) for req in requests]
pool.wait()
end = time.time()
print(end - start)
例子3:先转换参数形式,再带入线程池
def getuserdic():
username_list = ['xiaozi', 'administrator']
password_list = ['root', '', 'abc123!', '123456', 'password', 'root']
userlist = []
for username in username_list:
user = username.rstrip()
for password in password_list:
pwd = password.rstrip()
userdic = {}
userdic['user'] = user
userdic['pwd'] = pwd
tmp = (None, userdic)
userlist.append(tmp)
return userlist
(17)定时线程
# 定时线程,就是指定某个时间长度后才执行的线程
import threading
import time
import os
def go():
os.system("notepad")
timethread = threading.Timer(5, go) # 5秒后线程执行
timethread.start()
(18)后台线程
# threading.Tread创建的子线程,主线程必须等待所有子线程结束后才会结束,这种子线程被称为前台线程
# 后台线程则是指该子线程无论是否结束,主线程运行完毕后就会结束
import threading
import win32api
class messageThread(threading.Thread):
# def __init__(self):
# threading.Thread.__init__(self)
def run(self):
win32api.MessageBox(0, "来自支付宝的问候", "你的账户很危险", 6)
for i in range(5):
t = messageThread()
t.setDaemon(True) # 设置为后台线程,主线程不等后台线程
t.start()
print("game over")
(19)TLS(线程独立存储空间)
# TLS: Thread Local Space,线程之间相互独立的存储空间
import threading
import time
data = threading.local() #线程之间相互独立的存储空间
f1 = lambda x : x+1
f2 = lambda x : x+"1"
def printdata(func, x):
data.x = x #data是一个类,动态绑定,线程独立 #data.x每个线程中是独立
print(id(data.x)) # 不同的地址
for i in range(5):
data.x = func(data.x)
# time.sleep(1)
print(threading.current_thread().name, data.x)
threading.Thread(target=printdata, args=(f1, 1)).start()
threading.Thread(target=printdata, args=(f2, "1")).start()
end