线程


每日测验

  • 说出你所知道的关于人工智能相关的API网站

  • 互斥锁的作用

  • 队列的作用及你所接触到的队列模块及该模块的常用方法

  • 简述生产者消费者模型

你们之间的差距:每天下课之后以及放假时间

平时多利用截图功能,将可能遗忘或者稍微难理解的知识点截图并加上自己的文件见解,然后保存到自己的手机相册中

昨日内容回顾

  • 进程对象属性及其他方法

    from multiprocessing import Process,current_process
    import os
    ?
    ?
    current_process().pid  # 查看当前进程号
    os.getpid()  # 查看当前进程号
    os.getppid()  # 查看当前进程的父进程号
    """
    windows终端命令
    tasklist
    tasklist |findstr PID
    mac终端命令
    ps aux
    ps aux|grep PID
    """
  • 僵尸进程与孤儿进程

    """
    僵尸进程:进程结束后不会立刻释放占用的资源(PID),会保留一段时间共父进程查看
    孤儿进程:子进程存活,父进程意外死亡,孤儿进程操作系统会自动回收相应资源
    """
  • 守护进程

    """
    被守护进程结束之后守护进程立刻也跟着结束
    """
    # 如何开启 在start语句之前写以下代码即可
    p.daemon = True
    p.start()
  • 互斥锁

    """
    多个人在操作同一份数据的时候会出现数据错乱的问题
    针对上述问题我们通常都是加锁处理
    ?
    作用:
    将并发变成串行,牺牲了程序运行的效率但是保证了数据的安全
    ?
    注意:
    只在操作数据的部分加锁即可
    锁尽量不要自己去处理 很容易造成死锁现象
    ?
    扩展:行锁 表锁
    操作表格中的一行数据的时候其他人都不能操作
    操作一张表的时候其他人都不能操作
    """
    from multiprocessing import Lock
    mutex = Lock()
    # 抢锁
    mutex.acquire()
    # 释放锁
    mutex.release()
    ?
    # 模拟抢票
  • 队列Queue

    """
    队列:先进先出
    堆栈:先进后出
    """
    from multiprocessing import Queue
    q = Queue()  # 括号内可以放数字来限制队列的大小
    q.put()  # 放数据 当队列满了再放 阻塞
    q.get()  # 取数据 当队列空了再取 阻塞
    ?
    q.full()  # 判断队列是否满了
    q.empty()  # 判断队列是否空了
    q.get_nowait()  # 取数据的时候如果没有数据直接报错
    q.get(timeout=5)  # 取数据的时候如果没有数据等5s还没有则直接报错
  • 进程间通信

    """
    进程之间是无法直接进行数据交互的,但是可以通过队列或者管道实现数据交互
    管道:
    队列:管道+锁
    ?
    本地测试的时候才可能会用到Queue,实际生产用的都是别人封装好的功能非常强大的工具
    redis
    kafka
    RQ
    """
  • 生产者与消费者模型

    """
    生产者 + 消息队列 + 消费者
    为何要有消息队列的存在 是为了解决供需不平衡的问题
    """
    # JoinableQueue
    """
    可以被等待的q
    你在往队列中放数据的时候 内部有一个计数器自动加1
    你在从队列中取数据的时候 调用task_done() 内部计时器自动减1
    q.join() 当计数器为0的时候才继续往下运行
    """
  • 线程理论

    """
    进程:资源单位
    线程:执行单位
    线程才是真正干活的人,干活的过程中需要的资源由线程所在的进程提供
    ?
    每一个进程肯定都自带一个线程
    ?
    同一个进程内可以创建多个线程
    """
    ?
    """
    开进程
    申请内存空间  
    ”拷贝代码“
    消耗资源较大
    开线程
    同一个进程内创建多个线程 无需上述两部操作,消耗资源相对较小

    ?
    智商
    情商
    搜商
    """

今日内容概要

  • 开启线程的两种方式

  • TCP服务端实现并发的效果

  • 线程对象的join方法

  • 线程间数据共享

  • 线程对象属性及其他方法

  • 守护线程

  • 线程互斥锁

  • GIL全局解释器锁

  • 多进程与多线程的实际应用场景

今日内容详细

开启线程的两种方式

# from multiprocessing import Process
# from threading import Thread
# import time
#
#
# def task(name):
#     print('%s is running'%name)
#     time.sleep(1)
#     print('%s is over'%name)
#
#
# # 开启线程不需要在main下面执行代码 直接书写就可以
# # 但是我们还是习惯性的将启动命令写在main下面
# t = Thread(target=task,args=('egon',))
# # p = Process(target=task,args=('jason',))
# # p.start()
# t.start() # 创建线程的开销非常小 几乎是代码一执行线程就已经创建了
# print('主')
?
?
?
from threading import Thread
import time
?
?
class MyThead(Thread):
   def __init__(self, name):
       """针对刷个下划线开头双下滑线结尾(__init__)的方法 统一读成 双下init"""
       # 重写了别人的方法 又不知道别人的方法里有啥 你就调用父类的方法
       super().__init__()
       self.name = name
?
   def run(self):
       print('%s is running'%self.name)
       time.sleep(1)
       print('egon DSB')
?
?
if __name__ == '__main__':
   t = MyThead('egon')
   t.start()
   print('主')

TCP服务端实现并发的效果

import socket
from threading import Thread
from multiprocessing import Process
"""
服务端
  1.要有固定的IP和PORT
  2.24小时不间断提供服务
  3.能够支持并发
   
从现在开始要养成一个看源码的习惯
我们前期要立志称为拷贝忍者 卡卡西 不需要有任何的创新
等你拷贝到一定程度了 就可以开发自己的思想了
"""
server =socket.socket()  # 括号内不加参数默认就是TCP协议
server.bind(('127.0.0.1',8080))
server.listen(5)
?
?
# 将服务的代码单独封装成一个函数
def talk(conn):
   # 通信循环
   while True:
       try:
           data = conn.recv(1024)
           # 针对mac linux 客户端断开链接后
           if len(data) == 0: break
           print(data.decode('utf-8'))
           conn.send(data.upper())
       except ConnectionResetError as e:
           print(e)
           break
   conn.close()
?
# 链接循环
while True:
   conn, addr = server.accept()  # 接客
   # 叫其他人来服务客户
   # t = Thread(target=talk,args=(conn,))
   t = Process(target=talk,args=(conn,))
   t.start()
?
?
"""客户端"""
import socket
?
?
client = socket.socket()
client.connect(('127.0.0.1',8080))
?
while True:
   client.send(b'hello world')
   data = client.recv(1024)
   print(data.decode('utf-8'))

线程对象的join方法

from threading import Thread
import time
?
?
def task(name):
   print('%s is running'%name)
   time.sleep(3)
   print('%s is over'%name)
?
?
if __name__ == '__main__':
   t = Thread(target=task,args=('egon',))
   t.start()
   t.join()  # 主线程等待子线程运行结束再执行
   print('主')

同一个进程下的多个线程数据是共享的

from threading import Thread
import time


money = 100


def task():
global money
money = 666
print(money)


if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(money)

线程对象属性及其他方法

from threading import Thread, active_count, current_thread
import os,time


def task(n):
# print('hello world',os.getpid())
print('hello world',current_thread().name)
time.sleep(n)


if __name__ == '__main__':
t = Thread(target=task,args=(1,))
t1 = Thread(target=task,args=(2,))
t.start()
t1.start()
t.join()
print('主',active_count()) # 统计当前正在活跃的线程数
# print('主',os.getpid())
# print('主',current_thread().name) # 获取线程名字

守护线程

# 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=('egon',))
# t.daemon = True
# t.start()
# print('主')

"""
主线程运行结束之后不会立刻结束 会等待所有其他非守护线程结束才会结束
因为主线程的结束意味着所在的进程的结束
"""


# 稍微有一点迷惑性的例子
from threading import Thread
import time


def foo():
print(123)
time.sleep(1)
print('end123')


def func():
print(456)
time.sleep(3)
print('end456')


if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print('主.......')

线程互斥锁

from threading import Thread,Lock
import time


money = 100
mutex = Lock()


def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release()


if __name__ == '__main__':

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(money)

GIL全局解释器锁

Ps:博客园密码:xiaoyuanqujing@666

"""
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.)
"""
"""
python解释器其实有多个版本
Cpython
Jpython
Pypypython
但是普遍使用的都是CPython解释器

在CPython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行
同一个进程下的多个线程无法利用多核优势!!!
疑问:python的多线程是不是一点用都没有???无法利用多核优势

因为cpython中的内存管理不是线程安全的
内存管理(垃圾回收机制)
1.应用计数
2.标记清楚
3.分代回收

"""

"""
重点:
1.GIL不是python的特点而是CPython解释器的特点
2.GIL是保证解释器级别的数据的安全
3.GIL会导致同一个进程下的多个线程的无法同时执行即无法利用多核优势(******)
4.针对不同的数据还是需要加不同的锁处理
5.解释型语言的通病:同一个进程下多个线程无法利用多核优势
"""

GIL与普通互斥锁的区别

from threading import Thread,Lock
import time


mutex = Lock()
money = 100


def task():
global money
# with mutex:
# tmp = money
# time.sleep(0.1)
# money = tmp -1
mutex.acquire()
tmp = money
time.sleep(0.1) # 只要你进入IO了 GIL会自动释放
money = tmp - 1
mutex.release()


if __name__ == '__main__':
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(money)



"""
100个线程起起来之后 要先去抢GIL
我进入io GIL自动释放 但是我手上还有一个自己的互斥锁
其他线程虽然抢到了GIL但是抢不到互斥锁
最终GIL还是回到你的手上 你去操作数据
"""

同一个进程下的多线程无法利用多核优势,是不是就没有用了

"""
多线程是否有用要看具体情况
单核:四个任务(IO密集型\计算密集型)
多核:四个任务(IO密集型\计算密集型)
"""
# 计算密集型 每个任务都需要10s
单核(不用考虑了)
多进程:额外的消耗资源
多线程:介绍开销
多核
多进程:总耗时 10+
多线程:总耗时 40+
# IO密集型
多核
多进程:相对浪费资源
多线程:更加节省资源

代码验证

# 计算密集型
# from multiprocessing import Process
# from threading import Thread
# import os,time
#
#
# def work():
# res = 0
# for i in range(10000000):
# res *= i
#
# if __name__ == '__main__':
# l = []
# print(os.cpu_count()) # 获取当前计算机CPU个数
# start_time = time.time()
# for i in range(12):
# p = Process(target=work) # 1.4679949283599854
# t = Thread(target=work) # 5.698534250259399
# t.start()
# # p.start()
# # l.append(p)
# l.append(t)
# for p in l:
# p.join()
# print(time.time()-start_time)



# IO密集型
from multiprocessing import Process
from threading import Thread
import os,time


def work():
time.sleep(2)

if __name__ == '__main__':
l = []
print(os.cpu_count()) # 获取当前计算机CPU个数
start_time = time.time()
for i in range(4000):
# p = Process(target=work) # 21.149890184402466
t = Thread(target=work) # 3.007986068725586
t.start()
# p.start()
# l.append(p)
l.append(t)
for p in l:
p.join()
print(time.time()-start_time)

总结

"""
多进程和多线程都有各自的优势
并且我们后面在写项目的时候通常可以
多进程下面再开设多线程
这样的话既可以利用多核也可以介绍资源消耗
"""

作业

  • 整理并发编程三天内容理论,用自己的概述

  • 需要掌握如何开设进程和如何开设线程的代码

  • 利用多进程或多线程自己实现TCP服务端的并发

  • 整理python基础阶段知识点及项目代码,ATM购物车,选课系统一定要自己脱稿从头到位敲出来

  • 预习并发编程剩余知识点,参考博客小猿取经

  •