Bộ thu gom rác Python là gì và nó hoạt động như thế nào?

Python là một trong những ngôn ngữ lập trình phổ biến nhất hiện nay, nó đứng thứ ba trong “ngôn ngữ TIOBE của năm” vào năm 2021. Tính dễ sử dụng và cộng đồng lớn mạnh của Python đã khiến nó trở thành một ứng dụng phổ biến cho phân tích dữ liệu, ứng dụng web và tự động hóa tác vụ.

Trong bài viết này, tôi sẽ đề cập đến các chi tiết về bộ thu gom rác trong Python. Trước tiên, chúng ta sẽ xem xét các kiến thức cơ bản về quản lý bộ nhớ và lý do tại sao chúng ta cần thu gom rác. Sau đó, đi đến xem xét cách Python thực hiện thu gom rác và cuối cùng là cách bạn nên nghĩ về việc thu gom rác khi viết các ứng dụng Python của mình.

Bộ thu gom rác Python là gì và tại sao chúng ta cần nó?

Nếu Python là ngôn ngữ lập trình đầu tiên của bạn, thì toàn bộ ý tưởng về thu gom rác có thể sẽ xa lạ với bạn. Hãy bắt đầu với những điều cơ bản.

Quản lý bộ nhớ

Một ngôn ngữ lập trình sử dụng các đối tượng trong chương trình của nó để thực hiện các hoạt động. Đối tượng bao gồm các biến đơn giản như strings, integers hoặc boolean. Chúng cũng bao gồm các cấu trúc dữ liệu phức tạp hơn như lists, hashes hoặc classes.

Giá trị của các đối tượng trong chương trình của bạn được lưu trữ trong bộ nhớ để truy cập nhanh. Trong nhiều ngôn ngữ lập trình, một biến trong mã chương trình của bạn chỉ đơn giản là một con trỏ đến địa chỉ của đối tượng trong bộ nhớ. Khi một biến được sử dụng trong một chương trình, quá trình sẽ đọc giá trị từ bộ nhớ và hoạt động trên nó.

Trong các ngôn ngữ lập trình ban đầu, hầu hết các lập trình viên chịu trách nhiệm quản lý toàn bộ bộ nhớ trong các chương trình của họ. Điều này có nghĩa là trước khi tạo một danh sách hoặc một đối tượng, bạn cần cấp phát bộ nhớ cho biến của mình. Sau khi thực hiện xong, bạn cần phân bổ nó để “giải phóng” bộ nhớ cho những người dùng khác.

Điều này có thể dẫn đến hai vấn đề:

  1. Quên giải phóng bộ nhớ. Nếu bạn không giải phóng bộ nhớ của mình khi sử dụng xong. Điều đó có thể dẫn đến rò rỉ bộ nhớ và chương trình của bạn sẽ sử dụng quá nhiều bộ nhớ theo thời gian. Đối với các ứng dụng dài hạn, điều này có thể gây ra sự cố nghiêm trọng.
  2. Giải phóng bộ nhớ quá sớm. Loại vấn đề thứ hai là giải phóng bộ nhớ khi nó vẫn đang được sử dụng. Điều này có thể khiến chương trình của bạn gặp sự cố nếu nó cố gắng truy cập một giá trị trong bộ nhớ không tồn tại hoặc nó có thể làm hỏng dữ liệu. Một biến tham chiếu đến bộ nhớ đã được giải phóng được gọi là con trỏ treo.

Những vấn đề này chắc chắn là điều mà bạn không mong muốn, vì vậy các ngôn ngữ mới hơn đã thêm tính năng quản lý bộ nhớ tự động.

Quản lý bộ nhớ tự động và thu gom rác

Với tính năng quản lý bộ nhớ tự động, các lập trình viên không cần phải tự quản lý bộ nhớ nữa. Đúng hơn, runtime sẽ xử lý điều này cho họ.

Có một số phương pháp khác nhau để quản lý bộ nhớ tự động, phổ biến là sử dụng reference counting. Với reference counting, runtime theo dõi tất cả các tham chiếu đến một đối tượng. Khi một đối tượng không có tham chiếu đến nó, nó sẽ không thể sử dụng được nữa theo mã chương trình và có thể bị xóa.

Đối với các lập trình viên, quản lý bộ nhớ tự động bổ sung một số lợi ích cho họ. Đầu tiên là việc phát triển chương trình mà không cần suy nghĩ về chi tiết bộ nhớ cấp thấp. Hơn nữa, nó có thể giúp họ tránh rò rỉ bộ nhớ hoặc những con trỏ treo nguy hiểm.

Tuy nhiên, quản lý bộ nhớ tự động đi kèm với một cái giá phải trả. Chương trình của bạn sẽ cần sử dụng thêm bộ nhớ và tính toán để theo dõi tất cả các tham chiếu của nó. Hơn nữa, nhiều ngôn ngữ lập trình có tính năng quản lý bộ nhớ tự động sẽ sử dụng quy trình “stop-the-world” để thu gom rác, tất cả các quá trình đang thực thi sẽ bị dừng trong khi trình thu gom rác tìm kiếm và xóa các đối tượng cần thu gom.

Với những tiến bộ trong xử lý máy tính từ định luật Moore và dung lượng RAM lớn hơn trong các máy tính mới hơn, lợi ích của quản lý bộ nhớ tự động thường vượt trội hơn những nhược điểm. Do đó, hầu hết các ngôn ngữ lập trình hiện đại như Java, Python và Golang đều sử dụng tính năng quản lý bộ nhớ tự động này.

Đối với các ứng dụng chạy một thời gian dài, nơi hiệu suất là rất quan trọng, một số ngôn ngữ vẫn có quản lý bộ nhớ thủ công. Ví dụ cổ điển về điều này là C ++, chúng tôi cũng thấy quản lý bộ nhớ thủ công trong Objective-C, ngôn ngữ được sử dụng cho macOS và iOS. Đối với các ngôn ngữ mới hơn, Rust cũng đang sử dụng quản lý bộ nhớ thủ công.

Bây giờ chúng ta đã biết về quản lý bộ nhớ và thu gom rác nói chung, hãy cùng tìm hiểu cụ thể hơn về cách thức hoạt động của tính năng này trong Python.

Hãy thử dùng trình biên dịch mã miễn phí của Stackify, Prefix, để viết mã tốt hơn trên máy trạm của bạn. Prefix hoạt động với .NET, Java, PHP, Node.js, Ruby và Python.

Cách Python thực hiện thu gom rác

Trong phần này, chúng ta sẽ đề cập đến cách thức hoạt động của bộ thu gom rác trong Python.

Phần này giả định rằng bạn đang sử dụng CPython implementation của Python. CPython là cách triển khai đang được sử dụng rộng rãi nhất. Tuy nhiên, vẫn có những cách triển khai khác của Python, chẳng hạn như PyPy, Jython (dựa trên Java) hoặc IronPython (dựa trên C #).

Để xem bạn đang sử dụng Python nào, hãy chạy lệnh sau trong terminal của bạn (Linux):

Có hai khía cạnh để quản lý bộ nhớ và thu gom rác trong Python:
·        Reference counting
·        Generational garbage collection
Hãy khám phá từng khía cạnh này bên dưới.
Reference counting trong Cpython
Cơ chế thu gom rác chính trong CPython là thông qua reference counts. Bất cứ khi nào bạn tạo một đối tượng bằng Python, đối tượng C bên dưới sẽ có cả hai kiểu Python (chẳng hạn như list, dict hoặc function) và reference count.
Ở cấp độ cơ bản, reference count của đối tượng Python được tăng lên bất cứ khi nào đối tượng được tham chiếu và nó sẽ giảm đi khi một đối tượng được tham chiếu. Nếu reference count của một đối tượng là 0, bộ nhớ cho đối tượng sẽ được phân bổ.
Mã chương trình của bạn không thể tắt tính năng Python reference counting. Điều này trái ngược với generational garbage collector được thảo luận dưới đây.
Một số người cho rằng reference counting là một công cụ thu gom rác thấp kém. Nó có một số nhược điểm, bao gồm không có khả năng phát hiện các tham chiếu theo chu kỳ như được thảo luận bên dưới. Tuy nhiên, reference counting là tốt vì bạn có thể xóa ngay một đối tượng khi nó không có tham chiếu.
Xem reference counts trong Python
Bạn có thể sử dụng mô-đun sys từ thư viện chuẩn Python để kiểm tra reference counts cho một đối tượng cụ thể. Có một số cách để tăng reference count cho một đối tượng, chẳng hạn như:
·        Gán một đối tượng cho một biến.
·        Thêm một đối tượng vào cấu trúc dữ liệu, chẳng hạn như thêm vào list hoặc thêm dưới dạng một thuộc tính trên một class instance.
·        Truyền đối tượng làm đối số cho một hàm.
Hãy sử dụng REPL Python và mô-đun sys để xem cách xử lý reference counts.
Đầu tiên, trong terminal của bạn, hãy nhập python vào Python REPL.
Thứ hai, nhập mô-đun sys vào REPL của bạn. Sau đó, tạo một biến và kiểm tra reference count của nó:
Lưu ý rằng có hai tham chiếu đến biến a của chúng ta. Một là tạo biến, hai là khi chúng ta truyền biến a vào hàm sys.getrefcount().
Nếu bạn thêm biến vào cấu trúc dữ liệu, chẳng hạn như list hoặc dictionary, bạn sẽ thấy reference count tăng lên:
Như đã hiển thị ở trên, reference count tăng lên khi được thêm vào list hoặc dictionary.
Trong phần tiếp theo, chúng ta sẽ tìm hiểu về generational garbage collector, là công cụ thứ hai mà Python sử dụng để quản lý bộ nhớ.
Generational garbage collection
Ngoài chiến lược reference counting để quản lý bộ nhớ, Python cũng sử dụng một phương pháp được gọi là generational garbage collector.
Cách dễ nhất để hiểu tại sao chúng ta cần generational garbage collector là lấy ví dụ.
Trong phần trước, chúng ta đã thấy rằng việc thêm một đối tượng vào một mảng hoặc một đối tượng sẽ làm tăng reference count của nó. Nhưng điều gì sẽ xảy ra nếu bạn thêm một đối tượng vào chính nó?
Trong ví dụ trên, chúng ta đã xác định một lớp mới. Sau đó, chúng ta tạo một thể hiện của lớp và gán cá thể đó là một thuộc tính của chính nó. Cuối cùng, chúng ta xóa phiên bản.
Bằng cách xóa, phiên bản đó không còn có thể truy cập được trong chương trình Python của chúng ta nữa. Tuy nhiên, Python không xóa phiên bản này khỏi bộ nhớ bởi vì nó không có số lượng tham chiếu bằng 0 (có một tham chiếu đến chính nó).
Chúng ta gọi loại vấn đề này là reference cycle và bạn không thể giải quyết nó bằng reference counting. Đây là đặc điểm của generational garbage collector, có thể truy cập được bằng mô-đun gc trong thư viện chuẩn.
Thuật ngữ generational garbage collector
Có hai khái niệm chính mà chúng ta cần hiểu với generational garbage collector:
1.      Khái niệm đầu tiên là của một thế hệ.
2.      Khái niệm chính thứ hai là ngưỡng.
Bộ thu gom rác đang theo dõi tất cả các đối tượng trong bộ nhớ. Một đối tượng mới bắt đầu vòng đời của nó trong thế hệ đầu tiên của bộ thu gom rác. Nếu Python thực hiện quy trình garbage collection trên một thế hệ và một đối tượng tồn tại, nó sẽ chuyển sang thế hệ thứ hai, cũ hơn. Trình thu gom rác Python có tổng cộng ba thế hệ, một đối tượng sẽ chuyển sang thế hệ cũ hơn bất cứ khi nào nó vẫn tồn tại sau quá trình thu gom rác trên thế hệ hiện tại.
Đối với mỗi thế hệ, mô-đun thu gom rác có một số ngưỡng đối tượng nhất định. Nếu số lượng đối tượng vượt quá ngưỡng đó, bộ thu gom rác sẽ kích hoạt quá trình thu gom. Đối với bất kỳ đối tượng nào vẫn còn tồn tại trong quá trình đó, chúng sẽ được chuyển sang thế hệ cũ hơn.
Không giống như cơ chế reference counting, bạn có thể thay đổi hành vi của generational garbage collector trong chương trình Python của mình. Điều này bao gồm việc thay đổi các ngưỡng để kích hoạt quy trình thu gom rác trong mã của bạn. Ngoài ra, bạn có thể kích hoạt quy trình thu gom rác theo cách thủ công hoặc tắt nó hoàn toàn.
Hãy xem cách bạn có thể sử dụng mô-đun gc để kiểm tra thống kê thu gom rác hoặc thay đổi hành vi của trình thu gom rác.
Sử dụng mô-đun GC
Trong terminal của bạn, nhập python để thả vào Python REPL.
Nhập mô-đun gc vào session của bạn. Sau đó, bạn có thể kiểm tra các ngưỡng đã định cấu hình của trình thu gom rác bằng phương thức get_threshold():

Theo mặc định, Python có ngưỡng 700 cho thế hệ mới nhất và 10 cho mỗi thế hệ trong số hai thế hệ cũ.

Bạn có thể kiểm tra số lượng đối tượng trong mỗi thế hệ của mình bằng phương thức get_count():

Trong ví dụ này, chúng ta có 596 đối tượng ở thế hệ mới nhất, hai đối tượng ở thế hệ tiếp theo và một đối tượng ở thế hệ cũ nhất.

Như bạn thấy, Python tạo ra một số đối tượng theo mặc định trước khi bạn bắt đầu thực thi chương trình của mình. Bạn có thể kích hoạt quy trình thu gom rác thủ công bằng cách sử dụng phương thức gc.collect():

Chạy quy trình thu gom rác sẽ dọn dẹp một lượng lớn các đối tượng – có 577 đối tượng ở thế hệ đầu tiên và ba đối tượng khác ở các thế hệ cũ hơn.
Bạn có thể thay đổi các ngưỡng để kích hoạt trình thu gom rác bằng cách sử dụng phương thức set_threshold() trong mô-đun gc:

Trong ví dụ trên, chúng ta đã tăng từng ngưỡng từ các ngưỡng mặc định của chúng. Việc tăng ngưỡng này sẽ làm giảm tần suất hoạt động của bộ thu gom rác. Điều này sẽ ít tốn kém hơn về mặt tính toán nhưng bạn lại phải giữ các đối tượng chết tồn tại lâu hơn trong chương trình của mình.

Bây giờ bạn đã biết reference counting và mô-đun thu gom rác hoạt động như thế nào, hãy thảo luận về cách bạn nên áp dụng điều này khi viết các ứng dụng Python của mình.

Với tư cách là lập trình viên, trình thu gom rác của Python có ý nghĩa gì đối với bạn

Chúng ta đã dành một chút thời gian để thảo luận về quản lý bộ nhớ nói chung và việc triển khai nó bằng Python nói riêng. Bây giờ đã đến lúc làm cho nó trở nên hữu ích. Bạn nên sử dụng thông tin này như thế nào với tư cách là lập trình viên các chương trình Python?

Quy tắc chung: Không thay đổi hành vi của trình thu gom rác

Theo nguyên tắc chung, có lẽ bạn không nên nghĩ về việc thu gom rác của Python quá nhiều. Một trong những lợi ích chính của Python là nó cho phép các lập trình viên gia tăng năng suất. Một phần lý do cho điều này là vì đây là ngôn ngữ cấp cao xử lý một số các chi tiết cấp thấp cho các lập trình viên.

Quản lý bộ nhớ thủ công phù hợp hơn với các môi trường hạn chế. Nếu bạn thấy mình có những hạn chế về hiệu suất mà bạn cho rằng có thể liên quan đến cơ chế thu gom rác của Python, thì việc tăng sức mạnh cho môi trường thực thi của bạn sẽ hữu ích hơn thay vì thay đổi quy trình thu gom rác theo cách thủ công. Trong thế giới của định luật Moore, điện toán đám mây và cheap memory, người dùng có thể dễ dàng truy cập đến nhiều nguồn năng lượng hơn.

Điều này thậm chí còn là thực tế vì Python thường không giải phóng bộ nhớ trở lại hệ điều hành cơ bản. Bất kỳ quy trình thu gom rác thủ công nào bạn thực hiện để giải phóng bộ nhớ có thể không mang lại cho bạn kết quả như mong muốn. Để biết thêm chi tiết về lĩnh vực này, hãy tham khảo bài đăng về quản lý bộ nhớ bằng Python.

Tắt bộ thu gom rác

Để điều đó sang một bên, có những tình huống mà bạn có thể muốn tự quản lý quá trình thu gom rác. Hãy nhớ rằng không thể tắt tính năng reference counting, cơ chế thu gom rác chính trong Python. Hành vi thu gom rác duy nhất mà bạn có thể thay đổi là generational garbage collector trong mô-đun gc.

Một trong những ví dụ thú vị hơn về việc thay đổi generational garbage collector đến từ việc Instagram đã tắt hoàn toàn trình thu gom rác.

Instagram sử dụng Django, một khuôn khổ web Python phổ biến cho các ứng dụng web của mình. Nó chạy nhiều phiên bản của ứng dụng web trên một phiên bản máy tính duy nhất. Các phiên bản này được chạy bằng cơ chế master-child trong đó các bộ xử lý con chia sẻ bộ nhớ với bộ xử lý chính.

Nhóm nhà phát triển Instagram nhận thấy rằng bộ nhớ được chia sẻ sẽ giảm mạnh ngay sau khi một child process sinh ra. Khi đào sâu hơn, họ nhận thấy rằng nguyên nhân đến từ trình thu gom rác.

Instagram đã vô hiệu hóa mô-đun thu gom rác bằng cách đặt ngưỡng cho tất cả các thế hệ thành 0. Sự thay đổi này đã giúp các ứng dụng web của họ chạy hiệu quả hơn 10%.

Mặc dù ví dụ này rất thú vị, nhưng hãy đảm bảo rằng bạn đang ở trong tình huống tương tự trước khi đi theo con đường này. Instagram là một ứng dụng quy mô web phục vụ hàng triệu người dùng. Đối với họ, việc sử dụng một số hành vi không theo tiêu chuẩn để tận dụng từng inch hiệu suất từ các ứng dụng web là rất xứng đáng. Nhưng đối với hầu hết các lập trình viên, hành vi tiêu chuẩn của Python xung quanh việc thu gom rác là vừa đủ.

Nếu bạn nghĩ rằng, bạn có thể muốn quản lý thủ công trình thu gom rác bằng Python, đảm bảo rằng bạn hiểu vấn đề trước. Hãy sử dụng các công cụ như Stackify’s Retrace để đo lường hiệu suất ứng dụng và xác định các vấn đề. Sau khi bạn hiểu đầy đủ vấn đề, hãy thực hiện các bước để khắc phục nó.

Phần kết

Trong bài viết này, chúng ta đã tìm hiểu về trình thu gom rác trong Python. Bắt đầu bằng những kiến thức cơ bản về quản lý bộ nhớ và tạo ra tính năng quản lý bộ nhớ tự động. Sau đó, xem xét cách thực hiện thu gom rác bằng Python thông qua automatic reference counting và generational garbage collector. Cuối cùng, với tư cách là một lập trình viên Python, chúng ta đã xem xét vấn đề này là quan trọng như thế nào.

Mặc dù Python đã xử lý hầu hết các phần khó trong quản lý bộ nhớ, nhưng vẫn sẽ hữu ích nếu bạn biết điều gì đang xảy ra. Sau khi đọc bài viết này, tôi hy vọng rằng bạn đã biết mình nên tránh các chu trình tham chiếu nào trong Python và biết nơi để tham khảo nếu cần kiểm soát nhiều hơn đối với trình thu gom rác của bạn.

Viết một bình luận