Structure Binding ใครว่า ฟังก์ชันใน C++ คืนค่าได้แค่ค่าเดียว ?

TL;DR มันก็ได้ค่าเดียวแหละ เอาตรง ๆ แค่มันมีฟีเจอร์ที่สามารถแยกค่าออกมาเป็นค่าย่อย ๆ แล้วจับใส่ตัวแปรหลาย ๆ ตัวได้ต่างหาก

ผมว่า คนที่เขียนภาษาอย่าง Go มา คงจะเคยเอาไปข่มคนที่เขียนภาษาอื่น ๆ ประมาณว่า “ภาษาชั้นสามารถคืนค่าจากฟังก์ชันได้หลายๆ ค่าพร้อมกันเฟร้ย ภาษาแกทำได้มั้ยเล่า”

แบบ

var result, err = Process(input)

ซึ่งในภาษา C++ เราคงเขียนแบบประมาณว่า ….

auto err = Process(&result);

ซึ่งมันไม่เท่ห์เอาซะเลย (ฮา)

… แต่เดี๋ยวก่อน ถ้าผมจะบอกว่าเราเขียนในรูปเดียวกันได้ในภาษา C++ ล่ะ ?? คุณจะเชื่อมั้ย ? …

ในมาตรฐาน C++17 มีฟีเจอร์ใหม่ที่มีชื่อว่า Structure Binding ซึ่งทำให้เราสามารถเขียนโค๊ดในรูปนี้ได้

auto [result, error] = Process(input);

สนใจหรือยังครับ 😀

รายละเอียดอยู่ที่การ declare function

ฟังก์ชัน Process นี่ ดูจากการเรียกฟังก์ชันแล้วน่าจะเป็นการคืนหลาย ๆ ค่า ใช่ไหมครับ ? ถ้าดูเผิน ๆ ก็เป็นแบบนั้นครับ แต่ฟังก์ชันนี้มีหน้าตาประมาณนี้ต่างหาก

struct Outcome {
    Result result,
    Error error
}

Outcome Process(const Input& input);

ครับ มันก็คืนค่าเดียวนี่แหละ ง่าย ๆ แต่ตอนรับค่าเนี่ย แทนที่เราจะเขียนแบบ …

auto outcome = Process(input);
auto result = outcome.result;
auto err = outcome.error;

เราก็จับย่อมันเหลือบรรทัดเดียวซะเลย เป็น auto [result, err] = ... แทน ซึ่งนอกจากจะประหยัดตัวแปรไปได้หนึ่งตัวแล้ว ก็ไม่ต้องมานั่งเขียนอีกสองบรรทัดอีกต่างหาก

โดยหลัก ๆ ของการ structure binding คือ เรา bind ตัวแปรหนึ่งตัว เข้ากับสมาชิกของ structure หนึ่งตัว ตามลำดับ เท่านั้นเอง

แต่นั่นหมายถึงว่า เราต้องสร้าง struct ใหม่ทุกครั้งที่เขียนฟังก์ชัน ??

แน่นอนว่าถ้าเราต้องมานั่งสร้าง struct ใหม่สำหรับทุก ๆ ฟังก์ชัน แบบเอา result ไปผูกกับ error เป็นอีก type นึง ผมว่ามันก็คงน่ารำคาญน่าดู โชคดีว่าเราไม่ต้องทำแบบนั้นครับ เพราะว่าใน standard library มี std::pair ให้เราใช้ได้ง่าย ๆ

ไอ้ฟังก์ชัน Process() ข้างบน ก็ย่อลงมาเหลือแค่นี้ได้ครับ

std::pair<Result, Error> Process(const Input& input);

จากนั้นก็ใช้ได้เหมือนเดิม

แล้วถ้าคืนค่ามากกว่าสองตัวล่ะ ??

ผมว่าถ้าเห็นชื่อ type แล้ว คงเข้าใจได้ว่า มันเก็บค่าได้แค่สองค่าเท่านั้น ซึ่งอันนี้ถูกต้องแล้วครับ std::pair เป็น struct ที่มีสมาชิกเป็นตัวแปรสองตัว ดังนั้นจะเก็บค่ามากกว่านั้นก็คงไม่ได้ครับ

แต่ใน C++ เองก็ยังมีอีก type นึงให้เราใช้ นั่นคือ std::tuple นั่นเอง วิธีใช้ไม่ต่างกันเลยครับ อย่างเช่น …

std::tuple<float, float, float, Error> GetPosition();

auto [x, y, z, err] = GetPosition();

อะไรแบบนี้

std::pair กับ std::tuple นั้นมีความแตกต่างในรายละเอียดพอสมควร อย่างการเข้าถึงสมาชิกของ std::pair นั้นสามารถทำได้ง่าย ๆ ผ่านสมาชิกที่ชื่อ first และ second แต่ของ std::tuple จะต้องใช้ฟังก์ชันน get() ซึ่งวุ่นวายกว่าพอสมควร แต่ถ้าเรา structure binding แล้ว ทั้งสองตัวก็สามารถเรียกใช้ได้ไม่แตกต่างกันมากครับ

อ้อ std::pair นั้นดูจะเป็นมิตรกับตัว optimizer มากกว่าครับ เพราะความเรียบง่ายของตัว type เองนั่นแหละ

Structure Binding ทำอะไรได้อีก

จริง ๆ ฟีเจอร์ structure binding ก็ตามชื่อ คือการสร้าง binding เข้าหาสมาชิกแต่ละตัวใน struct ซึ่ง ที่ผมพูดมาทั้งหมดเป็นการสร้างตัวแปรที่รับค่าจาก structure แต่อีกอย่างที่ทำได้คือการสร้าง reference จำนวนนึงที่ refer ไปหาสมาชิก แทนที่จะเป็นการสร้าง variable แล้วก็อปปี้ค่าใส่ครับ

Position pos{};
auto &[x,y,z] = pos;

คราวนี้ แทนที่ x, y, z จะมีค่าเป็นของตัวเอง ก็กลายเป็นแค่ reference ที่ refer ไปหาสมาชิกของ pos อีกทีแทนครับ

const parameter reference … ทำไมคอมไพล์ไม่ผ่านฟะ ? กับอีกเรื่องพื้น ๆ ที่ลืมได้ตลอด

ช่วงนี้กลับมาเขียน C++ เล่น ๆ อีกครับ แล้วก็แบบ เขียนตามความเคยชิน เขียน ๆ ไปเรื่อย ๆ แล้วก็ … เฮ่ย ทำไมมันคอมไพล์ไม่ผ่านล่ะเนี่ย??

ติดอยู่หลายวันเหมือนกัน คือ ก็ทำให้มันคอมไพล์ผ่านได้ แต่ก็เป็นการเปลี่ยนไปใช้ฟีเจอร์อื่นแทน ไม่ได้ใช้แบบที่คุ้นเคย ก็เลยรู้สึกคาใจ นั่งหาอยู่สองวันก็เพิ่งมาถึงบางอ้อ …

อ้อ เวร … แค่นี้นี่เองที่หลงลืม แก่แล้วสิเนี่ย 555

… เอ้า พูดมาก็เยอะ ยังไม่บอกเลยว่าปัญหาคืออะไร แต่ผมว่าหลาย ๆ ท่านเห็นหัวเรื่องก็น่าจะรู้แล้วล่ะ …

และนี่คือโค๊ดเจ้าปัญหาครับ

#include <iostream>

class Device {
public:
    int GetID() {return id;}
    Device(const int& _id){ id = _id;}
private:
    int id;
};

class Window {
public:
    void Setup(const Device& device){
        std::cout<<"Using Device"<<device.GetID();
    }
};

int main(int argc, char** argv) {
    Device d{10};
    Window w;
    w.Setup(d);
}

เขียนแค่นี้หลายคนน่าจะเห็นละว่าปัญหาอยู่ที่ตรงไหน ในขณะที่อีกคนก็ยังงง ๆ ผมจะเริ่มจากจุดที่ดูประหลาดที่สุดสำหรับคนที่มาจากภาษาอื่นละกันนะครับ

void Setup(const Device& device)

ตรงนี้จริง ๆ เราจะเขียนว่า void Setup(Device device) ก็ได้ แต่ไม่เป็นที่นิยม คือไม่ใช่กระแดะต้องเขียนยาว ๆ แต่การเขียนรูปนี้เป็นการป้องกันการก็อปปี้ค่าจากตอนที่ถูกเรียนเข้าไปในฟังก์ชันที่ถูกเรียก เราใช้ reference แทนเพื่อเป็นการอ้างอิงไปถึงค่านั้นโดยตรง และเพื่อเป็นการป้องกันการแก้ไขข้อมูลค่าของ parameter โดยที่ไม่ตั้งใจ (เพราะสิ่งที่เราทำตรงนี้คือการ pass-by reference ซึ่งค่าที่ส่งเข้ามากับค่าที่ใช้ตอนเรียกฟังก์ชันจะเป็นค่าเดียวกัน) เราก็เลยใส่เอาไว้ว่าเป็น const Device& นะ

แต่พอคอมไพล์เราก็จะได้ error ดังนี้

$ clang++ main.cpp 
main.cpp:14:36: error: 'this' argument to member function 'GetID' has type 'const Device', but function is not marked const
        std::cout<<"Using Device"<<device.GetID();
                                   ^~~~~~
main.cpp:5:9: note: 'GetID' declared here
    int GetID() {return 0;}
        ^
1 error generated.

เจ๊งซะงั้น ?

คือถ้าฟังก์ชันนี้รับค่าเป็น integer มันจะใช้ได้ครับ แต่พอรับเป็น const Device& มันจะพัง 🙂

แล้วจะแก้ไงดี ?

คำตอบแรกที่เข้ามาในหัวหลังจากที่มันคอมไพล์ไม่ผ่านคือ … เอา const ออกครับ กลายเป็น void Setup(Device& device) มันก็คอมไพล์ผ่านแหละ แต่การที่เรา pass-by reference แล้วไม่แก้ไขอะไรเลยเนี่ย มันเป็น code smell อย่างนึงแฮะ คือมันดูแปลก ๆ เพราะปรกติเรามักจะใช้ pass-by reference เพื่อแก้ไขค่าในตัว parameter ที่ส่งเข้ามาครับ การเรียกลักษณะนี้เลยดูแปลก ๆ

และที่สำคัญคือ ถ้าเเอาออกแล้ว เราจะเรียกฟังก์ชันนี้กับค่าที่เป็น ค่าคงที่ หรือค่าที่คืนมาจากฟังก์ชันอื่น หรือการสร้าง temporary object ไม่ได้ เพราะมันมีกฎว่า non-const reference จะต้องชี้ไปหาตัวแปรสักตัวนึงเสมอ (ในขณะที่ const reference สามารถเป็น refer หา temporary object ได้ครับ)

 $ clang++ main.cpp 
main.cpp:21:13: error: non-const lvalue reference to type 'Device' cannot bind to a temporary of type 'Device'
    w.Setup(Device(2));
            ^~~~~~~~~
main.cpp:13:25: note: passing argument to parameter 'device' here
    void Setup( Device& device) {
                        ^
1 error generated.

แล้วจะแก้ไงดี … ลองกลับไปดูที่ฟังก์ชัน Setup() อีกทีนะครับ

ตัวค่า device เนี่ย มี type เป็น const Device& ใช่มั้ยครับ นั่นหมายความว่า ตัวแปรตัวนี้เป็นตัวแปรแบบ const ซึ่งความหมายของมันคือ เป็น read-only ดังนั้นคอมไพล์เลอร์จะยอมให้อ่านค่าจากตัวแปรตัวนี้เท่านั้น ไม่สามารถเขียนอะไรลงไปเพื่อเปลี่ยนค่าได้

ซึ่ง ไอ้ ฟังก์ชัน GetID() เนี่ย เราก็เห็นอยู่แล้วว่า มันไม่ได้แก้ไขค่าอะไรในตัว class Device

แต่คอมไพล์เลอร์ของ C++ มันไม่ได้ฉลาดขนาดนั้นครับ มันไม่สามารถที่จะแค่อ่านฟังก์ชันแล้รู้เลยว่า เออฟังก์ชันนี้มันไม่ได้แก้ไขค่าอะไรนะ เราต้องบอกมันเองว่า ฟังก์ชันนี้ไม่ได้แก้ไขค่า

ซึ่งในทาง C++ เราจะบอกมันว่า ฟังก์ชันนี้เป็น constant member function

ซึ่งสามารถทำได้โดยการเติม keyword const เข้าไปท้าย function signature ครับ

ก็จะเป็นราว ๆ นี้

#include <iostream>

class Device {
public:
    int GetID() const {return id;}
    Device(const int& _id){ id = _id;}
private:
    int id;
};

class Window {
public:
    void Setup(const Device& device) {
        std::cout<<"Using Device"<<device.GetID();
    }
};

int main(int argc, char** argv) {
    Device d{10};
    Window w;
    w.Setup(d);
    w.Setup(Device(2));
}

แค่นี้ก็คอมไพล์ผ่าน รันได้เรียบร้อย

คราวนี้ก็เขียนเอาไว้ เผื่อเดี๋ยวลืมอีก แน่นอนว่าเดี๋ยวก็ต้องลืมอีก นี่ไม่ใช่ครั้งแรกที่ผมลืมกฎข้อนี้ครับ 555

NAS ตัวใหม่ที่เข้าถึงได้ตลอดเวลา

เมื่อ 4 ปีก่อน ผมเปลี่ยน NAS มาเป็น Zyxel NSA325v2 ซึ่งพอคิด ๆ ไป 4 ปีมันก็เร็วเหมือนกันแฮะ จริง ๆ แล้วเจ้า Zyxel+Arch Linux ที่ใช้เองก็ใช้งานได้ดีไม่งอแงอะไรครับ แต่ว่าเราเริ่มรู้สึกว่ามันไม่ค่อยจะพอใช้แล้ว

เมื่อสักปลายปีที่แล้วผมเริ่มศึกษา Unreal Engine 4 จริงจัง แล้วเจอตออยู่อย่างคือขนาดโปรเจค โปรเจคของ UE ที่ผมทำอยู่มีขนาดราว ๆ 20-30GB ซึ่งถ้าไปใช้บริการฟรีอย่าง Github หรือ Bitbucket แล้วจะเจอแคปขนาดโปรเจคที่ 2GB เท่านั้น ถ้าอยากได้พื้นที่มากกว่านี้ก็ต้องจ่ายเพิ่ม แล้วพอจ่ายเพิ่มเองก็ได้แค่ราว ๆ 50GB เท่านั้น ก็ขึ้นอยู่กับเวลาเท่านั้นเองว่าขนาดโปรเจคมันจะไปได้ขนาดนั้นเมื่อไหร่

ผมก็เลยลองมองหาออปชันอื่นที่น่าจะทำให้เราทำงานได้ง่ายกว่า ก็พบว่า เอ๊ะแล้วถ้าเราใช้ NAS เราเป็น VCS ล่ะ NAS ตัวเก่าที่ผมใช้มีขนาดถึง 4TB ดังนั้นจะเอามาใช้เพื่อการนี้ย่อมไม่มีปัญหาอยู่แล้ว แต่ปัญหาอยู่ที่จะเข้าถึงได้อย่างไรมากกว่า

เข้าถึงได้ตลอดเวลา ผ่าน Zerotier

ออปชันแรกที่ผมดูคือการใช้ Dynamic DNS แต่เนื่องจากเนตเวิร์คที่บ้านเนี่ย IPv4 อยู่หลัง NAT เลยไม่สามารถเข้าถึงได้จากเนตเวิร์คส่วนใหญ่ครับ ดังนั้นออปชันนี้ก็เลยตกไป (ซึ่งจริง ๆ ก็ดีนะ เพราะวิธีนี้ไม่ค่อยปลอดภัยเท่าไหร่)

วิธีที่สองที่ดูคือการใช้ VPN แทน ซึ่งอันนี้โชคดีอย่างนึง คือมีคนสอนมาว่ามี service ที่ชื่อว่า Zerotier ซึ่งให้บริการ VPN ฟรี ตัวซอฟต์แวร์เป็น Open Source และมีการเข้ารหัสตลอดทาง สิ่งที่เราต้องทำคือสมัครเป็นสมชิกของบริการนี้ ติดตั้ง Client ที่ทุกเครื่องที่ต้องการเข้าถึง VPN สร้าง Network ในระบบขึ้นมาแล้วก็ Join ซะ (รายละเอียดจะไม่พูดถึงนะครับตรงนี้)

ปัญหาคือ ไอ้เจ้า Client เนี่ย มันไม่รองรับ NAS ตัวที่ผมใช้ครับ …

NAS เครื่องใหม่…หรือเครื่องเก่า

ผมตัดสินใจ … หรือเรียกว่าตัดใจก็ได้มั้ง? ที่จะเปลี่ยน NAS ใหม่อีกครั้ง ทั้ง ๆ ที่เครื่องเก่าก็ยังใช้งานได้ดี คราวนี้ไอเดียคือ เราจะให้มันรัน service มากกว่าแค่เป็น NAS และตัวดาวน์โหลด BitTorrent เจ้า NAS ตัวใหม่นี่จะเป็นตัวที่ดูแลเรื่องการพัฒนาซอฟต์แวร์และสามารถรันซอฟต์แวร์ที่เราเขียนขึ้นเพื่อใช้เองได้ด้วย ดังนั้นมันจะต้องเป็นเครื่องที่แรงพอสมควร

ในตอนแรกก็คิดว่าจะประกอบคอมขึ้นมาเพื่อการนี้โดยเฉพาะ แต่พอคิดเรื่องจำนวนเงินที่ต้องจ่าย … ผมก็ถอยไปออปชันที่สอง ก็คือไปเอาคอมเก่าของพี่ชายมาใช้ (พี่ชายผมทุกวันนี้ใช้แลปท็อปและไม่ค่อยเล่นเกมคอมแล้ว) แต่พอได้มาเสร็จเมนบอร์ดดันมาเสีย ก็เลยต้องเปลี่ยนเมนบอร์ด ผมมีซีพียูเก่าอยู่ (เป็น Core i5 4460) ก็เอามาใช้กับคอมเครื่องนี้

HDD เปลี่ยนใหม่ทั้งหมด เป็น WD RED 4TB x2 กับ Seagate Ironwolf 4TB สามตัวนี้เอามาต่อกันเป็น Software RAID5 ความจุรวม 8TB (ผมยังมี 2TB อีกสามตัว แต่ยังไม่ได้ใส่ เพราะว่าเคสไม่มีที่แล้ว)

Arch Linux OS เพื่อนเก่า

ตอนแรกเลยก็คิดว่า จะไปทาง NAS OS เต็มตัว ก็เลยติดตั้ง FreeNAS ไป แต่สุดท้ายก็ไม่ชอบ มันมีปัญหาอย่างนึงคือทุก Service ที่เราติดตั้งไปมันกลายเป็น VM ตัวใหม่ ดังนั้นถ้าเราจะใช้ Zerotier เราก็ต้องตามไปติดตั้งมันทุก VM (หรือตั้งค่าให้มัน route ผ่าน VM เครื่องนึงที่รัน Zerotier) จริง ๆ แล้วชอบ Web UI มันนะครับ ดูเหมือนทำงานด้วยสะดวก แต่มันไม่เข้ากับวิธีที่เราใช้ มันก็เลยต้องจากไป

ตัวเลือกต่อไปก็เลยเป็นตัวเลือกเก่าที่เราใช้บริการมานาน นั่นคือ Arch Linux ซึ่งสำหรับคนที่เคยใช้จะรู้ว่าการติดตั้งให้มันทำงานได้นั้นต้องใช้เวลาและความอดทนระดับนึง (เพราะต้องคอนฟิกทุกอย่างที่เราใช้) แต่พอผ่านจุดนึงไปแล้วเราแทบไม่ต้องไปยุ่งอะไรกับมันอีก

Service เดิมที่เคยใช้

Service เดิมที่ผมใช้ประจำ ก็คือ Samba กับ Transmission ซึ่งพอร่วมกับเครื่องที่สเปคสูงขึ้น มันก็รันเร็วขึ้นมากครับ

Samba ตอนนี้สามารถก็อปปี้ไฟล์ได้ด้วยความเร็วเฉี่ยว ๆ 100MB/s ซึ่งก็แทบจะติดเพดานของ Gigabit Ethernet แล้ว ส่วนตัว Transmission เองก็สามารถรองรับ torrent file ได้มากขึ้น ไม่มีอาการดีเลย์ตอนเปิดเครื่องละ

อ้อ ผมใช้ Nginx เป็น Web Server ซึ่งก่อนหน้านี้แทบไม่ได้ใช้อะไรจริงจัง แต่คราวนี้มันจะรับบทเป็นพระเอกในฐานะ Reverse Proxy Server ครับ

Service ใหม่ กับการรองรับกับการพัฒนาซอฟต์แวร์

สำหรับ Service ใหม่ ๆ ตัวแรกที่ผมติดตั้งก่อนเพื่อนคือ Gitea ตัวนี้เป็น Git Hosting Service ที่หน้าตาแทบจะลอก Github มาเลย (ฟีเจอร์ก็ใกล้เคียงกัน) ตัวนี้เขียนบน Go ครับ

Gitea in action

ตัวที่สองคือ Jenkins คือดู CI Server อยู่หลายตัว แล้วก็พบว่า สุดท้ายแล้วก็หนี Industry Standard ไม่พ้น

Jenkins

Jenkins มีหน้าที่บิลด์โค๊ดทุกครั้งที่ผม push อะไรเข้าไปใน Gitea แล้วหลังจากนั้นก็จะ Deploy Code ไปรันบนเครื่องทันที … ฟังดูดีใช่ไหมครับ ปัญหาคือแล้วจะรันยังไงล่ะ ???

ตอนแรกคือลืมคิดไปเลย เพราะว่าถ้าจะให้ Jenkins มันติดตั้งโปรแกรมที่ผมเขียนลงในเครื่องเนี่ย มันต้องมีสิทธิเทียบเท่า Root แล้วผมอ่านวิธี Deploy มันแล้วปวดหมองมาก (ฮา) ผมก็เลยไปเลือกใช้อีกออปชันนึงแทน นั่นคือ Docker เพราะว่ามันอยู่นอกเหนือตัว OS ระบบดังนั้น เราก็เลยไม่ต้องใช้สิทธิระดับ Root (ถึงแม้ว่า docker users จะมีสิทธิค่อนข้างสูงก็ตาม) ก็สามารถติดตั้งได้เหมือนกัน แล้วตอนรันก็แค่เรียก docker run เท่านั้นเอง

ซึ่งตอนนี้มีโปรแกรมนึงที่ผมเขียนด้วย go แล้วสร้างเป็น docker image เก็บไว้ในเครื่อง เป็นโปรแกรมที่จะดึง rss feed มาอ่านแล้วไปเรียก transmission เพื่อเพิ่มไฟล์ torrent ใหม่เข้าไปโดยที่ผมไม่ต้องมานั่งกดเอง โปรแกรมนี้จะตั้งให้รันหลังจากบูทเครื่องแล้ว 15 นาที และรันซ้ำทุกวันที่เวลาเดียวกัน การตั้งค่าให้รันตามเวลานี้ตั้งผ่าน systemd timer ครับ

ส่วนโปรแกรมอื่นเดี๋ยวคงตามมา ผมเริ่มทดลองสร้างอะไรแปลก ๆ ให้รันบนเครื่องนี้บ้างแล้วเหมือนกัน (หลัก ๆ ใช้ Go และ .Net Core) เอาไว้เดี๋ยวเอารูปมาโชว์ครับ แน่นอนว่าผมใช้คนเดียวไม่แบ่งใคร (ฮา)

Performance นอกบ้าน

อันนี้เป็นประเด็นหลักที่ย้ายเครื่องเลย (ฮา) ก็เลยต้องพูดถึงสักหน่อย

  • การก็อปปี้ไฟล์เข้าออกผ่าน Samba ก็ทำได้ถึงราว ๆ 30MB/s ครับ (ขึ้นกับเนตเวิร์คที่เราใช้ด้วย) ผมว่าไม่เลวนะ
  • การ clone/push-pull git อันนีได้ไม่สูงเท่า ก็ได้ราว ๆ 10MB/s (ขึ้นกับเนตเวิร์คเช่นเคย) ถ้าไม่ใช่โปรเจคใหญ่ก็ไม่มีปัญหามั้ง แต่ถ้าใหญ่หน่อยก็อาจจะต้องรอครับ แต่มันไม่ตัดนะ

ผมว่าความเร็วประมาณนี้ถือว่าโอเคนะ คือมันไม่เร็วเท่า Hosting แต่ราคาถูกกว่าแน่ ๆ

ปัญหาที่เจอ

มีปัญหาบ้างเล็กน้อย คือเหมือน Router จะมีปัญหาเวลารันไปนาน ๆ มันดันไปบล็อก zerotier เฉยเลย ก็รีสตาร์ท router ใหม่เท่านั้นเอง

อ้อ ค่าไฟน่าจะเพิ่มขึ้นพอสมควรครับ ห้องอาจจะร้อนบ้าง อันนี้ก็ต้องดูนิดนึง

สรุป

สรุปคือ เนื่องจาก NAS เก่าไม่ตอบโจทย์ความต้องการที่เปลี่ยนไป ผมก็เลยสร้างเครื่องใหม่ขึ้นมาแทน โดยมีสเปคโดยรวมสูงกว่ามาก นอกจากนี้ผมใช้ Zerotier เพื่อเข้าถึงบริการต่าง ๆ จากนอกบ้าน ผ่านระบบเครือข่ายเสมือน (VPN) ทำให้ไม่ต้องใช้ public ip และมีความปลอดภัยมากกว่าเพราะว่าไม่ได้เปิดให้คนภายนอกเข้าถึงได้ ความเร็วที่ใช้จากภายนอกก็ถือว่าไม่ได้เลวร้ายครับ ใช้ได้โอเคเลย

ผมติดตั้งซอฟต์แวร์ที่ใช้กับการพัฒนาซอฟต์แวร์มากขึ้น เป็นวง CI/CD ที่สมบูรณ์สำหรับซอฟต์แวร์เขียนใช้เองบางตัว แล้วตัว git ก็ใช้งานได้ดีกับเงื่อนไขที่เรามี ก็เลยไม่ต้องใช้ github ละ

ข้อเสียก็มีบ้าง ปัญหาก็มีบ้าง แต่โดยรวมกับสองสามเดือนที่ผ่านมาก็ถือว่าแฮปปี้ครับ