sailan
一、进程引入
通过前面操作系统进程方面的学习,我们了解到程序的运行需要操作系统将其加载到内存,系统为它分配CPU资源才能运行,这种执行中的程序可称为进程。程序和进程的区别在于,程序是指令的集合它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。
二、线程
2.1 什么是线程
进程它提供了多道编程,让我们的多个指令任务感觉CPU一直在服务自己,可以大幅提高计算机的利用率。但其实进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位,创建进程所需的资源要远大于线程。
注意:
进程是资源分配的最小单位,线程是CPU调度的最小单位。每一个进程中至少有一个线程。
2.2 多线程
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
2.3 多线程的存在意义
多线程指的是,在一个进程中开启多个线程,详细的讲分为下面几点:
- 多线程共享一个进程的地址空间;
- 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用;
- 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度;
- 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python);
详细见:e机欧n
三、案例操作解析
3.1 开启线程的两种方式
threading模块:threading模块的接口和multiprocess模块的相同,在使用方面也很相识;
方式一
from threading import Thread
import time
def task():
print('开始')
time.sleep(1)
print('结束')
if __name__ == '__main__':
t=Thread(target=task,) # 实例化得到一个对象
t.start() # 对象.start()启动线程
print('主')
方式二
from threading import Thread
import time
class MyThread(Thread):
def run(self):
print('开始')
time.sleep(1)
print('结束')
if __name__ == '__main__':
t=MyThread()
t.start()
print('主')
3.2 同一个进程下的多个线程数据共享
案例
from threading import Thread
import time
money = 99
def task(n):
global money
money=n
print('开始')
# time.sleep(n)
print('结束')
if __name__ == '__main__':
t = Thread(target=task, args=(2,))
t.start()
t1 = Thread(target=task, args=(66,))
t1.start()
t.join()
t1.join()
print(money)
print('主')
3.3 线程对象部分方法
-
线程t.name t.getName()
-
当前进程下有几个线程存活active_count
-
t1.is_alive() 当前线程是否存活
-
t1.ident 当作是线程id号
-
current_thread() 获得当前线程对象
3.4 守护线程
无论是进程还是线程,都遵循,守护xxx会等待主xxx运行完毕后被销毁需要强调的是,运行完毕并非终止运行;
- 对主进程来说,运行完毕指的是主进程代码运行完毕;
- 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕;
详细解释:
- 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
- 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
3.5 线程互斥锁
from threading import Thread,Lock
import time
import random
money = 99
def task(n,mutex):
global money
# 在修改数据的时候,枷锁
mutex.acquire()
temp = money
time.sleep(0.1)
money = temp - 1
# 修改完以后,释放锁,其它线程就能再次抢到锁
mutex.release()
if __name__ == '__main__':
ll=[]
mutex=Lock()
for i in range(10):
t = Thread(target=task, args=(i,mutex))
t.start()
ll.append(t)
for i in ll:
i.join()
print(money)
3.6 GIL全局解释器锁
3.6.1理论
-
python的解释器有很多,cpython,jpython,pypy(python写的解释器)
-
python的库多,库都是基于cpython写起来的,其他解释器没有那么多的库
-
cpython中有一个全局大锁,每条线程要执行,必须获取到这个锁
-
为什么会有这个锁呢?python的垃圾回收机制导致线程是不安全的,所以所有线程要抢到GIL才能执行
-
cpython的多线程不是真正的多线程,同一时刻,只有一个线程在执行,不能利用多核优势,cpython的多线程其实就是单线程
-
某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行
-
总结:cpython解释器中有一个全局锁(GIL),线程必须获取到GIL才能执行,我们开的多线程,不管有几个cpu,同一时刻,只有一个线程在执行(python的多线程,不能利用多核优势)
-
在单核情况下:开多线程还是开多进程?不管干什么都是开线程;
-
在多核情况下:
如果是计算密集型,需要开进程,能被多个cpu调度执行;
如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
以上两句话,只针对与cpython解释器
PS: 线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来
3.6.2 验证GIL锁的存在方式
from threading import Thread
from multiprocessing import Process
def task():
while True:
pass
if __name__ == '__main__':
for i in range(6):
# t=Thread(target=task) # 因为有GIL锁,同一时刻,只有一条线程执行,所以cpu不会满
t=Process(target=task) # 由于是多进程,进程中的线程会被cpu调度执行,6个cpu全在工作,就会跑满
t.start()
3.6.3 GIL与普通互斥锁的区别
GIL锁是不能保证数据的安全,普通互斥锁来保证数据安全
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
mutex.acquire()
temp = money
time.sleep(1)
money = temp - 1
mutex.release()
if __name__ == '__main__':
ll=[]
for i in range(10):
t = Thread(target=task)
t.start()
ll.append(t)
for t in ll:
t.join()
print(money)
3.6.4 io密集型和计算密集型
'''
-----以下只针对于cpython解释器
-在单核情况下:
-开多线程还是开多进程?不管干什么都是开线程
-在多核情况下:
-如果是计算密集型,需要开进程,能被多个cpu调度执行
-如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
'''
from threading import Thread
from multiprocessing import Process
import time
# 计算密集型
# def task():
# count = 0
# for i in range(100000000):
# count += i
#
#
# if __name__ == '__main__':
# ctime = time.time()
# ll = []
# for i in range(10):
# t = Thread(target=task) # 开线程:42.68658709526062
# # t = Process(target=task) # 开进程:9.04949426651001
# t.start()
# ll.append(t)
#
# for t in ll:
# t.join()
# print(time.time()-ctime)
# io密集型
def task():
time.sleep(2)
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(400):
t = Thread(target=task) # 开线程:2.0559656620025635
# t = Process(target=task) # 开进程:9.506720781326294
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
四、生产消费者模型
生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
这个模型也体现了面向对象的设计理念:低耦合
import time
import random
from multiprocessing import Process, Queue, JoinableQueue
def producer(name, food, q):
for i in range(10):
data = '%s 制造了%s' % (name, food)
# 模拟制造食物延迟
time.sleep(random.randint(1, 3))
print(data)
q.put(food)
def consumer(name, q):
while True:
food = q.get()
# 模拟吃食物延迟
time.sleep(random.randint(1, 3))
print('%s消费了%s' % (name, food))
q.task_done() # 把队列中维护的数字减一
if __name__ == '__main__':
# q = Queue()
# 内部存了一个数字,放一个数字会加一
# 消费一个数字减一
q = JoinableQueue()
# 创造生产者
p = Process(target=producer, args=('egon', '包子', q))
p.start()
p1 = Process(target=producer, args=('alex', '泔水', q))
p1.start()
# 创造消费者
c = Process(target=consumer, args=('鸡哥', q))
c.start()
c1 = Process(target=consumer, args=('王铁蛋', q))
c1.start()
c2 = Process(target=consumer, args=('李铁柱', q))
c2.start()
# 主结束,消费进程也结束,把每个消费进程都设置成守护进程
# 等待所有生产者生产结束,主进程再结束
p.join()
p1.join()
q.join() # 会卡再者,一直等待q队列中数据没有了,才继续往下走
print('生产者结束了,主进程结束')
# JoinableQueue()
# 每放一个值,数字加一
# 取值不会减一,q.task_done()
# q.join() 一直阻塞,当q没有值了,才继续走
五、常用接口
5.1 信号量Semaphore
Semaphore: Semaphore管理一个内置的计数器, 每当调用acquire()时内置计数器-1; 调用release() 时内置计数器+1; 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
PS: 信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程
from threading import Thread, Semaphore
import time
import random
sm = Semaphore(3) # 数字表示可以同时有多少个线程操作
a = 1
def task(name):
global a
sm.acquire()
print('%s 正在蹲坑' % name)
a -= 1
print(a)
time.sleep(random.randint(1, 5))
sm.release()
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=('屌丝男%s号' % i,))
t.start()
5.2 Event
同进程的一样线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行;
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
案例一:
from threading import Thread, Event
import time
event = Event()
def girl(name):
print('%s 现在不单身,正在谈恋爱'%name)
time.sleep(10)
print('%s 分手了,给屌丝男发了信号'%name)
event.set()
def boy(name):
print('%s 在等着女孩分手'%name)
event.wait() # 只要没来信号,就卡在者
print('女孩分手了,机会来了,冲啊')
if __name__ == '__main__':
lyf = Thread(target=girl, args=('刘亦菲',))
lyf.start()
for i in range(10):
b = Thread(target=boy, args=('屌丝男%s号' % i,))
b.start()
案例二:起两个线程,第一个线程读文件的前半部分,读完发一个信号,另一个进程读后半部分,并打印
from threading import Thread, Event
import time
import os
event = Event()
# 获取文件总大小
size = os.path.getsize('a.txt')
def read_first():
with open('a.txt', 'r', encoding='utf-8') as f:
n = size // 2 # 取文件一半,整除
data = f.read(n)
print(data)
print('我一半读完了,发了个信号')
event.set()
def read_last():
event.wait() # 等着发信号
with open('a.txt', 'r', encoding='utf-8') as f:
n = size // 2 # 取文件一半,整除
# 光标从文件开头开始,移动了n个字节,移动到文件一半
f.seek(n, 0)
data = f.read()
print(data)
if __name__ == '__main__':
t1=Thread(target=read_first)
t1.start()
t2=Thread(target=read_last)
t2.start()
5.3 死锁现象
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁;
# 死锁现象,张三拿到了A锁,等B锁,李四拿到了B锁,等A锁
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
def eat_apple(name):
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name):
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['egon', 'alex', '铁蛋']
for name in ll:
t1 = Thread(target=eat_apple, args=(name,))
t2 = Thread(target=eat_egg, args=(name,))
t1.start()
t2.start()
解决方法,递归锁;
5.3.1 递归锁
在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
# 递归锁(可重入),同一个人可以多次acquire,每acquire一次,内部计数器加1,每relaese一次,内部计数器减一
# 只有计数器不为0,其他人都不获得这把锁
from threading import Thread, Lock,RLock
import time
# 同一把锁
# mutexA = Lock()
# mutexB = mutexA
# 使用可重入锁解决(同一把锁)
# mutexA = RLock()
# mutexB = mutexA
mutexA = mutexB =RLock()
def eat_apple(name):
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name):
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['egon', 'alex', '铁蛋']
for name in ll:
t1 = Thread(target=eat_apple, args=(name,))
t2 = Thread(target=eat_egg, args=(name,))
t1.start()
t2.start()
5.4 线程queue
线程间通信,因为共享变量会出现数据不安全问题,用线程queue通信,不需要加锁,内部自带,queue是线程安全的;
进程queue和线程queue是不一样的两个功能;
'''
三种线程Queue
-Queue:队列,先进先出
-PriorityQueue:优先级队列,谁小谁先出
-LifoQueue:栈,后进先出
'''
#如何使用
q=Queue(5)
q.put("lqz")
q.put("egon")
q.put("铁蛋")
q.put("钢弹")
q.put("金蛋")
# 取值
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# print(q.get()) # 卡住
# q.get_nowait()
# 是否满,是否空
print(q.full())
print(q.empty())
LifoQueue
q=LifoQueue(5)
q.put("lqz")
q.put("egon")
q.put("铁蛋")
q.put("钢弹")
q.put("金蛋")
print(q.get())
# 金蛋
PriorityQueue:数字越小,级别越高
q=PriorityQueue(3)
q.put((-10,'金蛋'))
q.put((100,'银蛋'))
q.put((101,'铁蛋'))
# q.put((1010,'铁dd蛋')) # 不能再放了
print(q.get())
print(q.get())
print(q.get())
5.5 定时器Timer
用来定制延时任务;
from threading import Timer
def task(name):
print('我是大帅比--%s'%name)
if __name__ == '__main__':
t = Timer(1, task,args=('sailan',)) # 本质是开了一个线程,延迟一秒执行
行
t.start()
六、线程池
concurrent.futures模块介绍:
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor:进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
基本方法:
submit(fn, *args, **kwargs):异步提交任务
map(func, *iterables, timeout=None, chunksize=1):取代for循环submit的操作
shutdown(wait=True):相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前
result(timeout=None):取得结果
add_done_callback(fn):回调函数
done():判断某一个线程是否完成
cancle():取消某个任务
进程池的使用与线程池一致
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from threading import Thread
import time
import random
pool = ThreadPoolExecutor(5) # 数字是池的大小
# pool = ProcessPoolExecutor(5) # 数字是池的大小
def task(name):
print('%s任务开始' % name)
time.sleep(random.randint(1, 4))
print('任务结束')
return '%s 返回了'%name
def call_back(f):
print(f.result())
if __name__ == '__main__':
# 终极使用
for i in range(10): # 起了100个线程
# 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行
pool.submit(task,'屌丝男%s号' % i).add_done_callback(call_back)
shutdown的用法
from concurrent.futures import ThreadPoolExecutor
import time
pool = ThreadPoolExecutor(3)
def task(name):
print('%s 开始'%name)
time.sleep(1)
print('%s 结束'%name)
if __name__ == '__main__':
for i in range(20):
pool.submit(task, '屌丝%s' % i)
# 放到for外面,等待所有任务执行完成,主线程再继续走
pool.shutdown(wait=True) # 等待所有任务完成,并且把池关闭
# 问题,在关了也就在此之后,就不可再提交任务了
print('主') # 立马执行,20个线程都执行完了,再执行