多路轉接方案:select poll epoll 介紹和對比

来源:https://www.cnblogs.com/yourfriendyo/archive/2023/09/17/multi_plexing.html
-Advertisement-
Play Games

1. IO模型 記憶體和外設的交互叫做IO,網路IO就是將數據在記憶體和網卡間拷貝。 IO本質就是等待和拷貝,一般等待耗時往往遠高於拷貝耗時。所以提高IO效率就是儘可能減少等待時間的比重。 IO模型 簡單對比解釋 阻塞IO 阻塞等待數據到來 非阻塞IO 輪詢等待數據到來 信號驅動 信號遞達時再來讀取或寫 ...


1. IO模型

記憶體和外設的交互叫做IO,網路IO就是將數據在記憶體和網卡間拷貝。

IO本質就是等待和拷貝,一般等待耗時往往遠高於拷貝耗時。所以提高IO效率就是儘可能減少等待時間的比重。

IO模型 簡單對比解釋
阻塞IO 阻塞等待數據到來
非阻塞IO 輪詢等待數據到來
信號驅動 信號遞達時再來讀取或寫入數據
多路轉接 讓大批線程等待,自身讀取數據
非同步通信 讓其他進程或線程進行等待和讀取,自身獲取結果

1.1 阻塞IO

執行流在某個文件描述符下讀取數據時,執行流一直等待IO條件就緒後讀取數據,這就是阻塞IO。

1.2 非阻塞IO

執行流會以迴圈的方式反覆嘗試讀取數據,如果IO條件未就緒,執行流會直接返回繼續其他任務。

非阻塞讀取方式

可通過fcntl設置文件的狀態。

非阻塞讀取時,數據未就緒是以出錯的形式返回的,錯誤碼為EAGINEWOULDBLOCK,信號導致讀取未成功錯誤碼為EINTR

void set_nonblock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0) {
        perror("fcntl failed");
        return;
    }
    if (fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0) {
        perror("fcntl failed");
        return;
    }
}

int main() {
    set_nonblock(0);
    char buf[64] = {0};

    while (true) {
        ssize_t n = read(0, buf, sizeof(buf) - 1);
        if (n > 0)
        {
            buf[n - 1] = 0;
            std::cout << buf << std::endl;
        }
        else if (n == 0)
        {
            perror("end of file");
            break;
        }
        else
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK) // 非阻塞數據未就緒返回
                continue;
            else if (errno == EINTR) // IO被信號中斷返回
                continue;
            else
            {
                perror("read error");
                break;
            }
        }
    }
    return 0;
}

較為雞肋,一般不用。

1.3 信號驅動

IO事件就緒時,內核通過SIGIO信號通知進程。等待的過程是非同步的,但拷貝數據是同步的,所以我們認為信號驅動也是同步IO。

但信號處理是非同步的,所以數據提取可能不及時。

1.4 多路轉接

內核提供select、poll、epoll等多路轉接方案,最高可同時等待幾百個文件。拷貝數據的任務仍由進程完成,等待數據的任務交給內核。

1.5 非同步通信

只要自身完全沒有參與IO等待和拷貝就是非同步通信,否則就是同步。

將緩衝區提供給非同步介面,介面等待並拷貝將數據至緩衝區,最後通知進程。進程不參與IO可直接處理數據,所以是非同步的。

非同步IO系統提供有一些對應的系統介面,但大多使用複雜,也不建議使用。非同步IO也有更好的替代方案。

IO事件就緒

IO事件就緒可分為讀事件就緒和寫事件就緒。

一般接收緩衝區設有高水位,高於該水位讀事件就緒,發送緩衝區設有低水位,低於該水位寫事件就緒。

因為頻繁讀寫內核緩衝區需要狀態切換,會附帶一系列的處理工作,導致效率下降。

 

2. 多路轉接

Linux下多路轉接的方案常見的有三種:select、poll、epoll,select出現是最早的,使用也是最繁瑣的。

2.1 select

select的介面

select能夠等待多個fd的IO條件是否就緒。

#include <sys/select.h>
int select(int nfds, fd_set* rfds, fd_set* wfds, fd_set* efds, struct timeval* timeout);

struct timeval {
    time_t       tv_sec;   /* seconds */
    suseconds_t  tv_usec;  /* microseconds */
};
參數 解釋
nfds fd的總個數,select遍歷fdset結構的範圍(被等待的fd的最大值+1)
readfds 調用時表示需要關註的讀事件,返回時表示那些事件已經就緒
writefds 調用時表示需要關註的寫事件,返回時表示那些事件已經就緒
exceptfds 調用時表示需要關註的異常事件,返回時表示那些事件已經就緒。如對端關閉,讀寫異常等
timeout 調用時表示本次調用阻塞等待時間,返回時表示此次返回剩餘的等待時間
返回值 大於0表示就緒fd的個數,為0表示本次調用結束,–1表示出錯

fd_set的介面

fd_set是文件描述符的點陣圖結構,下標表示文件描述符,比特位內容表示是否需要等待。

// fd_set操作函數
void FD_CLR  (int fd, fd_set *set); // 清除
int  FD_ISSET(int fd, fd_set *set); // 檢測
void FD_SET  (int fd, fd_set *set); // 設置
void FD_ZERO (        fd_set *set); // 置零

select的使用

const int GPORT = 8080;
const int GSIZE = 10;
enum event_type {
    read_event   = 0x1 << 1,
    write_event  = 0x1 << 2,
    except_event = 0x1 << 3,
};
struct fd_collection {
    fd_collection() {}
    fd_collection(const fd_collection& fds) {
        _rfds = fds._rfds, _wfds = fds._wfds, _efds = fds._efds, _maxfd = fds._maxfd;
    }
    bool set(int event, int fd) {
        if (_fdarr.size() >= GSIZE) return false;
        if (event & read_event)   _rfds.set(fd);
        if (event & write_event)  _wfds.set(fd);
        if (event & except_event) _wfds.set(fd);
        _fdarr.push_back(fd);
        if (_maxfd < fd) _maxfd = fd;
        return true;
    }
    void clear(int fd) {
        _rfds.clear(fd);
        _wfds.clear(fd);
        _efds.clear(fd);
        for (int i = 0; i < _fdarr.size(); i++)
            if (_fdarr[i] == fd) _fdarr[i] = -1;
    }
    class file_descptrs {
    public:
        file_descptrs() { bzero(); }
        ~file_descptrs() {}
        void set  (int fd) { FD_SET(fd, &_set);          }
        void clear(int fd) { FD_CLR(fd, &_set);          }
        bool isset(int fd) { return FD_ISSET(fd, &_set); }
        void bzero()       { FD_ZERO(&_set);             }
        fd_set* get() { return &_set; }
    private:
        fd_set _set;
    };
    file_descptrs _rfds;
    file_descptrs _wfds;
    file_descptrs _efds;
    std::vector<int> _fdarr;
    int _maxfd = -1;
};

class select_server : public inet::tcp::server {
public:
    select_server(uint16_t port) : server(port), _wouldblock(true)
    {}
    select_server(uint16_t port, int sec, int usec) : server(port), _timeout({sec, usec})
    {}
    void start() {
        _fds.set(read_event, _sock);
        while (true) {
            int n = 0;
            struct timeval timeout = _timeout;
            fd_collection fds_cp(_fds);

if (_wouldblock)
n = select(fds_cp._maxfd+1, fds_cp._rfds.get(), fds_cp._wfds.get(), fds_cp._efds.get(),  nullptr);
else
n = select(fds_cp._maxfd+1, fds_cp._rfds.get(), fds_cp._wfds.get(), fds_cp._efds.get(), &timeout);
            switch (n) {
            case 0: INFO("time out: %.2f", timeout.tv_sec + timeout.tv_usec / 1.0 / 1000);
                break;
            case -1: ERROR("select error, %d %s", errno, strerror(errno));
                break;
            default: handler_event(fds_cp);
                break;
            }
        }
    }
private:
    void handler_event(fd_collection& resfds) {
        for (auto fd : _fds._fdarr) {
            if (fd == -1) continue;
            if (resfds._rfds.isset(fd)) {
                if (fd == _sock)  {
                    acceptor();
                } else {
                    std::string buf;
                    recver(fd, &buf);
                }
            }
            if (resfds._wfds.isset(fd)) {
                std::string msg = "test";
                sender(fd, msg);
            }
            if (resfds._efds.isset(fd)) {
                WARN("excepton event occurred, fd: %d", fd);
            }
        }
    }
    void acceptor() {
        std::string cip;
        uint16_t cport;
        int sock = accept(&cip, &cport);
        INFO("a connect %d has been accepted [%s:%d]", sock, cip.c_str(), cport);
        // if (!_fds.set(read_event | write_event | except_event, sock))
        if (!_fds.set(read_event, sock)) {
            close(sock);
            WARN("connect close, fd array is full");
        }
    }
    void recver(int fd, std::string* buf) {
        ssize_t s = recv(fd, buf, 1024);
        if (s > 0) {
            std::cout << *buf << std::endl;
        }
        else {
            if (s == 0) INFO("client quit");
            else WARN("recv error, %d %s", errno, strerror(errno));
            _fds.clear(fd);
            close(fd);
        }
    }
    void sender(int fd, const std::string& msg) {
        size_t s = send(fd, msg);
        if (s <= 0) {
            if (s == 0) INFO("client quit");
            else WARN("send error, %d %s", errno, strerror(errno));
            _fds.clear(fd);
            close(fd);
        }
    }
private:
    bool _wouldblock;
    struct timeval _timeout;
    fd_collection _fds;
};

select的優缺點

優點
一次等待多個fd,使IO等待時間重疊,一定程度上提高IO效率
缺點
調用前要重新設置fd集,調用後要遍歷檢測就緒fd,需要額外數組
select能夠檢測fd的個數上限太小
頻繁地將用戶數據拷貝到內核中
select內部遍歷fd_set結構以檢測就緒

 

2.2 poll

poll相比select在使用和實現上都有進步。不過重點是epoll。

poll的介面

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
};
參數 解釋
timeout 阻塞等待時間,不過採用整數單位是毫秒。
struct pollfd* nfds_t pollfd結構體數組以及數據長度
struct pollfd.fd:關註的文件描述符
struct pollfd.events:關註的事件類型
struct pollfd.revents:就緒的事件類型
事件類型 描述
POLLIN 數據(包括普通數據和優先數據)可讀
POLLRDNORM 普通數據可讀
POLLRDBAND 優先順序帶數據可讀(Linux 不支持)
POLLPRI 高優先順序數據可讀,比如 TCP 帶外數據
POLLOUT 數據(包括普通數據和優先數據)可寫
POLLWRNORM 普通數據可寫
POLLWRBAND 優先順序帶數據可寫
POLLRDHUP TCP 連接被對方關閉,或者對方關閉了寫操作,它由GNU引入
POLLERR 錯誤
POLLHUP 掛起。比如管道的寫端被關閉後,讀端描述符將收到 POLLHUP 事件
POLLNVAL 文件描述符沒有打開

poll的使用

const int   default_port    = 8080;
const int   default_size    = 20;
const int   default_timeout = -1;
const int   default_fd      = -1;
const short default_event   = 0;

class poll_server : public inet::tcp::server {
public:
    poll_server(uint16_t port) : server(port), _fds(new struct pollfd[default_size])
        , _cap(0), _timeout(default_timeout) {
        pollfd_arr_init();
    }
    void pollfd_arr_init() {
        for (int i = 0; i < default_size; i++) pollfd_init(_fds[i]);
    }
    void pollfd_init(struct pollfd& pf) {
        pf.fd = default_fd;
        pf.events = default_event;
        pf.revents = default_event;
    }
    void pollfd_clear(struct pollfd& pf) {
        pf.fd = default_fd;
        pf.events = default_event;
        pf.revents = default_event;
    }
    void start() {
        _fds[0].fd = _sock;
        _fds[0].events = POLLIN;
        ++_cap;
        while (true) {
            int timeout = _timeout;
            switch (poll(_fds.get(), _cap, timeout)) {
            case 0: INFO("time out: %d", timeout); break;
            case -1: ERROR("select error, %d %s", errno, strerror(errno)); break;
            default: event_handler(); break;
            }
        }
    }
private:
    void event_handler() {
        for (int i = 0; i < _cap; i++) {
            auto& fd = _fds[i].fd;
            auto& revents = _fds[i].revents;
            if (revents & POLLIN) {
                if (fd == _sock) {
                    acceptor();
                } else {
                    std::string buf;
                    recver(i, &buf);
                }
            }
            if (revents & POLLOUT) {
                std::string msg = "test";
                sender(i, msg);
            }
            if (revents & POLLERR){
                WARN("excepton event occurred, fd: %d", fd);
            }
        }
    }
    void acceptor() {
        std::string cip;
        uint16_t cport;
        int newfd = accept(&cip, &cport);
        if (_cap >= default_size) {
            close(newfd);
            WARN("connect close, fd array is full");
            return;
        }
        for (int i = 0; i < default_size; i++) {
            if (_fds[i].fd == default_fd) {
                _fds[i].fd = newfd;
                _fds[i].events = POLLIN | POLLOUT;
                _cap++;
                break;
            }
        }
        INFO("a connect %d has been accepted [%s:%d]", newfd, cip.c_str(), cport);
    }
    void recver(int i, std::string* buf) {
        ssize_t s = recv(_fds[i].fd, buf, 1024);
        if (s > 0) {
            std::cout << *buf << std::endl;
        } else {
            if (s == 0) INFO("client quit");
            else WARN("recv error, %d %s", errno, strerror(errno));
            close(_fds[i].fd);
            pollfd_clear(_fds[i]);
            --_cap;
        }
    }
    void sender(int i, const std::string& msg) {
        size_t s = send(_fds[i].fd, msg);
        if (s <= 0) {
            if (s == 0) INFO("client quit");
            else WARN("send error, %d %s", errno, strerror(errno));
            close(_fds[i].fd);
            pollfd_clear(_fds[i]);
            --_cap;
        }
    }
private:
    std::unique_ptr<struct pollfd[]> _fds;
    int _cap;
    int _timeout;
};

poll的優缺點

優點
監視fd的個數無上限
將事件輸入輸出分離,避免原始數據被修改
缺點
返回後仍需要遍曆數組檢測就緒事件
poll內部仍需要內核自己遍歷檢測就緒事件
每次調用都要將pollfd結構從內核空間拷貝到用戶空間

 

2.3 epoll

epoll的介面

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

typedef union epoll_data {
    void*    ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};
epoll_create 負責創建epoll模型
size 目前size被忽略,為相容可寫128/256
返回值 epoll句柄
epoll_ctl 負責用戶告訴內核那些事件需要關註
epfd epoll句柄
op 指定相關操作
EPOLL_CTL_ADD:添加事件
EPOLL_CTL_MOD:修改事件
EPOLL_CTL_DEL:刪除事件
fd 事件關註的文件描述符
epoll_event 用來指定fd上關註的事件
epoll_wait 負責內核告訴用戶那些事件就緒
epfd epoll句柄
epoll_event 輸出緩衝區,存放已就緒的事件
maxevents 緩衝區的長度
timeout 阻塞等待的時間
返回值 就緒事件的個數
events巨集常量取值 解釋
EPOLLIN 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)
EPOLLOUT 表示對應的文件描述符可以寫
EPOLLPRI 表示對應的文件描述符有緊急的數據可讀(帶外數據)
EPOLLERR 表示對應的文件描述符發生錯誤
EPOLLHUP 表示對應的文件描述符被掛斷
EPOLLET 將EPOLL設為邊緣觸發(Edge Triggered)模式
EPOLLONESHOT 只監聽一次事件,本次之後自動將該fd刪去

epoll的使用

epoll_server 封裝最終版

epoll的原理

  1. epoll模型中用紅黑樹保存註冊的fd和事件,用就緒隊列保存就緒的fd和事件。
  2. epoll_ctl的本質就是新增修改刪除紅黑樹的節點,並對fd對應的文件中註冊回調函數。
  3. 如果事件就緒,內核在將硬體數據拷貝至內核緩衝區後,還會自動執行回調將紅黑樹節點添加到就緒隊列中。
  4. epoll_wait負責檢查是否有事件就緒,本質就是檢測就緒隊列為空。

epoll的工作模式

epoll有兩種工作方式,分別是水平觸發LT和邊緣觸發ET。

LTET的概念

  • LT水平觸發:只要事件一直就緒,就會一直通知。
  • ET邊緣觸發:只有事件就緒或再次就緒時,才會通知一次。
LT水平觸發

事件就緒時,可以不立刻處理或只部分處理。

只要事件處於就緒狀態,每次調用epoll_wait都會通知該事件就緒,直到處理完畢處於未就緒狀態。

ET邊緣觸發

設置事件為EPOLLET,表示對於該事件使用ET模式。

事件就緒時必須一次性處理清空數據,否則下次是不會通知該事件就緒的,直到該事件再次就緒。

LTET的讀寫特點

數據剩餘ET不會提醒,所以必須一次性讀取所有數據,但如果讀取時剛好無數據就會被阻塞。 所以ET必須採用非阻塞讀寫。

LT模式事件就緒時讀取一定不會被阻塞,因為一定有數據。

LTET的效率對比

一般ET的效率>=LT的效率。原因如下:

  1. 一般ET通知次數比LT少,也就是系統調用次數少。
  2. ET會倒逼程式員一次讀取全部數據,所以底層TCP會更新出更大的滑動視窗。

LTET的應用場景

  • ET要求程式必須一次性讀取所有數據,再讓上層處理,ET重IO效率。
  • LT可以只交付部分數據,儘快讓上層處理,LT重處理效率。

ET高IO,LT高響應。

epoll的優缺點

優點 解釋
介面分離解耦 每次調用不需要重新設置事件集,做到輸入輸出事件分離
使用簡單高效 調用後用戶不需要遍歷,內核提供就緒事件緩衝區
輕量數據拷貝 不需要頻繁的進行將數據從內核和用戶之間的拷貝
無遍歷效率高 底層不需要遍歷,利用回調將就緒事件添加到就緒隊列中
沒有數量限制 文件描述符數目無上限

epoll的寫入設置

  • 只有讀取緩衝區有數據,讀事件才會就緒。所以讀事件可以一直關註,我們稱為常設置。
  • 只要寫入緩衝區沒有滿,寫事件就一直就緒。所以寫事件按需設置,寫入完成後立即關閉,否則會一直觸發。

一般構建響應後,直接發送數據,只有當緩衝區滿的時候,再將沒寫完的數據交給epoll處理。

select、poll、epoll都是如此,但epoll的ET模式可以常設置寫事件。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕雲。本文章系列以一個真實的並已成功上線的軟體項目——碼如雲(https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取捨。 本系列包含以下文章: DDD入門 ...
  • 系統設計之緩存五種策略 當我們在架構中引入緩存時,緩存和資料庫之間的同步就變得不可避免。 讓我們看看如何保持數據同步的五種常見策略。 1)閱讀策略: 緩存在一邊 通讀2)寫策略:寫周圍 回信 寫通緩存策略經常組合使用。例如,write-around 通常與 cache-aside 一起使用,以確保緩 ...
  • 前一篇水文中,老周演示了 QAbstractItemModel 抽象類的繼承方法。其實,在 Qt 的庫裡面,QAbstractItemModel 類也派生了兩個基類,能讓開發者繼承起來【稍稍】輕鬆一些。 這兩個類是 QAbstractListModel 和 QAbstractTableModel。 ...
  • 註釋可以用來解釋Python代碼。註釋可以用來使代碼更易讀。註釋可以用來在測試代碼時防止執行。 創建註釋 註釋以#開始,Python會忽略它們: 示例:獲取您自己的Python註釋 # 這是一個註釋 print("Hello, World!") 註釋可以放在一行的末尾,Python會忽略行的其餘部分 ...
  • 目錄 線程簡介 線程實現(重點) 線程狀態 線程同步(重點) 線程通信問題 線程實現: 方式一:繼承Thread類 /** * TODO * @author 清蓮孤舟 * @CreateDate 2023/9/17/9:28 * 創建線程的方式一:通過繼承Thread類實現 */ //繼承Threa ...
  • 什麼是 Prometheus Prometheus 是一個開源的系統監控和警報工具,最初由 SoundCloud 開發,並於 2012 年發佈為開源項目。它是一個非常強大和靈活的工具,用於監控應用程式和系統的性能,並根據預定義的規則觸發警報。以下是對 Prometheus 的詳細介紹: 特點和優勢: ...
  • 寫在前面 上一小節中我們從0到1 使用Vite搭建了一個Vue3項目,並集成了Element Plus 實現了一個簡單的增刪改查頁面。 這一篇中我們將使用IDEA快速搭建一個SpringBoot3.x的項目。 一、創建項目 1、File->new->project 2、選擇“Spring Initi ...
  • 超級好用繪圖工具(Draw.io+Github) 方案簡介 繪圖工具:Draw.io 存儲方式: Github 1 Draw.io 1.2 簡介 ​ 是一款免費開源的線上流程圖繪製軟體,可以用於創建流程圖、組織結構圖、網路圖、UML圖等各種類型的圖表。它提供了豐富的圖形元素和編輯功能,使用戶能夠輕鬆 ...
一周排行
    -Advertisement-
    Play Games
  • 下麵是一個標準的IDistributedCache用例: public class SomeService(IDistributedCache cache) { public async Task<SomeInformation> GetSomeInformationAsync (string na ...
  • 這個庫提供了在啟動期間實例化已註冊的單例,而不是在首次使用它時實例化。 單例通常在首次使用時創建,這可能會導致響應傳入請求的延遲高於平時。在註冊時創建實例有助於防止第一次Request請求的SLA 以往我們要在註冊的時候實例單例可能會這樣寫: //註冊: services.AddSingleton< ...
  • 最近公司的很多項目都要改單點登錄了,不過大部分都還沒敲定,目前立刻要做的就只有一個比較老的項目 先改一個試試手,主要目標就是最短最快實現功能 首先因為要保留原登錄方式,所以頁面上的改動就是在原來登錄頁面下加一個SSO登錄入口 用超鏈接寫的入口,頁面改造後如下圖: 其中超鏈接的 href="Staff ...
  • Like運算符很好用,特別是它所提供的其中*、?這兩種通配符,在Windows文件系統和各類項目中運用非常廣泛。 但Like運算符僅在VB中支持,在C#中,如何實現呢? 以下是關於LikeString的四種實現方式,其中第四種為Regex正則表達式實現,且在.NET Standard 2.0及以上平... ...
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他們的程式記憶體會偶發性暴漲,自己分析了下是非托管記憶體問題,讓我幫忙看下怎麼回事?哈哈,看到這個dump我還是非常有興趣的,居然還有這種游戲幣自助機類型的程式,下次去大玩家看看他們出幣的機器後端是不是C#寫的?由於dump是linux上的程式,剛好win ...
  • 前言 大家好,我是老馬。很高興遇到你。 我們為 java 開發者實現了 java 版本的 nginx https://github.com/houbb/nginx4j 如果你想知道 servlet 如何處理的,可以參考我的另一個項目: 手寫從零實現簡易版 tomcat minicat 手寫 ngin ...
  • 上一次的介紹,主要圍繞如何統一去捕獲異常,以及為每一種異常添加自己的Mapper實現,並且我們知道,當在ExceptionMapper中返回非200的Response,不支持application/json的響應類型,而是寫死的text/plain類型。 Filter為二方包異常手動捕獲 參考:ht ...
  • 大家好,我是R哥。 今天分享一個爽飛了的面試輔導 case: 這個杭州兄弟空窗期 1 個月+,面試了 6 家公司 0 Offer,不知道問題出在哪,難道是杭州的 IT 崩盤了麽? 報名面試輔導後,經過一個多月的輔導打磨,現在成功入職某上市公司,漲薪 30%+,955 工作制,不咋加班,還不捲。 其他 ...
  • 引入依賴 <!--Freemarker wls--> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> ...
  • 你應如何運行程式 互動式命令模式 開始一個互動式會話 一般是在操作系統命令行下輸入python,且不帶任何參數 系統路徑 如果沒有設置系統的PATH環境變數來包括Python的安裝路徑,可能需要機器上Python可執行文件的完整路徑來代替python 運行的位置:代碼位置 不要輸入的內容:提示符和註 ...