文章目录
线程
进程:进程就是一个资源单位,它是用来申请内存空间的。
线程:线程才是真正的执行单位,每一个进程中都会自带一个线程。
有了进程为什么要有线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
如果开一个进程,需要申请内存空间,然后将代码拷贝到申请的内存空间中。这两个过程都是比较耗时。但是开线程是不需要申请内存空间,所以开线程的开销远远小于开进程的开销!!!
进程和线程的关系
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程操作系统中,进程不是一个可执行的实体。
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。
threading模块
multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍
开启线程的两种方式
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(1)
print('%s is over'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('dog',))
t.start() # 开启线程的速度非常快,几乎代码执行完线程就已经开启
print('主')
from threading import Thread
import time
class MyThread(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
print('%s is running' % self.name)
time.sleep(1)
print('%s is over'%self.name)
if __name__ == '__main__':
t = MyThread('cat')
t.start()
print('主')
线程之间数据的共享
线程之间的数据是可以进行共享的。
from threading import Thread
x = 100
def task():
global x
x = 666
t = Thread(target=task)
t.start()
t.join()
print(x)
'''
输出:666
'''
线程对象的其他属性和方法
sfrom threading import Thread,active_count,current_thread
import os
import time
def task(name):
# print('%s is running'%name,os.getpid())
print('%s is running'%name,current_thread().name,current_thread().getName())
time.sleep(1)
print('%s is over'%name)
def info(name):
print('%s is running' % name, current_thread().name, current_thread().getName())
time.sleep(1)
print('%s is over' % name)
t = Thread(target=task,args=('线程1',))
t1 = Thread(target=info,args=('线程2',))
t.start()
t1.start()
t.join()
print(active_count()) # 当前存活的线程数
print(os.getpid())
print(current_thread().name)
print(current_thread().getName())
线程互锁
from threading import Thread,Lock
import time
import random
mutex = Lock()
n = 100
def task():
global n
mutex.acquire()
tmp = n
time.sleep(0.1)
n = tmp -1
mutex.release()
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(n)
守护线程
from threading import Thread
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == '__main__':
# t1=Thread(target=foo)
# t2=Thread(target=bar)
t1=Process(target=foo)
t2=Process(target=bar)
t1.daemon=True
t1.start()
t2.start()
print("main-------")
'''
123
main-------
456
end456
'''
'''
main-------
123
456
end456
'''
'''
main-------
456
end456
'''
全局解释器锁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解释器解释之后才能被cpu执行
- 同一时刻同一个进程内多个线程无法实现并行,但是可以实现并发
- 为什么要有GIL是因为它内部的垃圾回收机制不是线程安全的
- 垃圾回收机制也是一个任务,跟你的代码不是串行运行,如果是串行会明显有卡顿这个垃圾回收到底是开进程还是开线程?肯定是线程,线程肯定也是一段代码,所以想运行也必须要拿到python解释器
- 假设能够并行,会出现什么情况?一个线程刚好要造一个a=1的绑定关系之前,这个垃圾线程来扫描,矛盾点就来了,谁成功都不对!
- 也就意味着在Cpython解释器上有一把GIL全局解释器锁
- 同一个进程下的多个线程不能实现并行但是能够实现并发,多个进程下的线程能够实现并行
Python中的多线程到底有没有用?
Python中的多线程到底有没有用,要看解决的是什么问题了。现在分别有两种任务:
-
单核情况下:四个任务
-
多核情况下:四个任务
计算密集型:一个任务要计算十秒,四个进程和四个线程,肯定是进程快
IO密集型:任务都是纯io情况下,线程开销比进程小,肯定是线程好
计算密集型案例代码:
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res=0
for i in range(100000000):
res*=i
if __name__ == '__main__':
l=[]
print(os.cpu_count()) # 本机为12核
start=time.time()
for i in range(12):
# p=Process(target=work) #耗时8s多
p=Thread(target=work) #耗时44s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
IO密集型案例代码
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为12核
start=time.time()
for i in range(400):
p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
# p=Thread(target=work) #耗时2s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
GIL与自定义互斥锁
不同的数据需要加不同的锁才能保证数据的安全,GIL锁只是对线程加锁,对数据并没有加锁的效果
from threading import Thread,Lock
import time
mutex=Lock()
n=100
def task():
global n
with mutex:
temp=n
time.sleep(0.1)
n=temp-1
if __name__ == '__main__':
l=[]
for i in range(100):
t=Thread(target=task)
l.append(t)
t.start()
for t in l:
t.join()
print(n)
# 对于修改不同的数据,需要加不同的锁进行处理
对于修改不同的数据,需要加不同的锁进行处理
死锁与递归锁
自定义锁一次acquire必须对应一次release,不能连续acquire
递归锁可以连续的acquire,每acquire一次计数加一
from threading import Thread,Lock,RLock
import time
# mutexA=Lock()
# mutexB=Lock()
mutexB=mutexA=RLock()
class Mythead(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire()
print('%s 抢到A锁' %self.name)
mutexB.acquire()
print('%s 抢到B锁' %self.name)
mutexB.release()
mutexA.release()
def f2(self):
mutexB.acquire()
print('%s 抢到了B锁' %self.name)
time.sleep(2)
mutexA.acquire()
print('%s 抢到了A锁' %self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(100):
t=Mythead()
t.start()
信号量
自定义的互斥锁如果是一个厕所,那么信号量就相当于公共厕所,门口挂着多个厕所的钥匙。抢和释放跟互斥锁一致
from threading import Thread,Semaphore
import time
import random
sm = Semaphore(5) # 公共厕所里面有五个坑位,在厕所外面放了五把钥匙
def task(name):
sm.acquire()
print('%s正在蹲坑'%name)
# 模拟蹲坑耗时
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()
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)
if __name__ == '__main__':
t = Thread(target=light)
t.start()
for i in range(10):
t = Thread(target=car,args=('%s'%i,))
t.start()
线程queue
同一个进程下的线程数据都是共享的为什么还要用queue?queue本身自带锁的功能,能够保证数据的安全
# 我们现在的q只能在本地使用,后面我们会学基于网络的q
import queue
先进先出
queue.Queue() #先进先出
q=queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
后进先出->堆栈
queue.LifoQueue() #后进先出->堆栈
q=queue.LifoQueue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
优先级用数字表示,数字越小优先级越高
queue.PriorityQueue() #优先级
q=queue.PriorityQueue(3) #优先级,优先级用数字表示,数字越小优先级越高
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())
进程池与线程池
实现并发的手段有两种,多线程和多进程。注:并发是指多个任务看起来是同时运行的。主要是切换+保存状态。
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。
线程池主要用于:
1)需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2)对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
进程池/线程池的使用:
我们需要导入concurrent.futures模块下的ThreadPoolExecutor和ProcessPoolExecutor
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time
import os
# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5)
# pool = ProcessPoolExecutor(5) # 创建进程池,默认是当前计算机的cpu个数,其他与线程池代码一样
def task(n):
print(n, os.getpid())
time.sleep(2)
return n ** 2
if __name__ == '__main__':
t_ls = []
for i in range(20):
future = pool.submit(task, i)
# print(future) # 如果在此处直接打印结果,会使得每次执行都要等待结果,变成同步提交
t_ls.append(future)
# 关闭池子并且等待池子中所有的任务运行完毕,再去拿列表中的结果
pool.shutdown()
for p in t_ls:
print('>>>:',p.result())
print('主')
# 输出:
'''
0 6168
1 6168
2 6168
3 6168
4 6168
5 61686 6168
78 6168
9 6168
6168
>>>: 0
>>>: 1
>>>: 4
>>>: 9
>>>: 16
>>>: 25
>>>: 36
>>>: 49
>>>: 64
>>>: 81
>>>主
>>>'''
以上代码可以实现在子线程执行完后,再统一去拿执行的结果,结果是有序的;但是如果我们想一边执行一边获取结果呢?这时候回调函数就派上了用场。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os
# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5) # 创建了一个池子,池子里面有5个线程,进程池和线程池的唯一区别就是这里
# 不指定参数的情况,进程池默认是当前计算机cpu个数
# pool = ProcessPoolExecutor(5) # 创建了一个池子,池子里面有5个进程
def task(n):
print(n, os.getpid())
time.sleep(2)
return n ** 2
"""
提交任务的方式
同步:提交任务之后,原地等待任务的返回结果,再继续执行下一步代码
异步:提交任务之后,不等待任务的返回结果(通过回调函数拿到返回结果并处理),直接执行下一步操作
"""
# 回调函数:异步提交之后一旦任务有返回结果,自动交给另外一个去执行
def call_back(n):
print("拿到了结果:%s" % n.result())
if __name__ == '__main__':
t_list = []
for i in range(20):
future = pool.submit(task, i).add_done_callback(call_back) # 异步提交任务,通过回掉函数拿到执行结果
t_list.append(future) # 如果在此处直接打印结果print(future.result()),会使得每次执行等待结果,变成同步提交
# print(t_list) # 前面回调函数拿走结果之后,列表里面全剩了None
print('主')
'''
执行结果:
0 8516
1 8516
2 8516
3 8516
4主
8516
拿到了结果:0
5 8516
拿到了结果:1
6 8516
拿到了结果:4
拿到了结果:9
7 8516
拿到了结果:16
8 8516
9 8516
拿到了结果:25
拿到了结果:36
拿到了结果:49
拿到了结果:81
拿到了结果:64
'''
模拟爬虫:
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread
import time,random,os
import requests
def get(url):
print('%s GET %s' %(current_thread().name,url))
time.sleep(3)
response=requests.get(url)
if response.status_code == 200:
res=response.text
else:
res='下载失败'
return res
def parse(future):
time.sleep(1)
res=future.result()
print('%s 解析结果为%s' %(current_thread().name,len(res)))
if __name__ == '__main__':
urls=[
'https://www.baidu.com',
'https://www.sina.com.cn',
'https://www.tmall.com',
]
p=ThreadPoolExecutor(4)
for url in urls:
future=p.submit(get,url)
# 异步调用:提交完一个任务之后,不在原地等待,而是直接执行下一行代码,会导致任务是并发执行的,,结果futrue对象会在任务运行完毕后自动传给回调函数
future.add_done_callback(parse) # parse会在任务运行完毕后自动触发,然后接收一个参数future对象
p.shutdown(wait=True)
print('主',current_thread().name)