python3中将thread模块进行了规范内置,更名为_thread,友好的提醒如果你不是并发之编程的骨灰级爱好者,请不要尝试使用_thread进行操作,而是推荐使用操作更加灵活使用更加简洁的threading模块进行并发编程的处理。
_thread模块多线程并发任务的简单实现如下:
import _thread
import time
# 定义函数,函数执行循环遍历指定的循环
def test(num):
for i in range(num):
print(_thread.get_indent(), ":", i)
# 通过_thread.start_new_thread()启动两个线程,分别执行test函数
_thread.start_new_thread(test,(2, ))
_thread.start_new_thread(test, (3,))
# 让主线程休眠3s, 等待子线程执行结束
time.steep(3)
1 python中的多线程
官方推荐的_threading模块的多线程并发编程机制,结合时下流行的面向过程/面向对象的编程处理模式,主要有两种操作方式
1.函数式的线程创建方式,适合面向过程程序的并发编程实现
2.面向对象的创建方式,适合面向对象程序的并发编程实现
1.1 threading模块属性和方法
名称 | 描述 |
---|---|
Thread | 线程类,用于创建和管理线程 |
Event | 事件类,用于线程同步 |
Condition | 条件类,用于线程同步 |
Lock/RLock | 锁类,用于线程同步 |
Timer | 延时线程,用于在一定事件后执行一个函数 |
Semaphore/BoundedSemaphore | 信号量类,用于线程同步 |
active_count()/activeCount() | 获取当前 alive 状态的所有线程数量 |
current_thread()/currentThread() | 获取当期正在执行的线程对象 |
get_ident() | 获取运行中程序当前线程的唯一编号 |
enumerate() | 获取所有 alive 状态线程列表 |
local | 线程局部数据类 |
stack_size([size]) | 获取线程占用内存栈的大小 |
main_thread() | 获取主线程 |
1.2 Thread类型属性和方法
名称 | 描述 |
---|---|
__init__(group,target,name,args,kwargs) | 构造方法,创建线程类型 |
is_alive()/isAlive() | 判断当前线程是否 alive 状态 |
run() | 线程执行方法,自定义线程必须重写该函数 |
start() | 线程启动方法 |
join([timeout=None]) | 线程独占,等待当前线程运行结束或者超时 |
ident | 标识当前线程的唯一编号 |
name | 当前线程名称 |
daemon | 布尔值,判断当前线程是否守护线程 |
2 函数式开发实现
通过threading模块的Thread函数,可以实现多线程程序的运行
案例需求:火车站窗口售票
单线程实现模式:相当于只有一个窗口售票
import time
# 总票数
count = 10
def sale_ticket():
'''售票函数,每次出票一张'''
global count
while count > 0:
print("售出一张票", count)
count -= 1
time.sleep(0.5)
else:
print("售票结束,没有票了..")
if __name__ == "__main__":
# 循环去窗口买票
sale_ticket()
多线程售票:相当于多个窗口同时售票
# 引入模块
import threading, time
count = 10
def sale_ticket():
global count
while count > 0:
print(threading.current_thread().getName(), "售出一张票:", count)
count -= 1
time.sleep(0.5)
else:
print(threading.current_thread().getName(), "没有票了")
if __name__ == "__main__":
# 定义多个线程(窗口)
t1 = threading.Thread(name="窗口 1", target=sale_ticket)
t2 = threading.Thread(name="窗口 2", target=sale_ticket)
t3 = threading.Thread(name="窗口 3", target=sale_ticket)
t4 = threading.Thread(name="窗口 4", target=sale_ticket)
t5 = threading.Thread(name="窗口 5", target=sale_ticket)
# 启动五个窗口同时售票
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
2.1 线程状态-join
join:线程join状态是独占模式,当前线程独占CPU运行单元,必须等待当前线程执行完成或者超时之后,才能运行其他线程
依然是火车售票代码,窗口1线程join执行
# 引入模块
import threading, time
count = 10
def sale_ticket():
global count
while count > 0:
print(threading.current_thread().getName(), "售出一张票:", count)
count -= 1
time.sleep(0.5)
else:
print(threading.current_thread().getName(), "没有票了")
if __name__ == "__main__":
# 定义多个线程(窗口)
t1 = threading.Thread(name="窗口 1", target=sale_ticket)
t2 = threading.Thread(name="窗口 2", target=sale_ticket)
t3 = threading.Thread(name="窗口 3", target=sale_ticket)
t4 = threading.Thread(name="窗口 4", target=sale_ticket)
t5 = threading.Thread(name="窗口 5", target=sale_ticket)
# 启动五个窗口同时售票
t1.start()
# 线程 t1 调用 join,独占模式运行,等待 t1 线程运行结束或者超时,才能继续运行其他线程
t1.join()
t2.start()
t3.start()
t4.start()
t5.start()
2.2 线程状态-daemon
线程对象的daemon属性用于标识某个线程是否守护线程
一旦主线程执行退出,无论守护线程是否执行完成,都会直接退出!
改造售票程序【将窗口设置为守护线程:假装主线程就是火车站】
火车站一旦关闭,窗口不论剩余是否有票,都会直接退出
# 引入模块
import threading, time
count = 10
def sale_ticket():
global count
while count > 0:
print(threading.current_thread().getName(), "售出一张票:", count)
count -= 1
time.sleep(0.5)
else:
print(threading.current_thread().getName(), "没有票了")
if __name__ == "__main__":
# 定义线程(窗口)
t1 = threading.Thread(name="窗口 1", target=sale_ticket)
t1.daemon = True
# 启动售票
t1.start()
2.3 线程管理-锁【Lock/Rlock】
多线程程序在运行过程中,由于多个线程防问的是同一部分数据,很容易造成共享数据访问冲突的现象e,如果一旦出现冲突程序就会出现执行结果不符合期望的结果
修改售票程序,两个窗口同时售票,修改代码如下:
# 引入模块
import threading, time
cunt = 10
def sale_ticket():
global count
while count > 0:
print(threading.current_thread().getName(), "售出一张票:", count)
time.sleep(0.5)
count -= 1
else:
print(threading.current_thread().getName(), "没有票了")
if __name__ == "__main__":
# 定义线程(窗口)
t1 = threading.Thread(name="窗口 1", target=sale_ticket)
t2 = threading.Thread(name="窗口 2", target=sale_ticket)
# 启动售票
t1.start()
t2.start()
可能你并没有从上述程序中看到任何BUG的存在,其实上述程序已经造孽了,原因在于多个线程访问共享数据的问题被一行time.sleep(0.5)
给放大了。此时共享数据的修改操作,在多线程的情况下,是需要通过锁定的方式进行独占修改的!
python中提供了两种线程锁的操作
同步锁/互斥锁:Lock
可重用锁:RLock
锁的操作主要是获取锁和释放锁两种
acquire()获取锁,上锁,锁定
release()释放前,开锁,解锁
修改上述售票程序,给售票代码添加线程锁
# 引入模块
import threading, time
count = 10
def sale_ticket():
global count
# 添加锁定数据
lock.acquire()
while count > 0:
print(threading.current_thread().getName(), "售出一张票:", count)
time.sleep(0.5)
count -= 1
else:
print(threading.current_thread().getName(), "没有票了")
# 释放锁
lock.release()
if __name__ == "__main__":
# 定义锁对象
lock = threading.Lock()
# 定义线程(窗口)
t1 = threading.Thread(name="窗口 1", target=sale_ticket)
t2 = threading.Thread(name="窗口 2", target=sale_ticket)
# 启动售票
t1.start()
t2.start()
此时在运行程序,就不会再出现重复售票的情况了
2.4 线程管理-死锁【Dead Lock】
线程锁固然功能强大,可以管理多个线程之间的共享数据问题
但是同时它的强大也带来了比较纠结的问题,需要开发人员对于锁定的数据有一个良好的认知,否则特别容易造成死锁的现象,比较著名的哲学家吃饭问题就是死锁的典型代表
由于计算机运算速度较快,所以有两种方案可以将问题放大
给执行函数添加休眠时间
添加线程数量
死锁并不是每次都会出现的,而是程序在执行过程中,根据系统CPU时间片的切换机制恰好遇到了重复上锁的情况,就会死锁
import threading, time
def zhexuejia1():
if lock1.acquire():
if lock2.acquire():
# time.sleep(0.01)
print("你给我叉子,我就给你刀子")
lock2.release()
lock1.release()
def zhexuejia2():
if lock2.acquire():
if lock1.acquire():
# time.sleep(0.01)
print("你给我刀子,我就给你叉子")
lock1.release()
lock2.release()
if __name__ == "__main__":
lock1 = threading.Lock()
lock2 = threading.Lock()
for i in range(1000):
zxj = threading.Thread(name="哲学家 A" + str(i), target=zhexuejia1)
zxj2 = threading.Thread(name="哲学家 B" + str(i), target=zhexuejia2)
zxj.start()
zxj2.start()
实际项目开发过程中,一定要注意死锁情况的影响
这样的情况可以通过可重用锁Rlock进行锁定处理!
2.5 线程管理-事件【Event】
线程锁解决了多个线程访问共享数据时冲突的问题,如果多个线程之间需要通信应该怎么解
决呢?此时就需要用到多个线程之间的可以用于互相通信的处理对象了,该处理对象必须满
足如下基本条件
该对象能同时被多个线程访问
该对象可以被标记不同的状态
*该对象可以用于控制线程等待|运行之间的切换
python提供了一个事件对象Event,可以基本满足上述条件,完成线程之间的通信
名称 | 描述 |
---|---|
set() | 添加一个标记状态 |
isSet()/is_set() | 检查事件对象是否被标记 |
clear() | 清除标记状态 |
wait() | 事件对象操作的当前线程等待,直到该对象被标记状态 |
2.6 油条的故事-Event实现
需求:一个冬天的早晨,顾客去小摊贩哪里买油条;由于油条都是现炸,所以顾客需要等待
小摊贩生产油条;小摊贩生产好油条之后顾客可以进餐;结束后打招呼离开
分析:这里有两个线程:小摊贩线程和顾客线程,顾客线程运行开始必须等待,小摊贩线程
工作生产油条,当油条生产出来之后唤醒顾客线程,此时小摊贩线程等待;顾客就餐完毕之
后准备离开,唤醒小摊贩进程结账走人!
'''
小贩:生产油条
顾客:消费油条
需求:顾客去消费
两个线程的通信:事件对象:threading.Event
set()添加标记
wait()线程等待-如果当前事件对象被标记~继续运行
clear()清除标记
'''
import threading, time
# 定义事件对象
event = threading.Event()
def xiao_fan():
print("XF:炸油条.....")
time.sleep(2)
# 添加标记
event.set()
event.clear()
print("XF:卖油条")
event.wait()
print("XF:结账完毕,谢谢光临")
def gu_ke():
# 线程等待~等待事件对象被标记
event.wait()
print("GK:买油条")
print("GK:吃油条")
time.sleep(2)
print("GK:油条真好吃..")
event.set()
event.clear()
if __name__ == "__main__":
xf = threading.Thread(target=xiao_fan)
gk = threading.Thread(target=gu_ke)
xf.start()
gk.start()
2.7 线程管理-条件【Condition】
生产者消费者问题
生产者负责生产食物,将食物存储在列表中;消费者负责消费,也就是从列表中删除数据;
这里的存储食物的列表,我们限制了长度,最多容纳 20 个食物数据
condition对象的属性和方法
名称 | 描述 |
---|---|
acquire() | 锁定 |
release() | 解锁 |
wait() | 释放锁,同时阻塞当前线程,等待被唤醒 |
wait_for() | 释放锁,同时阻塞当前线程,等待被唤醒 |
notify() | 唤醒 |
notify_all() | 唤醒所有等待该 condition 条件的线程 |
2.8 生产者消费者 问题-Condition实现
需求:生产者消费者问题,描述的是多个线程之间的通信处理方式和手段。
多个生产者线程生产食品放到指定的食品容器中,并唤醒所有的消费者线程开始就餐
如果食品容器容量饱和,则所有生产者线程等待
多个消费者线程在指定的食品容器中获取食物就餐,并唤醒所有的生产者线程开始生产
如果食品容器中没有任何食品了,则所有消费者线程等待
import threading, time, random
# 定义食物列表
foods = list()
# 创建一个线程条件对象
con = threading.Condition()
def product():
'''生产者函数:负责生产数据'''
while True:
time.sleep(0.5)
con.acquire()
if len(foods) < 20:
_no = random.randint(0, 20)
print("生产者{}生产了:".format(threading.current_thread().getName()), _no)
foods.append(_no)
print("PRO--", len(foods))
con.notify()
else:
con.wait()
print("生产者{}----等待".format(threading.current_thread().getName()))
con.release()
def consumer():
'''消费者函数:负责删除数据'''
while True:
time.sleep(0.5)
con.acquire()
if len(foods) > 0:
_no = foods.pop()
print("消费者{}消费了".format(threading.current_thread().getName()), _no)
print("CUS--", len(foods))
con.notify()
else:
con.wait()
print("消费者{}---等待".format(threading.current_thread().getName()))
con.release()
if __name__ == "__main__":
# 创建多个生产者线程
for i in range(5):
p = threading.Thread(name="_p" + str(i), target=product)
p.start()
# 创建多个消费者线程
for j in range(2):
c = threading.Thread(name="_c" + str(j), target=consumer)
c.start()
2.9 线程管理-队列【Queue】
多线程并发编程的重点,是线程之间共享数据的访问问题和线程之间的通信问题
为了解决线程之间数据共享问题,PYTHON 提供了一个数据类型【队列】可以用于在多线程
并发模式下,安全的访问数据而不会造成数据共享冲突
python 中的 queue 模块提供的队列类型 Queue 的操作模式如下
名称 | 描述 |
---|---|
put([timeout=None]) | 向队列中添加数据,队列如果满了,一直阻塞直到超时或者队列中有数据被删除之后添加成功 |
get([timeout=None]) | 从队列中获取数据,如果队列为空,一直阻塞直到超时或者队列中添加数据之后获取成功 |
2.10 生产者消费者问题-Queue队列实现
import threading, time, random, queue
foods = queue.Queue(5)
def product():
while True:
time.sleep(0.5)
try:
_no = random.randint(0, 10)
print(threading.current_thread().getName(), "生产者开始生产食物:", _no)
foods.put(_no, timeout=1)
except:
print(threading.current_thread().getName(),"篮子中食物已满,等待消费者消费")
def cusumer():
while True:
time.sleep(0.5)
try:
_no = foods.get(timeout=1)
print(threading.current_thread().getName(),"消费者消费食物", _no)
except:
print(threading.current_thread().getName(),"篮子空了,等待生产者生产数据")
if __name__ == "__main__":
# 生产者
for i in range(5):
p = threading.Thread(name="_p" + str(i), target=product)
p.start()
# 消费者
for j in range(2):
c = threading.Thread(name="_c" + str(j), target=cusumer)
c.start()
3 面向对象多线程基本语法
面向对象的多线程,主要是让自定义类型派生自 threading.Thread 类
重写 Thread 类型的 run()方法,然后创建自定义线程类的对象之后,调用 start()方法启动
# 引入依赖的模块
import threading, time
# 定义锁对象
lock = threading.Lock()
# 定义票数量
count = 20
class TicketSaleWindow(threading.Thread):
'''自定义线程类型,继承 threading.Thread 类型'''
def __init__(self, name):
'''初始化函数:初始化线程名称'''
super().__init__(name=name)
def run(self):
'''重写线程执行 run()方法,该方法会再 start()方法调用时自动执行'''
global count
while True:
time.sleep(0.5)
lock.acquire()
if count > 0:
print(threading.current_thread().getName(), "售出:", count)
count -= 1
lock.release()
else:
print(threading.current_thread().getName(), "售罄了")
lock.release()
break
if __name__ == "__main__":
for i in range(3):
# 创建线程类型
win = TicketSaleWindow("窗口" + str(i))
# 启动线程
win.start()