多任务的概念:
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
生理过程(从生到死) 创建 -> 就绪 -> 运行 -> 阻塞 -> 死亡
线程和进程的创建一定要在主函数中,且主任务和子任务一起往下执行,遇到join()方法,主任务会等子任务执行完在结束
线程:
线程可以理解成程序中的一个可以执行的分支, 它是cup调度的基本单元。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。
from threading import Thread
import time
#1. 如果多个线程执行的都是同一个函数的话,各自之间不会有影响,各是个的
def test(arg):
time.sleep(1)
print("thread"+str(arg))
for i in range(5): #5个线程同时执行
t = Thread(target=test,args=(i,))
t.start()
结果:
thread0
thread2
thread1
thread3
thread4
线程代码的封装:
import threading,time
class MyThread(threading.Thread):
def run(self):
for i in range(5):
time.sleep(1)
print('I`m '+self.name+ '进程 '+str(i)) #name当前的线程名
if __name__=='__main__':
t=MyThread()
t.start()
多线程的创建:
import threading,time
def dance():
print('子线程1:',threading.current_thread().name)
while True:
print('跳舞...')
time.sleep(1)
def sing():
print('子线程2:',threading.current_thread().name)
while True:
print('在唱歌...')
time.sleep(1)
def dance1(count):
print('子线程1:',threading.current_thread().name)
for i in range(count):
print('跳舞...')
time.sleep(1)
else:
print('任务完成...')
def sing1(count):
print('子线程1:',threading.current_thread().name)
for i in range(count):
print('跳舞...')
time.sleep(1)
else:
print('任务完成...')
def main():
"""多任务开始"""
print('main线程:',threading.current_thread().name)
thread1 = threading.Thread(target=dance1,args=(5,))
thread2 = threading.Thread(target=sing1,args=(3,))
thread1.start() # 开启线程
thread2.start()
if __name__ == '__main__':
"""单线程----->>多线程
cpu调度的单位是线程,无序的切换任务来提高效率
"""
main()
线程注意点:
1.线程之间执行是无序的
2.主线程会等待所有的子线程结束而结束
3.设置守护线程的目的是当,主线程结束后结束守护线程
4.如果想要子线程结束以后,主线程再执行相关代码可以使用join,阻塞子线程
5.线程共享全局变量,造成资源竞争数据丢失,解决办法:join(),互斥锁
多线程间共享全局变量,造成对共享数据的出错,使用互斥锁,给各个进程上锁,当这个进程释放后,下一个进程才开始执行,依次循环,保护好共享的安全性。
#多线程间共享数据出错:
from threading import Thread
import time
g_num = 0 #全局变量
def test1():
global g_num
for i in range(1000000):
g_num += 1
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
g_num += 1
print("---test2---g_num=%d"%g_num)
p1 = Thread(target=test1)
p1.start()
#time.sleep(3) #取消屏蔽之后 再次运行程序,数据就不会变化,因为test1进程在3s内足够执行完,不造成数据的错乱。
p2 = Thread(target=test2)
p2.start()
print("---g_num=%d---"%g_num)
结果:
---g_num=234497---
---test1---g_num=1143699
---test2---g_num=1342951
#使用互斥锁:
from threading import Thread, Lock
import time
g_num = 0
def test1():
global g_num
#这个线程和test2线程都在抢着 对这个锁 进行上锁,如果有一方成功的上锁,那么导致另外一方会堵塞(一直等待)到这个锁被解开为止
mutex.acquire()
for i in range(1000000):
g_num += 1
mutex.release()
#用来对mutex指向的这个锁 进行解锁,,,只要开了锁,那么接下来会让所有因为这个锁 被上了锁 而堵塞的线程 进行抢着上锁
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
mutex.acquire()
for i in range(1000000):
g_num += 1
mutex.release()
print("---test2---g_num=%d"%g_num)
#创建一把互斥锁,这个锁默认是没有上锁的
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
#time.sleep(3) #取消屏蔽之后 再次运行程序,结果会不一样,,,为啥呢?
p2 = Thread(target=test2)
p2.start()
print("---g_num=%d---"%g_num)
#互斥锁放到for循环里,最后是200万,第一个都在执行
-
同步:
同步就是协同步调,按预定的先后次序进行运行如:你说完,我再说。
如进程,线程同步,可理解为进程或线程甲和乙一块配合,A执行到一定程度时要依靠乙的某个结果,于是停下来,示意乙运行;乙依言执行,再将结果给阿; 一个再继续操作。
from threading import Thread,Lock
from time import sleep
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print("------Task 1 -----")
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print("------Task 2 -----")
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print("------Task 3 -----")
sleep(0.5)
lock1.release()
#使用Lock创建出的锁默认没有“锁上”
lock1 = Lock()
#创建另外一把锁,并且“锁上”
lock2 = Lock()
lock2.acquire()
#创建另外一把锁,并且“锁上”
lock3 = Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
结果:
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
蟒的队列模块中提供了同步的,线程安全的队列类,包括FIFO(先入先出)队列队列,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue.Queue可以使用队列来实现线程间的同步。
#队列解耦, - 解决死锁问题
- 异步:
from multiprocessing import Pool
import time
import os
#异步的理解:主进程正在做某件事情,突然 来了一件更需要立刻去做的事情,
#那么这种,在父进程去做某件事情的时候 并不知道是什么时候去做,的模式 就称为异步
def test():
print("---进程池中的进程---pid=%d,ppid=%d--"%(os.getpid(),os.getppid()))
for i in range(3):
print("----%d---"%i)
time.sleep(1)
return "hahah"
def test2(args):
print("---callback func--pid=%d"%os.getpid())
print("---callback func--args=%s"%args) #hahah
if __name__ == '__main__':
pool = Pool(3)
pool.apply_async(func=test,callback=test2)
time.sleep(5)
print("----主进程-pid=%d----"%os.getpid())
结果:
---进程池中的进程---pid=7664,ppid=5612--
----0---
----1---
----2---
---callback func--pid=5612
---callback func--args=hahah
----主进程-pid=5612----
- GIL
在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
python针对不同类型的代码执行效率也是不同的:
1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
使用建议?
python下想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
进程
在Python中进行多任务,通过并行处理任务。正在运行着的代码,成为就进程
一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源。(进程本质上是资源的集合)
一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程。
每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
from multiprocessing import Process
import time,random
def test():
for i in range(random.randint(1,5)):
print('-----%d----'%i)
time.sleep(1)
if __name__ == "__main__":
p = Process(target=test)
p.start() #让这个进程开始执行test函数里的代码
p.join()#堵塞
print('------main-------')
多进程的实现:
from multiprocessing import Pool
import os
import random
import time
#多进程的实现-利用进程池实现:
def worker(num):
for i in range(5):
print("===pid=%d==num=%d="%(os.getpid(), num))
time.sleep(1)
if __name__ == '__main__':
#3表示 进程池中对多有3个进程一起执行
pool = Pool(3)
for i in range(10):
print("---%d---"%i)
#向进程池中添加任务
#注意:如果添加的任务数量超过了 进程池中进程的个数的话,那么不会导致添加不进入
# 添加到进程中的任务 如果还没有被执行的话,那么此时 他们会等待进程池中的
# 进程完成一个任务之后,会自动的去用刚刚的那个进程 完成当前的新任务
pool.apply_async(worker, (i,)) #非堵塞的方式
pool.close()#关闭进程池,相当于 不能够再次添加新任务了
pool.join()#主进程 创建/添加 任务后,主进程 默认不会等待进程池中的任务执行完后才结束
#而是 当主进程的任务做完之后 立马结束,,,如果这个地方没join,会导致
#进程池中的任务不会执行
#方法1:
#p1=Process(target=xxx)
#p1.start()
#方法2:
# pool=Pool(3)
# pool.apply_async(xxxx)
进程间的通讯:
from multiprocessing import Process, Queue
import os, time, 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('所有数据都写入并且读完')
#进程池间的通讯:
#如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue()
协程
线程和进程的操作是由程序触发系统接口,最后的执行者是系统,它本质上是操作系统提供的功能。而协程的操作则是程序员指定的,在python中通过yield,人为的实现并发处理。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。协程,则只使用一个线程,分解一个线程成为多个“微线程”,在一个线程中规定某个代码块的执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO)。
常用第三方模块gevent和greenlet。(本质上,gevent是对greenlet的高级封装,因此一般用它就行,这是一个相当高效的模块。)
from gevent import monkey; monkey.patch_all()
import gevent
import requests
def f(url):
print('GET: %s' % url)
resp = requests.get(url)
data = resp.text
print('%d bytes received from %s.' % (len(data), url))
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])