2009-02-02 32 views
42

Đây là câu hỏi chi tiết về C#.C# thread safety with get/set

Giả sử tôi đã có một lớp học với một đối tượng, và đối tượng được bảo vệ bởi một ổ khóa:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     return property; 
    } 
    set { 
     property = value; 
    } 
} 

Tôi muốn có một chủ đề bỏ phiếu để có thể truy vấn bất động sản đó. Tôi cũng muốn chuỗi cập nhật các đặc tính của đối tượng đó thỉnh thoảng và đôi khi người dùng có thể cập nhật thuộc tính đó và người dùng muốn có thể xem thuộc tính đó.

Mã sau có khóa đúng dữ liệu không?

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     lock (mLock){ 
      return property; 
     } 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

By 'đúng', những gì tôi muốn nói là, nếu tôi muốn gọi

MyProperty.Field1 = 2; 

hoặc bất cứ điều gì, lĩnh vực này sẽ bị khóa trong khi tôi làm bản cập nhật? Cài đặt được thực hiện bởi toán tử equals bên trong phạm vi của hàm 'get' hay chức năng 'get' (và do đó khóa) kết thúc trước, sau đó thiết lập, và sau đó 'set' được gọi, do đó bỏ qua khoá?

Chỉnh sửa: Vì điều này rõ ràng sẽ không thực hiện thủ thuật, điều gì sẽ xảy ra? Tôi có cần phải làm điều gì đó như:

Object mLock = new Object(); 
MyObject property; 
public MyObject MyProperty { 
    get { 
     MyObject tmp = null; 
     lock (mLock){ 
      tmp = property.Clone(); 
     } 
     return tmp; 
    } 
    set { 
     lock (mLock){ 
       property = value; 
     } 
    } 
} 

mà ít nhiều chỉ đảm bảo rằng tôi chỉ có quyền truy cập vào một bản sao, nghĩa là nếu tôi có hai chủ đề gọi là 'nhận' cùng một lúc, họ sẽ bắt đầu với cùng một giá trị của Field1 (phải không?). Có cách nào để đọc và viết khóa trên thuộc tính có ý nghĩa không? Hoặc tôi nên hạn chế bản thân mình để khóa trên các phần của các chức năng hơn là dữ liệu chính nó?

Chỉ để ví dụ này có ý nghĩa: MyObject là trình điều khiển thiết bị trả về trạng thái không đồng bộ. Tôi gửi lệnh qua cổng nối tiếp và sau đó thiết bị sẽ trả lời các lệnh đó theo thời gian ngọt ngào của riêng nó. Ngay bây giờ, tôi có một chủ đề thăm dò ý kiến ​​của nó ("Bạn vẫn còn ở đó? Bạn có thể chấp nhận lệnh?"), Một chuỗi chờ phản hồi trên cổng nối tiếp ("Chỉ có chuỗi trạng thái 2, mọi thứ đều tốt") và sau đó là chuỗi giao diện người dùng có các lệnh khác ("Người dùng muốn bạn thực hiện việc này.") và đăng phản hồi từ trình điều khiển ("Tôi vừa làm xong, bây giờ cập nhật giao diện người dùng bằng"). Đó là lý do tại sao tôi muốn khóa chính đối tượng, thay vì các trường của đối tượng; đó sẽ là một số lượng lớn các khóa, a và b, không phải mọi thiết bị của lớp này đều có hành vi tương tự, chỉ là hành vi chung, vì vậy tôi phải viết mã nhiều hộp thoại riêng lẻ nếu tôi cá nhân hoá các khóa.

Trả lời

34

Không, mã của bạn sẽ không khóa quyền truy cập vào các thành viên của đối tượng được trả lại từ MyProperty. Nó chỉ khóa MyProperty.

sử dụng ví dụ của bạn thực sự là hai hoạt động cuộn thành một, tương đương với điều này:

// object is locked and then immediately released in the MyProperty getter 
MyObject o = MyProperty; 

// this assignment isn't covered by a lock 
o.Field1 = 2; 

// the MyProperty setter is never even called in this example 

Tóm lại - nếu hai luồng truy cập MyProperty cùng một lúc, các getter sẽ chặn một thời gian ngắn các chủ đề thứ hai cho đến khi nó trả về đối tượng cho luồng đầu tiên, nhưng nó sẽ trả về đối tượng cho chuỗi thứ hai. Cả hai chủ đề sau đó sẽ có quyền truy cập đầy đủ, mở khóa đối tượng.

EDIT để đáp ứng với thông tin chi tiết trong câu hỏi

tôi vẫn không chắc chắn 100% những gì bạn đang cố gắng để đạt được, nhưng nếu bạn chỉ muốn truy cập nguyên tử để đối tượng sau đó bạn không thể khóa mã gọi điện thoại đối với chính đối tượng đó sao?

// quick and dirty example 
// there's almost certainly a better/cleaner way to do this 
lock (MyProperty) 
{ 
    // other threads can't lock the object while you're in here 
    MyProperty.Field1 = 2; 
    // do more stuff if you like, the object is all yours 
} 
// now the object is up-for-grabs again 

Không lý tưởng, nhưng chừng nào tất cả các truy cập vào các đối tượng được chứa trong lock (MyProperty) phần sau đó phương pháp này sẽ được thread-safe.

+0

Đồng ý. Tôi đã cung cấp giải pháp mẫu cho những gì bạn đang yêu cầu để trả lời câu hỏi Singleton http://stackoverflow.com/questions/7095/is-the-c-static-constructor-thread-safe/7105#7105 – Zooba

+0

vâng, tôi ' có lẽ sẽ đi với khóa đối tượng như bạn đã mô tả. Cảm ơn. – mmr

+0

Điểm tốt, tuy nhiên nếu ông có một số MyProperty' được gọi là 'p' và được gọi là hai chủ đề cùng một lúc:' p = new MyObject (12) 'và' p = new MyObject (5) 'sau đó * this * truy cập sẽ được đồng bộ . Tuy nhiên thành viên 'Field1' sẽ * không * bị khóa. Tôi khá chắc chắn đây là những gì bạn đang nói, chỉ cần cố gắng để hiểu nó cho bản thân mình. – Snoopy

1

Trong ví dụ mã bạn đã đăng, nhận được không bao giờ được tạo trước.

Trong một ví dụ phức tạp hơn:

MyProperty.Field1 = MyProperty.doSomething() + 2; 

Và dĩ nhiên giả sử bạn đã làm một:

lock (mLock) 
{ 
    // stuff... 
} 

Trong doSomething() sau đó tất cả các cuộc gọi khóa sẽ không là đủ để đảm bảo đồng bộ hóa trên toàn bộ đối tượng. Ngay sau khi trả về hàm doSomething(), khóa bị mất, sau đó việc bổ sung được thực hiện, và sau đó việc chuyển nhượng diễn ra, sẽ khóa lại.

Hoặc, để viết nó một cách khác để bạn có thể giả vờ như ổ khóa không thực hiện amutomatically, và viết lại này giống như "mã máy" với một hoạt động trên mỗi dòng, và nó trở nên rõ ràng:

lock (mLock) 
{ 
    val = doSomething() 
} 
val = val + 2 
lock (mLock) 
{ 
    MyProperty.Field1 = val 
} 
+0

hunh. Có lẽ tôi là tài sản hiểu lầm, nhưng trình gỡ rối chắc chắn có vẻ như nó đang bước vào chức năng 'get', đặc biệt là khi tôi đặt một điểm dừng ở đó, chỉ với một nhiệm vụ đơn giản. – mmr

+0

Tôi sẽ thừa nhận rằng tôi không chắc chắn 100% rằng nó không đòi hỏi phải có được, nhưng tôi không thấy lý do tại sao nó lại xảy ra. Nếu nó gọi một get, điều tương tự tôi đã nói sẽ áp dụng, chỉ cần thay thế doSomething() bằng hàm get của bạn. – SoapBox

+0

Nó gọi một nhận được, mỗi lần. Nếu không thì tham chiếu đến từ đâu? –

1

Các vẻ đẹp của đa luồng là bạn không biết thứ gì sẽ xảy ra. Nếu bạn đặt thứ gì đó trên một sợi, nó có thể xảy ra trước, nó có thể xảy ra sau khi nhận được.

Mã bạn đã đăng bằng khóa thành viên trong khi nó được đọc và viết. Nếu bạn muốn xử lý trường hợp giá trị được cập nhật, có lẽ bạn nên xem xét các hình thức đồng bộ hóa khác, chẳng hạn như events. (Kiểm tra các phiên bản tự động/thủ công). Sau đó, bạn có thể cho biết chuỗi "bỏ phiếu" của bạn rằng giá trị đã thay đổi và đã sẵn sàng để đọc lại.

2

Phạm vi khóa trong ví dụ của bạn ở vị trí không chính xác - nó cần nằm trong phạm vi thuộc tính của lớp 'MyObject' hơn là vùng chứa.

Nếu MyObject lớp đối tượng của tôi được sử dụng đơn giản để chứa dữ liệu mà một chủ đề muốn ghi vào và một chủ đề khác (chuỗi giao diện người dùng) đọc từ đó bạn có thể không cần thiết lập và xây dựng nó một lần.

Cũng nên xem xét việc đặt khóa ở cấp thuộc tính là mức ghi của độ chi tiết khóa; nếu có nhiều hơn một thuộc tính có thể được viết để đại diện cho trạng thái của một giao dịch (ví dụ: tổng số đơn đặt hàng và tổng khối lượng) thì có thể tốt hơn để có khóa ở cấp MyObject (tức là khóa (myObject.SyncRoot) .. .)

0

Trong phiên bản đã chỉnh sửa của bạn, bạn vẫn không cung cấp cách an toàn để cập nhật MyObject. Bất kỳ thay đổi nào đối với các thuộc tính của đối tượng sẽ cần phải được thực hiện bên trong khối được đồng bộ/khóa.

Bạn có thể viết người định cư riêng lẻ để xử lý việc này, nhưng bạn đã cho biết rằng điều này sẽ rất khó vì các trường số lớn.Nếu thực sự là trường hợp (và bạn chưa cung cấp đủ thông tin để đánh giá điều này), một cách khác là viết một setter sử dụng sự phản chiếu; điều này sẽ cho phép bạn vượt qua trong một chuỗi đại diện cho tên trường, và bạn có thể tự động tra cứu tên trường và cập nhật giá trị. Điều này sẽ cho phép bạn có một setter duy nhất có thể hoạt động trên bất kỳ số trường nào. Điều này không dễ dàng hoặc hiệu quả nhưng nó sẽ cho phép bạn xử lý một số lượng lớn các lớp và các trường.

12

Lập trình đồng thời sẽ khá dễ dàng nếu cách tiếp cận của bạn có thể hoạt động. Nhưng không, những tảng băng trôi mà chìm rằng Titanic là, ví dụ, khách hàng của lớp học của bạn làm điều này:

objectRef.MyProperty += 1; 

Các đọc-ghi sửa đổi chủng tộc là khá rõ ràng, có những cái tồi tệ hơn. Hoàn toàn không có gì bạn có thể làm để đảm bảo an toàn cho tài sản của bạn, ngoài việc biến nó thành bất biến. Đó là khách hàng của bạn mà cần phải đối phó với đau đầu. Bị buộc phải ủy thác loại trách nhiệm đó cho một lập trình viên ít có khả năng làm cho nó đúng là Achilles-heel của lập trình đồng thời.

+0

nó sẽ không tuyệt vời nếu nó có thể được dễ dàng này? Tôi chắc chắn có lý do tại sao nó không phải là, nhưng tôi chỉ không biết các chi tiết đủ để biết tại sao mã không hoạt động theo cách này. – mmr

+0

Điều quan trọng là phải hiểu điều này, đừng cố gắng lập trình đồng thời nếu bạn không. Google "cập nhật nguyên tử". –

+0

Tôi hiểu nguyên tử, nhưng tôi mơ hồ về những thứ nguyên tử trong C#. Chỉ là bài tập kiểu cơ bản, phải không? Chỉ định nguyên tử với thứ gì đó như Lồng vào nhau có thể hữu ích ... – mmr

4

Như những người khác đã chỉ ra, một khi bạn trả lại đối tượng từ bộ thu, bạn sẽ mất quyền kiểm soát đối tượng truy cập đối tượng và khi nào. Để làm những gì bạn muốn làm, bạn sẽ cần phải đặt một khóa bên trong đối tượng.

Có lẽ tôi không hiểu đầy đủ hình ảnh, nhưng dựa trên mô tả của bạn, có vẻ như bạn nhất thiết phải có khóa cho từng trường riêng lẻ. Nếu bạn có một tập hợp các trường chỉ đơn giản là đọc và viết thông qua các getters và setters, bạn có thể có thể lấy đi với một khóa duy nhất cho các lĩnh vực này. Rõ ràng là có khả năng bạn sẽ không cần thiết serialize hoạt động của các chủ đề của bạn theo cách này. Nhưng một lần nữa, dựa trên mô tả của bạn, nó không có vẻ như bạn đang tích cực truy cập vào đối tượng.

Tôi cũng khuyên bạn nên sử dụng sự kiện thay vì sử dụng chuỗi để thăm dò trạng thái thiết bị. Với cơ chế bỏ phiếu, bạn sẽ nhấn vào khóa mỗi lần chuỗi truy vấn thiết bị. Với cơ chế sự kiện, khi trạng thái thay đổi, đối tượng sẽ thông báo cho bất kỳ người nghe nào. Tại thời điểm đó, chủ đề 'bỏ phiếu' của bạn (sẽ không còn được bỏ phiếu) sẽ thức dậy và nhận trạng thái mới. Điều này sẽ hiệu quả hơn nhiều.

Như một ví dụ ...

public class Status 
{ 
    private int _code; 
    private DateTime _lastUpdate; 
    private object _sync = new object(); // single lock for both fields 

    public int Code 
    { 
     get { lock (_sync) { return _code; } } 
     set 
     { 
      lock (_sync) { 
       _code = value; 
      } 

      // Notify listeners 
      EventHandler handler = Changed; 
      if (handler != null) { 
       handler(this, null); 
      } 
     } 
    } 

    public DateTime LastUpdate 
    { 
     get { lock (_sync) { return _lastUpdate; } } 
     set { lock (_sync) { _lastUpdate = value; } } 
    } 

    public event EventHandler Changed; 
} 

chủ đề 'bỏ phiếu' của bạn sẽ giống như thế này.

Status status = new Status(); 
ManualResetEvent changedEvent = new ManualResetEvent(false); 
Thread thread = new Thread(
    delegate() { 
     status.Changed += delegate { changedEvent.Set(); }; 
     while (true) { 
      changedEvent.WaitOne(Timeout.Infinite); 
      int code = status.Code; 
      DateTime lastUpdate = status.LastUpdate; 
      changedEvent.Reset(); 
     } 
    } 
); 
thread.Start(); 
+0

Thú vị. Tôi sẽ phải xem xét điều này. Ấn tượng đầu tiên của tôi là nó sẽ không hoạt động, bởi vì thiết bị tôi đang sử dụng sẽ không đưa ra thông báo trạng thái trừ khi được hỏi. Vì vậy, nếu nó ở chế độ 'prep', không có dấu hiệu nào trừ khi bạn hỏi. – mmr

+0

Vâng, nếu bạn phải cài đặt thiết bị để báo cáo trạng thái, thì việc bỏ phiếu có lẽ là điều duy nhất bạn có thể làm. Tình cờ, có cơ chế gọi lại nào để thiết bị báo cáo trạng thái định kỳ không? Tôi ghét bỏ phiếu vì nó không hiệu quả, nhưng đôi khi, bạn không có lựa chọn nào khác. –

+0

Thật không may, nó là một thiết bị nối tiếp thực sự-- nó chỉ đáp ứng việc bỏ phiếu. Đây không phải là USB, đây là 9 chân. Old school retro tất cả các cách, tôi đoán. – mmr

-1

Liệu C# khóa không bị các vấn đề khóa giống như các ngôn ngữ khác rồi?

E.G.

var someObj = -1; 

// Thread 1 

if (someObj = -1) 
    lock(someObj) 
     someObj = 42; 

// Thread 2 

if (someObj = -1) 
    lock(someObj) 
     someObj = 24; 

Điều này có thể có vấn đề với cả hai chủ đề cuối cùng sẽ nhận được khóa và thay đổi giá trị. Điều này có thể dẫn đến một số lỗi lạ. Tuy nhiên bạn không muốn khóa không cần thiết đối tượng trừ khi bạn cần. Trong trường hợp này, bạn nên xem xét việc kiểm tra khóa kép.

// Threads 1 & 2 

if (someObj = -1) 
    lock(someObj) 
     if(someObj = -1) 
      someObj = {newValue}; 

Chỉ cần lưu ý điều gì đó.