1. 线程
1.1 线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
import time
def test(m):
print(f"test{m}")
time.sleep(1)
def ceshi(m):
print(f"ceshi{m}")
time.sleep(2)
start = time.time()
test(1)
ceshi(2)
end = time.time()
print(end-start) #3.0004477500915527
引入线程
import time
import threading
def test(m):
print(f"test{m}")
time.sleep(1)
def ceshi(m):
print(f"ceshi{m}")
time.sleep(2)
start = time.time()
#创建线程
#线程1
t1 = threading.Thread(target=test, args=(1,))
#线程2
t2 = threading.Thread(target=ceshi, args=(2,))
#启动线程
t1.start()
t2.start()
end = time.time()
#两个线程同时执行,主线程执行完,没有等待子线程
print(end-start) #0.0010666847229003906
#等待子线程执行完
t1.join()
t2.join()
end = time.time()
print(end-start) #2.0019893646240234,比之前的3秒少了
线程对比
import threading
import time
start = time.time()
def add(n):
sum=0
for i in range(n):
sum+=i
# add(90000000)
# add(80000000)
# end = time.time()
# print(end-start) #16.6631920337677
#引入线程
t1 = threading.Thread(target=add, args=(90000000,))
t2 = threading.Thread(target=add, args=(80000000,))
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(end-start) #13.966155529022217
1.2 GIL锁
来源:https://www.cnblogs.com/xinfan1/p/10981601.html
全名是(Global Interpreter Lock 全局解释器锁)
官方解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from eecuting Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
释义:
在CPython中,这个全局解释器锁,也称之为GIL,是一个互斥锁,防止多个线程。在同一时间执行python字节码,这个锁是非常重要的,因为CPython的内存管理是非线程安全的,很多其他的特性依赖于GIL锁,所以即使它影响了程序效率也无法将其直接去除。
总结:
在CPython中,GIL会把线程的并行编程串行,导致效率降低。
但需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。GIL也仅存在与CPython中
这并不是Python这门语言的问题,而是CPython这个解释器的问题。
非线程安全: 即多个线程访问同一个资源,会有问题
线程安全:即多个线程访问同一个资源,不会有问题
CPython中有一个互斥锁,防止线程同一时间执行python代码
该锁只存在Cpython中。
之所以用CPython的原因:
1.C编译过的结果可以让计算机直接识别
2.C语言中大量现成的库(算法),CPython可以无缝链接c语言的任何现成代码。
内存管理:
垃圾回收机制
python中不需要手动管理内存,它有个回收垃圾线程GC,也是python自带的内存管理机制。
GIL锁的加锁和解锁时机:
加锁的时机:在调用解释器时立即加锁
解锁时机:
1.当前线程遇到了IO时释放。
2.当前线程执行时间超过设定值时释放。
关于GIL的性能讨论:
GIL的优点:
保证了CPython中的内存管理是线程安全的
GIL的缺点:
互斥锁的特性使得多线程无法并行
但我们并不能因此就否认Python这门语言,其原因如下:
GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点
在单核处理器下,多线程之间本来就无法真正的并行执行
在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了
举个例子:
任务1 从网络上下载一个网页,等待网络IO的时间为1分钟,解析网页数据花费,1秒钟
任务2 将用户输入数据并将其转换为大写,等待用户输入时间为1分钟,转换为大写花费,1秒钟
单核CPU下:1.开启第一个任务后进入等待。2.切换到第二个任务也进入了等待。一分钟后解析网页数据花费1秒解析完成切换到第二个任务,转换为大写花费1秒,那么总耗时为:1分+1秒+1秒 = 1分钟2秒
多核CPU下:1.CPU1处理第一个任务等待1分钟,解析花费1秒钟。1.CPU2处理第二个任务等待1分钟,转换大写花费1秒钟。由于两个任务是并行执行的所以总的执行时间为1分钟+1秒钟 = 1分钟1秒
可以发现,多核CPU对于总的执行时间提升只有1秒,但是这边的1秒实际上是夸张了,转换大写操作不可能需要1秒,时间非常短!
上面的两个任务都是需要大量IO时间的,这样的任务称之为IO密集型,与之对应的是计算密集型即IO操作较少大部分都是计算任务。
对于计算密集型任务,Python多线程的确比不上其他语言!为了解决这个弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算密集任务。
总结:
1.单核下无论是IO密集还是计算密集GIL都不会产生任何影响
2.多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略
3.Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程
另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的happy,直接就能使用各种现成的算法
进程池与线程池:
池表示一个容器,本质上就是一个存储进程或线程的列表
池中存储线程还是存储进程?
如果是IO密集型就存储线程,计算密集型就存储进程
为什么需要进程/线程池?
在很多情况下需要控制进程或线程的数量在一个合理的范围,例如TCP程序中,一个客户端对应一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。
线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配。
同步异步-阻塞非阻塞:
阻塞与非阻塞说的是程序的运行的状态
并发与并行说的是程序处理的方式
同步与异步说的是任务的提交或执行方式
同步:发起任务后,进入等待状态,必须等到任务执行完成后才能继续执行
异步:发起任务后,不需要等待,可以执行其他的操作。
同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺cpu执行权,而同步调用则不会!
异步回调:
什么是异步回调:
异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数。
为什么需要异步回调?:
之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调
总结:
-
GIL
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。 -
为什么会有GIL
由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。
Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。
慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,本且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅 -
GIL的影响
从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。
那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的IO操作的时候,能释放GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python比你想的更糟。
1.3 threading模块
1.3.1 调用方式
- 直接调用
import threading
import time
def test(num): #定义每个线程要运行的函数
print(f"runing number{num}")
time.sleep(3)
if __name__=="__main__":
t1 = threading.Thread(target=test,args=(1,)) #生成一个线程实例
t2 = threading.Thread(target=test,args=(2,)) #生成另一个线程实例
t1.start() #启动线程
t2.start() #启动另一个线程
print(t1.getName()) #获取线程名
print(t2.getName())
- 继承调用
import threading
import time
class MyThread(threading.Thread):
def __init__(self, num):
threading.Thread.__init__(self)
self.num = num;
def run(self): #定义每个线程要运行的函数
print(f"runing number{self.num}")
time.sleep(3)
if __name__=="__main__":
t1 = MyThread(1)
t2 = MyThread(2)
t1.start() #启动线程
t2.start() #启动另一个线程
print(t1.getName()) #获取线程名
print(t2.getName())
1.3.2 Join & Daemon
setDaemon(True):
将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
join():
在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
- 简单实例
import threading
from time import ctime,sleep
import time
def music(func):
for i in range(2):
print ("Begin listening to %s. %s" %(func,ctime()))
sleep(4)
print("end listening %s"%ctime())
def move(func):
for i in range(2):
print ("Begin watching at the %s! %s" %(func,ctime()))
sleep(5)
print('end watching %s'%ctime())
if __name__ == '__main__':
t1 = threading.Thread(target=music,args=("消愁",)) #生成一个线程实例
t2 = threading.Thread(target=move,args=("釜山行",)) #生成另一个线程实例
t1.start() #启动线程
t2.start() #启动另一个线程
print(t1.getName()) #获取线程名
print(t2.getName())
#共用时间10秒左右
# Begin listening to 消愁. Wed Sep 9 10:36:31 2020
# Begin watching at the 釜山行! Wed Sep 9 10:36:31 2020
# end listening Wed Sep 9 10:36:35 2020
# Begin listening to 消愁. Wed Sep 9 10:36:35 2020
# end watching Wed Sep 9 10:36:36 2020
# Begin watching at the 釜山行! Wed Sep 9 10:36:36 2020
# end listening Wed Sep 9 10:36:39 2020
# end watching Wed Sep 9 10:36:41 2020
- 详细分析join作用
import threading
from time import ctime,sleep
import time
def music(func):
for i in range(2):
print ("Begin listening to %s. %s" %(func,ctime()))
sleep(4)
print("end listening %s"%ctime())
def move(func):
for i in range(2):
print ("Begin watching at the %s! %s" %(func,ctime()))
sleep(5)
print('end watching %s'%ctime())
threads = []
t1 = threading.Thread(target=music,args=('消愁',))
threads.append(t1)
t2 = threading.Thread(target=move,args=('釜山行',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
# t.setDaemon(True)
t.start()
# t.join()
t1.join()
# t2.join()########考虑这三种join位置下的结果?
print ("all over %s" %ctime())
- t1.join()
等待t1线程结束后,进程就推出,总时间取决于t1的等待时间,用时8秒左右 - t2.join()
等待t2线程结束后,进程就推出,总时间取决于t2的等待时间,用时10秒左右 - t.join()
整个进程的推出时间取决于执行最长时间的线程 - t.setDaemon(True)
主线程执行完毕后自动退出,不会等待子线程的执行结果。而且随着主线程退出,子线程也消亡 - t.setDaemon(False)
用时18秒左右,join里的timeout参数无效。主线程会一直等待子线程结束。
说明:
- python 默认参数创建线程后,不管主线程是否执行完毕,都会等待子线程执行完毕才一起退出,有无join结果一样
- 如果创建线程,并且设置了daemon为true,即thread.setDaemon(True), 则主线程执行完毕后自动退出,不会等待子线程的执行结果。而且随着主线程退出,子线程也消亡。
- join方法的作用是阻塞,等待子线程结束,join方法有一个参数是timeout,即如果主线程等待timeout,子线程还没有结束,则主线程强制结束子线程。
- 如果线程daemon属性为False, 则join里的timeout参数无效。主线程会一直等待子线程结束。
- 如果线程daemon属性为True, 则join里的timeout参数是有效的, 主线程会等待timeout时间后,结束子线程。此处有一个坑,即如果同时有N个子线程join(timeout),那么实际上主线程会等待的超时时间最长为 N * timeout, 因为每个子线程的超时开始时刻是上一个子线程超时结束的时刻。
其他
thread 模块提供的其他方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
# 除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
# run(): 用以表示线程活动的方法。
# start():启动线程活动。
# join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
1.4 同步锁
import time
import threading
s = time.time()
def addNum():
global num #在每个线程中都获取这个全局变量
# num-=1
temp=num
print('--get num:',num )
time.sleep(0.1)
num =temp-1 #对此公共变量进行-1操作
num = 100 #设定一个共享变量
thread_list = []
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num ) #99
e = time.time()
print(e-s)
我们发现当time.sleep(0.1)时,num=99, 当time.sleep(0.00001),num=93,
这是为什么呢?
- 这是因为动作太快(完成这个动作在切换的时间内)
- if sleep(1),现象会更明显,100个线程每一个一定都没有执行完就进行了切换,我们说过sleep就等效于IO阻塞,1s之内不会再切换回来,所以最后的结果一定是99.
那我们怎么解决呢?
多个线程都在同时操作同一个共享资源,所以造成了资源破坏,怎么办呢?
有同学会想用join呗,但join会把整个线程给停住,造成了串行,失去了多线程的意义,而我们只需要把计算(涉及到操作公共数据)的时候串行执行。
所以,我们需要引入同步锁,如下:
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
# num-=1
lock.acquire()
temp=num
print('--get num:',num )
#time.sleep(0.1)
num =temp-1 #对此公共变量进行-1操作
lock.release()
num = 100 #设定一个共享变量
thread_list = []
lock=threading.Lock() #设置锁
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
问题解决,但请问:同步锁与GIL的关系?
Python的线程在GIL的控制之下,线程之间,对整个python解释器,对python提供的C API的访问都是互斥的,这可以看作是Python内核级的互斥机制。但是这种互斥是我们不能控制的,我们还需要另外一种可控的互斥机制———用户级互斥。内核级通过互斥保护了内核的共享资源,同样,用户级互斥保护了用户程序中的共享资源。
GIL 的作用是:对于一个解释器,只能有一个thread在执行bytecode。所以每时每刻只有一条bytecode在被执行一个thread。GIL保证了bytecode 这层面上是”线程安全“的。
但是如果你有个操作比如 x += 1,这个操作需要多个bytecodes操作,在执行这个操作的多条bytecodes期间的时候可能中途就换thread了,这样就出现了data races的情况了。
那我的同步锁也是保证同一时刻只有一个线程被执行,是不是没有GIL也可以?是的;那要GIL有什么用?
GIL:保证同一时刻只能有一个线程进入解释器中,对于执行时间很短的,没有影响,对于自行时间很长,就会导致数据混乱;
1.5 线程死锁和递归锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:
import threading,time
class myThread(threading.Thread):
def doA(self):
lockA.acquire()
print(self.name,"gotlockA",time.ctime())
time.sleep(3)
lockB.acquire()
print(self.name,"gotlockB",time.ctime())
lockB.release()
lockA.release()
def doB(self):
lockB.acquire()
print(self.name,"gotlockB",time.ctime())
time.sleep(2)
lockA.acquire()
print(self.name,"gotlockA",time.ctime())
lockA.release()
lockB.release()
def run(self):
self.doA()
self.doB()
if __name__=="__main__":
lockA=threading.Lock()
lockB=threading.Lock()
threads=[]
for i in range(5):
threads.append(myThread())
for t in threads:
t.start()
for t in threads:
t.join()#等待线程结束,后面再讲。
解决方案:
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
lockA=threading.Lock()
lockB=threading.Lock() #--> lock=threading.RLock()
应用
转账
import time
import threading
class Account:
def __init__(self, _id, balance):
self.id = _id
self.balance = balance
self.lock = threading.RLock()
def withdraw(self, amount):
with self.lock:
self.balance -= amount
def deposit(self, amount):
with self.lock:
self.balance += amount
def drawcash(self, amount):#lock.acquire中嵌套lock.acquire的场景
with self.lock:
interest=0.05
count=amount+amount*interest
self.withdraw(count)
def transfer(_from, to, amount):
#锁不可以加在这里 因为其他的其它线程执行的其它方法在不加锁的情况下数据同样是不安全的
_from.withdraw(amount)
to.deposit(amount)
alex = Account('alex',1000)
yuan = Account('yuan',1000)
t1=threading.Thread(target = transfer, args = (alex,yuan, 100))
t1.start()
t2=threading.Thread(target = transfer, args = (yuan,alex, 200))
t2.start()
t1.join()
t2.join()
print('>>>',alex.balance)
print('>>>',yuan.balance)
1.6 条件变量同步(Condition)
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。
lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传人锁,对象自动创建一个RLock()。
- wait():条件不满足时调用,线程会释放锁并进入等待阻塞;
- notify():条件创造后调用,通知等待池激活一个线程;
- notifyAll():条件创造后调用,通知等待池激活所有线程。
import threading,time
from random import randint
class Producer(threading.Thread):
def run(self):
global L
while True:
val=randint(0,100)
print('生产者',self.name,":Append"+str(val),L)
if lock_con.acquire():
L.append(val)
lock_con.notify()
lock_con.release()
time.sleep(3)
class Consumer(threading.Thread):
def run(self):
global L
while True:
lock_con.acquire()
if len(L)==0:
lock_con.wait()
print('消费者',self.name,":Delete"+str(L[0]),L)
del L[0]
lock_con.release()
time.sleep(0.25)
if __name__=="__main__":
L=[]
lock_con=threading.Condition()
threads=[]
for i in range(5):
threads.append(Producer())
threads.append(Consumer())
for t in threads:
t.start()
for t in threads:
t.join()
1.7 同步条件(Event)
条件同步和条件变量同步差不多意思,只是少了锁功能,因为条件同步设计于不访问共享资源的条件环境。event=threading.Event():条件环境对象,初始值 为False;
- event.isSet():返回event的状态值;
- event.wait():如果 event.isSet()==False将阻塞线程;
- event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
- event.clear():恢复event的状态值为False。
实例1
import threading,time
class Boss(threading.Thread):
def run(self):
print("BOSS:今晚大家都要加班到22:00。")
event.isSet() or event.set()
time.sleep(5)
print("BOSS:<22:00>可以下班了。")
event.isSet() or event.set()
class Worker(threading.Thread):
def run(self):
event.wait()
print("Worker:哎……命苦啊!")
time.sleep(0.25)
event.clear()
event.wait()
print("Worker:OhYeah!")
if __name__=="__main__":
event=threading.Event()
threads=[]
for i in range(5):
threads.append(Worker())
threads.append(Boss())
for t in threads:
t.start()
for t in threads:
t.join()
实例2
import threading,time
import random
def light():
if not event.isSet():
event.set() #wait就不阻塞 #绿灯状态
count = 0
while True:
if count < 10:
print('\033[42;1m--green light on---\033[0m')
elif count <13:
print('\033[43;1m--yellow light on---\033[0m')
elif count <20:
if event.isSet():
event.clear()
print('\033[41;1m--red light on---\033[0m')
else:
count = 0
event.set() #打开绿灯
time.sleep(1)
count +=1
def car(n):
while 1:
time.sleep(random.randrange(10))
if event.isSet(): #绿灯
print("car [%s] is running.." % n)
else:
print("car [%s] is waiting for the red light.." %n)
if __name__ == '__main__':
event = threading.Event()
Light = threading.Thread(target=light)
Light.start()
for i in range(3):
t = threading.Thread(target=car,args=(i,))
t.start()
1.8 信号量(Semaphore)
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
实例:
import threading,time
class myThread(threading.Thread):
def run(self):
if semaphore.acquire():
print(self.name)
time.sleep(5)
semaphore.release()
if __name__=="__main__":
semaphore=threading.Semaphore(5)
thrs=[]
for i in range(100):
thrs.append(myThread())
for t in thrs:
t.start()
1.9 多线程利器(queue)
queue列队类的方法
import queue
# 1. 创建一个“队列”对象
q = queue.Queue(maxsize = 3) #maxsize = 3
#Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。
#可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。
# 2. 将一个值放入队列中
q.put("a")
q.put("b")
q.put("c")
q.put("d", block=False, timeout=5)
#调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为
# 1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。
# 如果block为0,put方法将引发Full异常。
# 3. 将一个值从队列中取出
#先进先出
print(q.get()) #a
print(q.get()) #b
print(q.get()) #c
# print(q.get(timeout=5))
# 其他方法
# q.qsize() 返回队列的大小
# q.empty() 如果队列为空,返回True,反之False
# q.full() 如果队列满了,返回True,反之False
# q.full 与 maxsize 大小对应
# q.get([block[, timeout]]) 获取队列,timeout等待时间
# q.get_nowait() 相当q.get(False)
# 非阻塞 q.put(item) 写入队列,timeout等待时间
# q.put_nowait(item) 相当q.put(item, False)
# q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
# q.join() 实际上意味着等到队列为空,再执行别的操作
实例1:
import threading,queue
from time import sleep
from random import randint
class Production(threading.Thread):
def run(self):
while True:
r=randint(0,100)
q.put(r)
print("生产出来%s号包子"%r)
sleep(1)
class Proces(threading.Thread):
def run(self):
while True:
re=q.get()
print("吃掉%s号包子"%re)
if __name__=="__main__":
q=queue.Queue(10)
threads=[Production(),Production(),Production(),Proces()]
for t in threads:
t.start()
实例2:
import time,random
import queue,threading
q = queue.Queue()
def Producer(name):
count = 0
while count <20:
time.sleep(random.randrange(3))
q.put(count)
print('Producer %s has produced %s baozi..' %(name, count))
count +=1
def Consumer(name):
count = 0
while count <20:
time.sleep(random.randrange(4))
if not q.empty():
data = q.get()
print(data)
print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data))
else:
print("-----no baozi anymore----")
count +=1
p1 = threading.Thread(target=Producer, args=('A',))
c1 = threading.Thread(target=Consumer, args=('B',))
p1.start()
c1.start()
实例3:
#实现一个线程不断生成一个随机数到一个队列中(考虑使用Queue这个模块)
# 实现一个线程从上面的队列里面不断的取出奇数
# 实现另外一个线程从上面的队列里面不断取出偶数
import random,threading,time
from queue import Queue
#Producer thread
class Producer(threading.Thread):
def __init__(self, t_name, queue):
threading.Thread.__init__(self,name=t_name)
self.data=queue
def run(self):
for i in range(10): #随机产生10个数字 ,可以修改为任意大小
randomnum=random.randint(1,99)
print ("%s: %s is producing %d to the queue!" % (time.ctime(), self.getName(), randomnum))
self.data.put(randomnum) #将数据依次存入队列
time.sleep(1)
print ("%s: %s finished!" %(time.ctime(), self.getName()))
#Consumer thread
class Consumer_even(threading.Thread):
def __init__(self,t_name,queue):
threading.Thread.__init__(self,name=t_name)
self.data=queue
def run(self):
while 1:
try:
val_even = self.data.get(1,5) #get(self, block=True, timeout=None) ,1就是阻塞等待,5是超时5秒
if val_even%2==0:
print ("%s: %s is consuming. %d in the queue is consumed!" % (time.ctime(),self.getName(),val_even))
time.sleep(2)
else:
self.data.put(val_even)
time.sleep(2)
except: #等待输入,超过5秒 就报异常
print ("%s: %s finished!" %(time.ctime(),self.getName()))
break
class Consumer_odd(threading.Thread):
def __init__(self,t_name,queue):
threading.Thread.__init__(self, name=t_name)
self.data=queue
def run(self):
while 1:
try:
val_odd = self.data.get(1,5)
if val_odd%2!=0:
print ("%s: %s is consuming. %d in the queue is consumed!" % (time.ctime(), self.getName(), val_odd))
time.sleep(2)
else:
self.data.put(val_odd)
time.sleep(2)
except:
print ("%s: %s finished!" % (time.ctime(), self.getName()))
break
#Main thread
def main():
queue = Queue()
producer = Producer('Pro.', queue)
consumer_even = Consumer_even('Con_even.', queue)
consumer_odd = Consumer_odd('Con_odd.',queue)
producer.start()
consumer_even.start()
consumer_odd.start()
producer.join()
consumer_even.join()
consumer_odd.join()
print ('All threads terminate!')
if __name__ == '__main__':
main()
注意:列表是线程不安全的
import threading,time
li=[1,2,3,4,5]
#两个线程可能拿到同一个数据,而队每次都会拿到不同的,是因为队中本身有锁
def pri():
while li:
a=li[-1]
print(a)
time.sleep(1)
try:
li.remove(a)
except:
print('----',a)
t1=threading.Thread(target=pri,args=())
t1.start()
t2=threading.Thread(target=pri,args=())
t2.start()
2 进程
我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,
统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用
程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述
进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息
是进程存在的唯一标志
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期 的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进行一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序,数据和进程控制块三部分组成
线程和进程的区别
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多
- 资源共享:进程有自己的资源空间,线程共享进程的资源空间
2.1 multiprocessing模块–多进程
2.1.1 多进程概念
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
但在使用这些共享API的时候,我们要注意以下几点:
- 在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法 (实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。
- multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。应优先考虑Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式 (因为它们占据的不是用户进程的资源)。
- 多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。
- Process.PID中保存有PID,如果进程还没有start(),则PID为None。
window系统下,需要注意的是要想启动一个子进程,必须加上那句if name == “main”,进程相关的要写在这句下面。
2.1.2 调用方式
- 直接调用
from multiprocessing import Process
import time
def test(name):
time.sleep(1)
print('hello', name,time.ctime())
if __name__ == '__main__':
p_list=[]
for i in range(3):
p = Process(target=test, args=('frankshi',))
p_list.append(p)
p.start()
for i in p_list:
p.join()
print('end')
# hello frankshi Tue Sep 15 15:50:49 2020
# hello frankshi Tue Sep 15 15:50:49 2020
# hello frankshi Tue Sep 15 15:50:49 2020
# end
- 继承调用
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self):
super(MyProcess, self).__init__()
#self.name = name
def run(self):
time.sleep(1)
print ('hello', self.name,time.ctime())
if __name__ == '__main__':
p_list=[]
for i in range(3):
p = MyProcess()
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('end')
为了显示所涉及的单个进程ID,这里有一个扩展的示例:
from multiprocessing import Process
import os
import time
def info(title):
print(title)
print('module name:', __name__)
print('parent process:', os.getppid())
print('process id:', os.getpid())
if __name__ == '__main__':
p_list=[]
for i in range(3):
p = Process(target=info, args=('frankshi',))
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('end')
# frankshi
# module name: __mp_main__
# parent process: 18420
# process id: 1408
# frankshi
# module name: __mp_main__
# parent process: 18420 --主进程
# process id: 14732 -子进程1
# frankshi
# module name: __mp_main__
# parent process: 18420 --主进程
# process id: 12940 -子进程2
# end
2.1.3 Process类
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前还没有实现,库引用中提示必须是None;
target: 要执行的方法;
name: 进程名;
args/kwargs: 要传入方法的参数。
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
authkey
daemon:和线程的setDeamon功能一样
exitcode(进程在运行时为None、如果为–N,表示被信号N结束)
name:进程名字。
pid:进程号
import time
from multiprocessing import Process
def foo(i):
time.sleep(1)
print (i)
time.sleep(1)
if __name__ == '__main__':
p_list=[]
for i in range(10):
p = Process(target=foo, args=(i,))
#p.daemon=True
p_list.append(p)
for p in p_list:
p.start()
print (p.is_alive(),p.pid)
for p in p_list:
p.join()
print('main process end')
2.2 进程间通讯
不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方法:
Queues
from multiprocessing import Process, Queue
def f(q,n):
q.put([42, n, 'hello'])
if __name__ == '__main__':
q = Queue()
p_list=[]
for i in range(3):
p = Process(target=f, args=(q,i))
p_list.append(p)
p.start()
print(q.get())
print(q.get())
print(q.get())
for i in p_list:
i.join()
Pipes
Pipe() 返回一个双向连接管道,一对连接对象。
返回的两个连接对象表示管道的两端,每个连接对象都有 send() 和 recv(),请注意,如果两个进程(或线程)试图同时从管道的同一端读取或写入,则管道中的数据可能会损坏
from multiprocessing import Process, Pipe
import os
def f(conn):
conn.send("你好")
conn.send("约吗?")
print("主进程-->子进程接收:" +conn.recv())
print("主进程pid:" + str(os.getppid()))
print("子进程pid:" + str(os.getpid()))
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p1 = Process(target=f, args=(child_conn,))
p1.start()
print("子进程-->主进程接收:" +parent_conn.recv()) # prints "你好"
print("子进程-->主进程接收:" +parent_conn.recv()) # prints "约吗?"
parent_conn.send("不约") # prints "不约?"
p1.join()
p2 = Process(target=f, args=(child_conn,))
p2.start()
print("子进程-->主进程接收:" +parent_conn.recv()) # prints "你好"
print("子进程-->主进程接收:" +parent_conn.recv()) # prints "约吗?"
parent_conn.send("不约") # prints "不约?"
p2.join()
# 子进程-->主进程接收:你好
# 子进程-->主进程接收:约吗?
# 主进程-->子进程接收:不约
# 主进程pid:20856
# 子进程pid:12836
# 子进程-->主进程接收:你好
# 子进程-->主进程接收:约吗?
# 主进程-->子进程接收:不约
# 主进程pid:20856
# 子进程pid:17900
Managers
由Manager()返回的Manager对象控制一个服务器进程,该进程持有Python对象,并允许其他进程使用代理操作它们。
from multiprocessing import Process, Manager
def f(d, l,n):
d[n] = '1'
d['2'] = 2
d[0.25] = None
l.append(n)
print(l)
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict()
l = manager.list(range(5))
p_list = []
for i in range(10):
p = Process(target=f, args=(d, l,i))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print(d)
print(l)
2.3 进程同步
from multiprocessing import Process, Lock
def f(l, i):
l.acquire()
try:
print('hello world', i)
finally:
l.release()
if __name__ == '__main__':
lock = Lock()
for num in range(10):
Process(target=f, args=(lock, num)).start()
2.4 进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
apply
apply_async
from multiprocessing import Process,Pool
import time
def Foo(i):
time.sleep(2)
return i+100
def Bar(arg):
print('-->exec done:',arg)
pool = Pool(5)
for i in range(10):
pool.apply_async(func=Foo, args=(i,),callback=Bar)
#pool.apply(func=Foo, args=(i,))
print('end')
pool.close()
pool.join()
3. 协程
学习笔记,不做他用,参考:https://www.cnblogs.com/yuanchenqi/articles/5733873.html