python(线程,进程池和线程池。GIL锁)
今日内容概要
- 多进程实现tcp服务端并发
- 互斥锁代码实操
- 线程理论
- 创建线程的方式
- 线程诸多方法
- GIL全局解释锁
- 验证GIL的存在
- GIL与普通互斥锁
- python多线程是否有用
- 死锁现象
- 信号量
- event事件
- 进程池与线程池
- 协程
- 协程实现并发
多进程实现tcp服务端并发
代码实现:
import socket
from multiprocessing import Process
def get_sever():
sever = socket.socket()
sever.bind(('127.0.0.1',8080))
sever.listen(5)
return sever
def get_talk(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
if __name__ == '__main__':
sever = get_sever()
while True:
sock,addr =sever.accept()
# 开设多进程去聊天
p = Process(target=get_talk,args = (sock,))
p.start()
互斥锁代码实操
就是仅针对于 进程 有效的锁,当进程的任务开始之后,就会被上一把 “锁”;与之对应的是 线程锁 ,它们的原理几乎是一样的。
*互斥锁的加锁与解锁
互斥锁的使用方法:
通过 multiprocessing 导入 Manager 类
from multiprocessing import Manager
然后实例化 Manager
manager = Manager()
再然后通过实例化后的 manager 调用 它的 Lock() 函数
lock = manager.Lock()
接下来,就需要操作这个 lock 对象的函数:

代码演练:
import os
import time
import multiprocessing
# 定义一个work函数,打印输出,每次执行的次数与 该次数的进程号,增加线程锁
def work(count,lock):
lock.acquire() # 上锁
print('\'work\' 函数 第 {} 次执行,进程号为 {}'.format(count, os.getpid()))
time.sleep(3)
lock.release() # 解锁
return '\'work\' 函数 result 返回值为:{}, 进程ID为:{}'.format(count, os.getpid())
if __name__ == '__main__':
# 定义进程池的进程数量,同一时间每次执行最多3个进程
pool = multiprocessing.Pool(3)
manger = multiprocessing.Manager()
lock =manger.Lock()
results = [] # 产生的结果放在列表中
for i in range(21):
# ## 传入的参数是元组,因为我们只有一个 i 参数,所以我们要写成 args=(i,)
result = pool.apply_async(func=work,args = (i,lock))
results.append(result)
pool.close()
pool.join()
执行结果如下:

从上图中,可以看到每一次只有一个任务会被执行。由于每一个进程会被阻塞 3秒钟,所以我们的进程执行的非常慢。这是因为每一个进程进入到 work() 函数中,都会执行 上锁、阻塞3秒、解锁 的过程,这样就完成了一个进程的工作。下一个进程任务开始,重复这个过程… 这就是 互斥锁的概念
其实进程锁还有很多种方法,在 multiprocessing 中有一个直接使用的锁,就是 ``from multiprocessing import Lock。这个Lock的锁使用和我们刚刚介绍的Manager` 的锁的使用有所区别。(这里不做详细介绍,感兴趣的话可以自行拓展一下。)
线程理论
线程与多线程的概念
在上一章中我们讲到了进程与多进程的概念即使用的方法,这让我们也知道了,我们平时使用的一些软件就是一个个的进程,每个进程都需要一定的CPU与内存来进行程序的进程,多个进程启动之后,相互之间不受影响,通过并行执行程序可以提高我们程序的执行效率。
但是,如果我们在一台硬件设施上启动太多的进程,内存被分掉太多,就会影响硬件的执行效率,甚至可能造成资源空间空,从而造成死机的现象,有什么办法能解决这一问题呢?就是线程
什么是线程?
说道线程,其实是离不开进程的,因为我们讲过,每个软件程序的启动都是通过创建进程,这就奠定了一个概念,先有进程,再有线程。
进程与线程的区别:
进程需要用到CPU和内存作为口粮和跑道,当进程调集了足够的资源后做了这样的一件事。它萌生出一个线程,而线程是具体负责我们程序真正执行的逻辑,所以线程才是真正执行我们业务逻辑的角色。
所以的处结论:
进程:是资源单位,表示一块内存空间
线程:是执行单位,表示真正执行的代码指令
1.一个进程内可以开设多个线程
2.同一个进程下的多个线程数据是共享的
3.创建进程与线程的区别
创建进程的消耗要远远大于线程
线程(Thread)是操作系统最小的执行单元,进程至少由一个线程组成。
两者之间的关系:
所以进程提供线程执行程序的前置要求(拥有足够的资源),而线程在拿到进程提供的资源后去执行程序。这就是 线程与进程之间的关系 。

总结:
"线程是依赖于进程的" ,先有进程,才会有线程。并且进程中的主线程,还可以再去创建多个线程。这里大家可能会有一些疑问,我们该如何区分主线程与子线程的呢?其实很好理解,我们正常执行的脚本就是通过一个主进程下的主线程去完成的。
我们说过,在主进程下通过创建一些进程去做一些业务。那是主进程生成的子进程,其实严格的说也这是主进程下的主线程来帮助完成创建的。
多线程:
和进程一样,线程也可以执行多个线程。多线程与多进程非常的相似。只不过一个进程下的多个线程只会共享当前进程的内存资源,所以我们想一下,在一个进程中创建多个线程是否会比创建多个进程更加的节省资源呢?在某些情况下是的。我们作为初学先不要考虑太多的复杂场景,当下我们先只要认为多线程要比多进程更加的节省资源即可。
多线程的执行方式:

其实可以用另一个维度来看待,它可以是一个细长条,并且这个细长条被切割成了多个片段,我们把它叫做 CPU时间片。而每个线程就是在时间片上去工作的,大家可以看到当前的这个 CPU 的核被切割成了4片,这时候有4个线程进入工作了。每一个时间片都会去处理一个线程的业务,现在4个线程对应了4个时间片。如果再有一个线程进来,必然是没有足够的时间片让后来的线程使用了,就只能等待一会儿了。如果某个时间片里的线程执行完毕了,它就可以再去获取一个新的线程去执行。
我们把一个核心中在多个时间片上同时处理线程的行为叫做 "并发执行" 。这里一定要记住两个名词,多个CPU内核之间同时执行我们叫做 "并行" ;而单个内核之中多个线程同时工作,我们叫做 "并发" 。不过一般实际工作中,我们一般使用 "并发" 这个词来统称,而并发就代表着同时工作的意思。
python多线程是否有用
计算密集型:
所谓的计算密集型,就是要进行大量的计算,消耗CPU资源
IO密集型:
涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少, 任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。 对于IO密集型任务,任务越多,CPU效率越高.
需要分情况
1.单个CPU、多个CPU
2.IO密集型(有IO)、计算密集型(代码没有IO)
1.1单CPU
IO 密集型
多进程
申请额外的空间,消耗更多的资源
多线程
消耗资源相比较较少,通过多道技术
PS:多线程更有优势
计算密集型
多进程
申请额外的空间,消耗更多的资源(总耗时+申请空间+拷贝代码+切换)
多线程
消耗资源相较小,通过多道技术(总耗时+切换)
PS:多线程有优势
2.1多个CPU
IO密集型
多进程:总耗时(单个进程的耗时+IO+申请空间+拷贝代码)
多线程:总耗时(单个进程的耗时+IO)
ps:多线程有优势!!!
计算密集型
多进程:总耗时(单个进程的耗时)
多线程:总耗时(多个进程的综合)
ps:多进程完胜!!!
代码演示:
from threading import Thread
from multiprocessing import Process
import os
import time
def work():
# 计算密集型
res = 1
for i in range(1, 100000):
res *= i
if __name__ == '__main__':
# print(os.cpu_count()) # 12 查看当前计算机CPU个数
start_time = time.time()
# p_list = []
# for i in range(12): # 一次性创建12个进程
# p = Process(target=work)
# p.start()
# p_list.append(p)
# for p in p_list: # 确保所有的进程全部运行完毕
# p.join()
t_list = []
for i in range(12):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time)) # 获取总的耗时
"""
计算密集型
多进程:5.665567398071289
多线程:30.233906745910645
"""
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
# t_list = []
# for i in range(100):
# t = Thread(target=work)
# t.start()
# for t in t_list:
# t.join()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
"""
IO密集型
多线程:0.0149583816528320
多进程:0.6402878761291504
"""
可以看看其他语言和python语言的区别:

创建线程的方式
前面的学习我们知道进程的使用需要获取 CPU和内存 的资源,而线程则是利用进程的资源来执行业务,并且通过创建多个线程,对于资源的消耗相对来说会比较低,今天就来看一看线程的使用方法具体有哪些
线程的创建与使用
在python中有很多的多线程模块,在这里我们将使用threading模块进行创建。
threading模块

Thread 的动能介绍:通过调用 threading 模块的 Thread 类来实例化一个线程对象;它有两个参数: target 与 args (与创建进程时,参数相同)。target 为创建线程时要执行的函数,而 args 为是要执行这个函数时需要传入的参数。
代码演示:
方式一:
from threading import Thread
from multiprocessing import Process
import time
def task(name):
print(f'{name} is running')
time.sleep(0.1)
print(f'{name} is over')
# 进程运行时间
# if __name__ == '__main__':
# star_time = time.time()
# p_list = []
# for i in range(100):
# p = Process(target=task,args=('用户%s'%i,))
# p.start()
# p_list.append(p)
# for p in p_list:
# p.join()
# print(time.time() - star_time) # 结果:2.506303071975708
# 线程运行时间
star_time = time.time()
t_list = []
for i in range(100):
t=Thread(target=task,args=('用户%s'%i,))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(time.time()-star_time) # 结果:0.12558937072753906
t = Thread(target=task,args=('水水',))
t.start()
print('主线程')
# 输出结果:
0.12558937072753906
水水 is running
主线程
水水 is over
方式二:
class MyThread(Thread):
def run(self):
print('run is running')
time.sleep(1)
print('run is over')
obj = MyThread()
obj.start()
print('主线程')
#输出结果:
run is running
主线程
run is over
总结:通过上面的代码,我们可以发现线程的使用方法和进程方法是一模一样的,他们都可以互不干扰的执行程序,也可以使主线程的程序不需要等待子线程的任务之后在去执行,只不过在上面的代码中我们使用了join()函数进行了阻塞,这里可以把join()去掉看看效果。
与进程一样,线程也存在着一些问题:
- 线程执行的函数,也同样是无法获取返回值的
- 当多个线程同时修改文件一样会 造成被修改的文件的数据错乱 的错误(因为都是并发去操作一个文件,特别是在处理交易场景的时候尤为注意)。
关于线程中存在的问题同样是可以解决的,在面的 线程池与全局锁 我们会有详细的介绍
线程诸多方法

-
start 函数:启动一个线程;没有任何返回值和参数。
-
join 函数:和进程中的 join 函数一样;阻塞当前的程序,主线程的任务需要等待当前子线程的任务结束后才可以继续执行;参数为 timeout:代表阻塞的超时时间。
-
getName 函数:获取当前线程的名字。
-
setName 函数:给当前的线程设置名字;参数为 name:是一个字符串类型
-
is_alive 函数:判断当前线程的状态是否存活
-
setDaemon 函数:它是一个守护线程;如果脚本任务执行完成之后,即便进程池还没有执行完成业务也会被强行终止。子线程也是如此,如果希望主进程或者是主线程先执行完自己的业务之后,依然允许子线程继续工作而不是强行关闭它们,只需要设置 setDaemon() 为 True 就可以了。
-
current_thread() 查看线程号
-
active_count() 返回正在运行的线程数量
守护线程:
对于主进程来说:运行完毕值的是主进程代码运行完毕 对于主线程来说:运行完毕指的是主线程所在是的进程内所有非是守护线程统统运行完毕后主线程运行才算运行完毕。 1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束, 2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。 PS:无论是进程还是线程,都遵循守护 会等待 主 运行完毕后被销毁。需要强调的是:运行完毕并非终止运行PS:通过上面的介绍,会发现其实线程对象里面的函数几乎和进程对象中的函数非常相似,它们的使用方法和使用场景几乎是相同的。
GIL全局解释器锁
# 官方文档对GIL的解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing 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的存在,其他特性已经发展到依赖于GIL强制执行的保证。
1. 在CPython解释器中存在全局解释器锁简称GIL
python中解释器有很多类型
CPython JPython PyPython (常用的是CPython解释器)
2.GIL本质也是一把互斥锁 用来阻止同一个进程内多个线程同时执行(重要)
3.GIL的存在是因为CPython解释器中内存管理不是线程安全的(垃圾回收机制)
垃圾回收机制
引用计数、标记清除、分代回收
GIL的作用
因为有GIL锁使得python的多线程无法在多个CPU上去执行任务,他只能在单一的CPU上进行工作。
这也限制了多线程的性能,毕竟 Python 的多线程只能在一条跑道上运行。跑道满了,运行速度依然会慢。而在多个跑道上运行的任务必然是要比单一跑道效率会高很多。
之所以保留 GIL 锁,其实也是为了线程之间的安全。
验证GIL的存在
from threading import Thread
num = 100
def task():
global num
num -=1
t_list = []
for i in range(100):
t=Thread(target=task)
t.start()
t_list.append(t)
for i in t_list:
t.join()
print(num) # 0
GIL与普通互斥锁
既然CPython解释器中有GIL 那么我们以后写代码是不是就不需要操作锁了!!!
"""
GIL只能够确保同进程内多线程数据不会被垃圾回收机制弄乱
并不能确保程序里面的数据是否安全
"""
import time
from threading import Thread,Lock
num = 100
def task(mutex):
global num
mutex.acquire()
count = num
time.sleep(0.1)
num = count - 1
mutex.release()
mutex = Lock()
t_list = []
for i in range(100):
t = Thread(target=task,args=(mutex,))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(num) # 0
死锁现象
锁 的使用可以让我们对某个任务 在同一时间只能对一个进程进行开发,但是 锁也不可以乱用 。因为如果某些原因造成 锁没有正常解开 ,就会造成死锁的现象,这样就无法再进行操作了。
因为 锁如果解不开 ,后面的任务也就没有办法继续执行任务,所以使用锁一定要谨慎。
死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
# acquire()
# release()
from threading import Thread,Lock
import time
mutexA = Lock() # 产生一把锁
mutexB = Lock() # 产生一把锁
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexB.release()
print(f'{self.name}释放了B锁')
mutexA.release()
print(f'{self.name}释放了A锁')
def func2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
# time.sleep(1) # 若是这里不添加阻塞就会顺利的所有的人都能拿到所
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
print(f'{self.name}释放了A锁')
mutexB.release()
print(f'{self.name}释放了B锁')
for i in range(10):
obj = MyThread()
obj.start()
# 输出结果:这里是添加了time.sleep(1)
# Thread-1抢到了A锁
# Thread-1抢到了B锁
# Thread-1释放了B锁
# Thread-1释放了A锁
# Thread-1抢到了B锁
# Thread-2抢到了A锁
信号量
在python并发编程中信号量相当于多把互斥锁(公共厕所)
from threading import Thread, Lock, Semaphore
import time
import random
sp = Semaphore(5) # 一次性产生五把锁
class MyThread(Thread):
def run(self):
sp.acquire()
print(self.name)
time.sleep(random.randint(1, 3))
sp.release()
for i in range(20):
t = MyThread()
t.start()
event事件
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮着的 所有人都不能动')
time.sleep(3)
print('绿灯亮了 油门踩到底 给我冲!!!')
event.set()
def car(name):
print('%s正在等红灯' % name)
event.wait()
print('%s加油门 飙车了' % name)
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('熊猫PRO%s' % i,))
t.start()
进程池与线程池
进程和线程能否无限制的创建 答案是当然不可以
因为引荐的发展赶不上软件,有物理极限,如果我们在编写代码的过程中无限制的创建进程或者线程可能会导致计算机奔溃
“什么是池”
有池的存在,能降低程序的执行效率,但是你保障了计算机的硬件的安全
进程池:
就是提前建好固定数量的进程供后续程序的调用,超出则等待
线程池:
就是提前创建好固定的的数量的线程供后续程序的调用,超出则等待
进程池:

比如这个红色矩形阵列就代表一个进程池子,在这个池子中有6个进程。这6个进程会伴随进程池一起被创建,不仅如此,我们在学习面向对象的生命周期的时候曾经说过,每个实例化对象在使用完成之后都会被内存管家回收。
我们的进程也会伴随着创建与关闭的过程而被内存管家回收,每一个都是如此,创建于关闭进程的过程也会消耗一定的性能。而进程池中的进程当被创建之后就不会被关闭,可以一直被重复使用,从而避免了创建于关闭的资源消耗,也避免了创建于关闭的反复操作提高了效率。
当然,当我们执行完程序进程池关闭的时候,进程也随之关闭。
当我们有任务需要被执行的时候,会判断当前的进程池当中有没有空闲的进程(所谓空闲的进程其实就是进程池中没有执行任务的进程)。有进程处于空闲状态的情况下,任务会找到进程执行该任务。如果当前进程池中的进程都处于非空闲状态,则任务就会进入等待状态,直到进程池中有进程处于空闲状态才会进出进程池从而执行该任务。
这就是进程池的作用。
进程池的创建模块 - multiprocessing
创建进程池函数- Pool

Pool功能介绍:通过调用 "multiprocessing" 模块的 "Pool" 函数来帮助我们创建 "进程池对象" ,它有一个参数 "Processcount" (一个整数),代表我们这个进程池中创建几个进程。
进程池的常用方法:

- apply_async 函数:它的功能是将任务加入到进程池中,并且是通过异步实现的。它有两个参数:
func 与 agrs, func 是加入进程池中工作的函数;args 是一个元组,代表着签一个函数的参数,这和我们创建并使用一个进程是完全一致的。 - close 函数:当我们使用完进程池之后,通过调用 close 函数可以关闭进程池。它没有任何的参数,也没有任何的返回值。
- join 函数:它和我们上一章节学习的 创建进程的 join 函数中方法是一致的。只有进程池中的任务全部执行完毕之后,才会执行后续的任务。不过一般它会伴随着进程池的关闭
(close 函数)才会使用。
apply_async 函数演示案例
定义一个函数,打印输出该函数 每次被执行的次数 与 该次数的进程号
定义进程池的数量,每一次的执行进程数量最多为该进程池设定的进程数
代码演示:
import os
import time
import multiprocessing
def work(count): # 定义一个 work 函数,打印输出 每次执行的次数 与 该次数的进程号
print('\'work\' 函数 第 {} 次执行,进程号为 {}'.format(count, os.getpid()))
time.sleep(3)
# print('********')
if __name__ == '__main__':
pool = multiprocessing.Pool(3) # 定义进程池的进程数量,同一时间每次执行最多3个进程
for i in range(21):
pool.apply_async(func=work, args=(i,)) # 传入的参数是元组,因为我们只有一个 i 参数,所以我们要写成 args=(i,)
time.sleep(15) # 这里的休眠时间是必须要加上的,否则我们的进程池还未运行,主进程就已经运行结束,对应的进程池也会关闭。
线程池的创建——concurrent
concurrent 是 Python 的内置包,使用它可以帮助我们完成创建线程池的任务。
通过调用 concurrent 包的 futures 模块的 ThreadPoolExecutor 类,通过实例化 ThreadPoolExecutor 实现创建线程池的对象,它有一个参数来设置 线程池的数量。这和创建进程池设置的数量是完全相同的。

现成的常用方法:

submit 函数:通过 submit 函数将参数传入;该函数传入的参数也是传入要执行的函数与该函数的参数,由于它的参数并不用需要通过赋值语句的形式传入,只需要把相应的值传入就可以了(稍后会进行一个练习)。
done 函数:判断当前线程是否执行完成;返回值是 bool 类型。
result 函数:返回当前线程池中线程任务的执行结果,通过这种方法就可以获取线程池的返回值了。
线程池演示案例
1、定义一个函数实现循环的效果
2、定义一个线程池,设置线程的数量
代码演示:
import time
from concurrent.futures import ThreadPoolExecutor
def work(i):
print('第 {} 次循环'.format(i))
time.sleep(1) # 之所以每次都要使用 sleep 函数,是因为函数执行太快;通过 sleep 尝试模拟一下长时间的执行一个任务
if __name__ == '__main__':
thread_poor = ThreadPoolExecutor(4) # 实例化一个线程池,设置线程数量为4
for i in range(20):
thread_poor.submit(work, (i,)) # 利用 submit 函数将任务添加至 work 函数
PS:需要注意的是,运行结果有可能是出现将两个或者多个任务的结果在同一行打印输出,
这是因为在同一时间处理了多个线程的任务,这也叫 "并发"。
协程
"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发(效率极高)
在代码层面欺骗CPU 让CPU觉得我们的代码里面没有IO操作
实际上IO操作被我们自己写的代码检测 一旦有 立刻让代码执行别的
(该技术完全是程序员自己弄出来的 名字也是程序员自己起的)
核心:自己写代码完成切换+保存状态
"""
import time
from gevent import monkey;
monkey.patch_all() # 固定编写 用于检测所有的IO操作(猴子补丁)
from gevent import spawn
def func1():
print('func1 running')
time.sleep(3)
print('func1 over')
def func2():
print('func2 running')
time.sleep(5)
print('func2 over')
if __name__ == '__main__':
start_time = time.time()
# func1()
# func2()
s1 = spawn(func1) # 检测代码 一旦有IO自动切换(执行没有io的操作 变向的等待io结束)
s2 = spawn(func2)
s1.join()
s2.join()
print(time.time() - start_time) # 8.01237154006958 协程 5.015487432479858
协程实现并发
import socket
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有的IO操作(猴子补丁)
from gevent import spawn
def communication(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept() # IO操作
spawn(communication, sock)
s1 = spawn(get_server)
s1.join()
如何不断的提升程序的运行效率
多进程下开多线程 多线程下开协程

浙公网安备 33010602011771号