Câu hỏi Sự cần thiết cho sửa đổi dễ bay hơi trong kiểm tra khóa kép trong .NET


Nhiều văn bản nói rằng khi thực hiện khóa đã kiểm tra kép trong .NET, trường bạn đang khóa phải có trình sửa đổi dễ bay hơi được áp dụng. Nhưng tại sao chính xác? Xem xét ví dụ sau:

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

tại sao không "khóa (syncRoot)" thực hiện sự thống nhất bộ nhớ cần thiết? Có đúng là sau khi tuyên bố "khóa" cả đọc và viết sẽ biến động và vì vậy sự nhất quán cần thiết sẽ được hoàn thành?


76
2017-12-27 00:13


gốc


Điều này đã được nhai nhiều lần rồi. yoda.arachsys.com/csharp/singleton.html - Hans Passant
Thật không may trong bài viết đó Jon tham khảo 'dễ bay hơi' hai lần, và không tham chiếu trực tiếp đề cập đến các ví dụ mã mà ông đã đưa ra. - Dan Esparza
Xem bài viết này để hiểu mối quan tâm: igoro.com/archive/volatile-keyword-in-c-memory-model-explained  Về cơ bản nó là THEORETICALLY có thể cho JIT sử dụng một đăng ký CPU cho biến cá thể - đặc biệt nếu bạn cần thêm một chút mã trong đó. Do đó làm một câu lệnh if hai lần có khả năng có thể trả về cùng một giá trị bất kể nó thay đổi trên một luồng khác. Trong thực tế câu trả lời là một chút phức tạp, tuyên bố khóa có thể hoặc có thể không chịu trách nhiệm cho việc làm tốt hơn ở đây (tiếp theo) - user2685937
Về cơ bản, bất kỳ mã nào làm phức tạp hơn việc đọc hoặc đặt biến có thể kích hoạt JIT để nói, quên cố gắng tối ưu hóa điều này, hãy tải và lưu vào bộ nhớ vì nếu một hàm được gọi là JIT có khả năng sẽ cần phải lưu và tải lại thanh ghi nếu nó lưu trữ nó ở đó mỗi lần, thay vì làm điều đó nó chỉ ghi và đọc trực tiếp từ bộ nhớ mỗi lần. Làm cách nào để biết khóa không có gì đặc biệt? Nhìn vào liên kết tôi đã đăng trong bình luận trước đó của Igor (tiếp theo bình luận tiếp theo) - user2685937
(xem ở trên 2 ý kiến) - Tôi đã thử nghiệm mã của Igor và khi nó tạo ra một chủ đề mới, tôi thêm một khóa xung quanh nó và thậm chí làm cho nó vòng lặp. Nó vẫn sẽ không gây ra mã để thoát vì biến cá thể được đưa ra khỏi vòng lặp. Thêm vào vòng lặp while, một biến cục bộ đơn giản vẫn giữ nguyên biến trong vòng lặp - Bây giờ mọi thứ phức tạp hơn nếu như các câu lệnh hoặc một cuộc gọi phương thức hoặc có một cuộc gọi khóa sẽ ngăn chặn việc tối ưu hóa và làm cho nó hoạt động. Vì vậy, bất kỳ mã phức tạp nào thường buộc truy cập biến trực tiếp thay vì cho phép JIT tối ưu hóa. (tiếp theo bình luận tiếp theo) - user2685937


Các câu trả lời:


Dễ bay hơi là không cần thiết. Vâng, loại **

volatile được sử dụng để tạo ra một rào cản bộ nhớ * giữa lần đọc và ghi trên biến.
lock, khi được sử dụng, gây ra các rào cản bộ nhớ được tạo xung quanh khối bên trong lock, ngoài việc hạn chế quyền truy cập vào khối vào một chuỗi.
Các rào cản bộ nhớ làm cho nó để mỗi thread đọc giá trị mới nhất của biến (không phải là một giá trị cục bộ được lưu trữ trong một số thanh ghi) và trình biên dịch không sắp xếp lại các câu lệnh. Sử dụng volatile là không cần thiết ** vì bạn đã có khóa.

Joseph Albahari giải thích cách này tốt hơn tôi có thể.

Và hãy nhớ xem Jon Skeet's hướng dẫn triển khai singleton trong C #


cập nhật:
*volatile gây ra lần đọc của biến được VolatileReads và viết là VolatileWrites, trên x86 và x64 trên CLR, được thực hiện với một MemoryBarrier. Chúng có thể được hạt mịn hơn trên các hệ thống khác.

** Câu trả lời của tôi chỉ đúng nếu bạn đang sử dụng CLR trên bộ vi xử lý x86 và x64. Nó có thể là đúng trong các mô hình bộ nhớ khác, như trên Mono (và các triển khai khác), Itanium64 và phần cứng tương lai. Đây là những gì Jon đang đề cập đến trong bài viết của mình trong "gotchas" để kiểm tra khóa kép.

Thực hiện một trong {đánh dấu biến là volatile, đọc nó với Thread.VolatileReadhoặc chèn cuộc gọi đến Thread.MemoryBarrier} có thể cần thiết cho mã hoạt động bình thường trong tình huống mô hình bộ nhớ yếu.

Từ những gì tôi hiểu, trên CLR (ngay cả trên IA64), viết không bao giờ được sắp xếp lại (viết luôn luôn có ngữ nghĩa phát hành). Tuy nhiên, trên IA64, lần đọc có thể được sắp xếp lại trước khi viết, trừ khi chúng được đánh dấu dễ bay hơi. Không may, tôi không có quyền truy cập vào phần cứng IA64 để chơi với, vì vậy bất cứ điều gì tôi nói về nó sẽ là đầu cơ.

Tôi cũng đã tìm thấy những bài viết này hữu ích:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
bài viết của vance morrison (tất cả mọi thứ liên kết đến điều này, nó nói về khóa kiểm tra kép)
bài viết của chris brumme  (mọi thứ liên kết đến điều này)
Joe Duffy: Các biến thể bị hỏng của khóa đôi được kiểm tra 

loạt bài luis abreu về đa luồng cung cấp một cái nhìn tổng quan tốt đẹp về các khái niệm
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx 


57
2017-12-27 01:06



Liên kết đầu tiên thực sự rất hữu ích, cảm ơn;) - Konstantin
Jon Skeet thực sự nói rằng sửa đổi dễ bay hơi là cần thiết để tạo ra barier bộ nhớ thích hợp, trong khi tác giả liên kết đầu tiên nói rằng khóa (Monitor.Enter) sẽ là đủ. Ai thực sự là đúng ??? - Konstantin
@Konstantin có vẻ như Jon đã đề cập đến mô hình bộ nhớ trên bộ vi xử lý Itanium 64, vì vậy trong trường hợp đó, sử dụng dễ bay hơi có thể là cần thiết. Tuy nhiên, dễ bay hơi là không cần thiết trên bộ vi xử lý x86 và x64. Tôi sẽ cập nhật thêm một chút. - dan
Câu trả lời này có vẻ sai với tôi. Nếu volatile không cần thiết trên bất kì nền tảng sau đó nó có nghĩa là JIT không thể tối ưu hóa tải bộ nhớ object s1 = syncRoot; object s2 = syncRoot;đến object s1 = syncRoot; object s2 = s1; trên nền tảng đó. Điều đó dường như rất khó xảy ra với tôi. - Mehrdad
Ngay cả khi CLR không sắp xếp lại các bài viết (tôi nghi ngờ điều đó là đúng, nhiều tối ưu hóa rất tốt có thể được thực hiện bằng cách làm như vậy), nó vẫn sẽ bị lỗi miễn là chúng ta có thể nội tuyến lời gọi hàm tạo và tạo đối tượng tại chỗ (chúng ta có thể thấy một đối tượng được khởi tạo một nửa). Độc lập của bất kỳ mô hình bộ nhớ nào mà CPU cơ bản sử dụng! Theo Eric Lippert CLR trên Intel ít nhất là giới thiệu một membarrier sau khi các nhà xây dựng mà từ chối tối ưu hóa đó, nhưng đó không phải là yêu cầu của spec và tôi sẽ không đếm trên cùng một điều xảy ra trên ARM ví dụ .. - Voo


Có một cách để thực hiện nó mà không cần volatilecánh đồng. Tôi sẽ giải thích nó ...

Tôi nghĩ rằng nó là truy cập bộ nhớ sắp xếp lại bên trong khóa đó là nguy hiểm, như vậy mà bạn có thể có được một không hoàn thành khởi tạo dụ bên ngoài khóa. Để tránh điều này tôi làm điều này:

public sealed class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         // very fast test, without implicit memory barriers or locks
         if (instance == null)
         {
            lock (syncRoot)
            {
               if (instance == null)
               {
                    var temp = new Singleton();

                    // ensures that the instance is well initialized,
                    // and only then, it assigns the static variable.
                    System.Threading.Thread.MemoryBarrier();
                    instance = temp;
               }
            }
         }

         return instance;
      }
   }
}

Hiểu mã

Hãy tưởng tượng rằng có một số mã khởi tạo bên trong constructor của lớp Singleton. Nếu các lệnh này được sắp xếp lại sau khi trường được thiết lập với địa chỉ của đối tượng mới, thì bạn có một cá thể chưa hoàn chỉnh ... hãy tưởng tượng rằng lớp đó có mã này:

private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}

Bây giờ hãy tưởng tượng một cuộc gọi đến hàm tạo bằng cách sử dụng toán tử mới:

instance = new Singleton();

Điều này có thể được mở rộng đến các hoạt động này:

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

Điều gì sẽ xảy ra nếu tôi sắp xếp lại các hướng dẫn như sau:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

Liệu nó có làm cho một sự khác biệt? KHÔNG nếu bạn nghĩ về một sợi đơn. VÂNG nếu bạn nghĩ về nhiều chủ đề ... nếu chuỗi bị gián đoạn chỉ sau set instance to ptr:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized

Đó là những gì rào cản bộ nhớ tránh được, bằng cách không cho phép truy cập bộ nhớ sắp xếp lại:

ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;

Chúc mừng mã hóa!


31
2017-10-18 00:38



Nếu CLR cho phép truy cập vào một đối tượng trước khi nó được khởi tạo, đó là một lỗ hổng bảo mật. Hãy tưởng tượng một lớp đặc quyền mà chỉ có các hàm tạo công khai đặt "SecureMode = 1" và sau đó các phương thức cá thể của nó kiểm tra điều đó. Nếu bạn có thể gọi các phương thức instance đó mà không có constructor đang chạy, bạn có thể thoát khỏi mô hình bảo mật và vi phạm sandboxing. - MichaelGG
@MichaelGG: trong trường hợp bạn mô tả, nếu lớp đó sẽ hỗ trợ nhiều chủ đề để truy cập nó, thì đó là một vấn đề. Nếu lời gọi hàm tạo được trình bày bởi jitter, thì CPU có thể sắp xếp lại các lệnh theo cách mà tham chiếu được lưu trữ trỏ tới một cá thể không được khởi tạo hoàn toàn. Đây không phải là một vấn đề an ninh CLR vì nó có thể tránh được, đó là trách nhiệm của người lập trình để sử dụng: các rào cản bộ nhớ, khóa, và / hoặc các trường dễ bay hơi, bên trong hàm tạo của lớp đó. - Miguel Angelo
Một rào cản bên trong ctor không khắc phục được nó. Nếu CLR gán tham chiếu đến đối tượng mới được cấp phát trước khi ctor hoàn thành và không chèn một membarrier, thì một luồng khác có thể thực hiện một phương thức cá thể trên một đối tượng bán khởi tạo. - MichaelGG
Người đàn ông, mô hình bộ nhớ của Java 5 làm cho loại câu hỏi này dễ dàng hơn nhiều .. - user2864740
Đây là "mẫu thay thế" mà ReSharper 2016/2017 đề xuất trong trường hợp DCL trong C #. OTOH, Java làm đảm bảo rằng kết quả của new được khởi tạo hoàn toàn .. - user2864740


Tôi không nghĩ có ai đã trả lời câu hỏinên tôi sẽ thử.

Các biến động và đầu tiên if (instance == null) không phải là "cần thiết". Khóa sẽ làm cho mã này an toàn.

Vì vậy, câu hỏi là: tại sao bạn sẽ thêm đầu tiên if (instance == null)?

Lý do có lẽ là để tránh thực hiện phần mã bị khóa không cần thiết. Trong khi bạn đang thực hiện mã bên trong khóa, bất kỳ chủ đề nào khác cố gắng thực thi mã đó bị chặn, điều này sẽ làm chậm chương trình của bạn xuống nếu bạn cố gắng truy cập vào singleton thường xuyên từ nhiều luồng. Tùy thuộc vào ngôn ngữ / nền tảng, cũng có thể có tổng chi phí từ chính khóa mà bạn muốn tránh.

Vì vậy, kiểm tra null đầu tiên được thêm vào như là một cách thực sự nhanh chóng để xem nếu bạn cần khóa. Nếu bạn không cần phải tạo singleton, bạn có thể tránh khóa hoàn toàn.

Nhưng bạn không thể kiểm tra nếu tham chiếu là null mà không khóa nó theo một cách nào đó, vì do bộ nhớ đệm của bộ xử lý, một chuỗi khác có thể thay đổi nó và bạn sẽ đọc một giá trị "cũ" sẽ dẫn bạn nhập khóa không cần thiết. Nhưng bạn đang cố gắng để tránh một khóa!

Vì vậy, bạn làm cho singleton dễ bay hơi để đảm bảo rằng bạn đọc giá trị mới nhất, mà không cần phải sử dụng một khóa.

Bạn vẫn cần khóa bên trong vì dễ bay hơi chỉ bảo vệ bạn trong một lần truy cập vào biến - bạn không thể kiểm tra và đặt nó một cách an toàn mà không cần sử dụng khóa.

Bây giờ, điều này thực sự hữu ích?

Tôi sẽ nói "trong hầu hết các trường hợp, không".

Nếu Singleton.Instance có thể gây ra không hiệu quả do ổ khóa, sau đó tại sao bạn gọi nó thường xuyên đến nỗi đây sẽ là một vấn đề quan trọng? Toàn bộ điểm của một singleton là chỉ có một, do đó, mã của bạn có thể đọc và bộ nhớ cache tham chiếu singleton một lần.

Trường hợp duy nhất tôi có thể nghĩ về nơi bộ nhớ đệm này sẽ không thể là khi bạn có một số lượng lớn các chủ đề (ví dụ: một máy chủ sử dụng một luồng mới để xử lý mọi yêu cầu có thể tạo ra hàng triệu luồng rất ngắn, mỗi luồng mà sẽ phải gọi Singleton.Instance một lần).

Vì vậy, tôi nghi ngờ rằng khóa kép được kiểm tra là một cơ chế có vị trí thực sự trong các trường hợp rất quan trọng về hiệu suất, và sau đó mọi người đã kêu gọi "cách này là cách thích hợp để làm điều đó" mà không thực sự nghĩ về nó sẽ thực sự cần thiết trong trường hợp họ đang sử dụng nó cho.


7
2017-12-27 08:55



Đây là một nơi nào đó giữa sai và thiếu điểm. volatile không có gì liên quan đến khóa ngữ nghĩa trong khóa kiểm tra kép, nó phải liên quan đến mô hình bộ nhớ và sự kết hợp bộ nhớ cache. Mục đích của nó là để đảm bảo rằng một sợi không nhận được một giá trị mà vẫn đang được khởi tạo bởi một luồng khác, mà mẫu khóa kiểm tra kép không vốn đã ngăn chặn. Trong Java, bạn chắc chắn cần volatile từ khóa; trong .NET nó rất u ám, bởi vì nó sai theo ECMA nhưng đúng theo thời gian chạy. Dù bằng cách nào, lock chắc chắn không không phải chăm sóc nó. - Aaronaught
Huh? Tôi không thể nhìn thấy lời tuyên bố của bạn không đồng ý với những gì tôi đã nói, cũng như tôi đã nói rằng sự biến động là bất kỳ cách nào liên quan đến ngữ nghĩa khóa. - Jason Williams
Câu trả lời của bạn, giống như một số câu khác trong chủ đề này, tuyên bố rằng lock làm cho mã an toàn. Phần đó là đúng, nhưng mô hình khóa kiểm tra kép có thể làm cho nó không an toàn. Đó là những gì bạn dường như bị mất tích. Câu trả lời này dường như xoay quanh ý nghĩa và mục đích của một khóa kiểm tra kép mà không bao giờ giải quyết các vấn đề an toàn chủ đề đó là lý do cho volatile. - Aaronaught
Làm thế nào nó có thể làm cho không an toàn nếu instance được đánh dấu bằng volatile? - UserControl


AFAIK (và - hãy cẩn thận với điều này, tôi không làm nhiều thứ đồng thời). Khóa chỉ cho phép bạn đồng bộ hóa giữa nhiều ứng cử viên (chủ đề).

dễ bay hơi, mặt khác nói với máy tính của bạn để đánh giá lại giá trị mỗi lần, do đó bạn không vấp ngã khi một giá trị được lưu trữ (và sai).

Xem http://msdn.microsoft.com/en-us/library/ms998558.aspx và lưu ý báo giá sau:

Ngoài ra, biến được khai báo là biến động để đảm bảo rằng gán cho biến cá thể hoàn thành trước khi biến cá thể có thể được truy cập.

Mô tả dễ bay hơi: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


3
2017-12-27 00:20



Một 'khóa' cũng cung cấp một rào cản bộ nhớ, giống như (hoặc tốt hơn) dễ bay hơi. - Henk Holterman


Bạn nên sử dụng dễ bay hơi với mẫu khóa kiểm tra kép.

Hầu hết mọi người chỉ vào bài viết này là bằng chứng bạn không cần dễ bay hơi: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Nhưng họ không đọc được đến cùng: "Lời cảnh báo cuối cùng - Tôi chỉ đoán ở mô hình bộ nhớ x86 từ hành vi được quan sát trên các bộ vi xử lý hiện có. Do đó các kỹ thuật khóa thấp cũng dễ vỡ vì phần cứng và trình biên dịch có thể trở nên hung dữ hơn theo thời gian. Dưới đây là một số chiến lược để giảm thiểu tác động của sự mong manh này trên mã của bạn. Đầu tiên, bất cứ khi nào có thể, hãy tránh các kỹ thuật khóa thấp. (...) Cuối cùng, giả sử mô hình bộ nhớ yếu nhất có thể, sử dụng các khai báo dễ bay hơi thay vì dựa vào các đảm bảo ngầm định. "

Nếu bạn cần thuyết phục hơn thì hãy đọc bài viết này về thông số ECMA sẽ được sử dụng cho các nền tảng khác: msdn.microsoft.com/en-us/magazine/jj863136.aspx

Nếu bạn cần tiếp tục thuyết phục, hãy đọc bài viết mới hơn này để tối ưu hóa có thể được đặt trong đó ngăn không cho nó hoạt động mà không dễ bay hơi: msdn.microsoft.com/en-us/magazine/jj883956.aspx

Tóm lại nó "có thể" làm việc cho bạn mà không dễ bay hơi cho thời điểm này, nhưng không có cơ hội nó viết mã thích hợp và sử dụng phương pháp volatile hoặc volatileread / write. Các bài viết đề xuất làm khác đôi khi bỏ qua một số rủi ro có thể xảy ra khi tối ưu hóa JIT / trình biên dịch có thể ảnh hưởng đến mã của bạn, cũng như chúng tôi tối ưu hóa trong tương lai có thể xảy ra có thể phá vỡ mã của bạn. Cũng như các giả định đã đề cập trong phần trước, các giả định trước đây về làm việc mà không biến động đã có thể không giữ được ARM.


3
2018-02-23 04:18



Câu trả lời tốt. Câu trả lời đúng duy nhất về câu hỏi này đơn giản là "Không". Theo câu trả lời được chấp nhận này là sai. - Dennis Kassel


Các lock là đủ. Bản mô tả ngôn ngữ MS (3.0) tự đề cập đến kịch bản chính xác này trong §8.12, mà không đề cập đến volatile:

Cách tiếp cận tốt hơn là đồng bộ hóa   truy cập vào dữ liệu tĩnh bằng cách khóa   đối tượng tĩnh riêng. Ví dụ:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

2
2017-12-27 09:19



Jon Skeet trong bài viết của mình (yoda.arachsys.com/csharp/singleton.html) nói rằng dễ bay hơi là cần thiết cho rào cản bộ nhớ thích hợp trong trường hợp này. Marc, bạn có thể bình luận về điều này? - Konstantin
Ah, tôi đã không để ý đến cái khóa kiểm tra hai lần; chỉ đơn giản là: không làm điều đó ;-p - Marc Gravell♦
Tôi thực sự nghĩ rằng khóa đôi kiểm tra là một điều tốt hiệu suất-khôn ngoan. Ngoài ra nếu nó là cần thiết để làm cho một lĩnh vực dễ bay hơi, trong khi nó được truy cập trong khóa, sau đó khóa đôi cheked là không tồi tệ hơn nhiều so với bất kỳ khóa khác ... - Konstantin
Nhưng liệu nó có tốt như cách tiếp cận lớp học riêng biệt mà Jon đề cập không? - Marc Gravell♦


Tôi nghĩ rằng tôi đã tìm thấy những gì tôi đang tìm kiếm. Chi tiết trong bài viết này - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10.

Để tổng hợp - trong NET biến đổi dễ bay hơi thực sự là không cần thiết trong tình huống này. Tuy nhiên trong các mô hình bộ nhớ yếu hơn được viết trong constructor của đối tượng khởi tạo lazily có thể bị trì hoãn sau khi ghi vào trường, vì vậy các luồng khác có thể đọc tham số không null bị lỗi trong câu lệnh if đầu tiên.


2
2017-12-27 22:56



Ở dưới cùng của bài viết đó đọc kỹ, đặc biệt là câu cuối cùng tác giả nói: "Lời cảnh báo cuối cùng - Tôi chỉ đoán ở mô hình bộ nhớ x86 từ hành vi quan sát trên các bộ vi xử lý hiện có. Do đó, các kỹ thuật khóa thấp cũng dễ vỡ vì Phần cứng và trình biên dịch có thể tích cực hơn theo thời gian Dưới đây là một số chiến lược để giảm thiểu tác động của sự mong manh này trên mã của bạn, đầu tiên, bất cứ khi nào có thể, hãy tránh các kỹ thuật khóa thấp. sử dụng các khai báo dễ bay hơi thay vì dựa vào các đảm bảo ngầm định. " - user2685937
Nếu bạn cần thuyết phục hơn thì hãy đọc bài viết này về thông số ECMA sẽ được sử dụng cho các nền tảng khác: msdn.microsoft.com/en-us/magazine/jj863136.aspx Nếu bạn cần thuyết phục hơn, hãy đọc bài viết mới hơn này để tối ưu hóa có thể được đưa vào ngăn không cho nó hoạt động mà không dễ bay hơi: msdn.microsoft.com/en-us/magazine/jj883956.aspx Tóm lại, "có thể" làm việc cho bạn mà không dễ bay hơi vào lúc này, nhưng đừng có cơ hội viết mã thích hợp và sử dụng dễ bay hơi hoặc phương pháp volatileread / write. - user2685937