多任务编程:
- 作用:充分利用计算机多核资源,提高程序的运行效率。
- 实现方案:多进程;多线程
- 相关概念:
- 并发:多个任务在同一时间间隔内发生。表面上看像是多个任务同时进行,实际是任务在时间片上的轮转(即多个任务在内核上以极短的时间快速切换),也就是说,每个时刻只有一个任务占有内核资源。
- 并行:多个任务利用计算机多核资源而同时执行,称这些多个任务间为并行关系
一、线程概述
1、线程的概念
- 线程(Thread)是操作系统能够进行运算调度(分配内核)的最小单位,它被包含在进程之中,是进程的实际运作单位,一条线程指的是进程中一个单一顺序的控制流。
- 线程是OS直接支持的执行单元,一个进程中的线程既可以并行地执行不同任务。如,对于一个视频播放进程,显示视频用一个线程,播放音频用另一个线程,两个线程同时工作,才能同时观看画面和声音同步的视频。
2、其他说明
- 线程属于进程的一部分,一个可包含多个线程
- 多个线程之间的运行互不影响、各自运行
- 一个进程中的所有线程共享这个进程的资源
- 线程的创建和销毁所消耗的资源远小于进程
- 各线程具备各自的线程信息
3、Python中的线程模块
Python中的标准款提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需使用threading模块即可。
二、基于threading模块的多线程
1、思路
2、实现方法
①.创建线程对象
from threading import Thread
t = Thread([group, [target, [name, [args, [kwargs, [daemon]]]]]])
参数:
group:保留参数,为以后版本而保留
target:可调用对象,一般绑定要执行的目标函数;run()方法将调用此对象,默认值None,表示不调用任何内容
name:当前线程名称,默认创建格式为Thread-N
args:传递给target函数的参数元组
kwargs:传递给target函数的参数字典
daemon:默认None,表示从创建的进程继承;实际用户自定义进程一般为默认值为False
②.启动线程
t.start()
功能:开始线程活动,它在一个线程里做多只能被调用一次,大于一次将抛异常RuntimeError。
③.回收线程
t.join(timeout=None)
功能:等待,直到线程结束
import threading, os
from time import sleep
# 线程函数
def music(loop, m_name):
global a
a = 10000
print(a)
for i in range(loop):
sleep(2)
print(os.getpid(), "Play music: " + m_name)
if __name__ == "__main__":
a = 1
# 创建线程对象(分支线程)
t = threading.Thread(target=music, args=(5, ), kwargs={'m_name': "你不是真正的快乐"})
t.start() # 开启分支线程
# 主线程任务
for i in range(3):
sleep(3)
print(os.getpid(), "Have a dance ...")
t.join() # 回收线程
print("main thread a :", a) # 此处的a值已被线程修改
④.线程对象的其他相关方法/属性
t.name 线程名称
t.setName() 设置线程名称
t.getName() 获取线程名称
t.is_alive() 判断线程实例是否在运行
t.daemon 设置主线程和分支线程的退出关系
t.setDaemon() 设置daemon属性值
t.isDaemon() 判断是否为守护线程
from threading import Thread
from time import sleep
def fun():
sleep(3)
print("线程属性测试")
if __name__ == "__main__":
t = Thread(target=fun)
print(t.daemon)
t.daemon = True # 主线程退出分支线程也退出
t.start()
print("Thread name:", t.getName()) # 打印线程名称
t.setName("owhyt")
print(t.name)
print("is alive:", t.is_alive()) # 线程是否存活
⑤.自定义线程类——重写Thread类的run()方法
- 创建步骤
- 继承Thread类
- 重写__init__方法添加自定义属性,super方法加载父类属性
- 重写run()方法
- 使用方法
- 实例化自定义线程类
- 调用start()方法,会自动执行run()方法
- 调用join()回收进程
from threading import Thread
from time import sleep, ctime
class MyThread(Thread):
def __init__(self, target, args=(), kwargs=None, name=None):
super().__init__()
self.target = target
self.args = args
self.kwargs = kwargs
self.name = name
def run(self):
print(self.name)
self.target(*self.args, **self.kwargs)
def player(sec, song):
for i in range(2):
print("Playing %s:%s" % (song, ctime()))
sleep(sec)
if __name__ == "__main__":
t = MyThread(target=player, args=(3,), kwargs={'song': '画'}, name='happy')
t.start()
t.join()
三、线程间通信——全局变量(共享内存)
Ⅰ 相关概念
- 从第一个线程示例代码来看,在一个进程内的所有线程共享全局变量。使用全局变量完成多线程间数据共享的过程,称为线程间通信。
- 全局变量可以被多个线程共享,我们将有全局变量性质的、多个进程或线程都可以操作的数据资源,称为共享资源。
- 将一次只能被一个进程所使用的资源称为临界资源。
- 对临界资源的操作代码段为临界区。
Ⅱ 争夺共享资源的问题及解决方法
- 问题:由于线程可以对全局变量随意修改,这就可能造成多线程之间对全局变量的混乱操作。
- 解决方法:同步互斥机制
Ⅲ 线程/进程间的制约关系——同步互斥机制
- 同步:也称直接制约关系,多线程或多进程为完成任务所形成的一种协同工作关系,并按需在某些位置上协调它们的工作次序,而等待、传递信息所产生的制约关系。
- 互斥:也称间接制约关系,当一个进程/线程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
- 同步互斥在进程间通信的应用举例:
1.TCP网络通信对网络网络缓冲区的访问是互斥的
2.多个打印任务(进程)对打印机的访问是同步互斥的 - 为禁止多进程或多线程同时进入临界区,同步互斥机制应遵循以下准则:
1.空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
2.忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待
3.有限等待。对请求访问临界区的进程,应保证能在有限时间内进入临界区
4.让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程盲等
Ⅳ Python线程同步互斥的常用方法
1.线程锁 threading.Lock
①.简述
为防止多个线程同时读写共享资源而给资源引入的一个状态:锁定或非锁定。即,当一个线程占有资源时会进行加锁处理,此时其他线程无法操作该资源,直到解锁后方能操作。互斥保证每次只有一个线程进行写入操作,以确保多线程下数据的唯一性和正确性。
②.实现方法
mutex = threading.Lock()
功能:创建锁对象
mutex.acquire([blocking=True])
功能:上锁,在已上锁的情况再次锁定会导致阻塞
参数&返回值:
blocking 默认值为True,表示阻塞到锁被占用的线程释放为止,然后将上锁并返回True
设置为False时,当无法获取锁时立即返回False
mutex.release()
功能:解锁
说明:当锁对象处于未锁定状态而调用该方法,或,锁对象与调用acquire方法的锁对象不是同一个时,程序将出错
mutex = Lock() mutex.acquire() # 临界区 mutex.release() |
with Lock() as mutex: # 临界区 # with代码块自动加锁和解锁 |
from threading import Thread, Lock
import time
n = 100 # 共100张电影票
def task(mutex):
global n
# mutex.acquire()
# temp = n
# time.sleep(0.3)
# n = temp - 1
# print("购票成功,剩余%d张电影票" % n)
# mutex.release()
with mutex as m:
tmp = n
time.sleep(0.1)
n = tmp - 1
print("购票成功,剩余%d张电影票" % n)
if __name__ == "__main__":
mutex = Lock()
threads = []
for i in range(10):
t = Thread(target=task, args=(mutex, ))
threads.append(t)
t.start()
for t in threads:
t.join()
③.死锁及其处理
⑴ 定义
多个进程或线程因资源竞争而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。这种状态称为死锁。
⑵ 死锁产生的必要条件
- 互斥条件:进程要求对所分配的资源进行排他性控制,即,在一段时间内某资源仅为一个进程所占有。
- 不剥夺条件:进程所获资源在未使用完毕前,不能被其他进程强行夺走,换言之,只能由获得该资源的进程主动释放
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中的下一个进程所请求。
⑶ 预防死锁
- 设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁。但由于所施加的限制条件往往太严格,可能会导致系统资源利用率降低。
- 预防死锁的方法
1.在恰当的位置设置一定的延时时间。
2.使用重载锁RLock(),允许线程对资源进行重复加锁。- 针对复杂程序容易对临界资源重复加锁的情况
- RLock的内部维护着一个Lock和一个counter变量,counter变量记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
- 示例一:在适当的位置设置一定的延时时间
import time, threading
# 账户类
class Account:
def __init__(self, id, balance, lock):
self.id = id # 用户id
self.balance = balance # 存款金额
self.lock = lock # 锁
# 取钱
def withdraw(self, amount):
self.balance -= amount
# 存钱
def deposit(self, amount):
self.balance += amount
# 获取账户金额的函数
def get_balance(self):
return self.balance
# 转账函数
def transfer(from_, to, amount):
if from_.lock.acquire(): # 如果上锁成功则返回True(锁住自己的账户)
from_.withdraw(amount) # 自己账户金额减少
# time.sleep(0.5) # 设置该休眠时间导致死锁
if to.lock.acquire(): # 对方账户上锁
to.deposit(amount) # 对方账户金额增加
to.lock.release() # 对方账户解锁
from_.lock.release() # 自己账户解锁
print("转账完成")
if __name__ == "__main__":
# 创建两个账户
Hange = Account('Abby', 5000, threading.Lock())
Levi = Account('Levi', 3000, threading.Lock())
t = threading.Thread(target=transfer, args=(Hange, Levi, 1500))
t2 = threading.Thread(target=transfer, args=(Levi, Hange, 1000))
t.start()
# time.sleep(2) # 错开两次转账的时机,避免死锁
t2.start()
t.join()
t2.join()
print("Hange:", Hange.get_balance())
print("Levi:", Levi.get_balance())
- 示例二:使用重载锁RLock()
from threading import Thread, Lock, RLock
import time
num = 0
# lock = Lock()
lock = RLock()
class MyThread(Thread):
def fun1(self):
global num
with lock:
num -= 1
def fun2(self):
global num
if lock.acquire():
num += 1
if num > 5:
self.fun1()
print("num =", num)
lock.release()
def run(self):
while True:
time.sleep(2)
self.fun2()
for i in range(10):
t = MyThread()
t.start()
t.join()
2.事件对象threading.Event
这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。而该机制通过阻塞行为和解除阻塞行为,来达到同步互斥的目的。
实现方法
e = Event() # 创建线程Event对象; 一个Event对象管理一个内部标志。
e.wait([timeout=None]) # 阻塞等待e事件对象被set; 阻塞线程直到内部变量为true
e.set() # 设置e事件对象,使wait结束阻塞; 将内部标志设置为false
e.clear() # 使e事件对象回到未被设置状态 将内部标志设置为false
e.is_set() # 查看当前e是否被设置 当且仅当内部标志为true时返回True
代码示例如下,可对比threading.lock第一个示例:
from threading import Thread, Lock, Event
import time
n = 100 # 共100张电影票
def task():
global n
temp = n
time.sleep(0.3)
n = temp - 1
print("购票成功,剩余%d张电影票" % n)
e.set()
if __name__ == "__main__":
threads = []
for i in range(10):
e = Event()
t = Thread(target=task)
threads.append(t)
t.start()
e.wait()
for t in threads:
t.join()
Ⅴ 使用队列进行进程间通信
说到底,在主线程定义消息队列Queue,还是属于一种全局变量。
使用queue模块的Queue在线程间通信通常应用于生产者消费者模式。产生数据的模块称为生成者,处理数据的模块称为消费者。在生产者和消费者之间的缓冲区称为仓库。生产者负责网仓库运输商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式。
from queue import Queue
import random,threading,time
# 生产者类
class Producer(threading.Thread):
def __init__(self, name,queue):
threading.Thread.__init__(self, name=name)
self.data=queue
def run(self):
for i in range(5):
print("生成者%s将产品%d加入队列!" % (self.getName(), i))
self.data.put(i)
time.sleep(random.random())
print("生成者%s完成!" % self.getName())
# 消费者类
class Consumer(threading.Thread):
def __init__(self,name,queue):
threading.Thread.__init__(self,name=name)
self.data=queue
def run(self):
for i in range(5):
val = self.data.get()
print("消费者%s将产品%d从队列中取出!" % (self.getName(),val))
time.sleep(random.random())
print("消费者%s完成!" % self.getName())
if __name__ == '__main__':
print('-----主线程开始-----')
queue = Queue() # 实例化队列
producer = Producer('Producer',queue) # 实例化线程Producer,并传入队列作为参数
consumer = Consumer('Consumer',queue) # 实例化线程Consumer,并传入队列作为参数
producer.start() # 启动线程Producer
consumer.start() # 启动线程Consumer
producer.join() # 等待线程Producer结束
consumer.join() # 等待线程Consumer结束
print('-----主线程结束-----')
四、Python线程的GIL(全局解释器锁)问题
1、概念
由于在设计Python解释器时,开发人员加入了解释器锁,导致Python解释器同一时刻只能解释执行一个线程,无法并行执行,大大降低了线程的执行效率。
2、影响
因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以,Python多线程适合执行多阻塞高延迟IO任务,且执行效率比单线程更高。但是,其他情况对效率提升没有帮助。
在无阻塞状态下,多线程程序和单线程程序的执行效率差不多,甚至还不如单线程效率。但是,使用多进程解决相同的任务,效率明显提升。
3、解决措施
①.尽量使用进程完成无阻塞的并发行为。
②.不使用Cython解释器(C语言开发),转而用Jython解释器(Java开发)或IronPython解释器(.Net开发)等。
4、示例——GIL问题对计算/IO密集型程序运行的影响
代码如下:
import time
from threading import Thread
# 计算密集型函数
def count(x, y):
c = 0
while c < 7000000:
c += 1
x += 1
y += 1
# io密集型函数
def io():
write()
read()
def write():
f = open('test', 'w')
for i in range(1500000):
f.write("hello world\n")
f.close()
def read():
f = open('test')
lines = f.readlines()
f.close()
if __name__ == "__main__":
# 比较计算密集型程序单线程运行10次和10线程运行的耗时结果
time1 = time.time()
for i in range(10):
count(1, 1)
time2 = time.time()
run_time = time2 - time1
print("计算密集型程序单线程运行10次耗时:", run_time)
time3 = time.time()
jobs = []
for i in range(10):
t = Thread(target=count, args=(1, 1))
jobs.append(t)
t.start()
for i in jobs:
i.join()
time4 = time.time()
run_time2 = time4 - time3
print("计算密集型程序10线程运行一次耗时:", run_time2)
# 比较IO密集型程序单线程运行10次和10线程运行的耗时结果
time5 = time.time()
for i in range(10):
io()
time6 = time.time()
run_time3 = time6 - time5
print("IO密集型程序单线程运行10次耗时:", run_time3)
time7 = time.time()
tasks = []
for i in range(10):
t = Thread(target=io)
tasks.append(t)
t.start()
for i in jobs:
i.join()
time8 = time.time()
run_time4 = time8 - time7
print("IO密集型程序10线程运行一次耗时:", run_time4)
运行结果如下:
计算密集型程序单线程运行10次耗时: 10.351589679718018
计算密集型程序10线程运行1次耗时: 12.838714838027954
IO密集型程序单线程运行10次耗时: 6.566418409347534
IO密集型程序10线程运行1次耗时: 0.0047419071197509766
五、进程与线程的区别联系
1.进程时系统进行资源分配和调度的独立单位,线程是进程的一个实体,时CPU调度和分派的基本单位。
2.进程空间相互独立,数据互不干扰,有专门的通信方法;同一个进程中的多线程是内存共享的,它们使用全局变量进行通信。
3.进程的创建和销毁所耗资源比线程的多。
4.多个线程共享进程资源,在共享资源时往往需要同步互斥处理。