0
0
Lập trình
Flame Kris
Flame Krisbacodekiller

Tại sao Rust không cần Garbage Collector?

Đăng vào 3 ngày trước

• 9 phút đọc

Giới thiệu

Nếu bạn đến từ ngôn ngữ như Python hoặc JavaScript, bạn đã quen với việc không phải lo lắng về bộ nhớ. Khi bạn hoàn thành với một giá trị, bộ thu gom rác sẽ tự động giải phóng bộ nhớ cho bạn. Nó hoàn toàn tự động và hầu hết thời gian, nó hoạt động rất tốt.

Vậy bạn có thể đang nghĩ: Làm thế nào mà một ngôn ngữ cấp thấp, kiểu tĩnh như Rust có thể làm điều tương tự - nhưng không cần bộ thu gom rác?

Câu trả lời nằm ở hai điều: RAIIquản lý phạm vi nghiêm ngặt.

RAII là viết tắt của Resource Acquisition Is Initialization - một cách nói khác về việc:

  • Khi một đối tượng được tạo ra, nó cũng sẽ lấy bất kỳ tài nguyên nào mà nó cần (như bộ nhớ), và khi nó ra khỏi phạm vi, những tài nguyên đó sẽ tự động được giải phóng.

Trong C, bạn cần nhớ để free() bộ nhớ đó. Quên điều đó có thể gây ra rò rỉ hoặc tệ hơn. Nhưng trong Rust, việc dọn dẹp này được thực hiện cho bạn - tự động và an toàn - bằng cách sử dụng một tính năng đặc biệt gọi là Drop trait.

Trait Drop là gì?

Nói một cách đơn giản, trait Drop là một trait cho Rust biết phải làm gì khi một giá trị ra khỏi phạm vi và cần được dọn dẹp.

Mỗi khi bạn gọi drop(x), hoặc một biến đơn giản ra khỏi phạm vi ở cuối một hàm hoặc khối, Rust sẽ chạy mã được định nghĩa trong phần cài đặt trait Drop của giá trị đó.

Điều này cho phép Rust tự động giải phóng bộ nhớ và các tài nguyên khác không cần bộ thu gom rác.

Phần hay nhất? Hầu hết các kiểu trong thư viện chuẩn - như String, Vec, HashMap, và thậm chí cả các kiểu nguyên thủy - đã triển khai Drop. Nhưng bạn cũng có thể triển khai nó cho các kiểu của riêng bạn, và điều này mở ra một thế giới các mẫu mạnh mẽ cho quản lý tài nguyên, ghi log, dọn dẹp và nhiều hơn nữa.

Drop hoạt động như thế nào?

Bạn có thể đã thấy các khả năng, nhưng trước khi đi vào sử dụng thực tế, tốt nhất là hiểu cách mà tính năng này hoạt động bên trong. Hãy cùng tìm hiểu nhé.

Khi một biến ra khỏi phạm vi, Rust sẽ chạy mã mà bạn đã viết trong phần thực hiện trait Drop của bạn. Điều này cho phép bạn làm những việc như tắt các tác vụ nền một cách mềm mại, lưu trữ dữ liệu vào đĩa, xử lý tín hiệu hệ thống, hoặc chỉ đơn giản là dọn dẹp tài nguyên theo cách có thể dự đoán.

Nhưng nó không dừng lại ở đó.

Bạn có thể đang tự hỏi: "Rust có giải phóng bộ nhớ sau khi chạy drop() của tôi không?" Và câu trả lời là: Không - bạn không cần phải làm điều đó. Rust thực hiện phần khó cho bạn.

Sau khi chạy mã Drop tùy chỉnh của bạn (nếu bạn đã triển khai một cái), Rust sẽ tự động gọi drop() cho tất cả các trường của struct của bạn, theo thứ tự ngược lại của khai báo. Vì vậy, nếu bạn có:

Copy
struct MyStruct {
    first: i32,
    second: i32,
    third: i32,
}

Rust sẽ giải phóng third trước, sau đó là second, và cuối cùng là first.

  • Bạn muốn thử nghiệm? Thêm một println!() hoặc thread::sleep() trong mỗi trường hợp và quan sát thứ tự. Bạn vừa có được bài tập đầu tiên của mình.

Vì hầu hết các kiểu nguyên thủy và bản địa đã triển khai Drop đúng cách - giải phóng bộ nhớ, đóng các tay cầm tệp, giải phóng tài nguyên hệ điều hành - bạn thường không cần phải lo lắng về điều này.

Tại sao chúng ta cần sử dụng Drop?

Không, bạn vẫn chưa đến đích của mình. Đùa thôi - nhưng thật sự, nếu Rust đã thực hiện việc dọn dẹp cho bạn, tại sao bạn lại cần quan tâm đến việc triển khai Drop cho chính mình?

Chà, có khoảng 32.5 lý do tốt để sử dụng Drop. Nhưng nói chung, nó hữu ích bất kỳ khi nào bạn muốn thực hiện công việc bổ sung trước khi một thứ gì đó được dọn dẹp.

Ví dụ, giả sử bạn có một struct *Logger *** sống trong toàn bộ thời gian của chương trình. Khi nó được giải phóng, bạn có thể muốn lưu trữ nhật ký vào đĩa. Đó là điều mà bạn có thể làm bên trong một thực hiện Drop - mà không cần phải nhớ để gọi một hàm tắt thủ công.

Tôi thực sự đã làm điều tương tự trong một dự án gọi là Jomify. Không hoàn toàn giống nhau, nhưng cùng nguyên tắc.

Trait Drop được sử dụng khắp thư viện chuẩn của Rust:

  • Tự động mở khóa mutex

  • Dọn dẹp yêu cầu mạng trong các thư viện HTTP

  • Đảm bảo tắt mềm mại trong các framework web

  • Và quan trọng nhất: ghi log và gỡ lỗi

Hãy tưởng tượng một máy chủ bị panic một cách bất ngờ. Bạn có một tay cầm cơ sở dữ liệu tùy chỉnh, và nếu nó được giải phóng với một trạng thái nội bộ như state = 0, điều đó có thể chỉ ra một vấn đề. Với một thực hiện Drop, bạn có thể ghi lại trạng thái đó tự động ngay khi tay cầm ra khỏi phạm vi - mà không cần bất kỳ logic nào thêm trong đường dẫn mã chính của bạn.

  • Bạn thậm chí có thể kết nối với một hệ thống ghi log như AWS CloudWatch , ELK Stack , hoặc bất kỳ công cụ tổng hợp log nào, và sử dụng một EventBridge rule để kích hoạt cảnh báo hoặc các hàm Lambda khi các mục log xuất hiện vào thời điểm giải phóng cụ thể. Điều này biến các sự kiện giải phóng thành các tín hiệu hạ tầng mạnh mẽ - đặc biệt cho việc gỡ lỗi hoặc kích hoạt các hành động bù đắp.

Thật tuyệt vời.

Làm thế nào để chúng ta thực sự triển khai trait Drop?

Giống như bất kỳ trait nào khác.

Bạn viết impl Drop for YourType, và sau đó định nghĩa một hàm duy nhất: fn drop(&mut self).

Dưới đây là một ví dụ cơ bản:

Copy
struct ExampleStruct;

impl Drop for ExampleStruct {
    fn drop(&mut self) {
        println!("Đang giải phóng ở đây");
    }
}

Đó là tất cả. Ngay khi *ExampleStruct *** ra khỏi phạm vi, mã *drop() *** của bạn sẽ chạy.

Có thể bạn không nên triển khai Drop; Một số lưu ý

Dù đơn giản như nó có vẻ, có một số điều cần cẩn thận khi viết một thực hiện Drop:

  1. Không triển khai Drop cho các kiểu mà kế thừa Copy Rust đơn giản cấm điều này. Không phải vì lý do kỹ thuật, mà để tránh sự mơ hồ. Hãy tưởng tượng có hai bản sao giống hệt nhau của một giá trị - cái nào sẽ Drop chạy trên? Rust từ chối thậm chí xem xét kịch bản đó. Không có tang lễ đôi cho những bản sao đã sao chép.

  2. Không triển khai generic Drop<T> Rust không cho phép các thực hiện Drop generic như impl Drop for MyStruct. Điều này là do các xung đột monomorphization. Bạn phải cụ thể - ví dụ, *impl Drop for MyStruct<String>.* *** Vâng, điều đó có nghĩa là bạn sẽ phải lặp lại cho mỗi phiên bản mà bạn quan tâm.

  3. Không panic trong một drop() hàm Xin đừng. Panic trong drop() giống như la hét trong một chiếc taxi trong khi tài xế đang đậu - hỗn loạn và không cần thiết. Nó có thể làm sập toàn bộ chương trình hoặc kích hoạt những hành vi kỳ lạ khác.

  4. Không gọi .drop() thủ công - hãy sử dụng std::mem::drop() Nếu bạn muốn giải phóng một giá trị sớm, đừng gọi phương thức drop() của nó trực tiếp. Rust cũng sẽ không cho phép bạn việc đó. Thay vào đó, sử dụng std::mem::drop(value). Điều này chuyển quyền sở hữu một cách an toàn, chạy trait Drop, và ngăn chặn việc sử dụng giá trị đó thêm.

  5. Đáng lưu ý: drop(&mut self) nhận một tham chiếu biến, điều này đảm bảo rằng bạn không thể vô tình giải phóng cùng một giá trị hai lần. Đó cũng là lý do tại sao Rust không cho phép Drop + Copy - điều này sẽ phá vỡ lời hứa này.

Tóm lại, đây chỉ là một hook khác

Bạn có thể đổi tên Drop thành *Cleanup***, và điều đó sẽ hoàn toàn hợp lý. Nó thực chất là một hook vào quy trình dọn dẹp tự động của Rust. Rust xử lý công việc khó khăn về bộ nhớ - và bạn có một hook để thực hiện logic bổ sung khi một thứ gì đó sắp được dọn dẹp.

Một vài lưu ý nâng cao:

  • Rust hiện tại không hỗ trợ async Drop, mặc dù điều này đang được thảo luận.

  • Bạn có thể bỏ qua Drop bằng cách sử dụng các công cụ như std::mem::forget() hoặc ManuallyDrop.

  • Nếu bạn đang làm việc với con trỏ thô, các khối unsafe, hoặc các bộ phân bổ tùy chỉnh, thì trách nhiệm dọn dẹp rơi vào bạn - và Drop trở thành một công cụ mạnh mẽ trong bộ công cụ của bạn.

Bạn muốn đọc thêm?

Nếu bạn tò mò hoặc chỉ muốn đào sâu hơn về cách mà Drop thực sự hoạt động phía sau, hãy tham khảo những nguồn sau:

  • The Rust Book – Drop – Giải thích dễ hiểu từ sách Rust chính thức.

  • The Rustonomicon – Drop – Nói về hành vi không an toàn và cấp thấp của Drop, hữu ích nếu bạn muốn biết điều gì thực sự xảy ra.

  • std::mem::drop – Hàm drop thực tế mà bạn sử dụng khi muốn giải phóng một giá trị thủ công.

  • ManuallyDrop – Đối với các trường hợp mà bạn muốn kiểm soát chính xác khi nào các thứ được dọn dẹp (hoặc không).

  • RFC 3201 – Async Drop – Nói về ý tưởng làm cho Drop hỗ trợ async. Chưa có trong Rust nhưng có thể trong tương lai.

Bạn không cần phải đọc chúng, nhưng hãy tự do chia sẻ những gì bạn học được! Tôi đoán rằng đó là tất cả cho bây giờ, hẹn gặp lại tuần sau.

Chúc bạn một ngày tuyệt vời!!!

Tác giả: Ugochukwu Chizaram


Cảm ơn bạn đã là một phần của cộng đồng

Trước khi bạn đi:

Khi nào bạn đã sẵn sàng

Có 4 cách chúng tôi có thể giúp bạn trở thành một kỹ sư backend tuyệt vời:

  • Nền tảng MB: Tham gia cùng hàng ngàn kỹ sư backend học về kỹ thuật backend. Xây dựng các dự án backend thực tế, học từ các khóa học và lộ trình được đánh giá bởi chuyên gia, theo dõi việc học của bạn và đặt lịch trình, và giải quyết các bài tập, bài kiểm tra và thử thách kỹ thuật backend.
  • Học viện MB: "Học viện MB" là một khóa học Boot Camp kỹ thuật Backend nâng cao kéo dài 6 tháng để sản xuất ra các kỹ sư backend tuyệt vời.
  • Tham gia Backend Weekly: Nếu bạn thích các bài viết như thế này, bạn chắc chắn sẽ thích bản tin hàng tuần độc quyền của chúng tôi, chia sẻ các tài nguyên kỹ thuật backend độc quyền giúp bạn trở thành một Kỹ Sư Backend tuyệt vời.
  • Tìm việc làm Backend: Tìm hơn 2.000+ việc làm Backend từ xa được điều chỉnh quốc tế hoặc tiếp cận 50.000+ kỹ sư backend trên bảng việc làm Kỹ Thuật Backend số 1.
Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào