Ý nghĩa của việc mua lại tài nguyên là Khởi tạo (RAII) là gì?


Câu trả lời:


374

Đó là một cái tên thực sự khủng khiếp cho một khái niệm cực kỳ mạnh mẽ và có lẽ là một trong những điều số 1 mà các nhà phát triển C ++ bỏ lỡ khi họ chuyển sang các ngôn ngữ khác. Đã có một chút phong trào để cố gắng đổi tên khái niệm này thành Quản lý tài nguyên giới hạn phạm vi , mặc dù nó dường như chưa bắt kịp.

Khi chúng ta nói 'Tài nguyên', chúng ta không chỉ có nghĩa là bộ nhớ - đó có thể là xử lý tệp, ổ cắm mạng, xử lý cơ sở dữ liệu, đối tượng GDI ... Tóm lại, những thứ mà chúng ta có nguồn cung cấp hữu hạn và vì vậy chúng ta cần có khả năng kiểm soát việc sử dụng của họ. Khía cạnh 'Phạm vi giới hạn' có nghĩa là thời gian tồn tại của đối tượng bị ràng buộc với phạm vi của một biến, do đó, khi biến đó nằm ngoài phạm vi thì hàm hủy sẽ giải phóng tài nguyên. Một đặc tính rất hữu ích của việc này là nó tạo ra sự an toàn ngoại lệ lớn hơn. Ví dụ, so sánh điều này:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Với RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

Trong trường hợp sau này, khi ngoại lệ được ném và ngăn xếp không được xử lý, các biến cục bộ sẽ bị hủy để đảm bảo rằng tài nguyên của chúng ta được dọn sạch và không bị rò rỉ.


2
@the_mandrill: Tôi đã thử ideone.com/1Jjzuc chương trình này. Nhưng không có cuộc gọi hủy diệt. Tomdalling.com/blog/software-design/ từ nói rằng C ++ đảm bảo rằng hàm hủy của các đối tượng trên ngăn xếp sẽ được gọi, ngay cả khi một ngoại lệ được ném ra. Vì vậy, tại sao tàu khu trục không thực hiện ở đây? Là tài nguyên của tôi bị rò rỉ hoặc nó sẽ không bao giờ được giải phóng hoặc phát hành?
Kẻ hủy diệt

8
Một ngoại lệ được đưa ra, nhưng bạn không bắt được nó, vì vậy ứng dụng chấm dứt. Nếu bạn kết thúc bằng một thử {} Catch () {} thì nó hoạt động như mong đợi: ideone.com/xm2GR9
the_mandrill

2
Không hoàn toàn chắc chắn nếu Scope-Boundlà lựa chọn tên tốt nhất ở đây vì các chỉ định lớp lưu trữ cùng với phạm vi xác định thời lượng lưu trữ của một thực thể. Thu hẹp nó được thực hiện để giới hạn phạm vi có thể là một sự đơn giản hóa hữu ích, tuy nhiên nó không chính xác 100%
SebNag

125

Đây là một thành ngữ lập trình có nghĩa ngắn gọn là bạn

  • đóng gói một tài nguyên vào một lớp (mà hàm tạo của nó thường - nhưng không nhất thiết là ** - có được tài nguyên đó và hàm hủy của nó luôn giải phóng nó)
  • sử dụng tài nguyên thông qua một thể hiện cục bộ của lớp *
  • tài nguyên được tự động giải phóng khi đối tượng ra khỏi phạm vi

Điều này đảm bảo rằng bất cứ điều gì xảy ra trong khi tài nguyên đang được sử dụng, cuối cùng nó sẽ được giải phóng (cho dù là trả lại bình thường, phá hủy đối tượng chứa hoặc ném ngoại lệ).

Đó là một cách thực hành tốt được sử dụng rộng rãi trong C ++, vì ngoài cách an toàn để xử lý tài nguyên, nó còn giúp mã của bạn sạch hơn rất nhiều vì bạn không cần trộn mã xử lý lỗi với chức năng chính.

* Cập nhật: "cục bộ" có thể có nghĩa là một biến cục bộ hoặc một biến thành viên không phải là thành viên của một lớp. Trong trường hợp sau, biến thành viên được khởi tạo và hủy với đối tượng chủ sở hữu của nó.

** Update2: như @sbi đã chỉ ra, tài nguyên - mặc dù thường được phân bổ bên trong hàm tạo - cũng có thể được phân bổ bên ngoài và được truyền vào dưới dạng tham số.


1
AFAIK, từ viết tắt không ngụ ý đối tượng phải nằm trên biến cục bộ (stack). Nó có thể là biến thành viên của một đối tượng khác, vì vậy khi đối tượng 'giữ' bị phá hủy, đối tượng thành viên cũng bị phá hủy và tài nguyên được giải phóng. Trên thực tế, tôi nghĩ từ viết tắt chỉ có nghĩa là không có open()/ close()phương thức để khởi tạo và giải phóng tài nguyên, chỉ là hàm tạo và hàm hủy, do đó, 'giữ' tài nguyên chỉ là thời gian tồn tại của đối tượng, bất kể thời gian tồn tại đó là được xử lý bởi bối cảnh (ngăn xếp) hoặc rõ ràng (phân bổ động)
Javier

1
Trên thực tế không có gì nói tài nguyên phải được mua trong hàm tạo. Luồng tệp, chuỗi một bộ chứa khác làm điều đó, nhưng tài nguyên cũng có thể được chuyển đến hàm tạo, như thường thấy với các con trỏ thông minh. Vì câu trả lời của bạn là câu trả lời được đánh giá cao nhất, bạn có thể muốn khắc phục điều này.
sbi

Nó không phải là từ viết tắt, nó là viết tắt. Hầu hết mọi người đều phát âm nó là "ar ey ay ay" vì vậy nó không thực sự đủ điều kiện cho một từ viết tắt như nói DARPA, được phát âm là DARPA thay vì đánh vần. Ngoài ra, tôi muốn nói RAII là một mô hình chứ không phải là một thành ngữ đơn thuần.
dtech

@Peter Torok: Tôi đã thử ideone.com/1Jjzuc chương trình này. Nhưng không có cuộc gọi hủy diệt. Các tomdalling.com/blog/software-design/... nói rằng C ++ đảm bảo rằng các destructor của các đối tượng trên stack sẽ được gọi là, ngay cả khi một ngoại lệ được ném. Vì vậy, tại sao tàu khu trục không thực hiện ở đây? Là tài nguyên của tôi bị rò rỉ hoặc nó sẽ không bao giờ được giải phóng hoặc phát hành?
Kẻ hủy diệt

50

"RAII" là viết tắt của "Thu nhận tài nguyên là khởi tạo" và thực sự là một cách hiểu sai, vì nó không phải là mua lại tài nguyên (và khởi tạo một đối tượng) mà nó quan tâm, nhưng giải phóng tài nguyên (bằng cách phá hủy một đối tượng ).
Nhưng RAII là tên chúng tôi có và nó dính vào.

Về cơ bản, thành ngữ này bao gồm các tài nguyên (khối bộ nhớ, tệp mở, mutexes đã mở khóa, tên bạn) trong các đối tượng tự động, cục bộ và có hàm hủy của đối tượng đó giải phóng tài nguyên khi đối tượng bị phá hủy tại cuối phạm vi nó thuộc về:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Tất nhiên, các đối tượng không phải lúc nào cũng là đối tượng tự động cục bộ. Họ cũng có thể là thành viên của một lớp:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Nếu các đối tượng như vậy quản lý bộ nhớ, chúng thường được gọi là "con trỏ thông minh".

Có nhiều biến thể của điều này. Ví dụ, trong đoạn mã đầu tiên, câu hỏi đặt ra điều gì sẽ xảy ra nếu ai đó muốn sao chép obj. Cách dễ nhất sẽ chỉ đơn giản là không cho phép sao chép. std::unique_ptr<>, một con trỏ thông minh là một phần của thư viện tiêu chuẩn như đặc trưng của tiêu chuẩn C ++ tiếp theo, thực hiện điều này.
Một con trỏ thông minh như vậy, std::shared_ptrcó tính năng "sở hữu chung" của tài nguyên (một đối tượng được phân bổ động) mà nó nắm giữ. Đó là, nó có thể được sao chép tự do và tất cả các bản sao đề cập đến cùng một đối tượng. Con trỏ thông minh theo dõi có bao nhiêu bản sao đề cập đến cùng một đối tượng và sẽ xóa nó khi bản cuối cùng bị hủy.
Một biến thể thứ ba được đặc trưng bởistd::auto_ptr trong đó thực hiện một loại ngữ nghĩa di chuyển: Một đối tượng chỉ được sở hữu bởi một con trỏ và cố gắng sao chép một đối tượng sẽ dẫn đến (thông qua cú pháp cú pháp) trong việc chuyển quyền sở hữu đối tượng sang mục tiêu của hoạt động sao chép.


4
std::auto_ptrlà phiên bản lỗi thời của std::unique_ptr. std::auto_ptrloại ngữ nghĩa di chuyển mô phỏng càng nhiều càng tốt trong C ++ 98, std::unique_ptrsử dụng ngữ nghĩa di chuyển mới của C ++ 11. Lớp mới đã được tạo vì ngữ nghĩa di chuyển của C ++ 11 rõ ràng hơn (yêu cầu std::movengoại trừ tạm thời) trong khi nó được mặc định cho bất kỳ bản sao nào từ non-const in std::auto_ptr.
Jan Hudec

@JiahaoCai: Một lần, nhiều năm trước (trên Usenet), chính Stroustrup đã nói như vậy.
sbi

21

Tuổi thọ của một đối tượng được xác định bởi phạm vi của nó. Tuy nhiên, đôi khi chúng ta cần, hoặc nó hữu ích, để tạo một đối tượng sống độc lập với phạm vi nơi nó được tạo. Trong C ++, toán tử newđược sử dụng để tạo một đối tượng như vậy. Và để tiêu diệt đối tượng, toán tử deletecó thể được sử dụng. Các đối tượng được tạo bởi toán tử newđược phân bổ động, tức là được phân bổ trong bộ nhớ động (còn được gọi là heap hoặc lưu trữ miễn phí ). Vì vậy, một đối tượng được tạo bởi newsẽ tiếp tục tồn tại cho đến khi nó bị phá hủy rõ ràng bằng cách sử dụng delete.

Một số lỗi có thể xảy ra khi sử dụng newdeletelà:

  • Đối tượng bị rò rỉ (hoặc bộ nhớ): sử dụng newđể phân bổ một đối tượng và quên deleteđối tượng.
  • Xóa sớm (hoặc tham chiếu lơ lửng ): giữ một con trỏ khác đến một đối tượng, deleteđối tượng và sau đó sử dụng con trỏ khác.
  • Xóa đôi : cố gắng deletemột đối tượng hai lần.

Nói chung, các biến phạm vi được ưa thích. Tuy nhiên, RAII có thể được sử dụng thay thế newdeleteđể làm cho một đối tượng sống độc lập với phạm vi của nó. Một kỹ thuật như vậy bao gồm việc đưa con trỏ đến đối tượng được phân bổ trên heap và đặt nó vào một đối tượng xử lý / quản lý . Cái sau có một hàm hủy sẽ đảm nhiệm việc tiêu diệt đối tượng. Điều này sẽ đảm bảo rằng đối tượng có sẵn cho bất kỳ chức năng nào muốn truy cập vào nó và đối tượng bị hủy khi thời gian tồn tại của đối tượng xử lý kết thúc mà không cần dọn dẹp rõ ràng.

Các ví dụ từ thư viện chuẩn C ++ sử dụng RAII là std::stringstd::vector.

Hãy xem xét đoạn mã này:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

Khi bạn tạo một vectơ và bạn đẩy các phần tử đến nó, bạn không quan tâm đến việc phân bổ và giải quyết các phần tử đó. Vectơ sử dụng newđể phân bổ không gian cho các phần tử của nó trên heap và deleteđể giải phóng không gian đó. Bạn là người sử dụng vectơ, bạn không quan tâm đến các chi tiết triển khai và sẽ tin tưởng vectơ không bị rò rỉ. Trong trường hợp này, vectơ là đối tượng xử lý các phần tử của nó.

Những ví dụ khác từ thư viện chuẩn RAII sử dụng nằm std::shared_ptr, std::unique_ptrstd::lock_guard.

Một tên khác cho kỹ thuật này là SBRM , viết tắt của Scope-Bound Resource Management .


1
"SBRM" có ý nghĩa hơn đối với tôi. Tôi đã đi đến câu hỏi này bởi vì tôi nghĩ rằng tôi hiểu RAII nhưng cái tên đã làm tôi thất vọng, thay vào đó nghe nó được mô tả là "Quản lý tài nguyên giới hạn phạm vi" khiến tôi nhận ra ngay rằng tôi thực sự hiểu khái niệm này.
JShorthouse

Tôi không chắc tại sao điều này không đánh dấu đây là câu trả lời cho câu hỏi. Đó là một câu trả lời rất kỹ lưỡng và được viết tốt, cảm ơn @elmiomar
Abdelrahman Shoman

13

Cuốn sách Lập trình C ++ với các mẫu thiết kế được tiết lộ mô tả RAII như sau:

  1. Có được tất cả các nguồn lực
  2. Sử dụng tài nguyên
  3. Phát hành tài nguyên

Ở đâu

  • Các tài nguyên được triển khai như các lớp và tất cả các con trỏ đều có các lớp bao quanh chúng (làm cho chúng trở thành các con trỏ thông minh).

  • Tài nguyên có được bằng cách gọi các nhà xây dựng của họ và phát hành ngầm (theo thứ tự ngược lại để có được) bằng cách gọi các hàm hủy của chúng.


1
@Brandin Tôi đã chỉnh sửa bài đăng của mình để độc giả tập trung vào nội dung quan trọng, thay vì tranh luận về lĩnh vực màu xám của luật bản quyền về những gì cấu thành sử dụng hợp pháp.
Dennis

7

Có ba phần cho một lớp RAII:

  1. Tài nguyên bị hủy bỏ trong hàm hủy
  2. Thể hiện của lớp được phân bổ ngăn xếp
  3. Tài nguyên được mua trong hàm tạo. Phần này là tùy chọn, nhưng phổ biến.

RAII là viết tắt của "Thu nhận tài nguyên là khởi tạo." Phần "thu nhận tài nguyên" của RAII là nơi bạn bắt đầu một cái gì đó phải kết thúc sau đó, chẳng hạn như:

  1. Mở một tập tin
  2. Phân bổ một số bộ nhớ
  3. Có được một khóa

Phần "là khởi tạo" có nghĩa là việc mua lại xảy ra bên trong hàm tạo của một lớp.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


5

Quản lý bộ nhớ thủ công là một cơn ác mộng mà các lập trình viên đã và đang phát minh ra những cách để tránh kể từ khi phát minh ra trình biên dịch. Ngôn ngữ lập trình với bộ thu gom rác giúp cuộc sống dễ dàng hơn, nhưng với chi phí hiệu năng. Trong bài viết này - Loại bỏ người thu gom rác: Cách RAII , kỹ sư Toptal Peter Goodspeed-Niklaus cho chúng ta xem qua lịch sử của người thu gom rác và giải thích cách các khái niệm về quyền sở hữu và vay mượn có thể giúp loại bỏ người thu gom rác mà không ảnh hưởng đến sự đảm bảo an toàn của họ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.