线程的基础知识

概要

  • 消息队列

  • IPC(inter-producer-consumer)机制(进程间通信)

  • 生产者消费模型

  • 线程理论(重要)

  • 开设线程的两种方式

  • 线程实现TCP(Transmission Control Protocol)服务端并发

  • 线程join方法

  • 线程间数据共享

  • 守护线程

  • GIL(Global-interpreter-lock)全局解释器锁

内容

1.消息队列

由于目前的知识储备还不够直接学习消息队列 所以先学习内置中的队列

消息队列可以实现进程间通信(本地、网络),并且消息队列还起到了保存数据的功能(队列中的数据如果不被取走,会一直在队列中)
消息队列将生产者与消费者解耦合!!! 解耦合:就是把程序中互相不相关或有限相关的模块分割开来,把不同模块互相之间的关系用接口进行准确定义,解耦前,两个模块之间共享所有信息
生产者只需要将数据放入队列中即可 无需考虑是否有人消费
消费者之需要将数据从队列中取出 无需考虑是否有生产者生产

队列:先进先出(使用频率很高 因为现实生活中大部分都是使用的是队列)
队栈:先进后出(只在一些特定场景下会使用到)
    
# 以后我们会直接使用别人封装好的消息队列 实现各种数据传输
from multiprocessing import Queue
q = Queue(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
# print(q.get_nowait()) # 如果队列中没有值 计算机会报错

"""
full()  判断队列是否满了》》》转为布尔值
empty()  判断队列是否空了>>>>转为布尔值
get_nowait()  如果队列中没有值 直接保错

上述方法能否在并发的场景下精准使用?
答案是不能 因为我们可以往极限方向去想 就是在一个进程中 
1.我在取的时候 已经取完了然后使用empty() 然后在那瞬间put这边又要来一个值 这里通empty()出来的结果还算是正确的结果吗
2.我在放在时候 已经全部放完后使用full(),然后在那瞬间get()这边又取走了一个值 这里通过full()出来的结果还算是正确的结果吗

综上:不能用 之所以还介绍队列的目的是因为它还支持进程间数据通信 我们需要学习进程间数据通信
"""

2.IPC(inter-producer-consumer)机制(进程间通信)

1.主进程与子进程数据交互
2.两个子进程数据交互
本质:不同内存空间中的进程数据交互

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()
    
    # q.put(123)  # 主进程往队列中存放数据123
    print('主进程')
    
    运行之后产生的结果是:
        主进程
子进程consumer从队列中取值>>>: 子进程producer往队列中添加值    

3.生产者消费者模型

# 生产者 负责生产/制作数据
# 消费者 负责消费/处理数据

比如在爬虫领域中
	会先通过代码爬取网页数据 (爬取网页的代码就可以称之为是生产者)
    之后针对网页数据做筛选处理(处理网页的代码就可以称之为消费者)
    
 如果使用进程来掩饰
	除了有至少两个进程之外 还需要一个媒介(消息队列)
    
 以后遇到该模型需要考虑的问题其实就是供需平衡的问题
生产力与消费力要均衡

from multiprocessing import Process, Queue, JoinableQueue
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=('大厨jason', '沙县小吃', q))
    p2 = Process(target=producer, args=('大厨kevin', '韭菜炒蛋', q))
    c1 = Process(target=consumer,args=('tony', q))
    c2 = Process(target=consumer,args=('jerry', q))
    c1.daemon = True
    c2.daemon = True
    p1.start()
    p2.start()
    c1.start()
    c2.start()
    # 生产者生产完所有数据之后 往队列中添加结束的信号
    p1.join()
    p2.join()
    # q.put(None) # 结束信号的个数要跟消费者个数要一致才可以
    # q.put(None)
    # 队列中其实已经自己加了锁 所有多进程取值也不会冲突 并且取走了就没了
    q.join()  # 等待队列中数据全部被取出(一定要让生产这全部结束才能判断正确)
    """
    执行完上述的join方法表示消费者也已经消费完数据了"""

4.线程理论

# 什么是线程
进程:相当于一个资源单位
线程:进程中的执行单位
进程相当于车间(一个个空间),线程相当于车间里面的流水线(真正干活的)
"""一个进程中至少有一个线程
进程仅仅是在内存中开辟一块空间(提供线程工作所需的资源)
线程真正被CPU执行 线程需要的资源跟所在进程的去拿就可以"""

# 为什么要有线程
开设线程的消耗远远小于进程
开进程过程:1.申请内存空间 2.拷贝代码
开线程:一个进程内可以开设多个线程 无需申请内存空间 拷贝代码
    一个进程内的多个线程数据是共享的 都是在同一个线程内 大家的资源都是共享的
    
    
 """开发一个文本编辑器
     获取用户输入并实时展示到屏幕上
     并实时保存到硬盘中
     多种功能应该开设多线程而不是多进程"""

5.开设线程的两种方式

'进程与线程的代码实操几乎是一样的 主要是因为线程与进程的理论是同一个人写的 所以两者的用法几乎一样' 
from threading import Thread
import time

def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is over')
    
# 创建线程无需在__main__下面编写 但是为了统一 还是习惯在子代码中写
t = Thread(target=task,args=('jason',))
t.start()  # 创建线程的开销极小 几乎是一瞬间就可以创建
print('主线程')

class MyThread(Thread):
    def __init__(self,username):
        super().__init__()
        self.username = username
    def run(self):
        print(f'{self.username} jason is running')
        time.sleep(3)
        print(f'{self.username} is over')
        
 t = MyThread('jasonNB')
 t.start()
 print('主线程')

6.线程实现TCP服务端的并发

需要比对开设进程与线程的本质区别

import socket
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.dacode('utf8'))
        sock.send(data.upper())
        
while True:
    sock,addr = server.accept()
    # 每类一个客户端就创建一个线程做数据交互
    t = Thread(target=talk,args=(sock,))
    t.start()        

7.线下join方法

from threading import Thrad
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('主线程')

"主线程为什么要等着子线程结束后才会执行整个进程
因为主线程结束也就标志着整个进程的结束 为要确保子线程运行过程中所需要的各项资源 所以主线程不能先结束
"

8.同一个进程内的多个线程数据共享

from threading import Thread

money = 10000
def task():
    global money
    money = 1
    
t = Thread(target=task)
t.start()
t.join()
print(money)  # 1

9.线程对象属性与方法

1.验证一个进程下的多个线程是否真的处于一个进程
验证确实如此
2.统计进程下活跃的线程数
active_count()  # 注意主线程也算!!
3.获取线程的名字
1.current_thread().name
MainThread   主线程
Thread-1、Thread-2  子线程

2.self.name

10.守护线程

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=('jason',))
t2 = Thread(target=task, args=('kevin',))
t1.daemon = True
t1.start()
t2.start()
print('主线程')

11.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.)

1.回顾
python解释器的类别有很多(根据解释器是由什么编写的)
C-python、J-python、P-python(pyhton)

垃圾回收机制
应用计数、标记清除、分代回收

"""
GIL只存在于Cpython解释器中 不是python的特征
GIL是一把互斥锁用于阻止同一个进程下的多个线程同时执行
原因是因为Cpython解释器中的垃圾回收机制不是线程安全的

反向验证GIL的存在 如果不存在会产生垃圾回收机制与正常线程之间数据错乱
GIL是加在Cpython解释器上面的互斥锁
同一个进程下的多个线程要想执行必须先前抢GIL锁 所以同一个进程下多个线程肯定不能同时运行 即无法利用多核优势

强调:同一个进程下的多个线程不能同时执行即不能利用多核优势
	很多不懂python的程序员会喷python是垃圾 速度太慢 有多核都不能用
	
	反怼:虽然用一个进程下的多个线程不能利用多核优势 但是还可以开设多进程
	
再次强调:python的多线程就是垃圾
反怼:要结合实际情况而定
	如果多个任务都是IO密集型 那么多线程更有优势(消耗的资源更少)  多道技术:切换+保存状态
	如果多个任务都是计算密集型 那么多线程确实没有优势 但是可以用多进程来弥补
	cpu 越多越好
	
以后用python就可以多进程下面开设多线程从而达到效率最大化
"""

ps:1.所有的解释型语言都无法做到同一个进程下多个线程利用多核优势
   2.GIL在实际编程中其实不用考虑
posted @ 2022-04-20 23:00  一颗平凡的小石头  阅读(41)  评论(0)    收藏  举报