多任务简述
说明:
由于现在电脑一般都是多核CPU,所用的操作系他也是多任务操作系他,因此如果在同一时间内做多件事,会使得效率大幅提高,这就是多任务的优势。
分类:
-
目前实现多任务可以通过两种方法:
-
并发(在一段时间内交替执行多个任务,在某个时刻实际上只有一个任务在执行,只是每个任务交替执行的时间很短,任务交替执行的切换速度很快,在宏观上看起来就像是同时在做多件事情,一般用于单核CPU)
-
并行(在一段时间内,多核CPU中的每个核心单独做一件事,这是真正的同时在做多件事情)
-
途径有:
-
多进程(资源开销大,是真正的并行)
-
多线程(资源开销小,但实际上是并发)
进程实现多任务
进程:
进程(Process)是资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位,可以简单理解为一个正在运行的程序就是一个进程.例如:正在运行的QQ,微信等,他们都是一个进程。一个程序至少拥有一个进程。
默认情况下,程序运行后会创建一个主进程,用来执行代码。但我们可以通过创建子进程的方式,让操作系统为每个进程分配处理机(CPU),从而使得程序可以在多个进程下同时处理多件事,实现多任务。
进程的使用
# 导入多线程包
import multiprocessing as mtp
# 设计进程编号需要导入os
import os
# 测试函数
def test():
print("启动多任务")
# 创建进程类示例
'''
target :执行的目标任务名(函数名)
args :传递给任务的参数 - 元组形式(无参数时可省略) (注意一个参数时元组的写法,传参须按顺序)
kwargs :传递给任务的参数 - 字典形式(无参数时可省略) (传参按照函数的参数键传递)
name :进程名(一般不用设置)
grop :进程组
'''
newProcess = mtp.Process(target = test,name='进程名1',group=None)
newProcess.daemon = False # 设置创建的子进程为非守护主进程模式(详见下面的注意点3)
# 启动创的进程执行任务
newProcess.start()
# 获取进程编号
## 获取当前进程编号
pid = os.getpid()
print('pid:',pid)
## 获取当前进程的父进程编号
ppid = os.getppid()
print('ppid:',ppid)
pid: 7360
ppid: 21960
示例
import multiprocessing
import time
import os
def test1(times):
for i in range(times):
print("执行多进程任务1")
time.sleep(0.5)
def test2(times):
for i in range(times):
print("执行多进程任务2")
time.sleep(0.5)
newProcess1 = multiprocessing.Process(target = test1,args=(4,))
newProcess2 = multiprocessing.Process(target = test2,kwargs = {'times':4})
if __name__ == '__main__':
# 启动创的进程执行任务
startTime = time.time()
print("【单进程演示开始】")
test1(4)
test2(4)
endTime = time.time()
print(endTime - startTime)
print("【多进程演示开始】")
startTime = time.time()
newProcess1.start()
newProcess2.start()
newProcess1.join()
newProcess2.join()
endTime = time.time()
print('time',endTime - startTime)
【单进程演示开始】
执行多进程任务1
执行多进程任务1
执行多进程任务1
执行多进程任务1
执行多进程任务2
执行多进程任务2
执行多进程任务2
执行多进程任务2
time 4.047036647796631
【多进程演示开始】
执行多进程任务1
执行多进程任务2
执行多进程任务2
执行多进程任务1
执行多进程任务2
执行多进程任务1
执行多进程任务1
执行多进程任务2
time 2.1405797004699707
注意点
- 1.主进程会在所有子进程都结束之后再结局
- 2.主进程和子进程虽然是上下级的关系,但其实主进程和子进程也是同时执行的,并不是先执行子进程后再执行主进程
- 3.如果想要实现主进程结束后结束所有子进程,则需要设置创建的子进程为守护主进程模式。
线程实现多任务
线程: 线程是程序执行的最小单位,其同样可以用来实现多任务。但是如果使用多进程来实现多任务是比较耗费资源的,因为进程是分配资源的最小单位,一旦创建一个进程就必须要向其分配一定的资源,就像跟两个人聊QQ就需要打开两个QQ软件一样,很明显这是比较浪费资源的。
一个程序至少有一个进程,而一个进程至少有一个线程,也就说进程是线程的容器。
实际上进程只负责分记资源,而利用这些资源执行程序的是线程,同时线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但线程可与同属一个进程的其它线程共享进程所拥有的全部资源.这就像通过一个QQ软件(一个进程)打开两个窗口(两个线程)跟两个人聊天一样,实现多任务的同时也节省了资源。
线程的使用
# 导入多线程包
import threading
import time
# 测试函数
def test1(times):
for i in range(times):
print("正在执行多线程1")
time.sleep(3)
def test2(times):
for i in range(times):
print("正在执行多线程2")
time.sleep(3)
# 创建线程类示例
'''
target :执行的目标任务名(函数名)
args :传递给任务的参数 - 元组形式(无参数时可省略) (注意一个参数时元组的写法,传参须按顺序)
kwargs :传递给任务的参数 - 字典形式(无参数时可省略) (传参按照函数的参数键传递)
daemon : 是否设置为守护线程
name :线程名(一般不用设置)
grop :线程组
'''
newThread1 = threading.Thread(target = test1 , args = (3,) , daemon = True)
newThread2 = threading.Thread(target = test2 , kwargs = {'times':3} , daemon = True)
# 启动创的线程执行任务
print("【单线程演示开始】")
test1(3)
test2(3)
print("【多线程演示开始】")
newThread1.start()
newThread2.start()
newThread1.join()
newThread2.join()
print("【多线程演示结束】")
【单线程演示开始】
正在执行多线程1
正在执行多线程1
正在执行多线程1
正在执行多线程2
正在执行多线程2
正在执行多线程2
【多线程演示开始】
正在执行多线程1
正在执行多线程2
正在执行多线程1正在执行多线程2
正在执行多线程1正在执行多线程2
【多线程演示结束】
在上面的输出中,出现的奇怪的换行,这是因为线程的执行顺序是无序的。
线程的互斥
引入:
在之前的说明中,我们提到了线程共享进程的资源,那么线程也必定共享进程的变量资源,倘若我们有俩线程thread1,thread2。thread1对共享变量(初始值为0)做100次+1的操作,而thread2对刚才的共享变量做100次-1的操作,我们让这俩线程启动后,希望的值应该为0。但是实际上这个值是不确定的,可能是任意值。
import threading
total = 0
def add():
global total
for i in range(100):
total += 1
def desc():
global total
for i in range(100):
total -= 1
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
-46
说明:
我们这里假设每个线程只做一次+1或-1的操作,简要的分析输出结果为-1的情况:
- 1.thread1获取了共享变量temp的初始值0,在准备对其进行+1操作时,被操作系统剥夺处理机资源(CPU),然后其将保存被剥夺前的线程的相关信息(包括这个共享变量temp的值:0)
- 2.被剥夺的处理机资源给到了thread2,获取到了temp的值(0),并且其也在准备进行-1操作时被操作系他剥夺处理机资源,其也保存被剥夺前的自身线程的相关信息(包括这个共享变量temp的值:0)
- 3.处理机资源重新给到thread1,其用到之前保存下来的temp变量的值进行了+1操作,并将结果赋值给共享变量(此时共享变量值为0+1 = 1)
- 4.处理机资源重新给到thread2,其用到之前保存下来的temp变量(这里的temp值为0,虽然进程中temp的值刚刚被修改为1,但是线程不会去使用进程中的值,而是使用其保存的值)的值进行了-1操作,并将结果赋值给共享变量temp,此时temp的值为0-1=-1,并把结果赋值给temp。
- 5.最后输出temp的值:-1
为了防止这种情况的发生,我们希望对共享的一些资源进行可控的管理,因此引入了线程的互斥(对某一临界资源(也就是希望被可控管理的共享资源)在某一时刻,只允许一个线程去进行获取、修改等所有与之相关的操作),并使用互斥信号量mutex来管理线程的互斥,下面例子中的lock变量就可以作为互斥信号量。互斥信号量一般初始值为1,只有当互斥信号量等于1时才能把临界资源分配给某个线程,当将临界资源分配给某个线程后,互斥信号量将-1;当线程释放临界资源后,互斥信号量+1。
代码示例
Lock
import threading
from threading import Lock # 互斥锁
lock = Lock() # 创建锁实例
total = 0 # 临界资源(共享变量)
def add():
global total
global lock
for i in range(100):
lock.acquire() # 使用lock.acquire函数尝试获取锁,如果成功获取锁则可执行下面代码,否则将一直被阻塞在这,直至获得锁
total += 1
lock.release() # 该线程对临界资源(共享变量)的使用已经结束了,可以释放该资源给其他线程使用了,所有其释放锁
def desc():
global total
global lock
for i in range(100):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
'''
lock的常用方法
acquire(blocking=True, timeout=-1): 尝试获取锁。如果 blocking 为 True(默认值),则线程会阻塞直到获取到锁;如果设置为 False,则不会阻塞,如果无法立即获取锁会返回 False。timeout 用于指定阻塞的超时时间(以秒为单位),超过时间未获取到锁则返回 False。
release(): 释放锁。
locked(): 如果锁被某个线程持有,返回 True
'''
0
RLock
Rlock是为了解决在同一个线程中锁的多次acquire问题。但是一定要注意在一个线程中需要保证acquire和release的次数要一样。
import threading
from threading import RLock
lock = RLock()
total = 0
def add():
global total
global lock
for i in range(100):
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
def desc():
global total
global lock
for i in range(100):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
0
线程的同步
引入:
在某些情况下,我们需要某个线程先执行,某个线程后续才能正常运行。比如著名的消费者与生产者问题。生产者进程只有先执行,生产资源后,消费者线程才能进行消费。并且这种情况下,共享变量一般初始值为0,最大值不为1,而是视情况而定,也有可能没有最大值,这种共享变量在操作系他原理中我们称之为同步信号Semaphore。与互斥信号量类似,只有当同步信号量大于0时才能把临界资源分配给某个线程,当将临界资源分配给某个线程后,同步信号量将-1;当线程释放临界资源后,同步信号量+1。
代码示例
import time
import threading
se = threading.Semaphore(0) # se1同步信号量的初始值是0 表示生产者生产的商品剩余个数
# 消费者1线程函数
def customer1():
global se
print("customer1请求se信号量。")
se.acquire() # 向se信号量请求一个信号 表示消费产品
print("customer1请求信号量成功")
# 这里不同于之前的mutex变量 其不进行释放资源 表示资源(生产者产生的产品已被消费)
# 消费者2线程函数
def customer2():
global se
print("customer2请求se信号量。")
se.acquire() # 向se信号量请求一个信号
print("customer2请求信号量成功")
# 生产者线程函数
def producer():
# 释放两个se信号 表示生产了两件产品
se.release()
se.release()
# 主线程函数
def main():
t1 = threading.Thread(target=producer,name="thread_producer",daemon=True) # 创建producer子线程
t2 = threading.Thread(target=customer1,name="thread_customer1",daemon=True) # 创建cusotmer1子线程
t3 = threading.Thread(target=customer2,name="thread_customer2",daemon=True) # 创建customer2子线程
t1.start() # 启动producer线程
time.sleep(5) # 确保消费者先生产商品
t2.start() # 启动customer1线程
t3.start() # 启动customer2线程
t1.join()
t2.join()
t3.join()
print("主线程运行结束!")
if __name__ == "__main__":
main()
customer1请求se信号量。customer2请求se信号量。
customer1请求信号量成功
customer2请求信号量成功
主线程运行结束!
线程通讯
引入:
实际上,在上面的生产者消费者代码中,就已经用到了一种线程间通讯的方式–“全局变量(共享变量)”,除此之外,我们还可以使用queue消息队列来进行线程间通讯。发送线程可以通过put方法发送消息到queue信息队列,接收线程则使用get方法从信息队列中取消息。
代码示例
import threading
import queue
import time
#创建一个队列,参数为信息队列设定的最大长度
queue = queue.Queue(1) #创建一个先进先出的队列
count = 0
lock = threading.Lock()
#定义生产者线程类
class Producer(threading.Thread):
def run(self):
global queue,count,lock
while True:
time.sleep(4)
print("生产线程开始生产数据")
lock.acquire()
count+=2 # 假定其一次生产俩产品
msg="产品{}".format(count)
lock.release()
queue.put(msg) # 默认阻塞
print(msg)
#定义消费者线程类
class Consumer(threading.Thread):
def run(self):
global queue,count,lock
while True:
print('消费者线程开始消费线程了')
msg=queue.get() # 默认阻塞
print('消费线程得到了数据:{}'.format(msg))
lock.acquire()
print("消费者消费了一个产品")
count-=1
lock.release()
time.sleep(4)
if __name__ == '__main__':
t1 = Producer()
t2 = Consumer()
t1.start()
t2.start()
消费者线程开始消费线程了
生产线程开始生产数据
产品2
消费线程得到了数据:产品2
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了
产品3
消费线程得到了数据:产品3
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了
产品4
消费线程得到了数据:产品4
消费者消费了一个产品
消费者线程开始消费线程了生产线程开始生产数据
产品5
消费线程得到了数据:产品5
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了
产品6
消费线程得到了数据:产品6
消费者消费了一个产品
消费者线程开始消费线程了
生产线程开始生产数据
产品7
消费线程得到了数据:产品7
消费者消费了一个产品
消费者线程开始消费线程了生产线程开始生产数据
产品8
消费线程得到了数据:产品8
消费者消费了一个产品
...(程序为死循环,将一直运行)
注意点
- 1.线程内部的调度顺序是无序的,即有可能出现最先调用start方法启动的线程在最后才被调度启动的情况。因为i一个进程下的所有线程共享进程所有的资源,当然包括CPU的调度机会,而一个进程只能获取一个CPU的调度机会,因此一个进程内在某一时刻最多只有一个线程在被调度运行,其他线程处于就绪态。所有进程中的多任务是并发的。
- 2.主线程会在所有子线程都结束之后再结局
- 3.如果想要实现主线程结束后结束所有子线程,则需要设置创建的子线程为守护主线程模式。