Python 网络与并发编程(二)

文章目录

  • 线程Thread
    • 创建方式
    • join()
    • 守护线程
    • 全局锁GIL问题
    • 线程同步和互斥锁
    • 死锁
    • 信号量
    • 事件(Event)
    • 生产者和消费者模式

线程Thread

创建方式

Python的标准库提供了两个模块: _thread 和threading , _thread 是低级模块, threading 是高级模块,对_thread 进行了封装。绝大多数情况下,我们只需要使用threading 这个高级模块。线程的创建可以通过分为两种方式:
1. 方法包装
2. 类包装

线程的执行统一通过start() 方法

线程的创建方式(方法包装)

#encoding=utf-8
#方法方式创建线程
from threading import Thread
from time import sleep
def func1(name):
    for i in range(3):
        print(f"thread:{name} :{i}")
        sleep(1)
if __name__ == '__main__':
    print("主线程,start")
    #创建线程
    t1 = Thread(target=func1,args=("t1",))
    t2 = Thread(target=func1,args=("t2",))
    
#启动线程
t1.start()
t2.start()

主线程,start
thread:t1 :0
thread:t2 :0
thread:t1 :1
thread:t2 :1
thread:t1 :2thread:t2 :2

线程的创建方式(类包装)

#encoding=utf-8
#类的方式创建线程
from threading import Thread
from time import sleep
class MyThread(Thread):
    def __init__(self,name):
        Thread.__init__(self)
        self.name =name
    def run(self):
        for i in range(3):
            print(f"thread:{self.name} :{i}")
            sleep(1)
if __name__ == '__main__':
    print("主线程,start")
    #创建线程(类的方式)
    t1 = MyThread('t1')
    t2 = MyThread('t2')
    #启动线程
    t1.start()
    t2.start()
    print("主线程,end")

主线程,start
thread:t1 :0
thread:t2 :0
主线程,end
thread:t2 :1thread:t1 :1
thread:t2 :2thread:t1 :2

join()

之前的代码,主线程不会等待子线程结束。如果需要等待子线程结束后,再结束主线程,可使用join()方法。

#encoding=utf-8
from threading import Thread
from time import sleep
def func1(name):
    for i in range(3):
        print(f"thread:{name} :{i}")
        sleep(1)
if __name__ == '__main__':
    print("主线程,start")
    #创建线程
    t1 = Thread(target=func1,args=("t1",))
    t2 = Thread(target=func1,args=("t2",))
    #启动线程
    t1.start()
    t2.start()
    #主线程会等待t1,t2结束后,再往下执行
    t1.join()
    t2.join()
    print("主线程,end")

主线程,start
thread:t1 :0
thread:t2 :0
thread:t2 :1thread:t1 :1
thread:t1 :2thread:t2 :2
主线程,end

守护线程

在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过setDaemon(True|False)来设置是否为守护线程。守护线程的作用:为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。

#encoding=utf-8
from threading import Thread
from time import sleep
class MyThread(Thread):
    def __init__(self,name):
        Thread.__init__(self)
        self.name =name
    def run(self):
        for i in range(3):
            print(f"thread:{self.name}:{i}")
            sleep(1)
if __name__ == '__main__':
    print("主线程,start")
    #创建线程(类的方式)
    t1 = MyThread('t1')
    #t1设置为守护线程
    t1.daemon=True #t1.setDaemon(True)#3.10后被废弃,可以直接 t1.daemon=True
    #启动线程
    t1.start()
    print("主线程,end")

主线程,start
thread:t1:0
主线程,end
thread:t1:1
thread:t1:2

全局锁GIL问题

在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核…不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是“含有水分的线程”。
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷

线程同步和互斥锁

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

【示例】多线程操作同一个对象(未使用线程同步) 模拟取钱

# -*- coding: utf-8 -*-

from threading import Thread
from time import sleep
class Account:
    def __init__(self,money,name):
        self.money = money
        self.name = name
#模拟提款操作
class Drawing(Thread):
    def __init__(self,drawingNum,account):
        Thread.__init__(self)
        self.drawingNum = drawingNum
        self.account = account
        self.expenseTotal = 0
    def run(self):
        if self.account.money-self.drawingNum < 0:
            return
        sleep(1) #判断完后阻塞。其他线程开始运行。
        self.account.money -= self.drawingNum;
        self.expenseTotal += self.drawingNum;
        print(f"账户:{self.account.name},余额是:{self.account.money}")
        print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':
    a1 = Account(100,"zhangsan")
    draw1 = Drawing(80,a1) #定义取钱线程对象;
    draw2 = Drawing(80,a1) #定义取钱线程对象;
    draw1.start() #你取钱
    draw2.start() #你老婆取钱

账户:zhangsan,余额是:20账户:zhangsan,余额是:-60
账户:zhangsan,总共取了:80
账户:zhangsan,总共取了:80

没有线程同步机制,两个线程同时操作同一个账户对象,竟然从只有100元的账户,轻松取出80*2=160元,账户余额竟然成为了-60。这么大的问题,显然银行不会答应的。

我们可以通过“锁机制”来实现线程同步问题,锁机制有如下几个要点:
1 必须使用同一个锁对象
2 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
3 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
4 使用互斥锁会影响代码的执行效率
5 同时持有多把锁,容易出现死锁的情况

互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意: 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。

threading 模块中定义了Lock 变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
【示例】多线程操作同一个对象(增加互斥锁,使用线程同步)

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 20 09:20:33 2024

@author: Administrator
"""

#encoding=utf-8
from threading import Thread,Lock
from time import sleep
class Account:
    def __init__(self,money,name):
        self.money = money
        self.name = name
#模拟提款操作
class Drawing(Thread):
    def __init__(self,drawingNum,account):
        Thread.__init__(self)
        self.drawingNum = drawingNum
        self.account = account
        self.expenseTotal = 0
    def run(self):
        lock1.acquire()
        if self.account.money-self.drawingNum < 0:
            return
        sleep(1) #判断完后阻塞。其他线程开始运行。
        self.account.money -= self.drawingNum;
        self.expenseTotal += self.drawingNum;
        lock1.release()
        print(f"账户:{self.account.name},余额是:{self.account.money}")
        print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':
    a1 = Account(100,"zhangsan")
    lock1=Lock()
    draw1 = Drawing(80,a1) #定义取钱线程对象;
    draw2 = Drawing(80,a1) #定义取钱线程对象;
    draw1.start() #你取钱
    draw2.start() #你老婆取钱

账户:zhangsan,余额是:20
账户:zhangsan,总共取了:80

acquirerelease 方法之间的代码同一时刻只能有一个线程去操作
如果在调用acquire 方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire 方法会堵塞,直到这个互斥锁释放后才能再次上锁。

死锁

在多线程程序中,死锁问题很大一部分是由于一个线程同时获取多个锁造成的。举例:有两个人都要做饭,都需要“锅”和“菜刀”才能炒菜。

from threading import Thread, Lock
from time import sleep
def fun1():
    lock1.acquire()
    print('fun1拿到菜刀')
    sleep(2)
    lock2.acquire()
    print('fun1拿到锅')
    lock2.release()
    print('fun1释放锅')
    lock1.release()
    print('fun1释放菜刀')
def fun2():
    lock2.acquire()
    print('fun2拿到锅')
    lock1.acquire()
    print('fun2拿到菜刀')
    lock1.release()
    print('fun2释放菜刀')
    lock2.release()
    print('fun2释放锅')
if __name__ == '__main__':
    lock1 = Lock()
    lock2 = Lock()
    t1 = Thread(target=fun1)
    t2 = Thread(target=fun2)
    t1.start()
    t2.start()

fun1拿到菜刀
fun2拿到锅

都未同时拿到菜单和锅,这就没有办法做饭了

解决办法:死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

信号量

互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,我们同时想让N个(指定数值)线程访问?这时候,可以使用信号量。信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。

应用场景
1在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。
2 在做爬虫抓取数据时。

信号量底层就是一个内置的计数器。每当资源获取时(调用acquire)计数器-1,资源释放时(调用release)计数器+1。

#coding=utf-8
from threading import Thread, Lock
from time import sleep
from multiprocessing import Semaphore
"""
一个房间一次只允许两个人通过
若不使用信号量,会造成所有人都进入这个房子
若只允许一人通过可以用锁-Lock()
"""
def home(name, se):
    se.acquire() # 拿到一把钥匙
    print(f'{name}进入了房间\n')
    sleep(3)
    print(f'{name}走出来房间\n')
    se.release() # 还回一把钥匙
if __name__ == '__main__':
    se = Semaphore(2) # 创建信号量的对象,有两把钥匙
    for i in range(7):
        p = Thread(target=home, args=
        (f'tom{i}', se))
        p.start()

tom0进入了房间
tom1进入了房间

tom0走出来房间
tom1走出来房间

tom2进入了房间
tom3进入了房间

tom3走出来房间
tom2走出来房间

tom4进入了房间
tom5进入了房间

tom5走出来房间
tom4走出来房间

tom6进入了房间

tom6走出来房间

事件(Event)

事件Event主要用于唤醒正在阻塞等待状态的线程;
Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行

Event() 可以创建一个事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:

方法名说明
event.wait(timeout=None)调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行;
event.set()将event的标志设置为True,调用wait方法的所有线程将被唤醒
event.clear()将event的标志设置为False,调用wait方法的所有线程将被阻塞
event.is_set()判断event的标志是否为True
        
#coding:utf-8
#小伙伴们,围着吃火锅,当菜上齐了,请客的主人说:开吃!
#于是小伙伴一起动筷子,这种场景如何实现
import threading
import time
def chihuoguo(name):
    #等待事件,进入等待阻塞状态
    print(f'{name}已经启动')
    print(f'小伙伴{name}已经进入就餐状态!')
    time.sleep(1)
    event.wait()
    # 收到事件后进入运行状态
    print(f'{name}收到通知了.' )
    print(f'小伙伴{name}开始吃咯!')
if __name__ == '__main__':
    event = threading.Event()
    # 创建新线程
    thread1 = threading.Thread(target=chihuoguo, args=("tom", ))
    thread2 = threading.Thread(target=chihuoguo, args=("cherry", ))
    # 开启线程
    thread1.start()
    thread2.start()
    time.sleep(10)
    # 发送事件通知
    print('---->>>主线程通知小伙伴开吃咯!')
    event.set()

tom已经启动
小伙伴tom已经进入就餐状态!
cherry已经启动
小伙伴cherry已经进入就餐状态!
---->>>主线程通知小伙伴开吃咯!
tom收到通知了.
小伙伴tom开始吃咯!
cherry收到通知了.
小伙伴cherry开始吃咯!

生产者和消费者模式

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区是实现并发的核心,缓冲区的设置有3个好处:
1 实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
2 解耦了生产者和消费者
生产者不需要和消费者直接打交道
3 解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据

缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。

【示例】生产者消费者模式典型代码

from queue import Queue
from threading import Thread
from time import sleep
def producer():
    num = 1
    while True:
        if queue.qsize() < 5:
            print(f'生产:{num}号,大馒头')
            queue.put(f'大馒头:{num}号')
            num += 1
        else:
            print('馒头框满了,等待来人消费啊!')
            sleep(1)
def consumer():
    while True:
        print(f'获取馒头:{queue.get()}')
        sleep(1)
if __name__ == '__main__':
    queue = Queue()
    t = Thread(target=producer)
    t.start()
    c = Thread(target=consumer)
    c.start()
    c2 = Thread(target=consumer)
    c2.start()

生产:1号,大馒头
生产:2号,大馒头
生产:3号,大馒头
生产:4号,大馒头
生产:5号,大馒头
馒头框满了,等待来人消费啊!
获取馒头:大馒头:1号
获取馒头:大馒头:2号
获取馒头:大馒头:3号获取馒头:大馒头:4号
生产:6号,大馒头
生产:7号,大馒头
生产:8号,大馒头
生产:9号,大馒头
馒头框满了,等待来人消费啊!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/560125.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《王者荣耀》Hello Kitty 小兵皮肤完整设置指南

王者荣耀与三丽鸥的联动活动上线了 Hello Kitty 小兵皮肤&#xff0c;让我们的峡谷小兵们也能穿上漂亮的衣服啦&#xff01;这款皮肤极具卡哇伊风格&#xff0c;引起了许多玩家的关注。许多小伙伴都想知道如何使用这款 Hello Kitty 小兵皮肤&#xff0c;今天小编将为大家整理出…

STC单片机与串口触摸屏通讯程序

/***串口1切换通讯测试,单片机发送数据给触摸屏***/ /***切换到3.0 3.1发送数据到串口通信软件 ***/ /***设置温度 加热时间读写EEPROM正确 ***/ #include <REG52.H> //2023 3 5 L330 CODE2667 #include <intrin…

使用JDK自带工具进行JVM内存分析之旅

进行jvm内存分析可以排查存在和潜在的问题。 通过借助jdk自带的常用工具&#xff0c;可以分析大概可能的问题定位以及确定优化方向。 JVM内存分析有很多好处。 内存泄漏排查&#xff1a;JVM 内存泄漏是指应用程序中的对象占用的内存无法被垃圾回收器释放&#xff0c;导致内存…

遥瞻智慧:排水系统远程监控的卓越解决方案

遥瞻智慧&#xff1a;排水系统远程监控的卓越解决方案 在城市脉络的深层肌理中&#xff0c;排水系统犹如一条条隐秘的生命线&#xff0c;默默承载着城市的呼吸与律动。然而&#xff0c;如何以科技之眼&#xff0c;赋予这些无形网络以实时感知、精准调控的能力&#xff0c;使之…

基于机器学习的车辆状态异常检测

基于马氏距离的车辆状态异常检测&#xff08;单一传感器&#xff09; 基于多元自动编码器的车辆状态异常检测 基于单传感器平滑马氏距离的车辆状态异常检测 工学博士&#xff0c;担任《Mechanical System and Signal Processing》等期刊审稿专家&#xff0c;擅长领域&#xff1…

数据分析场景,连号相关业务

连号相关业务 业务场景&#xff1a;现在需要从a列一堆编号中&#xff0c;将连号范围在10以内的数据分别分成一组。 先看实先效果 演示的为db2数据库&#xff0c;需要含有窗口函数&#xff0c;或者可以获取到当前数据偏移的上一位数据 第一步&#xff1a;将A列数据正序第二步…

量子密钥分发系统的设计与实现(三):量子信号的产生、调制及探测技术讨论

之前的文章我们对量子密钥分发系统功能的光路子系统进行了较为全面的分析&#xff0c;我们理解了光路子系统是量子密钥分发系统的基础。本文我们主要探讨下量子信号产生、调制及探测的基础技术&#xff0c;算是一篇承上启下的文章吧&#xff0c;对相关的原理进行探讨&#xff0…

如何使用 ArcGIS Pro 制作边界晕渲效果

在某些出版的地图中&#xff0c;边界有类似于“发光”的晕渲效果&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro 制作这种晕渲效果&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的行政区划数据&#xff0c;除了行政区划数据&#xff0c…

wsl2 Ubuntu子系统内存只有一半的解决办法

物理机的内存是64G&#xff0c;在wsl2安装完Ubuntu20.04后&#xff0c;输入命令&#xff1a; free -g 发现只有32G&#xff0c;原因是默认只能获得物理机一半的内存&#xff1a; WSL 中的高级设置配置 | Microsoft Learn 因此可手动修改为与物理机同等大小&#xff1a; 1&a…

如何解决DDoS攻击?群联科技做出回答。

DDoS攻击&#xff08;分布式拒绝服务攻击&#xff09;是一种恶意利用多台傀儡机协同发起大规模网络流量&#xff0c;旨在压垮目标系统或网络资源&#xff0c;使其无法正常服务的网络攻击手段。由于现代计算机和网络性能的提升&#xff0c;单点发起的DoS攻击已难以奏效&#xff…

Day01——NestJS学习之了解、安装、运行

什么是 Nest.js&#xff1f; NestJs 官方简介: Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力&#xff0c;使用并完全支持 TypeScript &#xff08;仍然允许开发者使用纯 JavaScript 进行开发&#x…

数据仓库作业五:第8章 关联规则挖掘

目录 第8章 关联规则挖掘作业题 第8章 关联规则挖掘 作业题 1、设4-项集 X { a , b , c , d } X\{a,b,c,d\} X{a,b,c,d}&#xff0c;试求出由 X X X 导出的所有关联规则。 解&#xff1a; 首先生成项集的所有非空真子集。这包括&#xff1a; { a } , { b } , { c } , {…

ansible执行mysql脚本

目录 概述实践环境要求ansible yml脚本命令离线包 概述 ansible执行mysql脚本 实践 官网文档 环境要求 环境需要安装以下内容: 1.mysql客户端(安装了mysql即会有)2.安装MySQL-python (Python 2.X) 详细插件安装链接 ansible yml脚本 关键代码如下&#xff1a; # 剧本…

ROS2学习笔记(一) 基本概念

1. Node 节点 节点: 完成具体功能的模块 相关命令 #运行命令 ros2 run <package_name> <executable_name>#当前节点查询查询 ros2 node list#重映射 Remapping ros2 run <package_name> <executable_name> --ros-args --remap __node:<node_na…

KaiwuDB CTO 魏可伟:AIoT,用行业定义数据库

4月12日&#xff0c;由中国 DBA 联盟&#xff08;ACDU&#xff09;与墨天轮社区联合主办的第十三届数据技术嘉年华&#xff08;DTC 2024&#xff09;于北京盛大召开。KaiwuDB CTO 魏可伟受邀发表《智创当下&#xff0c;KaiwuDB 从多模到 AI 的探索实践》主题演讲&#xff0c;向…

Axure如何实现限制选择项数量的交互

大家经常会看到这样的功能设计&#xff1a;可以多选&#xff0c;但是限制多选。比如某招聘网站城市的选择只能选择5个。再选择第6个的时候会提示最多只能选择5项。 这个效果是我们经常会遇到的&#xff0c;在工作中也经常会遇到需要制作这样的效果。今天我们一起来看看&#xf…

Mac M3 安装Ollama和llama3,本地部署LobeChat和刘皇叔聊三国!

OllamaLobeChat&#xff0c;本地部署聊天助手 Ollama安装下载OllamaOllama常用指令和链接运行OllamaAPI 交互Ollama基于Llama 3角色扮演 LobeChat安装首先安装docker安装LobeChat的docker 镜像和运行 Ollama安装 下载Ollama 网址&#xff1a;https://ollama.com/ 支持macOS、…

产废端实时音视频监控系统在运输车辆驾驶室中的应用

实时音视频监控系统可通过在运输车辆驾驶室安装音视频摄录设备&#xff0c;实现将运输车辆内部及周围环境音视频数据通过移动网络实时回传指挥中心的功能。 前端摄录设备主要负责采集车内外的视音频信息&#xff0c;为了保障车辆及运输人员 的安全&#xff0c;应合理选择摄录设…

LeetCode 349.两个数组的交集(HashSet的使用)

给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2]示例 2&#xff1a; 输入&#xff1a;nums1 …

电商技术揭秘三十:知识产权保护浅析

电商技术揭秘相关系列文章&#xff08;上&#xff09; 相关系列文章&#xff08;中&#xff09; 电商技术揭秘二十&#xff1a;能化供应链管理 电商技术揭秘二十一:智能仓储与物流优化(上) 电商技术揭秘二十二:智能仓储与物流优化(下) 电商技术揭秘二十三&#xff1a;智能…