zoukankan      html  css  js  c++  java
  • UE4多线程概述

    为了提升游戏的运行帧率,减少卡顿,UE4中使用了大量的线程来提升游戏的并发程度,来释放GamePlay游戏线程的压力。

    具体包括:

    ① 将渲染的应用程序阶段的工作放在RenderThread中

    ② 将渲染命令提交放在RHIThread中

    ③ 将Actor及ActorComponent的Tick、GC Mark等放到TaskGraph中并行化

    ④ GC Sweep的内存释放逻辑放在FAsyncPurge线程中

    ⑤ 资源放到AsyncLoading线程中异步加载

    ⑥ 物理模拟会在PhysX内部的线程中计算

    ⑦ 声音放在AudioThread线程中

    ⑧ Stat统计数据的收集放到StatThread线程中

    ⑨ 在FAsyncWriter线程中写log到文件

    #include "CoreMinimal.h"
    
    DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);
    
    IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMultiThreadTest, "MyTest1.PublicTest.MultiThreadTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)  // 可被Automation System识别
    
    
    class FTest1Runable : public FRunnable
    {
    public:
        FTest1Runable(int32 index)
        {
            ThreadIndex = index;
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Contruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
        virtual ~FTest1Runable() override
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Destruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
        virtual bool Init() override   // 线程创建后,执行初始化工作   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Init: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            return true;
        }
        virtual uint32 Run() override   // 放置线程运行的代码   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 111 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            
            // FPlatformProcess::Sleep(xx)可让当前线程Suspend xx秒   注:其他线程不受影响
            switch (ThreadIndex)
            {
            case 0:
                FPlatformProcess::Sleep(20.0f);
            default:
                FPlatformProcess::Sleep(1.0f);
                break;
            }
            
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 222 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            FPlatformProcess::Sleep(2.0f);
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 333 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
            return 0;
        }
        virtual void Stop() override
        {
        }
    
        virtual void Exit() override   // 线程退出之前,进行清理工作   【在线程上执行】
        {
            UE_LOG(TestLog, Log, TEXT("FTest1Runable Exit: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        }
    
    private:
        int32 ThreadIndex;
    };
    
    
    bool FMultiThreadTest::RunTest(const FString& Parameters)
    {
        UE_LOG(TestLog, Log, TEXT("RunTest Begin tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
    
        // 调用FRunnableThread::Create来创建一个线程
        FRunnableThread* Test1Thread0 = FRunnableThread::Create(new FTest1Runable(0), TEXT("Test1Thread0"));  // Test1Thread0为线程名
        FRunnableThread* Test1Thread1 = FRunnableThread::Create(new FTest1Runable(1), TEXT("Test1Thread1"));
        FRunnableThread* Test1Thread2 = FRunnableThread::Create(new FTest1Runable(2), TEXT("Test1Thread2"));
    
        UE_LOG(TestLog, Log, TEXT("RunTest End tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter);
        return true;
    }

    Automation System(自动化测试系统)  菜单:Window -- Developer Tools -- Session Frontend

     

    执行流程解释如下:

    // 游戏线程 ID:0xe08     Test1Thread0 ID: 0xa67c     Test1Thread1 ID: 0x7104     Test1Thread2 ID: 0xb294
    
    // 在第6390帧  游戏线程调用FRunnableThread::Create创造出Test1Thread0、Test1Thread1、Test1Thread2
    //            各个线程被创建出来后,在自己的线程中立即调用了Init和Run方法
    [2020.09.28-07.11.59:411][390]TestLog: RunTest Begin tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:411][390]TestLog: FTest1Runable Contruct: ThreadIndex:0 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Init: ThreadIndex:0 tid:0xa67c FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Run: 111 ThreadIndex:0 tid:0xa67c FrameIndex:6390
    [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Contruct: ThreadIndex:1 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:1 tid:0x7104 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:1 tid:0x7104 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Contruct: ThreadIndex:2 tid:0xe08 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:2 tid:0xb294 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:2 tid:0xb294 FrameIndex:6390
    [2020.09.28-07.11.59:413][390]TestLog: RunTest End tid:0xe08 FrameIndex:6390
    
    // 在第6474帧
    // 过了1s,线程Test1Thread1、Test1Thread2执行到Run 222处
    [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:1 tid:0x7104 FrameIndex:6474
    [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:2 tid:0xb294 FrameIndex:6474
    
    
    // 在第6690帧
    // 再过了2s,线程Test1Thread1、Test1Thread2退出Run函数,并调用Exit函数,线程的生命周期结束
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:2 tid:0xb294 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:2 tid:0xb294 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:1 tid:0x7104 FrameIndex:6690
    [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:1 tid:0x7104 FrameIndex:6690
    
    // 在第8832帧
    // 过了10s,线程Test1Thread0执行到Run 222处
    [2020.09.28-07.12.20:419][833]TestLog: FTest1Runable Run: 222 ThreadIndex:0 tid:0xa67c FrameIndex:8832
    
    // 在第9072帧
    // 再过了2s,线程Test1Thread0退出Run函数,并调用Exit函数,线程的生命周期结束
    [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Run: 333 ThreadIndex:0 tid:0xa67c FrameIndex:9072
    [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Exit: ThreadIndex:0 tid:0xa67c FrameIndex:9072

    如果想在单线程运行模式下(带上-nothreading参数),以假线程FFakeThread的方式在游戏线程上来模拟执行的话,需要从FSingleThreadRunnable派生,并实现其GetSingleThreadInterface接口中返回当前this指针

    class FStatsThread
        : public FRunnable
        , private FSingleThreadRunnable
    {
    // ... ...
    public:
        virtual FSingleThreadRunnable* GetSingleThreadInterface() override
        {
            return this;
        }
    };

    1. FRunnable是线程可执行实体,其Run()函数为线程函数。Run()函数执行完,线程的生命周期也将结束

    2. FRunnableThread是所有线程的基类,FRunnable为其成员变量

    3. FRunnableThread相当于一个“外壳”,根据平台会创建出属于那个平台的线程。而FRunnable是“核”,定义了这个线程具体要做什么

    4. FThreadManager是全局的线程管理单例(通过静态函数FThreadManager::Get()得到单例),可获取到当前运行的所有线程

    5. FRunnableThreadWin用于windows平台,FRunnableThreadPThread(对pthread的封装)用于Android、iOS、Mac、Linux等平台

    6. FFakeThread用于单线程运行模式下(带上-onethread参数),以假线程的方式在游戏线程上来模拟执行

    7. FRunnableThread相关函数

       const uint32 GetThreadID() const; // 线程ID,唯一

       const FString & GetThreadName() const; // 线程名称 可重复

       EThreadPriority GetThreadPriority();  // 获取线程的优先级

       void SetThreadPriority( EThreadPriority NewPriority );  // 设置线程的优先级

       void Suspend( bool bShouldPause = true );   // bShouldPause为true时,pause线程;bShouldPause为false时,resume线程

       void WaitForCompletion();  // 阻塞并等待当前线程执行完毕

       bool Kill( bool bShouldWait = true );   // 会先执行 runnable 对象的Stop函数,然后根据 bShouldWait 参数决定是否等待线程执行完毕。如果不等待,则强制杀死线程,可能会造成内存泄漏

    线程 平台 解释

    主线程

     All

    windows:为游戏线程

    线程函数为:LaunchWindows.cpp下的WinMain函数

    Android:线程名为MainThread-UE4

    使用java代码来处理主消息循环

    Splash Activity:EngineBuildAndroidJavasrccomepicgamesue4SplashActivity.java

    游戏Activity:EngineBuildAndroidJavasrccomepicgamesue4GameActivity.java.template

    iOS:线程名为Thread <n>  如:Thead 1

    线程函数为LaunchIOS.cpp下的main函数

    游戏线程(消耗高)

     All

    Windows:线程名为Main Thread

    线程函数为:LaunchWindows.cpp下的WinMain函数

    Android:线程名为Thread-<n>  如:Thread-3

    线程函数为LaunchAndroid.cpp下的android_main函数

    iOS:线程名为Thread <n>  如:Thread 5

    线程函数为IOSAppDelegate.cpp下的MainAppThread函数

    线程id:uint32 GGameThreadId

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread)

    bool IsInGameThread();

    渲染线程(消耗高)

    class FRenderingThread : public FRunnable

    线程函数:RenderingThreadMain

     All

    线程名:RenderThread 1

    -norenderthread参数可在启动时强制不开启RenderThread

    注:开启-norenderthread参数时,最好是带上-NoLoadingScreen来禁掉loadingscreen,要不然会出现崩溃

    可用ToggleRenderingThread控制台命令来创建和销毁RenderThread

    销毁RenderThread时,渲染相关的逻辑会放回到游戏线程中

    渲染线程的创建逻辑在void StartRenderingThread()函数中

    销毁逻辑在void StopRenderingThread()函数中

    线程id:uint32 GRenderThreadId

    FRunnableThread* GRenderingThread;

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(RenderThread)

    bool GUseThreadedRendering; // 是否使用独立线程来渲染

    bool GIsThreadedRendering;  // 渲染是否在独立的线程中运行

    TAtomic<int32> GIsRenderingThreadSuspended;  // 渲染线程是否暂停

    TAtomic<int32> GSuspendRenderingTickables;  // rendering tickables是否应该更新   flush时应暂停更新

    bool IsInActualRenderingThread();  // 渲染线程存在,且当前线程为渲染线程

    bool IsInRenderingThread()

    bool IsInParallelRenderingThread();

    RHI线程

    class FRHIThread : public FRunnable

     All

    使用独立的渲染线程时,会根据情况来决定是否创建该线程

    当渲染线程销毁时,该线程也会销毁,RHI执行的逻辑会放回游戏线程中

    -rhithread参数时(缺省),由各个平台来决定启动时是否开启RHI线程

    对应bool GRHISupportsRHIThread变量,具体情况如下:

    DX11缺省不开启,可以通过#define EXPERIMENTAL_D3D11_RHITHREAD 1来缺省开启

    DX12在非Editor模式下默认开启

    OpenGL根据FeatureLevel和r.OpenGL.AllowRHIThread的值来决定是否缺省开启
    r.OpenGL.AllowRHIThread 0 // 不使用RHI线程
    r.OpenGL.AllowRHIThread 1 // 使用RHI线程(缺省)

    Vulklan缺省开启1个RHI线程
    r.Vulkan.RHIThread 0 // 不使用RHI线程
    r.Vulkan.RHIThread 1 // 使用1个RHI线程(缺省)
    r.Vulkan.RHIThread 2 // 使用多个RHI线程

    Metal会根据显卡芯片版本和r.Metal.IOSRHIThread的值来决定是否缺省开启
    r.Metal.IOSRHIThread 0 // 不使用RHI线程(缺省)
    r.Metal.IOSRHIThread 1 // 使用1个RHI线程

    -norhithread参数会在启动时强制不开启rhi线程

    如果不开启rhi线程,rhi的逻辑会跑在RenderingThread线程中

    在pc环境的运行时,可用r.RHIThread.Enable 0控制台命令切到这种方式来跑

    线程id:uint32 GRHIThreadId

    FRunnableThread* GRHIThread_InternalUseOnly

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RHIThread)

    // 创建独立的RHIThread,放加入到TaskGraph中,RHI会跑在TaskGraph的RHIThread上

    // 在pc环境的运行时,可用r.RHIThread.Enable 1控制台命令切到这种方式来跑

    bool GUseRHIThread_InternalUseOnly;  

    // 在pc环境的运行时,可用r.RHIThread.Enable 2控制台命令切到这种方式来跑

    bool GUseRHITaskThreads_InternalUseOnly;  // TaskGraph中使用Any Thread来跑

    // GUseRHIThread_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInDedicatedThread_InternalUseOnly;  --》bool IsRunningRHIInDedicatedThread()

    // GUseRHITaskThreads_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInTaskThread_InternalUseOnly; --》bool IsRunningRHIInTaskThread()

    // GUseRHIThread_InternalUseOnly或GUseRHITaskThreads_InternalUseOnly为true,且正在运行时,该值为true

    bool GIsRunningRHIInSeparateThread_InternalUseOnly; --》bool IsRunningRHIInSeparateThread()

    bool IsInRHIThread();

    RenderingThread心跳监视线程

    class FRenderingThreadTickHeartbeat : public FRunnable

    All

    线程名:RTHeartBeat 1

    负责执行rendering thread tickables

    使用独立的渲染线程时,才会创建该线程,来执行void TickRenderingTickables()

    当渲染线程销毁时,该线程也会销毁,TickRenderingTickables会放回游戏线程中来执行

    TAtomic<bool> GRunRenderingThreadHeartbeat  // 是否使用RenderingThread心跳监视线程

                                                                           // 当使用独立线程来渲染时,会被设置成true;独立渲染线程停止时,会被设置成false

    float GRenderingThreadMaxIdleTickFrequency = 40.f;  // tick频率  值越大tick次数越多,性能越差

    线程池线程

    class FQueuedThread : public FRunnable

    All

    线程名:PoolThread 0  ...  PoolThread <n>

    // Global thread pool for shared async operations
    extern CORE_API FQueuedThreadPool* GThreadPool;
    extern CORE_API FQueuedThreadPool* GIOThreadPool;
    extern CORE_API FQueuedThreadPool* GBackgroundPriorityThreadPool;

    #if WITH_EDITOR
    extern CORE_API FQueuedThreadPool* GLargeThreadPool;
    #endif

    TaskGraph线程(消耗高)

    class FTaskThreadAnyThread : public FTaskThreadBase

    注:class FTaskThreadBase : public FRunnable, FSingleThreadRunnable

     All

    线程名:TaskGraphThreadHP 0  ...  TaskGraphThreadHP <n>

               TaskGraphThreadNP 0  ...  TaskGraphThreadNP <n>

               TaskGraphThreadBP 0  ...  TaskGraphThreadBP <n>

    进程优先级:HP > NP > BP

    TaskGraph只在最开始构造函数中创建所有的线程,调用栈如下:

    UE4Editor-Core-Win64-Debug.dll!FTaskGraphImplementation::FTaskGraphImplementation(int __formal) Line 1158
    UE4Editor-Core-Win64-Debug.dll!FTaskGraphInterface::Startup(int NumThreads) Line 1692
    UE4Editor-Win64-Debug.exe!FEngineLoop::PreInitPreStartupScreen(const wchar_t * CmdLine) Line 2052
    UE4Editor-Win64-Debug.exe!FEngineLoop::PreInit(const wchar_t * CmdLine) Line 3606
    UE4Editor-Win64-Debug.exe!EnginePreInit(const wchar_t * CmdLine)
    UE4Editor-Win64-Debug.exe!GuardedMain(const wchar_t * CmdLine)
    UE4Editor-Win64-Debug.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow) Line 257

    AudioThread线程(消耗较高)

    class FAudioThread : public FRunnable

    线程函数:AudioThreadMain

     All

    线程名:AudioThread

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::AudioThread)

    bool IsInAudioThread();

    AudioMixerRenderThread线程(消耗高)

    class IAudioMixerPlatformInterface : public FRunnable, public FSingleThreadRunnable, public IAudioMixerDeviceChangedLister

     All 线程名:AudioMixerRenderThread

    class FAsyncLoadingThread final : public FRunnable, public IAsyncPackageLoader

     All

    在EDL(EventDrivenLoader)模式下,是不开启该独立线程的,在主线程中Loading

    bool GEventDrivenLoaderEnabled为ture时为EDL模式

    编辑器、standalone非cook版本为EDL模式,没有该线程

    Android、IOS等cook版本为Async模式,会开启该线程

    bool IsInAsyncLoadingThread(); --》bool IsInAsyncLoadingThreadCoreUObjectInternal()

    bool IsAsyncLoading(); --》bool IsAsyncLoadingCoreUObjectInternal()

    void SuspendAsyncLoading(); --》void SuspendAsyncLoadingInternal()

    void ResumeAsyncLoading(); --》void ResumeAsyncLoadingInternal()

    bool IsAsyncLoadingSuspended(); --》bool IsAsyncLoadingSuspendedInternal()

    bool IsAsyncLoadingMultithreaded(); --》bool IsAsyncLoadingMultithreadedCoreUObjectInternal()

    class FAsyncLoadingThread2 final : public FRunnable, public IAsyncPackageLoader   暂时没用
    class FAsyncLoadingThreadWorker : private FRunnable   暂时没用

    防屏保线程

    class FScreenSaverInhibitor : public FRunnable

    桌面平台

    宏PLATFORM_DESKTOP为1

    Windows、Linux、Mac

    线程名:ScreenSaverInhibitor

    StatsThread线程(消耗高)

    class FStatsThread : public FRunnable, FSingleThreadRunnable

     All

    线程名:StatsThread

    会被加入到TaskGraph系统中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::StatsThread)

    FMediaTicker线程

    class FMediaTicker : public FRunnable , public IMediaTicker

     All 线程名:FMediaTicker

    class FAsyncPurge : public FRunnable

     All

    extern int32 GMultithreadedDestructionEnabled;

    [/Script/Engine.GarbageCollectionSettings]

    gc.MultithreadedDestructionEnabled=True // 通过这个控制台命令来开启

    多线程GC清理


    class FSlateLoadingThreadTask : public FRunnable

     All

    线程名:SlateLoadingThread1

    LoadingScreen播放视频或Slate UI

    bool IsInSlateThread();

    class  FPreLoadScreenSlateThreadTask : public FRunnable  All

    线程名:SlateLoadingThread1

    引擎初始化播放视频

    bool IsInSlateThread();

    template<typename ResultType>
    class TAsyncRunnable : public FRunnable

     All TAsync 0
    class FAsyncWriter : public FRunnable, public FArchive  All

    FAsyncWriter_UAGame

    见:OutputDeviceFile.cpp的FAsyncWriter::Run()

    异步写log到文件

    class FOnlineAsyncTaskManager : public FRunnable, FSingleThreadRunnable  All OnlineAsyncTaskThreadNull DefaultInstance(1)
    class FLwsWebSocketsManager: public IWebSocketsManager, public FRunnable, public FSingleThreadRunnable  All LibwebsocketsThread
    class FHttpThread : FRunnable, FSingleThreadRunnable  All HttpManagerThread
    class FMessageRouter : public FRunnable, private FSingleThreadRunnable  All FMessageBus.DefaultBus.Router
    class FLiveLinkMessageBusDiscoveryManager : FRunnable  All LiveLinkMessageBusDiscoveryManager
    class FFileTransferRunnable : public FRunnable  All FFileTransferRunnable
    class TcpConsoleListener : FRunnable iOS  
    class FTcpListener : public FRunnable iOS  

    @implementation FIOSFramePacer

    -(void)run:(id)param

    iOS  
    windows平台Splash线程 windows 线程函数:StartSplashScreenThread
    TraceLog windows

    windows:线程名为TraceLog;WindowsTrace.cpp下ThreadCreate函数

    Android:线程名为bundle id;AndroidTrace.cpp下ThreadCreate函数

    IOS:线程名为Thread <n>;AppleTrace.cpp下ThreadCreate函数

    windows平台崩溃监视线程 windows 线程函数:CrashReportingThreadProc

    Shader编译线程

    class FShaderCompileThreadRunnableBase : public FRunnable

    编辑器

    线程名:ShaderCompilingThread

    拉起ShaderCompileWorker.exe进程进行shader编译

    DistanceField构建线程

    class FBuildDistanceFieldThreadRunnable : public FRunnable

    编辑器  

    class FAssetDataDiscovery : public FRunnable

    编辑器

    用于发现文件

    线程名:FAssetDataDiscovery

    class FAssetDataGatherer : public FRunnable

    编辑器

    从FAssetRegistry文件列表中搜集Asset数据

    线程名:FAssetDataGatherer

    class FVirtualTextureDCCCacheCleanup final : public FRunnable

    编辑器 线程名:FVirtualTextureDCCCacheCleanup
    class FDDCCleanup : public FRunnable 编辑器 线程名:FDDCCleanup

    Wwise编辑器连接线程

    class FAkWaapiClientConnectionHandler : public FRunnable

    编辑器或windows、mac下的非shipping版本

    线程名:WAAPIClientConnectionThread1

    在Android局内时抓的一个ue4stats文件中,里面统计的线程如下:

    参考

    【UE4源代码观察】观察UE4的线程使用

  • 相关阅读:
    flask基础之jijia2模板使用基础(二)
    python之微信公众号开发(基本配置和校验)
    flask插件系列之SQLAlchemy基础使用
    python基础之常用的高阶函数
    服务器部署之nginx的配置
    python之gunicorn的配置
    python内置模块之unittest测试(五)
    python之celery使用详解一
    git服务器的简单搭建
    python模块分析之logging日志(四)
  • 原文地址:https://www.cnblogs.com/kekec/p/13732486.html
Copyright © 2011-2022 走看看