线程的基本知识
1 > 消息队列
由于目前的知识储备还不够直接学习消息队列 所以先学习内置队列,对列的概念如下:
队列: 先进先出(使用频率很高)
堆栈: 先进后出(特定常见下用)
队列的使用代码如下:
from multiprocessing import Queue # 导入模块
q = Queue(5) # 创建一个队列,自定义一个队列的长度为5
q.put(111) # 朝队列当中存放数据
q.put(222) # 朝队列当中存放数据
q.put(333) # 朝队列当中存放数据
print(q.full()) # False 判断队列是否满了
q.put(444) # 朝队列当中存放数据
q.put(555) # 朝队列当中存放数据
print(q.full()) # True 判断队列是否满了
# q.put(666) # 超出最大长度 原地阻塞等待队列中出现空位
print(q.get()) # 输出队列当中的数据
print(q.get()) # 输出队列当中的数据
print(q.empty()) # False 判断队列是否空了
print(q.get()) # 输出队列当中的数据
print(q.get()) # 输出队列当中的数据
print(q.get()) # 输出队列当中的数据
print(q.empty()) # True 队列已经空了 输出True
# print(q.get()) # 队列中没有值 继续获取则阻塞等待队列中给值
print(q.get_nowait()) # 队列中如果没有值 直接报错(它急了)
需要注意的是上述的full()、empty()、get_nowait() 不能在并发的场景下使用。
2 > IPC机制(进程间通信)
我们可以利用队列的知识简单实现主进程与子进程交互和两个子进程数据的交互。他们的本质其实就是在不同的空间当中进程实现数据交互的功能。
代码如下:
''' 主进程与子进程做数据交互 '''
from multiprocessing import Process, Queue
def producer(q):
print('子进程producer 从队列中取值 >>>> :', q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=producer, args=(q,))
p.start()
''' 子进程与子进程做数据交互 '''
from multiprocessing import Process, Queue
def producer(q):
q.put('子进程producer往队列中添加值')
def consumer(q):
print('子进程consumer从队列中取值 >>>> :', q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=producer, args=(q,))
p1 = Process(target=consumer, args=(q,))
p.start()
p1.start()
print('主进程')
3 > 生产者消费者模型
生产者:负责生产/制作数据
消费者:负责消费/处理数据
在爬虫领域,我们会通过代码爬取页面数据(爬取页面的代码就可以称之为是生产者),之后针对网页数据做筛选处理的操作(处理网页的代码就可以称之为消费者)。如果想通过使用进程来演示,除了需要至少两个进程之外,还需要一个媒介(消息队列)。
在以后遇到该模型需要考虑的问题其实就是供需平衡的问题,就是需要将生产力与消费力做均衡处。
操作代码如下:
from multiprocessing import Process, Queue, JoinableQueue
import time
import random
def producer(name, food, q):
for i in range(5):
data = f'{name}生产了{food} {i} 个'
print(data)
time.sleep(random.randint(1, 3))
q.put(data)
def consumer(name, q):
while True:
food = q.get()
time.sleep(random.random())
print(f'{name} 吃了 {food}')
q.task_done() # 每次取过队列当中的数据必须给队列一个反馈
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=('大厨 bob', '韭菜炒鸡蛋', q))
p2 = Process(target=producer, args=('老八kevin', '蜜汁小汉堡', q))
c1 = Process(target=consumer, args=('涛涛',q))
c2 = Process(target=consumer, args=('龙龙',q))
'''
将消费者设置为守护进程
'''
c1.daemon = True
c2.daemon = True
p1.start()
p2.start()
c1.start()
c2.start()
# 生产者生产完所有数据之后 往队列中添加结束的信号
p1.join() # 当所有的子程序结束代码才会继续往下走
p2.join() # 当所有的子程序结束代码才会继续往下走
"""
队列中其实已经自己加了锁 所以多进程取值也不会冲突 并且取走了就没了
"""
q.join() # 等待队列中数据全部被取出(一定要让生产者全部结束才能判断正确)
"""
执行完上述的join方法表示消费者也已经消费完数据了
"""
4 > 线程理论
进程相当于车间(一个个空间),线程相当于车间里面的流水线(真正干活的)
一个进程中至少有一个线程。进程仅仅是在内存中开辟一块空间(提供线程工作所需的资源),线程真正被CPU执行,线程需要的资源跟所在进程的要。
4.1 > 开设线程的原因
开设线程的消耗远远小于进程,开进程需要申请内存空间,或者要拷贝一份代码。开线程的话我们可以在一个进程当中开设多个线程 无需申请内存空间、拷贝代码。并且一个进程内的多个线程数据是共享的。
4.2 > 开设线程的两种方式
进程与线程的代码实操几乎是一样的,操作代码如下:
''' 方式一 '''
from threading import Thread
import time
def task(name):
print(f'{name} is running ')
time.sleep(3)
print(f'{name} is over ')
t = Thread(target=task, args=('jason', ))
t.start()
print(
'主线程'
)
''' 方式二 '''
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, username):
super().__init__()
self.username = username
def run(self):
print(f'{self.username} is running ')
time.sleep(3)
print(f'{self.username} is over ')
t = MyThread('jasonNb')
t.start()
print('主线程')
4.3 > 线程实现TCP服务端的并发
操作代码如下:
'''服务端'''
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen()
def talk(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
while True:
sock, addr = server.accept()
t = Thread(target=talk, args=(sock, ))
t.start()
'''客户端'''
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
client.send(b'hello baby ~')
data = client.recv(1024)
print(data.decode('utf8'))
4.4 > 线程join方法
与进程join方法一致,都是主线程代码等待子线程代码运行完毕之后再往下执行。主线程为什么要等着子线程结束才会结束整个进程 ,因为主线程结束也就标志着整个进程的结束 要确保子线程运行过程中所需的各项资源。 操作代码如下:
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t = Thread(target=task, args=('jason', ))
t.start()
t.join()
print('主线程')
4.5 > 同一个进程内的多个线程数据共享
直接上代码:
from threading import Thread
money = 100000000
def task():
global money
money = 1
'''
子线程更改全局名称空间中(主线程)的money
'''
def task():
global money
money = 1
4.6 > 线程对象属性和方法
4.6.1 > 验证一个进程下的多个线程是否真的处于同一个进程
from threading import Thread
import os
def task():
print('子线程获取进程号 : >>>>>', os.getpid())
t = Thread(target=task)
t.start()
print('主线程获取进程号 : >>>>', os.getpid())
4.6.2 > 统计进程下活跃的线程数
使用模块 active_count
from threading import Thread, active_count
import os
import time
def task():
print('子线程获取进程号 : >>>>>', os.getpid())
time.sleep(3)
t = Thread(target=task)
t.start()
print(active_count())
print('主线程获取进程号 : >>>>', os.getpid())
4.6.3 > 获取线程的名字
1 > 使用模块 current_thread ,
2 > self.name
MainThread 主线程
Thread-1、Thread-2 子线程
操作代码如下:
from threading import Thread, active_count, current_thread
import os
import time
def task():
print('子线程获取进程号 : >>>>>', os.getpid())
time.sleep(3)
print(current_thread().name)
t = Thread(target=task)
t.start()
print(active_count())
print(current_thread().name)
print('主线程获取进程号 : >>>>', os.getpid())
from threading import Thread
class MyThread(Thread):
def __init__(self, username):
self.username = username
super().__init__()
def run(self):
print(self.name)
t = MyThread('bobNB')
t.start()
print('主线程')
4.7 > 守护线程
与守护进程类似,当主线程结束设置为守护线程的子线程立马结束。
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t1 = Thread(target=task, args=('bob',))
t1.daemon = True
t1.start()
print('主线程')
但是当有非守护线程存在的情况,守护线程的作用就相当于失效了,因为主线程要等待非守护线程结束才会结束,因为主线程结束也就标志着整个进程的结束 要确保非守护子线程运行过程中所需的各项资源。
5 > 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.)
GIL只存在于CPython解释器中,不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为CPython解释器中的垃圾回收机制不是线程安全的
反向验证GIL的存在 如果不存在会产生垃圾回收机制与正常线程之间数据错乱
GIL是加在CPython解释器上面的互斥锁
同一个进程下的多个线程要想执行必须先抢GIL锁 所以同一个进程下多个线程肯定不能同时运行 即无法利用多核优势
强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
虽然用一个进程下的多个线程不能利用多核优势 但是还可以开设多进程!!!
如果有人说 :python的多线程就是垃圾!!!
我们可以怒怼它说:要结合实际情况 如果多个任务都是IO密集型的 那么多线程更有优势(消耗的资源更少) ----多道技术:切换+保存状态 。如果多个任务都是计算密集型 那么多线程确实没有优势 但是可以用多进程 ---- CPU越多越好
以后用python就可以多进程下面开设多线程从而达到效率最大化
需要知道的就是:所有的解释型语言都无法做到同一个进程下多个线程利用多核优势