计算机中的多任务是指,操作系统同时完成多项任务的处理。此处,同时是指同一个时间段内,而非某个瞬时时间点
多任务处理是指用户在同一时间段内运行多个应用程序,每个应用程序就可以称之为一个任务
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集
一个程序的执行实例,每个程序提供执行程序所需的所有资源(资源的集合)
一个进程有虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等)、唯一定的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要至少有一个线程
python的os模块封装了常见的系统调用函数,其中包括fork(),可以让我们在程序中轻松的创建于进程
import os
pid = os.fork()
if pid == 0:
print("Zhengjiang University")
else:
print("City College")
在Unix/Linux,提供了fork()系统函数
fork() 子进程永远返回0,而父进程返回子进程的ID
一个父进程可以fork出很多子进程,父进程可以记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID
import os
pid = os.fork()
if pid < 0:
print("fork 调用失败")
elif pid == 0:
print("我是子进程:\t %s,我的父进程是:\t %s"%(os.getpid(),os.getppid()))
else:
print("我是父进程:\t %s,我的子进程是:\t %s"%(os.getpid(),pid))
print("父子进程都可以执行这里")
zht@zht-virtual-machine:~/process_demo$ python3 fork_demo2.py
我是子进程: 4679,我的父进程是: 4678
我是父进程: 4678,我的子进程是: 4679
父子进程都可以执行这里
父子进程都可以执行这里
多进程修改全局变量
#demo.py
import os
import time
a = 0
pid = os.fork()
if pid == 0:
a += 1
print("子进程num:%d" % a)
else:
time.sleep(1)
a += 1
print("父进程num:%d" % a)
zht@zht-virtual-machine:~$ python3 demo.py
子进程num:1
父进程num:1
多进程中,每个进程的所有数据都各自拥有一份,互不影响
多个fork
#mul_fork
import os
import time
pid = os.fork()
if pid == 0:
print("fork1_1")
else:
print("fork1_2")
pid = os.fork()
if pid == 0:
print("fork2_1")
else:
print("fork2_2")
zht@zht-virtual-machine:~$ python3 mul_fork.py
fork1_2
fork1_1
fork2_2
fork2_2
fork2_1
fork2_1
父子进程执行顺序无规律,完全取决于操作系统的调度算法
1.Process
import multiprocessing
格式:
Process([group [,target [,name [,args [,kwargs ] ] ] ] ] )
常用方法:
is_alive():判断进程实例是否正在进行
join([timeout]):等待进程实例执行结束,或者等待多少秒
start():启动进程实例(创建子进程)
run():如果未给定target参数,对这个对象调用satrt()方法时,就执行对象中的run()方法
terminate():不管任务是否完成,立即终止
常用属性:
name:当前进程的实例名,默认为Process-N,N为从1开始递增的整数
pid:当前进程实例的pid值
# 启动子进程并等待期结束
from multiprocessing import Process
import os
def run_process(name):
print("子进程运行,name=%s,pid=%d" % (name,os.getpid()))
if __name__ == '__main__':
print("父进程 %d" % os.getpid())
p = Process(target=run_process,args=('test',))
print("子进程将要执行")
p.start()
p.join()
print("子进程结束")
from multiprocessing import Process
import os
import time
# 子进程要执行的代码
def run_process(name, age, **kwargs):
for i in range(10):
print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age,os.getpid()))
print(kwargs)
time.sleep(0.5)
if __name__=='__main__':
print('父进程 %d.' % os.getpid())
p = Process(target=run_process, args=('test',18), kwargs={"m":20})
print('子进程将要执行')
p.start()
time.sleep(1)
p.terminate()
p.join()
print('子进程已结束')
类创建子进程
from multiprocessing import Process
import time
import os
#继承Process类
class Process_Class(Process):
# 模块中,Process类本身有__init__方法,现在这个子类重写Process方法
# 导致无法完全初始化一个Process类,进而就不能从这个类继承的一些方法和属性
# 解决方法,将继承类本身传递给Process.__init__,完成初始化操作
def __init__(self,interval):
Process.__init__(self)
self.interval = interval
#重写了Process类的run()方法
def run(self):
print("子进程(%s) 开始执行,父进程为(%s)"%(os.getpid(),os.getppid()))
t_start = time.time()
time.sleep(self.interval)
t_stop = time.time()
print("(%s)执行结束,耗时%0.2f秒"%(os.getpid(),t_stop-t_start))
if __name__=="__main__":
t_start = time.time()
print("当前程序进程(%s)"%os.getpid())
p1 = Process_Class(2)
p1.start()
p1.join()
t_stop = time.time()
print("(%s)执行结束,耗时%0.2f"%(os.getpid(),t_stop-t_start))
进程池的概念
子进程数量不多,可以使用Process方法;当数量巨大,成百上千个,手动创建工作量巨大,可以使用multiprocessing中的Pool方法
初始化Pool时,可以指定一个最大进程数,当有新的请求提交给Pool时,如果池未满,就可以创建新的进程来执行请求。但如果池中进程已满,即达到最大值,该请求就会被等待,直到池中有进程结束,才创建一个新的进程来执行
2.multiprocessing.Pool
常用的函数
apply_async(func[,args[,kwargs] ] ):使用非阻塞方式调用func(并行执行,阻塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的单参数,kwargs为传递给func的关键词参数
apply(func[,args[,kwargs] ] ):使用阻塞方式调用func
close():关闭Pool,使其不再接受新任务
terminate():不管任务是否完成,立即终止
join():主程序阻塞,等待子程序退出,一般在close()或者terminate()之后使用
#pro_pool.py
from multiprocessing import Pool
import os
import time
import random
def worker(msg):
t_start = time.time()
print("%s开始执行,进程号为%d" % (msg,os.getpid()))
time.sleep(random.random()*2)
t_stop = time.time()
print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start))
if __name__ == '__main__':
po = Pool(3) # 定义进程池,最大进程数为3
for i in range(0,10):
po.apply_async(worker,(i,))
print("start".center(11,"-"))
po.close()
po.join()
print("end".center(11, "-"))
zht@zht-virtual-machine:~$ python3 pro_pool.py
---start---
0开始执行,进程号为57069
1开始执行,进程号为57071
2开始执行,进程号为57070
0 执行完毕,耗时0.06
3开始执行,进程号为57069
3 执行完毕,耗时0.67
4开始执行,进程号为57069
2 执行完毕,耗时0.97
5开始执行,进程号为57070
4 执行完毕,耗时0.59
6开始执行,进程号为57069
1 执行完毕,耗时1.35
7开始执行,进程号为57071
6 执行完毕,耗时0.39
8开始执行,进程号为57069
8 执行完毕,耗时0.32
9开始执行,进程号为57069
5 执行完毕,耗时1.08
7 执行完毕,耗时1.84
9 执行完毕,耗时1.16
----end----
3.Queue ---- 进程间的通信
from multiprocessing import Queue
q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息
q.put("消息1")
q.put("消息2")
print(q.full()) #False
q.put("消息3")
print(q.full()) #True
#因为消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,第二个Try会立刻抛出异常
try:
q.put("消息4",True,2)
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())
try:
q.put_nowait("消息4")
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())
#推荐的方式,先判断消息列队是否已满,再写入
if not q.full():
q.put_nowait("消息4")
#读取消息时,先判断消息列队是否为空,再读取
if not q.empty():
for i in range(q.qsize()):
print(q.get_nowait())
初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)
常用的函数
Queue.qsize():返回当前队列包含的消息数量
Queue.empty():如果队列为空,返回True,反之False
Queue.full():如果队列满了,返回True,反之False
Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常
2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常
Queue.get_nowait():相当Queue.get(False)
Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常
2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常
Queue.put_nowait(item):相当Queue.put(item, False)
from multiprocessing import Process, Queue
import os
import time
import random
# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
while True:
if not q.empty():
value = q.get(True)
print('Get %s from queue.' % value)
time.sleep(random.random())
else:
break
if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 等待pw结束:
pw.join()
# 启动子进程pr,读取:
pr.start()
pr.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
print('')
print('所有数据都写入并且读完')
线程是操作系统能够运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位。一条线程是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并行执行的不同的任务
一条线程是一个execution context(执行上下文),即一个CPU执行时所需要的一串指令
假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。
线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。
import threading
import time
def download_music():
for i in range(5):
time.sleep(1)
print("---正在下载歌曲%d---" % i)
def play_music():
for i in range(5):
time.sleep(1)
print("---正在下载歌曲%d---" % i)
def main():
# 创建两个线程对象,target指向新开启的线程要执行的函数
t1 = threading.Thread(target=download_music)
t2 = threading.Thread(target=play_music)
t1.start()
t2.start()
if __name__ == '__main__':
main()
#download_music()
#play_music()
可以明显看出使用多线程并发的操作,花费时间要短很多
当我们调用start()时,才会真正的执行线程中的代码
import threading
import time
# 自定义类,threading.Thread
class MyThread(threading.Thread):
def run(self):
for i in range(5):
time.sleep(1)
#name 保存的是当前线程的名字
msg = "I am "+ self.name + " @ " + str(i)
print(msg)
if __name__ == '__main__':
t1 = MyThread()
t2 = MyThread()
t1.start()
t2.start()
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程中覆盖该方法。而创建自己的线程实例后,通过Thread的start()方法,可以启动该线程。当该线程获得执行的机会时,就会调用run()方法执行线程
注:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
线程何时开启,何时结束
import threading
import time
def test1():
for i in range(5):
time.sleep(1)
print("---子线程1---%d"%i)
print("子线程1中查看线程情况",threading.enumerate())
def test2():
for i in range(10):
time.sleep(1)
print("---子线程2---%d" % i)
print("子线程2中查看线程情况", threading.enumerate())
def main():
# threading.enumerate()可枚举当前运行的所有线程
print("创建线程之间的线程情况",threading.enumerate())
# 创建线程对象
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
time.sleep(1)
print("创建线程之间的线程情况", threading.enumerate())
t1.start()
t2.start()
time.sleep(1)
print("调用了thread.start()之后的线程情况",threading.enumerate())
t2.join() # 当t2线程执行完后,在执行后续代码
print('查看当前线程',threading.enumerate())
if __name__ == '__main__':
main()
线程的执行顺序
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
#name 保存的是当前线程的名字
msg = "I am "+ self.name + " @ " + str(i)
print(msg)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
假设有两个线程t1和t2,都要对一个变量g_num进行运算(+1),两个线程t1和t2分别对g_num各加10次,g_num的最终结果?
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("---in work1,g_num is %d ---" % g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("---in work2,g_num is %d ---" % g_num)
print("---线程创建之前g_num:%d" % g_num)
t1 = threading.Thread(target=work1,args=(10,))
t2 = threading.Thread(target=work2,args=(10,))
t1.start()
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("两个线程对同一变量操作之后的最终结果:%d" % g_num)
#---线程创建之前g_num:0
#---in work1,g_num is 10 ---
#---in work2,g_num is 20 ---
#两个线程对同一变量操作之后的最终结果:20
一种可能情况:
在num=0时,t1取得num=0,此时系统吧t1调度为"sleeping"的状态,t2转换为"running"的状态,t2也获得num=0。然后,t2对得到的值进行加1并赋给num,num=1,之后系统又将t2调度为"sleeping"的状态,把t1转换成"running"。线程t1又把它之前得到的0加1后赋给num。这种情况,明明两个线程都完成了一次+1工作,但结果还是num=1
如果我们将两个进程的参数调整为1000000,多次运行,结果不同。
说明多线程同时对一个全局变量进行操作,会出现资源竞争问题,从而数据结果不正确,导致线程安全问题
同步就是协同步调。按照预定的先后次序进行运行。好比交流,一个说完,另一个人再说。
进程和线程同步,可以理解为进程或者线程A和B一块配合,A执行一定程度时需要依赖B的某个结果,于是停下来,让B运行,再将结果给A,A再继续运行。如此往复,直至程序结束
通过线程同步
进行解决
思路:
当多个线程几乎同时修改某个共享数据时,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制就是引入互斥锁
互斥锁为我们的资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时,资源的状态为锁定,其他线程不能对其更改;直到该线程释放,资源状态变为“非锁定”状态,其他线程才能再次锁定该资源
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性
在threading模块中,定义了Lock()类,可以方便的处理锁定
mutex = threading.Lock() # 创建锁
mutex.acquire([blocking]) # 锁定
mutex.release() # 释放
说明:
在线程间共享多个资源是,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁
死锁一般很少发生,但一旦发生就会造成停止响应
import threading
import time
printer_mutex = threading.Lock() # 打印机锁
paper_mutext = threading.Lock() # 纸张锁
class ResumeThread(threading.Thread):
"""编写个人简历任务的线程"""
def run(self):
print("ResumeThread:编写个人简历任务")
# 使用打印机资源,先对打印机加锁
printer_mutex.acquire()
print("--ResumeThread:正在使用打印机资源--")
time.sleep(1) # 休眠1秒
# 使用纸张耗材,先对纸张耗材加锁
paper_mutext.acquire()
print("--正在使用纸张资源--")
time.sleep(1)
paper_mutext.release() # 释放纸张锁
# 释放打印机锁
printer_mutex.release()
class PaperListThread(threading.Thread):
"""盘点纸张耗材任务的线程"""
def run(self):
print("PaperListThread:盘点纸张耗材任务")
# 使用纸张耗材,先对纸张耗材加锁
paper_mutext.acquire()
print("--PaperListThread:正在盘点纸张耗材--")
time.sleep(1) # 休眠1秒
# 使用打印机资源,打印清单
printer_mutex.acquire()
print("--正在使用打印机资源--")
time.sleep(1)
printer_mutex.release() # 释放打印机锁
# 释放纸张耗材锁
paper_mutext.release()
if __name__ == '__main__':
t1 = ResumeThread()
t2 = PaperListThread()
t1.start()
t2.start()
让多个线程有序的执行
生产者消费者问题
也就有限缓冲问题,是一个多线程同步的经典案例
描述了一个两个固定大小缓冲区的线程,即所谓的“生产者”和“消费者”,在时间运行时会发生的问题
生产者的主要作用,生产一定量的数据放在缓冲器中,然后,重复此过程
与此同时,消费者也在缓冲器消耗这些数据
整个问题的关键是,生产者不会再缓冲器满时加入数据,消费者也不会再缓冲区空时消耗数据
解决办法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形
1.队列,先入先出
2.栈,后入先出
python中queue(py3)(py2,Queue),模块提供了一个同步的、线程安全的队列类,包括先入先出(FIFO)队列Queue,和后入先出(LIFO)队列LifoQueue和优先级队列PriorityQueue
这些队列实现了锁原语(原子操作,要么不做,要么做完),可以在线程中直接使用
可以使用队列来实现线程间的同步
FIFO队列实现生产者消费者问题
import threading
import time
class Task1(threading.Thread):
def run(self):
while True:
if lock1.acquire():
print("----Task1----")
time.sleep(0.5)
lock2.release()
class Task2(threading.Thread):
def run(self):
while True:
if lock2.acquire():
print("----Task2----")
time.sleep(0.5)
lock3.release()
class Task3(threading.Thread):
def run(self):
while True:
if lock3.acquire():
print("----Task3----")
time.sleep(0.5)
lock1.release()
# 使用Lock创建锁,默认没有锁上
lock1 = threading.Lock()
# 创建锁2,并且锁上
lock2 = threading.Lock()
lock2.acquire()
# 创建锁3,并且锁上
lock3 = threading.Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()