Threadsafe nghĩa là gì?


124

Gần đây, tôi đã cố gắng Truy cập hộp văn bản từ một chuỗi (không phải chuỗi giao diện người dùng) và một ngoại lệ đã được đưa ra. Nó cho biết điều gì đó về "mã không được an toàn trong luồng" và vì vậy tôi đã kết thúc bằng việc viết một đại biểu (mẫu từ MSDN đã giúp) và gọi nó thay thế.

Nhưng ngay cả như vậy tôi vẫn không hiểu tại sao tất cả các mã bổ sung lại cần thiết.

Cập nhật: Tôi có gặp bất kỳ sự cố nghiêm trọng nào không nếu tôi kiểm tra

Controls.CheckForIllegalCrossThread..blah =true

5
Thông thường, "an toàn chuỗi" có nghĩa là bất cứ điều gì người sử dụng thuật ngữ nghĩ rằng nó có nghĩa, ít nhất là đối với người đó. Do đó, nó không phải là một cấu trúc ngôn ngữ rất hữu ích - bạn cần phải cụ thể hơn nhiều khi nói về hành vi của mã luồng.


@dave Xin lỗi, tôi đã thử tìm kiếm, nhưng đã bỏ cuộc ... dù sao cũng cảm ơn ..
Vivek Bernard

1
một mã không phát sinhRace-Condition
Muhammad Babar

Câu trả lời:


121

Eric Lippert có một bài đăng trên blog rất hay với tựa đề Thứ mà bạn gọi là "an toàn chuỗi" là gì? về định nghĩa an toàn luồng như Wikipedia.

3 điều quan trọng được trích xuất từ ​​các liên kết:

“Một đoạn mã là an toàn cho chuỗi nếu nó hoạt động chính xác trong quá trình thực thi đồng thời bởi nhiều chuỗi”.

“Đặc biệt, nó phải đáp ứng nhu cầu nhiều luồng truy cập vào cùng một dữ liệu được chia sẻ,…”

“… Và nhu cầu về một phần dữ liệu được chia sẻ chỉ được truy cập bởi một chuỗi tại bất kỳ thời điểm nhất định nào.”

Chắc chắn đáng đọc!


25
Vui lòng tránh các câu trả lời chỉ liên kết vì nó có thể trở nên tồi tệ bất cứ lúc nào trong tương lai.
akhil_mittal


106

Nói một cách đơn giản nhất, threadsafe có nghĩa là an toàn khi được truy cập từ nhiều chủ đề. Khi bạn đang sử dụng nhiều luồng trong một chương trình và mỗi luồng đang cố gắng truy cập vào một cấu trúc dữ liệu chung hoặc vị trí trong bộ nhớ, một số điều tồi tệ có thể xảy ra. Vì vậy, bạn thêm một số mã phụ để ngăn chặn những điều tồi tệ đó. Ví dụ, nếu hai người đang viết cùng một tài liệu cùng một lúc, thì người thứ hai lưu sẽ ghi đè công việc của người thứ nhất. Sau đó, để đảm bảo an toàn cho chuỗi, bạn phải buộc người 2 phải đợi người 1 hoàn thành nhiệm vụ của họ trước khi cho phép người 2 chỉnh sửa tài liệu.


11
Đây được gọi là đồng bộ hóa. Đúng?
JavaTechnical

3
Đúng. Việc buộc các luồng khác nhau phải chờ truy cập vào tài nguyên được chia sẻ có thể được thực hiện bằng đồng bộ hóa.
Vincent Ramdhanie

Từ câu trả lời được chấp nhận của Gregory, anh ta đang nói "" Một đoạn mã là an toàn cho chuỗi nếu nó hoạt động chính xác trong quá trình thực thi đồng thời bởi nhiều chuỗi. " trong khi bạn đang nói "Để làm cho nó chủ đề an toàn sau đó, bạn phải lực lượng người từ 1 tới chờ đợi"; không được ông nói đồng thời là chấp nhận được trong khi bạn đang nói nó không phải là bạn có thể vui lòng giải thích?
Mật ong

Đó là một thứ tương tự. Tôi chỉ đề xuất một cơ chế đơn giản như một ví dụ về những gì tạo nên sự an toàn của mã. bất kể cơ chế được sử dụng là gì, mặc dù nhiều luồng chạy cùng một mã không nên can thiệp vào nhau.
Vincent Ramdhanie

Vì vậy, điều này chỉ áp dụng cho mã sử dụng các biến toàn cục và tĩnh sau đó? Sử dụng ví dụ của bạn về những người đang chỉnh sửa tài liệu, tôi cho rằng việc ngăn người 2 chạy mã viết tài liệu trên một tài liệu khác là không hợp lý.
Aaron Franke

18

Wikipedia có một bài viết về An toàn chủ đề.

Đây trang định nghĩa (bạn phải bỏ qua quảng cáo - xin lỗi) định nghĩa nó như sau:

Trong lập trình máy tính, an toàn luồng mô tả một phần chương trình hoặc quy trình có thể được gọi từ nhiều luồng lập trình mà không có sự tương tác không mong muốn giữa các luồng.

Một luồng là một đường dẫn thực thi của một chương trình. Một chương trình luồng đơn sẽ chỉ có một luồng và do đó vấn đề này không phát sinh. Hầu như tất cả các chương trình GUI đều có nhiều đường dẫn thực thi và do đó có các luồng - có ít nhất hai, một để xử lý hiển thị GUI và chuyển đầu vào của người dùng và ít nhất một đường khác để thực hiện các hoạt động của chương trình.

Điều này được thực hiện để giao diện người dùng vẫn đáp ứng trong khi chương trình đang hoạt động bằng cách giảm tải bất kỳ quá trình chạy dài nào xuống bất kỳ luồng không phải giao diện người dùng nào. Các luồng này có thể được tạo một lần và tồn tại trong suốt thời gian của chương trình, hoặc chỉ được tạo khi cần và hủy khi chúng kết thúc.

Vì các luồng này thường sẽ cần thực hiện các hành động phổ biến - i / o đĩa, xuất kết quả ra màn hình, v.v. - các phần mã này sẽ cần được viết theo cách mà chúng có thể xử lý việc được gọi từ nhiều luồng, thường là cùng lúc. Điều này sẽ liên quan đến những thứ như:

  • Làm việc trên các bản sao dữ liệu
  • Thêm khóa xung quanh mã quan trọng

8

Đơn giản, luồng an toàn có nghĩa là một phương thức hoặc cá thể lớp có thể được sử dụng bởi nhiều luồng cùng một lúc mà không có bất kỳ sự cố nào xảy ra.

Hãy xem xét phương pháp sau:

private int myInt = 0;
public int AddOne()
{
    int tmp = myInt;
    tmp = tmp + 1;
    myInt = tmp;
    return tmp;
}

Bây giờ cả luồng A và luồng B đều muốn thực thi AddOne (). nhưng A bắt đầu trước và đọc giá trị của myInt (0) vào tmp. Bây giờ vì một số lý do, bộ lập lịch quyết định tạm dừng luồng A và trì hoãn thực thi luồng B. Luồng B bây giờ cũng đọc giá trị của myInt (vẫn là 0) vào biến tmp của chính nó. Thread B kết thúc toàn bộ phương thức, vì vậy cuối cùng myInt = 1. Và 1 được trả về. Bây giờ lại đến lượt Thread A. Chủ đề A tiếp tục. Và thêm 1 vào tmp (tmp là 0 cho chủ đề A). Và sau đó lưu giá trị này trong myInt. myInt lại là 1.

Vì vậy, trong trường hợp này, phương thức AddOne đã được gọi hai lần, nhưng vì phương thức này không được triển khai theo cách an toàn của luồng nên giá trị của myInt không phải là 2, như mong đợi, mà là 1 vì luồng thứ hai đọc biến myInt trước khi luồng đầu tiên kết thúc. đang cập nhật nó.

Tạo ra các phương pháp an toàn chủ đề là rất khó trong những trường hợp không nhỏ. Và có khá nhiều kỹ thuật. Trong Java, bạn có thể đánh dấu một phương thức là đã đồng bộ hóa, điều này có nghĩa là chỉ một luồng có thể thực thi phương thức đó tại một thời điểm nhất định. Các chủ đề khác xếp hàng chờ. Điều này làm cho một chuỗi phương thức trở nên an toàn, nhưng nếu có nhiều việc phải thực hiện trong một phương thức, thì điều này sẽ lãng phí rất nhiều dung lượng. Một kỹ thuật khác là 'chỉ đánh dấu một phần nhỏ của phương thức là được đồng bộ hóa'bằng cách tạo một khóa hoặc semaphore, và khóa phần nhỏ này (thường được gọi là phần quan trọng). Thậm chí có một số phương thức được triển khai dưới dạng an toàn luồng không khóa, có nghĩa là chúng được xây dựng theo cách mà nhiều luồng có thể chạy qua chúng cùng lúc mà không bao giờ gây ra sự cố, đây có thể là trường hợp khi một phương thức chỉ thực hiện một lệnh gọi nguyên tử. Cuộc gọi nguyên tử là cuộc gọi không thể bị gián đoạn và chỉ có thể được thực hiện bởi một chuỗi tại một thời điểm.


nếu phương pháp AddOne được gọi là hai lần
Sujith PS

6

Trong thế giới thực, ví dụ cho giáo dân là

Giả sử bạn có tài khoản ngân hàng với Internet và ngân hàng di động và tài khoản của bạn chỉ có $ 10. Bạn đã thực hiện chuyển số dư sang tài khoản khác bằng ngân hàng di động và trong thời gian chờ đợi, bạn đã mua sắm trực tuyến bằng chính tài khoản ngân hàng đó. Nếu tài khoản ngân hàng này không an toàn, thì ngân hàng cho phép bạn thực hiện hai giao dịch cùng một lúc và sau đó ngân hàng sẽ phá sản.

Threadsafe có nghĩa là trạng thái của một đối tượng không thay đổi nếu đồng thời nhiều luồng cố gắng truy cập đối tượng.


5

Bạn có thể xem thêm lời giải thích từ cuốn sách "Java Concurrency in Practice":

Một lớp là luồng an toàn nếu nó hoạt động chính xác khi được truy cập từ nhiều luồng, bất kể việc lập lịch hoặc xen kẽ việc thực thi các luồng đó bởi môi trường thời gian chạy và không có đồng bộ hóa bổ sung hoặc phối hợp khác trên một phần của mã gọi.


4

Một mô-đun an toàn theo luồng nếu nó đảm bảo nó có thể duy trì các giá trị bất biến của nó khi đối mặt với việc sử dụng đa luồng và đồng thời.

Ở đây, một mô-đun có thể là một cấu trúc dữ liệu, lớp, đối tượng, phương thức / thủ tục hoặc hàm. Về cơ bản, đoạn mã có phạm vi và dữ liệu liên quan.

Bảo đảm có thể bị giới hạn trong một số môi trường nhất định như một kiến ​​trúc CPU cụ thể, nhưng phải giữ cho những môi trường đó. Nếu không có sự phân định rõ ràng các môi trường, thì nó thường được hiểu là nó giữ cho tất cả các môi trường mà mã có thể được biên dịch và thực thi.

Các mô-đun không an toàn theo luồng có thể hoạt động chính xác khi sử dụng đồng thời và phân luồng, nhưng điều này thường phụ thuộc vào sự may mắn và trùng hợp hơn là thiết kế cẩn thận. Ngay cả khi một số mô-đun không bị vỡ đối với bạn, nó có thể bị vỡ khi chuyển sang môi trường khác.

Lỗi đa luồng thường khó gỡ lỗi. Một số trong số chúng chỉ thỉnh thoảng xảy ra, trong khi những biểu hiện khác biểu hiện mạnh mẽ - điều này cũng có thể là do môi trường cụ thể. Chúng có thể biểu hiện dưới dạng kết quả sai tinh vi hoặc bế tắc. Chúng có thể làm xáo trộn cấu trúc dữ liệu theo những cách không thể đoán trước và khiến các lỗi dường như không thể khác xuất hiện trong các phần từ xa khác của mã. Nó có thể rất cụ thể cho ứng dụng, vì vậy thật khó để đưa ra một mô tả chung.


3

An toàn luồng: Chương trình an toàn luồng bảo vệ dữ liệu của nó khỏi các lỗi về tính nhất quán của bộ nhớ. Trong một chương trình đa luồng cao, một chương trình an toàn luồng không gây ra bất kỳ tác dụng phụ nào với nhiều thao tác đọc / ghi từ nhiều luồng trên cùng một đối tượng. Các luồng khác nhau có thể chia sẻ và sửa đổi dữ liệu đối tượng mà không có lỗi nhất quán.

Bạn có thể đạt được sự an toàn của chuỗi bằng cách sử dụng API đồng thời nâng cao. Trang tài liệu này cung cấp các cấu trúc lập trình tốt để đạt được sự an toàn của luồng.

Lock Objects hỗ trợ khóa các thành ngữ giúp đơn giản hóa nhiều ứng dụng đồng thời.

Người thực thi xác định một API cấp cao để khởi chạy và quản lý các luồng. Các triển khai Executor được cung cấp bởi java.util.concurrent cung cấp khả năng quản lý nhóm luồng phù hợp cho các ứng dụng quy mô lớn.

Bộ sưu tập đồng thời giúp dễ dàng quản lý bộ sưu tập dữ liệu lớn và có thể giảm đáng kể nhu cầu đồng bộ hóa.

Biến nguyên tử có các tính năng giảm thiểu đồng bộ hóa và giúp tránh lỗi nhất quán bộ nhớ.

ThreadLocalRandom (trong JDK 7) cung cấp hiệu quả tạo các số giả ngẫu nhiên từ nhiều chủ đề.

Tham khảo java.util.concurrentjava.util.concurrent.atomic gói quá cho các cấu trúc lập trình khác.


1

Rõ ràng bạn đang làm việc trong môi trường WinForms. Các điều khiển WinForms thể hiện mối quan hệ của luồng, có nghĩa là luồng mà chúng được tạo là luồng duy nhất có thể được sử dụng để truy cập và cập nhật chúng. Đó là lý do tại sao bạn sẽ tìm thấy các ví dụ trên MSDN và các nơi khác trình bày cách sắp xếp cuộc gọi trở lại luồng chính.

Thực hành WinForms bình thường là có một luồng duy nhất dành riêng cho tất cả công việc giao diện người dùng của bạn.


1

Tôi nhận thấy khái niệm http://en.wikipedia.org/wiki/Reentrancy_%28computing%29 là những gì tôi thường nghĩ về luồng không an toàn, đó là khi một phương thức có và dựa vào một tác dụng phụ như biến toàn cục.

Ví dụ: tôi đã thấy mã định dạng số dấu phẩy động thành chuỗi, nếu hai trong số này được chạy trong các chuỗi khác nhau, giá trị toàn cục của decimalSeparator có thể được thay đổi vĩnh viễn thành '.'

//built in global set to locale specific value (here a comma)
decimalSeparator = ','

function FormatDot(value : real):
    //save the current decimal character
    temp = decimalSeparator

    //set the global value to be 
    decimalSeparator = '.'

    //format() uses decimalSeparator behind the scenes
    result = format(value)

    //Put the original value back
    decimalSeparator = temp

-2

Để hiểu an toàn luồng, hãy đọc các phần dưới đây :

4.3.1. Ví dụ: Trình theo dõi xe sử dụng ủy quyền

Như một ví dụ quan trọng hơn về ủy quyền, hãy xây dựng một phiên bản của trình theo dõi phương tiện ủy quyền cho một lớp an toàn theo luồng. Chúng tôi lưu trữ các vị trí trong Bản đồ, vì vậy chúng tôi bắt đầu với việc triển khai Bản đồ an toàn theo chuỗi ConcurrentHashMap,. Chúng tôi cũng lưu trữ vị trí bằng cách sử dụng lớp Điểm bất biến thay vì MutablePoint, được hiển thị trong Liệt kê 4.6.

Liệt kê 4.6. Lớp Immutable Point được sử dụng bởi DelegateVehicleTracker.

 class Point{
  public final int x, y;

  public Point() {
        this.x=0; this.y=0;
    }

  public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

}

Pointlà luồng an toàn vì nó là bất biến. Các giá trị bất biến có thể được chia sẻ và xuất bản miễn phí, vì vậy chúng tôi không cần phải sao chép các vị trí khi trả lại chúng.

DelegatingVehicleTrackertrong Liệt kê 4.7 không sử dụng bất kỳ đồng bộ hóa rõ ràng nào; tất cả quyền truy cập vào trạng thái được quản lý bởi ConcurrentHashMapvà tất cả các khóa và giá trị của Bản đồ là bất biến.

Liệt kê 4.7. Ủy quyền an toàn chuỗi cho một ConcurrentHashMap.

  public class DelegatingVehicleTracker {

  private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;

  public DelegatingVehicleTracker(Map<String, Point> points) {
        this.locations = new ConcurrentHashMap<String, Point>(points);
        this.unmodifiableMap = Collections.unmodifiableMap(locations);
    }

  public Map<String, Point> getLocations(){
        return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable
    }

  public Point getLocation(String id) {
        return locations.get(id);
    }

  public void setLocation(String id, int x, int y) {
        if(locations.replace(id, new Point(x, y)) == null) {
             throw new IllegalArgumentException("invalid vehicle name: " + id);
        }
    }

}

Nếu chúng ta đã sử dụng MutablePointlớp ban đầu thay vì Point, chúng ta sẽ phá vỡ tính đóng gói bằng cách cho phép getLocationsxuất bản một tham chiếu đến trạng thái có thể thay đổi không an toàn cho luồng. Lưu ý rằng chúng tôi đã thay đổi hành vi của lớp theo dõi xe một chút; trong khi phiên bản giám sát trả về ảnh chụp nhanh các vị trí, phiên bản ủy quyền trả về chế độ xem không thể thay đổi nhưng “trực tiếp” về các vị trí xe. Điều này có nghĩa là nếu luồng A gọi getLocationsvà luồng B sau đó sửa đổi vị trí của một số điểm, những thay đổi đó sẽ được phản ánh trong Bản đồ được trả về luồng A.

4.3.2. Các biến trạng thái độc lập

Chúng ta cũng có thể ủy quyền an toàn luồng cho nhiều hơn một biến trạng thái cơ bản miễn là các biến trạng thái cơ bản đó độc lập, nghĩa là lớp tổng hợp không áp đặt bất kỳ biến trạng thái nào liên quan đến nhiều biến trạng thái.

VisualComponenttrong Liệt kê 4.9 là một thành phần đồ họa cho phép máy khách đăng ký bộ lắng nghe cho các sự kiện chuột và tổ hợp phím. Nó duy trì một danh sách các trình nghe đã đăng ký của từng loại, để khi một sự kiện xảy ra, các trình nghe thích hợp có thể được gọi ra. Nhưng không có mối quan hệ nào giữa tập hợp những người nghe chuột và những người nghe chính; hai là độc lập và do đó VisualComponentcó thể ủy thác các nghĩa vụ an toàn luồng của nó cho hai danh sách an toàn luồng cơ bản.

Liệt kê 4.9. Ủy quyền an toàn chuỗi cho nhiều biến trạng thái cơ bản.

public class VisualComponent {
    private final List<KeyListener> keyListeners 
                                        = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners 
                                        = new CopyOnWriteArrayList<MouseListener>();

  public void addKeyListener(KeyListener listener) {
        keyListeners.add(listener);
    }

  public void addMouseListener(MouseListener listener) {
        mouseListeners.add(listener);
    }

  public void removeKeyListener(KeyListener listener) {
        keyListeners.remove(listener);
    }

  public void removeMouseListener(MouseListener listener) {
        mouseListeners.remove(listener);
    }

}

VisualComponentsử dụng a CopyOnWriteArrayListđể lưu trữ từng danh sách người nghe; đây là cách triển khai Danh sách an toàn theo luồng đặc biệt thích hợp để quản lý danh sách người nghe (xem Phần 5.2.3). Mỗi Danh sách là an toàn luồng và bởi vì không có ràng buộc nào kết hợp trạng thái của một cái với trạng thái của cái kia, VisualComponentcó thể ủy thác trách nhiệm an toàn luồng của nó cho các đối tượng mouseListenerskeyListenersđối tượng bên dưới .

4.3.3. Khi ủy quyền thất bại

Hầu hết các lớp hỗn hợp không đơn giản như VisualComponent: chúng có các biến bất biến liên quan đến các biến trạng thái thành phần của chúng. NumberRangetrong Liệt kê 4.10 sử dụng hai AtomicIntegersđể quản lý trạng thái của nó, nhưng áp đặt một ràng buộc bổ sung — rằng số đầu tiên nhỏ hơn hoặc bằng số thứ hai.

Liệt kê 4.10. Lớp Dãy số không đủ bảo vệ những kẻ bất biến của nó. Đừng làm điều này.

public class NumberRange {

  // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

  public void setLower(int i) {
        //Warning - unsafe check-then-act
        if(i > upper.get()) {
            throw new IllegalArgumentException(
                    "Can't set lower to " + i + " > upper ");
        }
        lower.set(i);
    }

  public void setUpper(int i) {
        //Warning - unsafe check-then-act
        if(i < lower.get()) {
            throw new IllegalArgumentException(
                    "Can't set upper to " + i + " < lower ");
        }
        upper.set(i);
    }

  public boolean isInRange(int i){
        return (i >= lower.get() && i <= upper.get());
    }

}

NumberRangekhông thread-safe ; nó không bảo toàn bất biến mà ràng buộc thấp hơn và trên. Các setLowersetUpperphương pháp cố gắng tôn trọng sự bất biến này, nhưng làm như vậy kém. Cả hai setLowersetUpperđều là trình tự kiểm tra sau đó hành động, nhưng chúng không sử dụng đủ khóa để biến chúng thành nguyên tử. Nếu phạm vi số giữ (0, 10) và một luồng gọi setLower(5)trong khi luồng khác gọi setUpper(4), với một số thời gian không may mắn, cả hai sẽ vượt qua các kiểm tra trong bộ cài đặt và cả hai sửa đổi sẽ được áp dụng. Kết quả là phạm vi hiện giữ (5, 4) - trạng thái không hợp lệ . Vì vậy, trong khi các AtomicIntegers cơ bản là an toàn cho luồng, thì lớp hỗn hợp thì không . Bởi vì các biến trạng thái cơ bản lowerupperkhông độc lập, NumberRangekhông thể chỉ ủy quyền an toàn luồng cho các biến trạng thái an toàn luồng của nó.

NumberRangecó thể được thực hiện an toàn theo luồng bằng cách sử dụng khóa để duy trì các bất biến của nó, chẳng hạn như bảo vệ phía dưới và phía trên bằng một khóa chung. Nó cũng phải tránh xuất bản thấp hơn và cao hơn để ngăn khách hàng lật đổ những gì bất biến của nó.

Nếu một lớp có các hành động phức hợp, NumberRangethì việc ủy ​​quyền một lần nữa không phải là cách tiếp cận phù hợp cho an toàn luồng. Trong những trường hợp này, lớp phải cung cấp khóa riêng của nó để đảm bảo rằng các hành động phức hợp là nguyên tử, trừ khi toàn bộ hành động ghép cũng có thể được ủy quyền cho các biến trạng thái cơ bản.

Nếu một lớp bao gồm nhiều biến trạng thái an toàn luồng độc lập và không có hoạt động nào có bất kỳ chuyển đổi trạng thái không hợp lệ nào, thì nó có thể ủy quyền an toàn luồng cho các biến trạng thái cơ bản.

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.