2008-12-30 27 views
18

Giả sử tôi có một lớp học triển khai giao diện IDisposable. Một cái gì đó như thế này:Cách xử lý không đồng bộ?

http://www.flickr.com/photos/garthof/3149605015/

MyClass sử dụng một số nguồn lực không được quản lý, vì thế mà Dispose() phương pháp từ IDisposable phát hành những tài nguyên. MyClass nên được sử dụng như thế này:

using (MyClass myClass = new MyClass()) { 
    myClass.DoSomething(); 
} 

Bây giờ, tôi muốn thực hiện một phương pháp mà các cuộc gọi DoSomething() không đồng bộ. Tôi thêm một phương pháp mới để MyClass:

http://www.flickr.com/photos/garthof/3149605005/

Bây giờ, từ phía khách hàng, MyClass nên được sử dụng như thế này:

using (MyClass myClass = new MyClass()) { 
    myClass.AsyncDoSomething(); 
} 

Tuy nhiên, nếu tôi không làm bất cứ điều gì khác, điều này có thể không thành đối tượng myClass có thể được xử lý trước DoSomething() được gọi (và ném một bất ngờ ObjectDisposedException). Vì vậy, cuộc gọi đến phương thức Vứt bỏ() (ngầm hoặc rõ ràng) sẽ bị trì hoãn cho đến khi cuộc gọi không đồng bộ đến DoSomething() được thực hiện.

Tôi nghĩ rằng các mã trong phương pháp Dispose() nên được thực hiện một cách đồng bộ, và chỉ khi tất cả các cuộc gọi không đồng bộ được giải quyết. Tôi muốn biết đó có thể là cách tốt nhất để thực hiện điều này.

Cảm ơn.

LƯU Ý: Vì mục đích đơn giản, tôi chưa nhập chi tiết về cách phương thức Dispose() được triển khai. Trong cuộc sống thực, tôi thường theo dõi Dispose pattern.


UPDATE: Cảm ơn bạn rất nhiều vì đã trả lời của bạn. Tôi đánh giá cao nỗ lực của bạn. Là chakrithas commented, tôi cần rằng nhiều cuộc gọi đến DoSomething không đồng bộ có thể được thực hiện. Lý tưởng nhất, một cái gì đó như thế này sẽ hoạt động tốt:

using (MyClass myClass = new MyClass()) { 

    myClass.AsyncDoSomething(); 
    myClass.AsyncDoSomething(); 

} 

Tôi sẽ nghiên cứu semaphore đếm, có vẻ như những gì tôi đang tìm kiếm. Nó cũng có thể là một vấn đề thiết kế. Nếu tôi tìm thấy nó thuận tiện, tôi sẽ chia sẻ với bạn một số bit của trường hợp thực tế và những gì MyClass thực sự.

Trả lời

2

Vì vậy, ý tưởng của tôi là giữ số lượng AsyncDoSomething() đang chờ hoàn thành và chỉ xử lý khi số này đạt đến 0. cách tiếp cận ban đầu của tôi là:

public class MyClass : IDisposable { 

    private delegate void AsyncDoSomethingCaller(); 
    private delegate void AsyncDoDisposeCaller(); 

    private int pendingTasks = 0; 

    public DoSomething() { 
     // Do whatever. 
    } 

    public AsyncDoSomething() { 
     pendingTasks++; 
     AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoSomethingCallback), caller); 
    } 

    public Dispose() { 
     AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoDisposeCallback), caller); 
    } 

    private DoDispose() { 
     WaitForPendingTasks(); 

     // Finally, dispose whatever managed and unmanaged resources. 
    } 

    private void WaitForPendingTasks() { 
     while (true) { 
      // Check if there is a pending task. 
      if (pendingTasks == 0) { 
       return; 
      } 

      // Allow other threads to execute. 
      Thread.Sleep(0); 
     } 
    } 

    private void EndDoSomethingCallback(IAsyncResult ar) { 
     AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
     pendingTasks--; 
    } 

    private void EndDoDisposeCallback(IAsyncResult ar) { 
     AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
    } 
} 

Một số vấn đề có thể xảy ra nếu hai hay nhiều chủ đề cố gắng đọc/viết pendingTasks biến đồng thời, vì vậy từ khóa khóa nên được sử dụng để ngăn chặn tình trạng đua:

public class MyClass : IDisposable { 

    private delegate void AsyncDoSomethingCaller(); 
    private delegate void AsyncDoDisposeCaller(); 

    private int pendingTasks = 0; 
    private readonly object lockObj = new object(); 

    public DoSomething() { 
     // Do whatever. 
    } 

    public AsyncDoSomething() { 
     lock (lockObj) { 
      pendingTasks++; 
      AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller(); 
      caller.BeginInvoke(new AsyncCallback(EndDoSomethingCallback), caller); 
     } 
    } 

    public Dispose() { 
     AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoDisposeCallback), caller); 
    } 

    private DoDispose() { 
     WaitForPendingTasks(); 

     // Finally, dispose whatever managed and unmanaged resources. 
    } 

    private void WaitForPendingTasks() { 
     while (true) { 
      // Check if there is a pending task. 
      lock (lockObj) { 
       if (pendingTasks == 0) { 
        return; 
       } 
      } 

      // Allow other threads to execute. 
      Thread.Sleep(0); 
     } 
    } 

    private void EndDoSomethingCallback(IAsyncResult ar) { 
     lock (lockObj) { 
      AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState; 
      caller.EndInvoke(ar); 
      pendingTasks--; 
     } 
    } 

    private void EndDoDisposeCallback(IAsyncResult ar) { 
     AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
    } 
} 

Tôi thấy có vấn đề với cách tiếp cận này.Như việc phát hành các nguồn lực được đồng bộ thực hiện, một cái gì đó như thế này có thể làm việc:

MyClass myClass; 

using (myClass = new MyClass()) { 
    myClass.AsyncDoSomething(); 
} 

myClass.DoSomething(); 

Khi hành vi mong đợi nên để khởi động một ObjectDisposedException khi DoSomething() được gọi là bên ngoài sử dụng khoản. Nhưng tôi không tìm thấy điều này đủ tồi để suy nghĩ lại giải pháp này.

+1

Trong việc sử dụng (MyClass myclass = new ....) Ví dụ tôi nghĩ rằng đối tượng myclass trở nên chưa được đặt ở cuối khối sử dụng và có sẵn để được hoàn thành. Tôi khá chắc chắn để được an toàn, bạn cần phải gọi và chặn trên một phương pháp chờ đợi trước khi kết thúc khối sử dụng. –

5

Các phương pháp không đồng bộ thường có gọi lại cho phép bạn thực hiện một số thao tác khi hoàn thành.Nếu đây là trường hợp của bạn nó sẽ là một cái gì đó như thế này:

// The async method taks an on-completed callback delegate 
myClass.AsyncDoSomething(delegate { myClass.Dispose(); }); 

Một cách khác xung quanh đây là một wrapper async:

ThreadPool.QueueUserWorkItem(delegate 
{ 
    using(myClass) 
    { 
     // The class doesn't know about async operations, a helper method does that 
     myClass.DoSomething(); 
    } 
}); 
2

tôi sẽ không thay đổi mã bằng cách nào đó để cho phép định đoạt async. Thay vào đó, tôi sẽ đảm bảo khi cuộc gọi đến AsyncDoSomething được thực hiện, nó sẽ có một bản sao của tất cả các dữ liệu cần thiết để thực hiện. Phương pháp đó phải chịu trách nhiệm làm sạch tất cả nếu tài nguyên của nó.

10

Có vẻ như bạn đang sử dụng mẫu không đồng bộ dựa trên sự kiện (see here for more info about .NET async patterns) vì vậy những gì bạn thường có là sự kiện trên lớp phát sinh khi hoạt động async hoàn thành có tên DoSomethingCompleted (lưu ý rằng AsyncDoSomething thực sự phải là gọi là DoSomethingAsync để theo đúng mẫu). Với sự kiện này tiếp xúc với bạn có thể viết:

var myClass = new MyClass(); 
myClass.DoSomethingCompleted += (sender, e) => myClass.Dispose(); 
myClass.DoSomethingAsync(); 

Các thay thế khác là sử dụng mô hình IAsyncResult, nơi bạn có thể vượt qua một đại biểu mà các cuộc gọi phương pháp xử lý đến AsyncCallback tham số (biết thêm về mô hình này là trong trang ở trên quá). Trong trường hợp này bạn phải BeginDoSomethingEndDoSomething phương pháp thay vì DoSomethingAsync, và sẽ gọi nó là một cái gì đó giống như ...

var myClass = new MyClass(); 
myClass.BeginDoSomething(
    asyncResult => { 
         using (myClass) 
         { 
          myClass.EndDoSomething(asyncResult); 
         } 
        }, 
    null);   

Nhưng bất cứ cách nào bạn làm điều đó, bạn cần một cách để người gọi để được thông báo rằng hoạt động async đã hoàn thành để nó có thể xử lý đối tượng vào đúng thời điểm.

+0

Một mối quan tâm lớn mà tôi muốn có khi vứt bỏ một đối tượng trong một cuộc gọi lại không đồng bộ là tôi không biết bất kỳ cách nào có thể thực hiện chung để biết khi nào an toàn để thực hiện điều đó. Ngay cả khi không ai khác đang sử dụng đối tượng mà 'Dispose' được gọi, một người khác có thể đang sử dụng một đối tượng mà phương thức' Dispose' sẽ ảnh hưởng. – supercat

1

Bạn có thể thêm cơ chế gọi lại và chuyển chức năng dọn dẹp làm cuộc gọi lại.

var x = new MyClass(); 

Action cleanup =() => x.Dispose(); 

x.DoSomethingAsync(/*and then*/cleanup); 

nhưng điều này sẽ gây ra vấn đề nếu bạn muốn chạy nhiều cuộc gọi không đồng bộ khỏi cùng một cá thể đối tượng.

Một cách là triển khai counting semaphore đơn giản với số Semaphore class để đếm số lượng công việc không đồng bộ đang chạy.

Thêm bộ đếm vào MyClass và trên mọi cuộc gọi AsyncWhatever tăng bộ đếm, khi thoát khỏi nó, hãy truy cập nó. Khi semaphore là 0, sau đó lớp đã sẵn sàng để được xử lý.

var x = new MyClass(); 

x.DoSomethingAsync(); 
x.DoSomethingAsync2(); 

while (x.RunningJobsCount > 0) 
    Thread.CurrentThread.Sleep(500); 

x.Dispose(); 

Nhưng tôi nghi ngờ đó sẽ là cách lý tưởng. Tôi ngửi thấy một vấn đề thiết kế. Có lẽ một suy nghĩ lại về thiết kế MyClass có thể tránh được điều này?

Bạn có thể chia sẻ một chút triển khai MyClass không? Nó phải làm gì?

+0

Ngủ trong vòng lặp được coi là thực hành không tốt. Bạn nên sử dụng EventWaitHandle để kích hoạt một sự kiện khi kết thúc. –

+0

Thật không may, tôi bị giới hạn trong .NET CF 1.0 và lớp Semaphore chỉ được hỗ trợ kể từ .NET 2.0. – Auron

+0

Nó chỉ là một "ví dụ" ... tâm trí bạn. – chakrit

1

tôi xem xét nó không may rằng Microsoft không đòi hỏi như một phần của hợp đồng IDisposable rằng việc triển khai nên cho phép Dispose để được gọi từ bất kỳ bối cảnh luồng, vì không có cách nào lành mạnh việc tạo ra một đối tượng có thể buộc các tồn tại liên tục của các ngữ cảnh luồng mà nó được tạo ra. Có thể thiết kế mã để chủ đề tạo ra một đối tượng bằng cách nào đó sẽ xem đối tượng trở nên lỗi thời và có thể Dispose một cách thuận tiện, và do đó khi không còn cần đến bất kỳ thứ gì khác, là Dispose d, nhưng tôi không nghĩ rằng có một cơ chế tiêu chuẩn không yêu cầu hành vi đặc biệt trên một phần của chuỗi tạo Dispose. Đặt cược tốt nhất của bạn có lẽ là có tất cả các đối tượng quan tâm được tạo ra trong một chủ đề chung (có lẽ là chuỗi giao diện người dùng), hãy cố gắng đảm bảo rằng chuỗi sẽ tồn tại suốt đời của các đối tượng quan tâm và sử dụng Control.BeginInvoke để yêu cầu xử lý đối tượng. Với điều kiện là việc tạo đối tượng hoặc dọn dẹp sẽ không chặn bất kỳ khoảng thời gian nào, đó có thể là một cách tiếp cận tốt, nhưng nếu một trong hai hoạt động có thể chặn một cách tiếp cận khác nhau có thể cần thiết sử dụng Control.BeginInvoke ở đó].

Hoặc, nếu bạn có quyền kiểm soát việc triển khai IDisposable, hãy thiết kế chúng để chúng có thể được kích hoạt một cách an toàn một cách không đồng bộ. Trong nhiều trường hợp, điều đó sẽ "chỉ hoạt động" miễn là không ai cố gắng sử dụng vật phẩm khi nó được xử lý, nhưng đó không phải là một điều đã cho. Đặc biệt, với nhiều loại IDisposable, có một mối nguy hiểm thực sự mà nhiều cá thể đối tượng có thể thao tác cả một tài nguyên bên ngoài chung [ví dụ: một đối tượng có thể giữ List<> trường hợp đã tạo, thêm các trường hợp vào danh sách đó khi chúng được tạo và loại bỏ các cá thể trên Dispose; nếu các hoạt động danh sách không được đồng bộ hóa, Dispose không đồng bộ có thể làm hỏng danh sách ngay cả khi đối tượng đang được xử lý không được sử dụng khác.

BTW, mẫu hữu ích dành cho các đối tượng cho phép xử lý không đồng bộ trong khi đang sử dụng, với mong muốn rằng việc xử lý như vậy sẽ khiến bất kỳ thao tác nào xảy ra để ném ngoại lệ tại cơ hội thuận tiện đầu tiên. Những thứ như ổ cắm hoạt động theo cách này. Có thể không thể thực hiện thao tác đọc được thoát sớm mà không để lại socket trong trạng thái vô dụng, nhưng nếu ổ cắm không bao giờ được sử dụng, thì không có điểm để đọc tiếp tục chờ dữ liệu nếu một luồng khác đã xác định nó nên bỏ cuộc. IMHO, đó là cách tất cả các đối tượng IDisposable nên nỗ lực để hành xử, nhưng tôi biết không có tài liệu gọi cho một mô hình chung như vậy.

Các vấn đề liên quan