多任务介绍
什么是多任务呢?
在现实生活中 有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的; 试想,如果把唱歌和跳舞这2件事情分开依次完成的话,估计就没有那么好的效果了(想一下场景:先唱歌,然后在跳舞,O(∩_∩)O哈哈~)
下面我们用程序来模拟唱歌跳舞这件事情
from time import sleep def sing(): for i in range(3): print("正在唱歌...%d"%i) sleep(1) def dance(): for i in range(3): print("正在跳舞...%d"%i) sleep(1) if __name__ == '__main__': sing() #唱歌 dance() #跳舞
.运行结果如下:
正在唱歌...0 正在唱歌...1 正在唱歌...2 正在跳舞...0 正在跳舞...1 正在跳舞...2
很显然上面的程序并没有完成唱歌和跳舞同时进行的要求。如果想要实现“唱歌跳舞”同时进行,那么就需要一个新的方法,叫做:多任务
多任务的概念
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
线程介绍
线程可以理解成程序中的一个可以执行的分支, 它是cup调度0的基本单元。python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的使用
我们可采用线程完成上面代码唱歌跳舞一起进行,如下
import threading from time import sleep def sing(): for i in range(3): print("正在唱歌...%d"%i) sleep(1) def dance(): for i in range(3): print("正在跳舞...%d"%i) sleep(1) if __name__ == '__main__': # 创建线程1 # "target" : 线程执行的目标函数 my_thread1 = threading.Thread(target=sing) my_thread2 = threading.Thread(target=dance) # 开启线程执行任务 my_thread1.start() my_thread2.start()
threading模块中的enumerate( )方法可以查看当前运行起来的线程的列表
threading模块中的active_count( )方法可以查看当前运行起来的线程的个数
注意: 线程运行起来是无序的,是由操作系统决定的,主线程须等待子线程都执行完成以后才退出程序,如下:
import time import threading def work1(num): print(threading.current_thread()) for i in range(num): print("working") time.sleep(1) if __name__ == '__main__': # 创建1 # target=后面不要加上函数的小括号 # args 执行函数所需要的参数 first_thread = threading.Thread(target=work1,args=(5,)) # 守护主线程,如果主线程退出了,那么子线程直接销毁 first_thread.setDaemon(True) first_thread.start() time.sleep(2) print("主线等待完了") exit()
运行结果如下:
<Thread(Thread-1, started daemon 10500)>
working
working
主线等待完了
线程执行代码的封装
由上述代码可以看出通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承
threading.Thread
就可以了,然后重写run
方法。 如下import threading # 自定义线程 class MyThread(threading.Thread): def __init__(self, num): # 注意: 需要调用父类的构造方法 super(MyThread, self).__init__() self.num = num # 任务1 def work1(self): for i in range(self.num): print("我是在自定义线程里面执行的", self.name) # 重写run方法 def run(self): self.work1() # 创建线程 mythread = MyThread(5) mythread.start()
运行结果如下:
我是在自定义线程里面执行的 Thread-1 我是在自定义线程里面执行的 Thread-1 我是在自定义线程里面执行的 Thread-1 我是在自定义线程里面执行的 Thread-1 我是在自定义线程里面执行的 Thread-1
总结:
- 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
- 当线程的run()方法结束时该线程完成。
- 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式
线程与线程间共享全局变量
如下:
import time import threading # 全局变量 num = 0 # 任务1 def work1(): global num for i in range(10): num += 1 time.sleep(0.1) # 任务2 def work2(): print(num) if __name__ == '__main__': # 创建线程1 first_thread = threading.Thread(target=work1) # 开启线程1 first_thread.start() # time.sleep(2) # 等待线程1执行完成以后在执行下面的代码 first_thread.join() print("线程1执行完成了。。。") # 创建线程2 second_thread = threading.Thread(target=work2) second_thread.start()
运行结果如下:
线程1执行完成了。。。
10
但是多线程间共享全局变量当数值较大时也会出现问题
如下:
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 is %d---"%g_num) t1 = threading.Thread(target=work1, args=(1000000,)) t1.start() t2 = threading.Thread(target=work2, args=(1000000,)) t2.start() while len(threading.enumerate()) != 1: time.sleep(1) print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果如下:
---线程创建之前g_num is 0--- ----in work1, g_num is 1199388--- ----in work2, g_num is 1370989--- 2个线程对同一个全局变量操作之后的最终结果是:1370989
为什么呢?
因为
在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0然后t2对得到的值进行加1并赋给g_num,使得g_num=1然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
所以:
如果多个进程同时对同一个全局变量操作,会出现资源竞争问题从而
数据结果会不正确
那么 如何解决线程同时修改全局变量的问题呢?
首先得先了解同步的概念,同步就是协同步调,按预定的先后顺序先后依次进行运行。
同:可以理解为协同协助相互配合
如:进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作
解决线程同时修改全局变量的思路如下:
- 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
- t1对g_num的值进行+1
- t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
- 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性
互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
#创建锁 mutex = threading.Lock() #锁定 mutex.acquire([blocking]) #释放 mutex.release()
其中,锁定方法acquire可以有一个blocking参数
如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
如果设定blocking为False,则当前线程不会堵塞
使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
import threading # 全局变量 current_num = 0 # 全局互斥锁 lock = threading.Lock() # 任务1 def work1(num): # 上锁 lock.acquire() global current_num for i in range(num): current_num += 1 print("work1 num:", current_num) # 释放锁 lock.release() # 任务2 def work2(num): # 上锁 lock.acquire() global current_num for i in range(num): current_num += 1 print("work2 num:", current_num) # 释放锁 lock.release() if __name__ == '__main__': # 创建2个线程 first_thread = threading.Thread(target=work1, args=(1000000, )) second_thread = threading.Thread(target=work2, args=(1000000, )) first_thread.start() second_thread.start()
运行结果如下:
work1 num: 1000000
work2 num: 2000000
总结
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子
import threading # 创建全局互斥锁 lock = threading.Lock() # 取值任务 def get_value(index): mylist = [1,2,4,6,3] lock.acquire() if index >= len(mylist): print("数组越界了对应索引是:", index) # 假如索引越界,也要释放锁,否则其它线程一直等待锁的释放,那么这样其它线程执行不了代码 # lock.release() return print(mylist[index]) lock.release() if __name__ == '__main__': for i in range(10): # 创建线程 thread = threading.Thread(target=get_value, args=(i,)) thread.start()
运行结果如下:
1
2
4
6
3
数组越界了对应索引是: 5
注意 此时程序并没有退出 而是处于阻塞状态了,因为当输进去的索引大于列表时return直接中止函数了并没有对锁进行释放,其他进程都在等待锁被释放。从而处于阻塞状态。
进程
什么是进程,什么是程序?
程序: 例如xxx.py这是程序,是一个静态的
进程: 通俗来说一个程序或者软件运行起来就是叫做一个进程, 你可以想成一个公司,公司需要准备相应工作需要的资源,对应我们进程来说,同样需要准备相应资源让代码能够执行, 每个进程启动都需要向操作系统申请资源,所以进程是操作系统资源分配的一个基本单位
默认情况下,一个进程至少会有一个线程, 没有进程就没有线程, 因为线程是依附在进程里面的
进程的创建-multiprocessing
multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。不仅可以通过线程完成多任务,进程也是可以的
# 之前线程可以完成多任务, 现在这个进程也可以完成多任务, 原因是进程里面默认一个条主线程在工作 import multiprocessing import time import os # 任务1 def work1(): for i in range(10): print("work1:------", i) print("work1的当前进程:", multiprocessing.current_process()) # 获取当前进程id print("work1获取当前进程的进程id:", multiprocessing.current_process().pid, os.getpid()) # 获取当前进程的父进程的id print("work1获取父进程的id", os.getppid()) time.sleep(0.1) if __name__ == '__main__': print("当前进程:", multiprocessing.current_process()) print("获取当前进程的进程id:", multiprocessing.current_process().pid, os.getpid()) # 创建进程 sub_process = multiprocessing.Process(target=work1) # 启动进程 sub_process.start() # 主进程的操作 while True: print("我在主进程中执行。。。") time.sleep(0.1)
运行结果如下:
当前进程: <_MainProcess(MainProcess, started)> 获取当前进程的进程id: 5528 5528 我在主进程中执行。。。 我在主进程中执行。。。 work1:------ 0 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 1 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 2 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 3 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 4 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 5 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 6 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 7 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 8 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 work1:------ 9 work1的当前进程: <Process(Process-1, started)> work1获取当前进程的进程id: 11828 11828 work1获取父进程的id 5528 我在主进程中执行。。。 我在主进程中执行。。。 我在主进程中执行。。。 我在主进程中执行。。。 下面会一直循环下去
Process语法结构如下:
target : 如果传递了函数的引用,可以任务这个子进程就执行这里的代码
args : 给target指定的函数传递参数,以元祖的方式传递
kwargs:给target指定的函数传递命名参数
name:给进程设定一个名字,也可以不设定
group:指定进程组,大多数情况下用不到
Process创建的实里对象的常用方法:
start():启动子进程实例(创建子进程)
is_alive( ): 判断进程子进程是否还在活着
join(【timeout】):是否等待子进程执行结束,或等待多少秒
terminate( ):不管任务是否完成,立刻终止子进程
进程之间不共享全局变量
代码实例如下
# 进程之间不共享全局变量 import multiprocessing import time # 全局变量 mylist = list() # 写入数据的任务 def write_data(): for i in range(10): mylist.append(i) time.sleep(0.1) print("写入后的数据:", mylist) # 读取数据的任务 def read_data(): while True: print("读取全局变量的数据为:", mylist) time.sleep(0.5) if __name__ == '__main__': # 创建2个进程 write_process = multiprocessing.Process(target=write_data) read_process = multiprocessing.Process(target=read_data) # 开启进程 write_process.start() # 等待写入进程执行完成以后再去读取数据 write_process.join() read_process.start() # 进程之间不共享全局变量
运行结果如下:
写入后的数据: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
读取全局变量的数据为: []
读取全局变量的数据为: []
读取全局变量的数据为: []
读取全局变量的数据为: []
读取全局变量的数据为:
......
因为进程是操作系统分配的基本单位,每创建一个进程都会向操作系统申请资源,操作系统会开辟一个新的内存空间供它使用。。就好比两个不同的房间,我给我房间买了一些家具,不关我邻居的事
给子进程制定的函数传递参数:如下
from multiprocessing import Process import os from time import sleep def run_proc(name, age, **kwargs): for i in range(10): print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age, os.getpid())) print(kwargs) sleep(0.2) if __name__=='__main__': p = Process(target=run_proc, args=('test',18), kwargs={"m":20}) p.start() sleep(1) # 1秒中之后,立即结束子进程 p.terminate() p.join()
运行结果如下:
子进程运行中,name= test,age=18 ,pid=1564... {'m': 20} 子进程运行中,name= test,age=18 ,pid=1564... {'m': 20} 子进程运行中,name= test,age=18 ,pid=1564... {'m': 20} 子进程运行中,name= test,age=18 ,pid=1564... {'m': 20} 子进程运行中,name= test,age=18 ,pid=1564... {'m': 20}
------------------------------------
进程和线程对比
1:进程是操作系统资源分配的基本单位, 进程只提供运行所需要资源, 操作给进程分配资源
2 :程是进程中的执行实例,可以理解一个执行的分支,默认情况下一个进程只有一个线程,也就是有一个分支, 线程是cpu调度的基本单位
3 :程之间可以共享全局变量, 进程直接不能共享,能是独立的两个进程,只是变量名相同而已。
4 :同一个进程中,如果进程中某一个线程挂了,那么进程直接退出死掉了, 如果在不同进程情况下,某一个进程死了, 不会影响其它进程的执行。
5 :进程开发程序的健壮型强, 多线程开发,某一个线程挂断那么进程就退出了,健壮型没有进程强
6 :进程需要额外分配新的运行资源,但是线程之间共享进程资源
进程间通信 ---Queue
Process之间有时需要通信,操作系统提供了很多机制来实现进程间的通信。
可以使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息列队程序,首先用一个小实例来演示一下Queue的工作原理
import multiprocessing if __name__ == '__main__': # 创建消息队列 # 如果指定参数,那么表示最大消息个数为3 queue = multiprocessing.Queue(3) # 放置数据 queue.put("hello") queue.put(23) # put可以放置任意对象 queue.put([23,4345]) # 提示: 如果这个消息队列满了,那么在put就会一直等待队列有位置了才能放入进程,否则一直等待 # queue.put((3,32)) # timeout: 表示超时时间,如果超过规定的时间,队列里面还没有空闲的位置,那么直接崩溃了 #queue.put((3, 32), timeout=1) # 不等待,直接往队列里面放置,如果没有位置就直接崩溃 # queue.put_nowait("word") print(queue) # 获取队列中的数据 print(queue.get()) print(queue.get()) print(queue.get()) # 如果队列里面没有数据了,一直等待队列中有消息了才能获取数据 # print(queue.get()) # timeout 超过规定时间队列里面没有数据那么直接崩溃了 # print(queue.get(timeout=1)) # 里面就取,消息队列没有数据就崩溃 # queue.get_nowait()
运行结果如下:
<multiprocessing.queues.Queue object at 0x00000211D76BD6A0> hello 23 [23, 4345]
Queue的常用方法如下:
queue.put(timeout='' ) : 向队列中添加数据,timeout可以设置等待时间
queue.put_nowait( ) : 不等待直接往队列中放放置数据,如果没有位置直接崩溃
queue.get( ): 从队列中取数据,如果没有数据,会阻塞,直到队列中有新数据
queue.get_nowait( ): 不等待直接从队列中取数据,如果没有数据直接崩溃
queue.empty( ) : 判断队列是否为空
queue.full( ): 判断队列是否满了
Queue.qsize():返回当前队列包含的消息数量
我们可以以Queue为例,在父进程创建两个子进程,一个往Queue里写数据,一个从Queue中读数据
import multiprocessing import time # 任务1,写入数据 def write_data(queue): for i in range(10): if queue.full(): print("消息队列满了, 不能在添加了") break # 向队列写入数据 print('%s正在往队列中添加'%i) queue.put(i) time.sleep(0.1) # 任务2, 读取数据 def read_data(queue): while True: if queue.empty(): print("消息队列没有数据了。") break print(queue.get()) if __name__ == '__main__': # 创建消息队列 queue = multiprocessing.Queue(5) # 创建2个进程 write_process = multiprocessing.Process(target=write_data, args=(queue, )) read_process = multiprocessing.Process(target=read_data, args=(queue, )) # 执行进程 write_process.start() # 保证写入进程执行完成以后,再取获取队列中的数据 write_process.join() # 执行进程 read_process.start()
运行结果如下:
0正在往队列中添加 1正在往队列中添加 2正在往队列中添加 3正在往队列中添加 4正在往队列中添加 消息队列满了, 不能在添加了 0 1 2 3 4 消息队列没有数据了。
进程池Pool
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,请看下面的实例:
import multiprocessing import time import os # 复制任务 def copy_work(index): print("正在复制中。。。。", index) print("获取当前进程的编号:", multiprocessing.current_process().pid, os.getpid()) time.sleep(0.1) if __name__ == '__main__': # 创建进程池 # 3 表示最大有三个进程 pool = multiprocessing.Pool(3) for i in range(10): # 使用进程值中的进程调用任务 # pool.apply_async(func = copy_work, args = (i,)) pool.apply_async(copy_work, (i,)) # 提示: 主线程不会等待进程池中的任务执行完成 # 关闭进程池,不接收其它任务了 pool.close() # 等待进程池执行完成以后,代码继续执行 pool.join() print("进程池中任务执行完了")
运行结果如下:
正在复制中。。。。 0 获取当前进程的编号: 1008 1008 正在复制中。。。。 1 获取当前进程的编号: 9984 9984 正在复制中。。。。 2 获取当前进程的编号: 6648 6648 正在复制中。。。。 3 获取当前进程的编号: 1008 1008 正在复制中。。。。 4 获取当前进程的编号: 9984 9984 正在复制中。。。。 5 获取当前进程的编号: 6648 6648 正在复制中。。。。 6 获取当前进程的编号: 1008 1008 正在复制中。。。。 7 获取当前进程的编号: 9984 9984 正在复制中。。。。 8 获取当前进程的编号: 6648 6648 正在复制中。。。。 9 获取当前进程的编号: 1008 1008 进程池中任务执行完了
进程池中的Queue
如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的实例演示了进程池中的进程如何通信:
import multiprocessing import time # 写入数据任务1 def write_data(queue): for i in range(2): queue.put(i) time.sleep(0.1) # 读取数据任务2 def read_data(queue): while True: if queue.empty(): break print(queue.get()) if __name__ == '__main__': # 创建进程池中的queue queue = multiprocessing.Manager().Queue(2) # 创建进程池 pool = multiprocessing.Pool(3) # 执行写入数据 current_process = pool.apply_async(write_data, (queue,)) # time.sleep(1) current_process.wait() pool.apply_async(read_data, (queue,)) # 关闭进程池 pool.close() # 等待进程池任务执行完以后在继续执行代码 pool.join()
运行结果如下:
0
1
import time
import threading
def work1(num):
print(threading.current_thread())
for i in range(num):
print("working")
time.sleep(1)
if __name__ == '__main__':
# 创建1
# target=后面不要加上函数的小括号
# args 执行函数所需要的参数
first_thread = threading.Thread(target=work1,args=(5,))
# 守护主线程,如果主线程退出了,那么子线程直接销毁
first_thread.setDaemon(True)
first_thread.start()
time.sleep(2)
print("主线等待完了")
exit()