多任务
- 说起线程多多少少会和多任务挂钩,什么是多任务呢?
- 多任务其实就是在同一时间同时做很多件事情
- 并发:CPU 小于当前执行的任务,是假的多任务
- 并行:CPU 大于当前执行的任务,是真的多任务
- 多任务实现的几种方式:
- 线程
- 进程
- 协程
线程
- 线程,也被称为轻量进程,是处理器能够进行运算调度的最小单位,是进程内的一个执行单元,是进程的实际运作单位。
- 多线程类似于执行多个同时执行多个不同的程序,有以下的优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理
- 程序运行速度加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
Python中线程的创建方式
- 函数式:
import threading
import time
"""
使用线程来完成边唱歌边跳舞
"""
def sing():
for i in range(4):
print('I am a singer')
time.sleep(1)
def dance():
for i in range(4):
print('I am a dancer')
time.sleep(1)
def main():
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start() # 启动线程
t2.start() # 启动线程
if __name__ == '__main__':
main()
- 通过继承Threading.Thread类来创建线程
import threading
class A(threading.Thread):
pass
class B(threading.Thread):
pass
a = A()
b = B()
a.start()
b.start()
线程的具体分析
线程类Thread参数说明
- group:线程组,目前只能使用None
- target:执行的目标任务名
- args:以元组的方式给执行任务传参
- kwargs:以字典的方式给执行任务传参
- name:线程名,一般不用设置
子线程与主线程的执行
- setDaemon:用来保护主线程的
import threading
import time
def demo1():
for i in range(4):
print('--demo1--')
time.sleep(1)
def main():
t = threading.Thread(target=demo1)
t.setDaemon(True) # 必须放在线程启动之前
t.start()
# t.join()
print('111')
if __name__ == '__main__':
main()
# --demo1--
# 111
- join()
import threading
import time
def demo1():
for i in range(4):
print('--demo1--')
time.sleep(1)
def main():
t = threading.Thread(target=demo1)
# t.setDaemon(True)
t.start()
t.join()
print('111')
if __name__ == '__main__':
main()
# --demo1--
# --demo1--
# --demo1--
# --demo1--
# 111
综上情况:
setDaemon(True)是用来保护主线程的,主线程不会去等待子线程结束。
join():是用来阻塞线程的,主线程会等待子线程的结束,再去结束主线程
线程的启动与线程的数量
- 使用threading.enumerate()判断线程的数量从而确定线程启动的位置
import threading
import time
def demo1():
for i in range(3):
print('--demo1--')
time.sleep(1)
def demo2():
for i in range(4):
print('--demo2--')
time.sleep(1)
def main():
print(threading.enumerate())
t1 = threading.Thread(target=demo1, name='demo1')
t2 = threading.Thread(target=demo2, name='demo2')
print(threading.enumerate())
t1.start()
t2.start()
print(threading.enumerate()
# while True:
# print(threading.enumerate())
#
# if len(threading.enumerate()) <= 1:
# break
# time.sleep(1)
if __name__ == '__main__':
main()
# [<_MainThread(MainThread, started 7624)>]
# [<_MainThread(MainThread, started 7624)>]
# --demo1--
# --demo2--
# [<_MainThread(MainThread, started 7624)>, <Thread(demo1, started 11120)>, <Thread(demo2, started 7832)>]
# --demo2--
# --demo1--
# --demo2--
# --demo1--
# --demo2--
综上描述:线程是在 运行t.start()之后才开始启动的
线程间共享全局变量
import threading
import time
num = 100
def demo1():
global num
for i in range(1000000):
#for i in range(100):
num += 1
print('demo1-- {}'.format(num))
def demo2():
global num
for i in range(1000000):
#for i in range(100):
num += 1
print('demo2-- {}'.format(num))
def main():
t1 = threading.Thread(target=demo1)
t2 = threading.Thread(target=demo2)
t1.start()
t2.start()
print('--main-- {}'.format(num))
if __name__ == '__main__':
main()
# demo1-- 200
# demo2-- 300
# --main-- 300
# --main-- 336402
# demo1-- 1213786
# demo2-- 1321174
此处需要注意一个问题: 当我们循环的时候过大时候,会造成资源抢占混乱的情况。可以使用给线程上锁或者线程同步的方法 解决
线程之间传参
- 我们可以使用 args 和 kwargs 在线程间传递参数
import threading
import time
num = 100
def demo1(m):
global num
for i in range(m):
num += 1
print('--demo1-- {}'.format(num))
def demo2(m):
global num
for i in range(m):
num += 1
print('--demo2-- {}'.format(num))
def main():
t1 = threading.Thread(target=demo1, args=(100,))
t2 = threading.Thread(target=demo2, args=(100,))
t1.start()
t2.start()
print('--main-- {}'.format(num))
if __name__ == '__main__':
main()
# --demo1-- 200
# --demo2-- 300
# --main-- 300
- 使用kwargs 传值:
num = 100
def demo1(m):
global num
for i in range(m):
num += 1
print('--demo1-- {}'.format(num))
def demo2(m):
global num
for i in range(m):
num += 1
print('--demo2-- {}'.format(num))
def main():
t1 = threading.Thread(target=demo1, kwargs={'m': 100})
t2 = threading.Thread(target=demo2, kwargs={'m': 100})
t1.start()
t2.start()
print('--main-- {}'.format(num))
if __name__ == '__main__':
main()
# --demo1-- 200
# --demo2-- 300
# --main-- 300
互斥锁与死锁
- 互斥锁:对共享数据进行锁定,保证同一时刻只能有一个线程去操作,从而保证多线程的情况下数据的准确性
- 使用互斥锁的目的:保证多个线程访问共享数据时不会出现资源竞争以及数据混乱
- 上锁与解锁的过程:
- 当一个线程调用锁的 acquire() 方法获得锁时,锁就进入“Locked”状态
- 每次只有一个线程获得锁。当另一个线程想要获得锁时,该线程进入“Locked”状态,俗称“阻塞”,直到拥有锁的线程调用锁的“release()”方法释放锁后,锁进入“unLocked”状态
- 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使该线程进入运行(running)状态
# 1.创建锁
mutex = threading.Lock()
# 2.上锁
mutex.acquire()
# 3.解锁
mutex.release()
注意:
- 抢到锁的线程先执行,没有抢到锁的线程需要等待,等锁用完后需要释放,然后其他等待的线程再去抢这个锁,哪个线程先抢到哪个线程先执行。
- 具体哪个线程 抢到的锁不是由我们决定的,是由CPU的资源调度决定的
- 线程同步只能保证多个先线程安全访问竞争资源,最简单的同步机制是引入互斥锁
- 当我们要更改共享数据时,先将其锁定,此时资源的状态为“ 锁定 ”,其他线程不能更改;直到该线程释放资源,将资源的状态变为“ 非锁定 ”,其他的线程才能再次锁定改资源。互斥锁保证了每次只有一个线程进行功写入操作,从而保证了数据的准确性
使用互斥锁来解决之前的资源抢占导致数据混乱的问题:
import threading
import time
mutex = threading.Lock()
num = 100
def demo1(m):
global num
mutex.acquire()
for i in range(m):
num += 1
mutex.release()
print('--demo1-- {}'.format(num))
def demo2(m):
global num
mutex.acquire()
for i in range(m):
num += 1
mutex.release()
print('--demo2-- {}'.format(num))
def main():
t1 = threading.Thread(target=demo1, args=(1000000,))
t2 = threading.Thread(target=demo2, kwargs={'m': 1000000})
t1.start()
t2.start()
mutex.acquire()
print('--main-- {}'.format(num))
mutex.release()
if __name__ == '__main__':
main()
# --demo1-- 1000100
# --demo2-- 2000100
# --main-- 2000100
- 死锁:在线程间共享多个资源的时候,如果两个线程分别得到一部分资源并且同时等待对方释放的资源,这时就会形成死锁
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
线程同步
如果多个线程共同对某个数据进行修改,则可能出现不可预料的后果,为了保证数据的正确性,对多个线程进行同步。
使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间
import threading
from threading import Condition
# 方法一:
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name='天猫精灵')
self.cond = cond
def run(self):
self.cond.acquire()
print('{}:小爱同学'.format(self.name))
self.cond.notify()
self.cond.wait()
print('{}:现在几点了'.format(self.name))
self.cond.notify()
self.cond.release()
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name='小爱')
self.cond = cond
def run(self) -> None:
with self.cond:
# self.cond.acquire()
self.cond.wait()
print('{}:在'.format(self.name))
self.cond.notify()
self.cond.wait()
print('{}:你猜现在几点了?'.format(self.name))
self.cond.notify()
# self.cond.release()
if __name__ == '__main__':
# 实例化 Condition 对象
cond = threading.Condition()
# 实例化两个线程对象
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
# 启动线程
xiaoai.start()
tianmao.start()
# 方法二:
import threading
from threading import Condition
"""
天猫精灵:小爱同学
小爱:在
天猫精灵:现在几点了
小爱:你猜猜现在几点了
"""
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name='小爱同学')
self.cond = cond
def run(self) -> None:
with self.cond:
self.cond.wait()
print('{}:在'.format(self.name))
self.cond.notify()
self.cond.wait()
print('{}:你猜现在几点了'.format(self.name))
self.cond.notify()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name='天猫精灵')
self.cond = cond
def run(self) -> None:
with self.cond:
print('{}:小爱同学'.format(self.name))
self.cond.notify()
self.cond.wait()
print('{}:现在几点了?'.format(self.name))
self.cond.notify()
def main():
cond = Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
xiaoai.start()
tianmao.start()
if __name__ == '__main__':
main()
这里需要注意:Condition需要加锁和解锁,将唤醒和通知的方法 放到加锁和解锁之间。 或者使用上下文管理器代替Condition的上锁与解锁
-
线程同步队列queue
- Python的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
-
queue模块的常用方法有:
- queue.empty() 如果队列为空,返回True,反之False
- queue.full() 如果队列满了,返回True,反之False
- queue.get([block[, timeout]])获取队列,timeout等待时间
- queue.get_nowait() 相当Queue.get(False)
- queue.put(item) 写入队列,timeout等待时间
- queue.put_nowait(item) 相当Queue.put(item, False)
- queue.qsize() 返回队列的大小
from queue import Queue
# 实例化队列对象
q = Queue(4)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
# q.put_nowait(5) # queue.Full
print(q.empty()) # False
print(q.full()) # True
print(q.qsize()) # 4
print(q.get()) # 1
print(q.get()) # 2
print(q.get()) # 3
print(q.get()) # 4
"""
put_nowait()和get_nowait()方法一样。
put_nowait()向队列中存放数据,当队列已经满的时候,会直接抛出queue.Full异常,而put()则会一直等待
"""
"""
在线程中,访问⼀些全局变量,加锁是⼀个经常的过程。如果你是想把⼀些数据存储到某个队列中,那么Python内置了⼀个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括 FIFO(先进先出)队列Queue,LIFO(后⼊先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原⼦操作,即要么不做,要么都做完),能够在多线程中直接使⽤。可以使⽤队列来实现线程间的同步。
"""
总结
- 使用多线程可以完成多任务
- 只有线程启动,线程才会加入到活动线程列表
- 线程之间的执行时无序的
- 主线程会等待所有子线程结束后执行,如果有需要可以使用setDaemon 保护主线程
- 自定义线程不能指定target,因为自定义线程里面的任务都指定在 run 方法中执行
- 启动线程调用 start 方法,不要直接调用 run 方法
- 多线程共享全局变量,很方便在多个线程间共享数据
- 多个线程同时对同一个全局变量操作,会有可能出现资源竞争导致的数据错误问题
- 线程的同步可以解决资源竞争的问题,但是会使多任务变为单任务
- 锁的好处:
- 确保某段代码只能由一个线程从头至尾地完整执行
- 锁的坏处:
- 多线程执行变成了包含锁的某段代码,实际上只能以单线程模式执行,大大降低了执行效率
- 锁使用不好容易出现死锁的状况
- 使用互斥锁时要注意死锁问题,需要及时将锁释放
- 死锁一旦发生将会导致程序一直处于阻塞状态
本文介绍了Python中的线程,包括多任务概念、线程的创建与执行、线程间共享全局变量、互斥锁与死锁、线程同步方法。重点讲述了线程同步的实现,如使用Lock、Rlock、Condition和线程同步队列queue,强调了正确使用锁以避免资源竞争和死锁的重要性。
1132

被折叠的 条评论
为什么被折叠?



