【Skynet 入門實戰練習】游戲模塊劃分 | 基礎功能模塊 | timer 定時器模塊 | logger 日志服務模塊

文章目錄

    • 游戲模塊
    • 基礎功能模塊
      • 定時器模塊
      • 日志模塊
      • 通用模塊

游戲模塊

游戲從邏輯方面可以分為下面幾個模塊:

  • 注冊和登錄
  • 網絡協議
  • 數據庫
  • 玩法邏輯
  • 其他通用模塊

除了邏輯劃分,還有幾個重要的工具類模塊:

  • Excel 配置導表工具
  • GM 指令
  • 測試機器人
  • 服務器打包部署工具

本節先來實現幾個通用的基礎功能模塊。

基礎功能模塊

定時器模塊

在什么場景下,我們會要使用到定時器?

  • 每日任務的重置,比如游戲在每天的 0 點,需要定時進行刷新
  • 登錄流程的超時機制,對于長時間未通過驗證的連接,需要踢客戶端下線,避免占用服務端資源
  • 活動結算,在定期活動結束后,需要給所有用戶發放結算獎勵

服務端和客戶端都可以實現定時器邏輯,一般涉及到全服玩家的定時器,需要服務器來實現,針對個人玩家的定時器可以交給客戶端來實現。


skynet 中,通過 skynet.timeout(time, func) 實現定時任務,skynet 基本的時間單位是 10ms,即會在 0.01s 后執行一次 func 函數。

參考:https://github.com/cloudwu/skynet/wiki/LuaAPI

skynet.timeout(ti, func) 讓框架在 ti 個單位時間后,調用 func 這個函數。這不是一個阻塞 API ,當前 coroutine 會繼續向下運行,而 func 將來會在新的 coroutine 中執行。

skynet 的定時器實現的非常高效,所以一般不用太擔心性能問題。不過,如果你的服務想大量使用定時器的話,可以考慮一個更好的方法:即在一個service里,盡量只使用一個 skynet.timeout ,用它來觸發自己的定時事件模塊。這樣可以減少大量從框架發送到服務的消息數量。畢竟一個服務在同一個單位時間能處理的外部消息數量是有限的。

由此,考慮自己實現一個用于定時觸發事件的定時器模塊,并且不需要這么高的精度,采用以秒為單位實現定時器。

定時器模塊實現架構: skynet.timeout 實現循環定時器,每秒循環一次,查看并執行這一秒對應的回調函數。定時器 id 采用自增唯一映射每個定時回調函數。注冊回調函數時,會計算將要執行的秒數,存入對應的回調函數表。


基礎變量以及模塊初始化

local _M = {} local is_init = false   -- 標記模塊是否初始化
local timer_inc_id = 1  -- 定時器的自增 ID
local cur_frame = 0     -- 當前幀,一幀對應一秒
local cur_timestamp = 0 -- 當前時間戳,運行到的秒數
local timer_size = 0    -- 定時器數量
local frame_size = 0    -- 幀數量local timer2frame = {}  -- 定時器ID 映射 幀
local frame2cbs = {}    -- 幀 映射 多個回調任務
--[[frame: {timers: {timerid: { sec, cb, args, is_repeat },timerid: { sec, cb, args, is_repeat }}, size: 1}
]]if not is_init then is_init = true -- 初始化定時器模塊skynet.timeout(100, main_loop)
end return _M 
  • is_init:用于標記模塊是否初始化,即生成一次循環定時器,定時每秒執行 main_loop 函數
  • timer_inc_id:定時器的唯一標識 ID,每個定時器創建,都會自增
  • cur_frame:記錄當前循環是對應哪一幀,隨著循環自增
  • cur_timestamp:當前循環時間戳
  • timer_sizeframe_size:維護的定時器數量和幀數量
  • timer2frame:定時器 ID 對應的幀
  • frame2cbs:幀對應的回調函數表

回調函數表的結構 frame2cbs

frame: {timers: {timerid: { sec, cb, args, is_repeat },timerid: { sec, cb, args, is_repeat }}, size: 1
}

每幀對應回調函數表,有 timerssize 兩個字段,size 維護當前回調函數個數,timers 則是實際的回調函數表,以定時器 ID 映射對應的回調函數。

每個回調函數都存儲 seccbargsis_repeat 四個字段,表示 sec 秒后執行 cb 函數,攜帶 args 參數, is_repeat 表示是否是一個循環任務。


下面看每幀執行的函數 main_loop

local function now() return skynet.time() // 1 -- 截斷小數:.0
end     
-- 逐幀執行
local function main_loop()skynet.timeout(100, main_loop)cur_timestamp = now()cur_frame = cur_frame + 1-- 當前沒有定時器任務if timer_size <= 0 then return end -- 當前幀對應的回調任務local cbs = frame2cbs[cur_frame]if not cbs then return end -- 當前幀的回調任務數量為0if cbs.size <= 0 then frame2cbs[cur_frame] = nil frame_size = frame_size - 1 -- 該幀執行完畢return end -- task: {sec, cb, args, is_repeat}for timerid, task in pairs(cbs.timers) do local f = task[2] local args = task[3]local ok, err = xpcall(f, traceback, unpack(args, 1, args.n))if not ok then logger.error("timer", "crontab is run in error:", err)end del_timer(timerid) -- 執行成功與否都需要刪掉當前這個定時器local is_repeat = task[4]if is_repeat then local sec = task[1]init_timer(timerid, sec, f, args, is_repeat)end end -- 當前這一幀所有任務執行完,并且這一幀沒有刪(雙重保障(del_timer)),刪掉當前幀if frame2cbs[cur_frame] then frame2cbs[cur_frame] = nil frame_size = frame_size - 1end 
end 

這里在入口處,我們就立即需要執行 skynet.timeout(100, main_loop),實現循環定時,并且沒有多余其他操作,保證一下秒定時的準確。

skynet.time():當前 UTC 時間(單位是秒, 精度是 ms)

主要邏輯:判斷當前幀是否有任務,有則執行 frame2cbs[cur_frame].timers 回調函數表中的回調函數,執行完后進行刪除和判斷該回調是否是循環定時任務,是則重新創建該回調的新定時器。


再來看定時器的創建和刪除邏輯

init_timer

local function init_timer(id, sec, f, args, is_repeat)-- 第一步:定時器 id 映射 幀local offset_frame = sec -- sec 幀后開始當前任務 -- 矯正幀數if now() > cur_timestamp then offset_frame = offset_frame + 1end -- 實際計算執行幀local fix_frame = cur_frame + offset_frame-- 第二步:該幀 映射 定時器任務local cbs = frame2cbs[fix_frame]if not cbs then -- 創新當前幀的任務集cbs = { timers = {}, size = 1 }frame2cbs[fix_frame] = cbs frame_size = frame_size + 1 else cbs.size = cbs.size + 1end cbs.timers[id] = {sec, f, args, is_repeat}timer2frame[id] = fix_frametimer_size = timer_size + 1if timer_size >= 500 then logger.warn("timer", "timer is too many!")end 
end 

創建定時器任務,對應需要修改 frame2cbstimer2frame 表。回調函數加入當前幀的回調表中,回調的定時器ID映射當前幀,一并維護一下定時器和幀的數量統計。

在函數的開始,我們進行了對幀的校正。保證回調任務在未來幀中執行,而不會在當前幀中繼續添加任務。

del_timer

-- 刪除定時器
local function del_timer(id) -- 獲取定時器id 映射 幀local frame = timer2frame[id]if not frame then return end -- 獲取該幀對應的任務local cbs = frame2cbs[frame]if not cbs or not cbs.timers then return end -- 如果這個幀中的定時器任務存在if cbs.timers[id] then cbs.timers[id] = nil -- 刪除該定時器任務cbs.size = cbs.size - 1 -- 當前幀的任務數 -1end -- 當前刪掉了這一幀的最后一個定時器任務if cbs.size == 0 then frame2cbs[frame] = nil -- 置空frame_size = frame_size - 1 -- 幀數 -1 end -- 當前定時器id對應的幀置空,且定時器數量 -1timer2frame[id] = nil timer_size = timer_size - 1
end 

刪除定時器邏輯很好理解,傳入定時器 ID,找到 ID 對應的幀,看該幀中是否存在這個任務,存在就刪除并維護幀數和定時器數量。


接口實現:

-- 新增定時器 timer,sec 秒后執行函數 f
-- 返回定時器 ID
function _M.timeout(sec, f, ...)assert(sec > 0)timer_inc_id = timer_inc_id + 1init_timer(timer_inc_id, sec, f, pack(...), false)return timer_inc_id
end function _M.timeout_repeat(sec, f, ...) assert(sec > 0)timer_inc_id = timer_inc_id + 1init_timer(timer_inc_id, sec, f, pack(...), true)return timer_inc_id
end -- 取消定時器任務
function _M.cancel(id)del_timer(id)
end -- 檢查定時器是否存在
function _M.exist(id)if timer2frame[id] then return true end return false 
end -- 獲取定時器還有多久執行
function _M.get_remain(id)local frame = timer2frame[id] if frame then return frame - cur_frameend return -1
end 

完整代碼:timer.lua


日志模塊

日志系統一般分為 4 個等級:

  • DEBUG:調試用的日志,線上運行時屏蔽不輸出
  • INFO:普通日志,線上運行時輸出,流程的關鍵步驟都需要有 INFO 日志
  • WARN:數據異常,但不影響正常流程的時候輸出
  • ERROR:數據異常,且需要人工處理的時候輸出

日志服務模塊配置如下:

-- log conf
logger = "log"
logservice = "snlua"
logpath = "log"
logtag = "game"
-- debug | info | warn | error 
log_level = "debug"

參考官方 wiki

logger 它決定了 skynet 內建的 skynet_error 這個 C API 將信息輸出到什么文件中。如果 logger 配置為 nil ,將輸出到標準輸出。你可以配置一個文件名來將信息記錄在特定文件中。

logservice 默認為 "logger" ,你可以配置為你定制的 log 服務(比如加上時間戳等更多信息)。可以參考 service_logger.c 來實現它。注:如果你希望用 lua 來編寫這個服務,可以在這里填寫 snlua ,然后在 logger 配置具體的 lua 服務的名字。在 examples 目錄下,有 config.userlog 這個范例可供參考。

配置中,指定 loggerlog.lua 這個日志服務,logservicesnlua 表示這個日志服務是 lua 服務。其余的三個參數作為鍵值對存儲在配置中,用于實現服務模塊時取出使用。logpath 指定為日志存放的目錄路徑,logtag 指定為日志進程標識,log_level 可選四種日志級別。

這里先來看日志模塊:lualib/logger.lua

local skynet = require "skynet"local loglevel = {debug = 0,info = 1,warn = 2,error = 3,
}local logger = {_level = nil,_fmt = "[%s] [%s] %s", -- [info] [label] msg_fmt2 = "[%s] [%s %s] %s", --[info] [label labeldata] msg
}local function init_log_level()if not logger._level thenlocal level = skynet.getenv "log_level"local default_level = loglevel.debuglocal valif not level or not loglevel[level] thenval = default_levelelseval = loglevel[level]endlogger._level = valend
endfunction logger.set_log_level(level)local val = loglevel.debugif level and loglevel[level] thenval = loglevel[level]endlogger._level = val
endlocal function formatmsg(loglevel, label, labeldata, args)local args_len = #argsif args_len > 0 thenfor k, v in pairs(args) dov = tostring(v)args[k] = vendargs = table.concat(args, " ")elseargs = ""endlocal msglocal fmt = logger._fmtif labeldata ~= nil thenfmt = logger._fmt2msg = string.format(fmt, loglevel, label, labeldata, args)elsemsg = string.format(fmt, loglevel, label, args)endreturn msg
end--[[
logger.debug("map", "user", 1024, "entered this map")
logger.debug2("map", 1, "user", 2048, "leaved this map")
]]
function logger.debug(label, ...)if logger._level <= loglevel.debug thenlocal args = {...}local msg = formatmsg("debug", label, nil, args)skynet.error(msg)end
end
function logger.debug2(label, labeldata, ...)if logger._level <= loglevel.debug thenlocal args = {...}local msg = formatmsg("debug", label, labeldata, args)skynet.error(msg)end
endfunction logger.info(label, ...)if logger._level <= loglevel.info thenlocal args = {...}local msg = formatmsg("info", label, nil, args)skynet.error(msg)end
end
function logger.info2(label, labeldata, ...)if logger._level <= loglevel.info thenlocal args = {...}local msg = formatmsg("info", label, labeldata, args)skynet.error(msg)end
endfunction logger.warn(label, ...)if logger._level <= loglevel.warn thenlocal args = {...}local msg = formatmsg("warn", label, nil, args)skynet.error(msg)end
end
function logger.warn2(label, labeldata, ...)if logger._level <= loglevel.warn thenlocal args = {...}local msg = formatmsg("warn", label, labeldata, args)skynet.error(msg)end
endfunction logger.error(label, ...)if logger._level <= loglevel.error thenlocal args = {...}local msg = formatmsg("error", label, nil, args, debug.traceback())skynet.error(msg)end
end
function logger.error2(label, labeldata, ...)if logger._level <= loglevel.error thenlocal args = {...}local msg = formatmsg("error", label, labeldata, args, debug.traceback())skynet.error(msg)end
endskynet.init(init_log_level)return logger

這個日志模塊主要暴露的四個接口分別對應四個日志等級,并且只有當前日志等級 log_level 低于當前 API 對應的等級才可以輸出。如果程序測試階段,那么指定 debug 級,就會獲得所有日志。如果程序上線指定 error 級,那么只會關注到最高級別的錯誤日志。

error 等級日志額外輸出了調用堆棧,方便查看錯誤問題所在的位置。

skynet.init:若服務尚未初始化完成,則注冊一個函數等服務初始化階段再執行;若服務已經初始化完成,則立刻運行該函數。

下面再來看一下日志服務代碼:service/log.lua

local skynet = require "skynet"
require "skynet.manager"
local time = require "utils.time"-- 日志目錄
local logpath = skynet.getenv("logpath") or "log"
-- 日志文件名
local logtag  = skynet.getenv("logtag") or "game"
local logfilename = string.format("%s/%s.log", logpath, logtag)
local logfile = io.open(logfilename, "a+")-- 寫文件
local function write_log(file, str) file:write(str, "\n")file:flush()print(str)
end -- 切割日志文件,重新打開日志
local function reopen_log() -- 下一天零點再次執行local future = time.get_next_zero() - time.get_current_sec()skynet.timeout(future * 100, reopen_log)if logfile then logfile:close() end local date_name = os.date("%Y%m%d%H%M%S", time.get_current_sec())local newname = string.format("%s/%s-%s.log", logpath, logtag, date_name)os.rename(logfilename, newname) -- logfilename文件內容剪切到newname文件logfile = io.open(logfilename, "a+") -- 重新持有logfilename文件
end -- 注冊日志服務處理函數
skynet.register_protocol {name = "text", id = skynet.PTYPE_TEXT, unpack = skynet.tostring, dispatch = function(_, source, str)local now = time.get_current_time()str = string.format("[%08x][%s] %s", source, now, str)write_log(logfile, str)end 
}-- 捕捉sighup信號(kill -l) 執行安全關服邏輯
skynet.register_protocol {name = "SYSTEM", id = skynet.PTYPE_SYSTEM, unpack = function(...) return ... end,dispatch = function()-- 執行必要服務的安全退出操作skynet.sleep(100)skynet.abort()end 
}local CMD = {} skynet.start(function()skynet.register(".log")skynet.dispatch("lua", function(_, _, cmd, ...)local f = CMD[cmd]if f then skynet.ret(skynet.pack(f(...)))else skynet.error(string.format("invalid command: [%s]", cmd))endend)local ok, msg = pcall(reopen_log)if not ok then print(msg)end 
end)

日志服務已經在配置中指定,logger = "log"、logservice = "snlua",不需要自行啟動這個日志服務。且項目中所有 skynet.error API 輸出的內容都被定向到了日志文件中,而不是輸出在控制臺。便于調試,write_log 寫日志函數,最后調用了 print 打印日志到了終端。

通過注冊 skynet.PTYPE_TEXT 文本類型消息,那么項目中的 skynet.error 輸出的日志都會經過本日志服務進行分發處理,由此在分發函數 dispatch = function(_, source, str) end 中處理日志消息,對所有的日志消息進行格式化的美觀輸出。

日志服務工作原理可以參考文章:https://www.jianshu.com/p/351ac2cfd98c/ ,本系列 skynet 偏原理性的東西不做深入講解。

在這里插入圖片描述

日志服務如果不做切割,全部放在一個文件中會導致日志文件日益增大,這里實現 reopen_log 函數,通過 skynet.timeout 定時每天零點對日志進行切割,包括在服務重啟時,也會對上次的日志文件 game.log 進行分割處理。

在這里插入圖片描述

日志服務還注冊了一種消息類型,skynet.PTYPE_SYSTEM,用來接收 kill -1 命令的信號,觸發保存數據的邏輯,待后續實現了緩存模塊在完善。


通用模塊

同樣在本章節,繼續實現幾個通用模塊,細心的小伙伴應該注意到了,在實現日志模塊、日志服務時,都有導入 utils.time 這個處理時間的一個模塊。

下圖是目前的模塊 lualib 文件夾的結構:

在這里插入圖片描述

  • time.lua
local skynet = require "skynet"local _M = {}-- 一秒只轉一次時間戳
local last_sec 
local current_str  -- 獲取當前時間戳
function _M.get_current_sec()return math.floor(skynet.time())
end-- 獲取下一天零點的時間戳
function _M.get_next_zero(cur_time, zero_point)zero_point = zero_point or 0 cur_time = cur_time or _M.get_current_sec() local t = os.date("*t", cur_time) if t.hour >= zero_point then t = os.date("*t", cur_time + 24 * 3600) end local zero_date = {year = t.year, month = t.month, day = t.day, hour = zero_point,min = 0, sec = 0,}return os.time(zero_date)
end -- 獲取當前可視化時間
function _M.get_current_time() local cur = _M.get_current_sec()if last_sec ~= cur then current_str = os.date("%Y-%m-%d %H:%M:%S", cur)last_sec = cur endreturn current_str
endreturn _M 

目前實現了三個接口:

  • get_current_sec:獲取當前時間戳
  • get_current_time:獲取當前可視化時間
  • get_next_zero:獲取下一天零點時間戳,可以自定義項目的刷新時間 zero_point

其中有一個小優化是 last_sec,current_str 設置上一秒時間戳,與當前可視化時間變量,保證一秒只會轉換一次。

os.time、os.date 的使用參考 lua 手冊


  • table.lua
local string = require "string"local _M = {}function _M.dump(t) local print_r_cache = {}local function sub_print_table(t, indent)if (print_r_cache[tostring(t)]) thenprint(indent .. "*" .. tostring(t))elseprint_r_cache[tostring(t)] = trueif (type(t) == "table") thenfor pos, val in pairs(t) doif (type(val) == "table") thenprint(indent .. "[" .. pos .. "] => " .. tostring(t) .. " {")sub_print_table(val, indent .. string.rep(" ", string.len(pos) + 8))print(indent .. string.rep(" ", string.len(pos) + 6) .. "}")elseif (type(val) == "string") thenprint(indent .. "[" .. pos .. '] => "' .. val .. '"')elseprint(indent .. "[" .. pos .. "] => " .. tostring(val))endendelseprint(indent .. tostring(t))endendendif (type(t) == "table") thenprint(tostring(t) .. " {")sub_print_table(t, "  ")print("}")elsesub_print_table(t, "  ")endprint()
endreturn _M 
  • string.lua
local string = require "string"local _M = {}function _M.split(str, sep) local arr = {}local i = 1for s in string.gmatch(str, "([^" .. sep .. "]+)") doarr[i] = si = i + 1endreturn arr
endreturn _M 

目前 table 模塊僅實現了 dump 接口,對表的美化輸出, string 模塊僅實現了對字符串的分割轉表,有需求在自定義添加更多的功能。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/164240.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/164240.shtml
英文地址,請注明出處:http://en.pswp.cn/news/164240.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

系列一、Spring整合MyBatis不忽略mapper接口同目錄的xxxMapper.xml

一、概述 默認情況下maven要求我們將xml配置、properties配置等都放在resources目錄下&#xff0c;如果我們強行將其放在java目錄&#xff0c;即將xxxMapper.xml和xxxMapper接口放在同一個目錄下&#xff0c;那么默認情況下maven打包時會將這個xxxMapper.xml文件忽略掉&#xf…

【辦公常識_1】寫好的代碼如何上傳?使用svn commit

首先找到對應的目錄 找到文件之后點擊SVN Commit

【標注數據】labelme的安裝與使用

這里寫目錄標題 下載標數據 下載 標數據 打開自動保存 創建矩形

NSGA-II求解微電網多目標優化調度(MATLAB)

一、NSGA-II簡介 NSGA-Ⅱ算法是Kalyanmoy Deb等人于 2002年在 NSGA 的基礎上提出的&#xff0c;它比 NSGA算法更加優越&#xff1a;它采用了快速非支配排序算法&#xff0c;計算復雜度比 NSGA 大大的降低&#xff1b;采用了擁擠度和擁擠度比較算子&#xff0c;代替了需要指定的…

Design Guidelines for 100 Gbps

文章目錄 Stratix V GT Transceiver ChannelsCFP2 Host Connector Assembly and PinoutStratix V GT to CFP2 Interface Layout DesignBoard Stack Up DimensionsExample Design Channel PerformanceSimulation Results for Stratix V GT to CFP2 Connector Layout Design Desi…

特征工程完整指南 - 第二部分

蘇米特班迪帕迪亞 照片由Dan Cristian P?dure?在Unsplash上拍攝 一、說明 DATA&#xff0c;通常被稱為原油&#xff0c;需要經過加工和清潔才能有效地用于各種用途。正如我們不直接使用來自其來源的石油一樣&#xff0c;數據也經過類似的處理以提取其真正價值。 二、特征選…

LabVIEW中如何達到NI SMU最大采樣率

LabVIEW中如何達到NI SMU最大采樣率 NISMU的數字化儀功能對于捕獲SMU詳細的瞬態響應特性或表征待測設備&#xff08;DUT&#xff09;響應&#xff08;例如線性調整率和負載調整率&#xff09;至關重要。沒有此功能&#xff0c;將需要一個外部示波器。 例如&#xff0c;假設在…

Docker start/stop/restart 命令

docker start&#xff1a;啟動一個或多個已經被停止的容器。 docker stop&#xff1a;停止一個運行中的容器。 docker restart&#xff1a;重啟容器。 語法 docker start [OPTIONS] CONTAINER [CONTAINER...]docker stop [OPTIONS] CONTAINER [CONTAINER...]docker restart…

設計循環隊列(詳解)

呀哈嘍&#xff0c;我是結衣 今天給大家帶來的內容如標題所述&#xff0c;我們來設計環形隊列&#xff0c;雖然隊列沒有講&#xff0c;但是我就是想講啊。那么環形隊列現在開始。 隊列的屬性 在設計環形隊列前&#xff0c;我們先要了解隊列的特點&#xff08;先進先出&#x…

鴻蒙(HarmonyOS)應用開發——ArkTs學習準備

介紹 前面我們已經介紹了&#xff0c;如何安裝HarmonyOS的IDE ,那么現在我們來介紹一下。HarmonyOS 開發的語言——ArkTs. ArkTS 是HarmonyOS的開發語言&#xff0c;他是typescript 的擴展&#xff0c;而typesrcipt是javascript的超集&#xff0c;如果你不太熟悉typescript語法…

qml Loader使用介紹

QML Loader 是 Qt Quick 框架中的一個元素,它允許你動態地加載和卸載 QML 組件。Loader 的作用主要體現在以下幾個方面: 延遲加載:Loader 允許你在需要時才加載組件,而不是在應用程序啟動時一次性加載所有組件。這樣可以加快應用程序的啟動時間,因為它只需要初始化用戶當前…

MIT_線性代數筆記:列空間和零空間

目錄 前言子空間綜述列空間 Column space零空間&#xff08;或化零空間&#xff09;Nullspaceb 值的影響 Other values of b 前言 本節繼續研究子空間&#xff0c;特別是矩陣的列空間&#xff08;column space&#xff09;和零空間&#xff08;nullspace&#xff09;。 子空間…

FreeRTOS的并行與并發思考

FreeRTOS的任務觸發是由滴答時鐘觸發SysTick中斷來觸發調度器執行或阻塞或掛起和切換任務的。 首先是任務的并發能力&#xff0c;FreeRTOS的任務執行是基于全搶占調度機制&#xff0c;任務優先級按在就緒列表中由高到低排布&#xff0c;系統首先執行最高優先級任務&#xff0c;…

Django web開發(一) - 前端

文章目錄 前端開發1.快速開發網站2.標簽2.1 編碼2.2 title2.3 標題2.4 div和span2.5 超鏈接2.6 圖片小結標簽的嵌套2.7 列表2.8 表格2.9 input系列2.10 下拉框2.11 多行文本用戶注冊案例: 用戶注冊GET 方式POST 方式表單數據提交優化 3.CSS樣式3.1 快速上手3.2 CSS應用方式1. 在…

Docker run 命令

docker run &#xff1a;創建一個新的容器并運行一個命令 語法 docker run [OPTIONS] IMAGE [COMMAND] [ARG...]OPTIONS說明&#xff1a; -a stdin&#xff1a;指定標準輸入輸出內容類型&#xff0c;可選STDIN/STDOUT/STDERR三項&#xff1b; -d&#xff1a;后臺運行容器&am…

SAP-部分字段變更

在SAP中部分字段是可以自行調整的&#xff0c;例如下圖 這個字段是客戶組1&#xff0c;已經被改成一級經理&#xff0c;現在來操作改回客戶組1 首先選擇字段點擊F1-技術信息-數據元素&#xff08;雙擊&#xff09; . . 保存&#xff0c;返回&#xff0c;激活&#xff0c;返…

redis運維(十八)pipeline

一 pipeline 流水線 說明&#xff1a; 這里講解的不是jenkins的pipeline流水線這里pipeline: 管道 redis為什么要提供pipeline功能 事務和pipeline ① pipeline的理念 強調&#xff1a;單純的pipeline跟事務沒有關系redis-cli --pipe --> 使用了pipeline機制說明&a…

排序算法總結

1 排序算法 1.1 快速排序 1.1.1 算法思想 先取一個隨機數&#xff0c;然后和數組的最后一個數交換 進行partition過程&#xff0c;也就是比數組最后一個數小的放在數組左邊&#xff0c;大的放在右邊&#xff0c;相等的在數組中間&#xff0c;最后把數組的最后一個數也要放到中…

【LeetCode刷題-回溯】-- 46.全排列

46.全排列 方法&#xff1a;回溯法 一種通過探索所有可能的候選解來找出所有的解的算法&#xff0c;如果候選解被確認不是一個解&#xff0c;回溯法會通過在上一步進行一些變化拋棄該解&#xff0c;即回溯并且再次嘗試 使用一個標記數組表示已經填過的數 class Solution {pu…