Làm thế nào an toàn luồng có thể được cung cấp bởi một ngôn ngữ lập trình tương tự như cách an toàn bộ nhớ được cung cấp bởi Java và C #?


10

Java và C # cung cấp sự an toàn cho bộ nhớ bằng cách kiểm tra giới hạn mảng và các kết xuất con trỏ.

Những cơ chế nào có thể được thực hiện thành ngôn ngữ lập trình để ngăn chặn khả năng xảy ra tình trạng chủng tộc và bế tắc?


3
Bạn có thể quan tâm đến những gì Rust làm: Đồng thời
Vincent Savard

2
Làm cho mọi thứ không thay đổi hoặc làm cho mọi thứ không đồng bộ với các kênh an toàn. Bạn cũng có thể quan tâm đến GoErlang .
Theraot

@Theraot "làm cho mọi thứ không đồng bộ với các kênh an toàn" - chúc bạn có thể giải thích về điều đó.
mrpyo

2
@mrpyo bạn sẽ không phơi bày các tiến trình hoặc luồng, mỗi cuộc gọi là một lời hứa, mọi thứ đều diễn ra đồng thời (với thời gian chạy lập lịch thực hiện của chúng và tạo / gộp các luồng hệ thống phía sau cảnh khi cần) và logic bảo vệ trạng thái nằm trong các cơ chế truyền thông tin xung quanh ... thời gian chạy có thể tự động tuần tự hóa bằng cách lập lịch, và sẽ có một thư viện chuẩn với giải pháp an toàn luồng cho các hành vi nhiều sắc thái, đặc biệt là nhà sản xuất / người tiêu dùng và tổng hợp là cần thiết.
Theraot

2
Nhân tiện, có một cách tiếp cận khả thi khác: bộ nhớ giao dịch .
Theraot

Câu trả lời:


14

Các cuộc đua xảy ra khi bạn có bí danh đồng thời của một đối tượng và, ít nhất, một trong những bí danh đang biến đổi.

Vì vậy, để ngăn chặn các cuộc đua, bạn cần phải thực hiện một hoặc nhiều điều kiện không đúng sự thật.

Phương pháp khác nhau giải quyết các khía cạnh khác nhau. Lập trình hàm nhấn mạnh tính bất biến giúp loại bỏ tính đột biến. Khóa / nguyên tử loại bỏ tính đồng thời. Các loại affine loại bỏ răng cưa (Rust loại bỏ răng cưa có thể thay đổi). Mô hình diễn viên thường loại bỏ răng cưa.

Bạn có thể hạn chế các đối tượng có thể được đặt bí danh để dễ dàng hơn để đảm bảo tránh các điều kiện trên. Đó là nơi các kênh và / hoặc kiểu truyền tin nhắn đến. Bạn không thể bí danh bộ nhớ tùy ý, chỉ là phần cuối của kênh hoặc hàng đợi được sắp xếp để không có cuộc đua. Thông thường bằng cách tránh tính đồng thời tức là khóa hoặc nguyên tử.

Nhược điểm của các cơ chế khác nhau là chúng hạn chế các chương trình mà bạn có thể viết. Càng hạn chế cùn, các chương trình càng ít. Vì vậy, không có răng cưa hoặc không có khả năng biến đổi, và dễ dàng để lý do, nhưng rất hạn chế.

Đó là lý do tại sao Rust gây ra sự khuấy động như vậy. Đó là một ngôn ngữ kỹ thuật (so với ngôn ngữ học thuật) hỗ trợ răng cưa và khả năng biến đổi nhưng có trình biên dịch kiểm tra rằng chúng không xảy ra đồng thời. Mặc dù không phải là lý tưởng, nhưng nó cho phép một lớp chương trình lớn hơn được viết an toàn hơn rất nhiều so với những người tiền nhiệm của nó.


11

Java và C # cung cấp sự an toàn cho bộ nhớ bằng cách kiểm tra giới hạn mảng và các kết xuất con trỏ.

Điều quan trọng trước tiên là nghĩ về cách C # và Java làm điều này. Họ làm như vậy bằng cách chuyển đổi hành vi không xác định trong C hoặc C ++ thành hành vi được xác định: làm hỏng chương trình . Không bao giờ có thể bắt gặp các ngoại lệ và chỉ mục mảng Null trong một chương trình C # hoặc Java chính xác; chúng không nên xảy ra ở nơi đầu tiên vì chương trình không nên có lỗi đó.

Nhưng đó là tôi nghĩ không có ý gì với câu hỏi của bạn! Chúng tôi hoàn toàn có thể dễ dàng viết một thời gian chạy "bế tắc an toàn" kiểm tra định kỳ để xem liệu có n luồng nào đang chờ nhau và chấm dứt chương trình nếu điều đó xảy ra, nhưng tôi không nghĩ rằng điều đó sẽ làm bạn hài lòng.

Những cơ chế nào có thể được thực hiện thành ngôn ngữ lập trình để ngăn chặn khả năng xảy ra tình trạng chủng tộc và bế tắc?

Vấn đề tiếp theo chúng tôi gặp phải với câu hỏi của bạn là "điều kiện chủng tộc", không giống như những bế tắc, rất khó phát hiện. Hãy nhớ rằng, những gì chúng ta sau khi an toàn trong luồng không phải là loại bỏ các cuộc đua . Những gì chúng tôi theo sau là làm cho chương trình chính xác cho dù ai là người chiến thắng trong cuộc đua ! Vấn đề với điều kiện cuộc đua không phải là hai luồng đang chạy theo thứ tự không xác định và chúng tôi không biết ai sẽ hoàn thành trước. Vấn đề với các điều kiện chủng tộc là các nhà phát triển quên rằng một số đơn hàng hoàn thiện luồng là có thể và không tính đến khả năng đó.

Vì vậy, câu hỏi của bạn về cơ bản sôi nổi đến "có một cách mà một ngôn ngữ lập trình có thể đảm bảo rằng chương trình của tôi là chính xác?" và câu trả lời cho câu hỏi đó là, trong thực tế, không.

Cho đến nay tôi chỉ phê bình câu hỏi của bạn. Hãy để tôi thử chuyển đổi bánh răng ở đây và giải quyết tinh thần của câu hỏi của bạn. Có sự lựa chọn nào mà các nhà thiết kế ngôn ngữ có thể đưa ra để giảm thiểu tình trạng khủng khiếp mà chúng ta gặp phải với đa luồng không?

Tình hình thực sự là khủng khiếp! Bắt mã đa luồng chính xác, đặc biệt là trên các kiến ​​trúc mô hình bộ nhớ yếu, là rất, rất khó. Đó là hướng dẫn để suy nghĩ về lý do tại sao nó khó khăn:

  • Nhiều luồng kiểm soát trong một quá trình rất khó để lý do. Một chủ đề là đủ khó!
  • Trừu tượng trở nên cực kỳ rò rỉ trong một thế giới đa luồng. Trong thế giới luồng đơn, chúng tôi được đảm bảo rằng các chương trình hoạt động như thể chúng được chạy theo thứ tự, ngay cả khi chúng không thực sự chạy theo thứ tự. Trong thế giới đa luồng, điều đó không còn nữa; tối ưu hóa sẽ vô hình trên một luồng duy nhất trở nên hữu hình và bây giờ nhà phát triển cần phải hiểu những tối ưu hóa có thể có đó.
  • Nhưng nó trở nên tồi tệ hơn. Đặc tả C # nói rằng việc triển khai KHÔNG bắt buộc phải có thứ tự đọc và ghi nhất quán có thể được tất cả các chủ đề đồng ý . Quan niệm rằng có "chủng tộc" nào cả, và có một người chiến thắng rõ ràng, thực sự không đúng! Hãy xem xét một tình huống trong đó có hai lần ghi và hai lần đọc cho một số biến trên nhiều luồng. Trong một thế giới hợp lý, chúng ta có thể nghĩ rằng "tốt, chúng ta không thể biết ai sẽ chiến thắng trong các cuộc đua, nhưng ít nhất sẽ có một cuộc đua và ai đó sẽ chiến thắng". Chúng ta không ở trong thế giới hợp lý đó. C # cho phép nhiều luồng không đồng ý về thứ tự xảy ra đọc và ghi; không nhất thiết phải có một thế giới nhất quán mà mọi người đang quan sát.

Vì vậy, có một cách rõ ràng rằng các nhà thiết kế ngôn ngữ có thể làm cho mọi thứ tốt hơn. Bỏ qua các chiến thắng hiệu suất của bộ xử lý hiện đại . Làm cho tất cả các chương trình, ngay cả những chương trình đa luồng, có một mô hình bộ nhớ cực kỳ mạnh mẽ. Điều này sẽ làm cho các chương trình đa luồng chậm hơn nhiều lần, hoạt động trực tiếp với lý do có các chương trình đa luồng ở vị trí đầu tiên: để cải thiện hiệu suất.

Ngay cả việc bỏ qua mô hình bộ nhớ, vẫn có những lý do khác khiến việc đa luồng trở nên khó khăn:

  • Ngăn chặn bế tắc đòi hỏi phải phân tích toàn bộ chương trình; bạn cần biết thứ tự toàn cầu trong đó có thể rút khóa và thực thi lệnh đó trên toàn bộ chương trình, ngay cả khi chương trình bao gồm các thành phần được viết bởi các tổ chức khác nhau vào các thời điểm khác nhau.
  • Công cụ chính mà chúng tôi cung cấp cho bạn để chế ngự đa luồng là khóa, nhưng khóa không thể được tạo .

Đó là điểm cuối cùng giải thích thêm. Theo "composable", ý tôi là như sau:

Giả sử chúng ta muốn tính một số nguyên cho trước. Chúng tôi viết một triển khai chính xác của tính toán:

int F(double x) { correct implementation here }

Giả sử chúng ta muốn tính một chuỗi đã cho int:

string G(int y) { correct implementation here }

Bây giờ nếu chúng ta muốn tính một chuỗi được nhân đôi:

double d = whatever;
string r = G(F(d));

G và F có thể được tạo thành một giải pháp chính xác cho vấn đề phức tạp hơn.

Nhưng ổ khóa không có tài sản này vì bế tắc. Một phương thức đúng M1 có các khóa theo thứ tự L1, L2 và một phương thức đúng M2 lấy các khóa theo thứ tự L2, L1, không thể được sử dụng trong cùng một chương trình mà không tạo ra một chương trình không chính xác. Khóa làm cho nó sao cho bạn không thể nói "mọi phương pháp riêng lẻ đều đúng, vì vậy toàn bộ điều này là chính xác".

Vì vậy, những gì chúng ta có thể làm, là nhà thiết kế ngôn ngữ?

Đầu tiên, đừng đến đó. Nhiều luồng điều khiển trong một chương trình là một ý tưởng tồi và chia sẻ bộ nhớ qua các luồng là một ý tưởng tồi, vì vậy đừng đặt nó vào ngôn ngữ hoặc thời gian chạy ở vị trí đầu tiên.

Đây rõ ràng là một người không bắt đầu.

Sau đó, chúng ta hãy chú ý đến câu hỏi cơ bản hơn: tại sao chúng ta có nhiều luồng ở vị trí đầu tiên? Có hai lý do chính, và chúng được đưa vào cùng một điều thường xuyên, mặc dù chúng rất khác nhau. Họ bị giới hạn bởi vì cả hai đều về quản lý độ trễ.

  • Chúng tôi tạo chủ đề, sai, để quản lý độ trễ IO. Cần phải viết một tệp lớn, truy cập cơ sở dữ liệu từ xa, bất cứ điều gì, tạo một luồng công nhân thay vì khóa luồng UI của bạn.

Ý kiến ​​tồi. Thay vào đó, sử dụng không đồng bộ luồng đơn thông qua coroutines. C # làm điều này đẹp. Java, không tốt lắm. Nhưng đây là cách chính mà các nhà thiết kế ngôn ngữ hiện tại đang giúp giải quyết vấn đề luồng. Các awaitnhà điều hành trong C # (lấy cảm hứng từ F # công việc không đồng bộ và tình trạng kỹ thuật khác) được đưa vào ngày càng nhiều ngôn ngữ.

  • Chúng tôi tạo ra các luồng, đúng cách, để bão hòa các CPU nhàn rỗi với công việc nặng về tính toán. Về cơ bản, chúng tôi đang sử dụng các chủ đề như các quy trình nhẹ.

Các nhà thiết kế ngôn ngữ có thể giúp đỡ bằng cách tạo ra các tính năng ngôn ngữ hoạt động tốt với tính song song. Ví dụ, hãy suy nghĩ về cách LINQ được mở rộng một cách tự nhiên đến PLINQ. Nếu bạn là một người nhạy cảm và bạn giới hạn các hoạt động TPL của mình ở các hoạt động gắn với CPU rất song song và không chia sẻ bộ nhớ, bạn có thể nhận được những chiến thắng lớn tại đây.

Ta còn làm gì khác được nữa?

  • Làm cho trình biên dịch phát hiện ra những lỗi sai nhất và biến chúng thành cảnh báo hoặc lỗi.

C # không cho phép bạn chờ đợi trong khóa, vì đó là công thức cho những bế tắc. C # không cho phép bạn khóa loại giá trị vì đó luôn là điều sai; bạn khóa trên hộp, không phải giá trị. C # cảnh báo bạn nếu bạn bí danh một biến động, bởi vì bí danh không áp đặt ngữ nghĩa thu nhận / phát hành. Có rất nhiều cách khác mà trình biên dịch có thể phát hiện các vấn đề phổ biến và ngăn chặn chúng.

  • Thiết kế các tính năng "hố chất lượng", trong đó cách tự nhiên nhất để làm điều đó cũng là cách chính xác nhất.

C # và Java đã tạo ra một lỗi thiết kế rất lớn bằng cách cho phép bạn sử dụng bất kỳ đối tượng tham chiếu nào làm màn hình. Điều đó khuyến khích tất cả các loại thực hành xấu khiến cho việc theo dõi các bế tắc trở nên khó khăn hơn và khó ngăn chặn chúng một cách tĩnh tại. Và nó lãng phí byte trong mọi tiêu đề đối tượng. Màn hình nên được yêu cầu bắt nguồn từ một lớp màn hình.

  • Một lượng lớn thời gian và nỗ lực của Microsoft Research đã cố gắng thêm bộ nhớ giao dịch phần mềm vào ngôn ngữ giống như C # và họ không bao giờ làm cho nó hoạt động đủ tốt để kết hợp nó vào ngôn ngữ chính.

STM là một ý tưởng hay và tôi đã chơi xung quanh với các triển khai đồ chơi trong Haskell; nó cho phép bạn soạn thảo các giải pháp chính xác từ các bộ phận chính xác một cách thanh lịch hơn nhiều so với các giải pháp dựa trên khóa. Tuy nhiên, tôi không biết đủ về các chi tiết để nói tại sao nó không thể được thực hiện để hoạt động ở quy mô; Hãy hỏi Joe Duffy lần sau khi bạn gặp anh ấy.

  • Một câu trả lời khác đã được đề cập bất biến. Nếu bạn có sự bất biến kết hợp với các coroutines hiệu quả thì bạn có thể xây dựng các tính năng như mô hình diễn viên trực tiếp vào ngôn ngữ của bạn; nghĩ Erlang chẳng hạn.

Đã có rất nhiều nghiên cứu về các ngôn ngữ dựa trên tính toán quá trình và tôi không hiểu rõ về không gian đó; hãy thử đọc một vài bài viết về nó và xem bạn có hiểu biết gì không.

  • Giúp các bên thứ ba dễ dàng viết các máy phân tích tốt

Sau khi tôi làm việc tại Microsoft trên Roslyn, tôi đã làm việc tại Coverity và một trong những điều tôi đã làm là lấy giao diện người dùng phân tích bằng Roslyn. Bằng cách có một phân tích từ vựng, cú pháp và ngữ nghĩa chính xác do Microsoft cung cấp, sau đó chúng tôi có thể tập trung vào công việc khó khăn trong việc viết các trình phát hiện tìm thấy các vấn đề đa luồng phổ biến.

  • Tăng mức độ trừu tượng

Một lý do cơ bản tại sao chúng ta có chủng tộc và bế tắc và tất cả những thứ đó là bởi vì chúng ta đang viết các chương trình nói phải làm gì , và hóa ra tất cả chúng ta đều tào lao khi viết các chương trình cấp bách; máy tính làm những gì bạn nói với nó, và chúng tôi bảo nó làm những điều sai trái. Nhiều ngôn ngữ lập trình hiện đại ngày càng nói nhiều về lập trình khai báo: nói kết quả bạn muốn và để trình biên dịch tìm ra cách hiệu quả, an toàn, chính xác để đạt được kết quả đó. Một lần nữa, hãy nghĩ về LINQ; chúng tôi muốn bạn nói from c in customers select c.FirstName, trong đó thể hiện một ý định . Hãy để trình biên dịch tìm ra cách viết mã.

  • Sử dụng máy tính để giải quyết các vấn đề máy tính.

Các thuật toán học máy tốt hơn ở một số nhiệm vụ so với các thuật toán được mã hóa bằng tay, mặc dù tất nhiên có nhiều sự đánh đổi bao gồm tính chính xác, thời gian để đào tạo, sự thiên vị được đưa ra bởi đào tạo xấu, v.v. Nhưng có thể là rất nhiều nhiệm vụ mà chúng ta hiện đang viết mã "bằng tay" sẽ sớm có thể tuân theo các giải pháp do máy tạo ra. Nếu con người không viết mã, họ sẽ không viết lỗi.

Xin lỗi đó là một chút lan man ở đó; đây là một chủ đề lớn và khó khăn và không có sự đồng thuận rõ ràng nào xuất hiện trong cộng đồng PL trong 20 năm tôi đã theo dõi tiến bộ trong không gian vấn đề này.


"Vì vậy, câu hỏi của bạn về cơ bản sôi nổi" có cách nào mà ngôn ngữ lập trình có thể đảm bảo rằng chương trình của tôi là chính xác không? "Và câu trả lời cho câu hỏi đó là, trong thực tế, không." - thực ra, điều đó hoàn toàn có thể - nó được gọi là xác minh chính thức, và trong khi bất tiện, tôi khá chắc chắn rằng nó được thực hiện thường xuyên trên phần mềm quan trọng, vì vậy tôi sẽ không gọi nó là không thực tế. Nhưng bạn là một nhà thiết kế ngôn ngữ có thể biết điều này ...
mrpyo

6
@mrpyo: Tôi nhận thức rõ. Có rất nhiều vấn đề. Đầu tiên: Tôi đã từng tham dự một hội nghị xác minh chính thức nơi nhóm nghiên cứu MSFT trình bày một kết quả mới thú vị: họ có thể mở rộng kỹ thuật của họ để xác minh các chương trình đa luồng có độ dài lên đến hai mươi dòng và trình xác minh chạy trong chưa đầy một tuần. Đây là một bài thuyết trình thú vị, nhưng không có ích gì với tôi; Tôi đã có một chương trình 20 triệu để phân tích.
Eric Lippert

@mrpyo: Thứ hai, như tôi đã đề cập, một vấn đề lớn với các khóa là một chương trình được làm bằng các phương thức an toàn luồng không nhất thiết phải là chương trình an toàn luồng. Chính thức xác minh các phương pháp riêng lẻ không nhất thiết phải giúp đỡ và toàn bộ phân tích chương trình là khó đối với các chương trình không tầm thường.
Eric Lippert

6
@mrpyo: Thứ ba, vấn đề lớn với phân tích chính thức là chúng ta đang làm gì? Chúng tôi đang trình bày một đặc điểm kỹ thuật của điều kiện tiên quyết và hậu điều kiện và sau đó xác minh rằng chương trình đáp ứng đặc điểm kỹ thuật đó. Tuyệt quá; trong lý thuyết đó là hoàn toàn có thể làm được. Ngôn ngữ nào là đặc điểm kỹ thuật được viết bằng? Nếu có một ngôn ngữ đặc tả rõ ràng, có thể kiểm chứng thì hãy viết tất cả các chương trình của chúng tôi bằng ngôn ngữ đó và biên dịch . Tại sao chúng ta không làm điều này? Bởi vì nó thực sự rất khó để viết các chương trình chính xác bằng ngôn ngữ cụ thể!
Eric Lippert

2
Phân tích một ứng dụng cho tính chính xác bằng cách sử dụng các điều kiện trước / sau điều kiện là có thể (ví dụ: sử dụng Hợp đồng mã hóa). Tuy nhiên, phân tích như vậy chỉ khả thi với điều kiện các điều kiện có thể ghép lại được, còn khóa nào thì không. Tôi cũng sẽ lưu ý rằng viết một chương trình theo cách cho phép phân tích đòi hỏi kỷ luật cẩn thận. Ví dụ, các ứng dụng không tuân thủ nghiêm ngặt Nguyên tắc thay thế Liskov có xu hướng chống lại phân tích.
Brian
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.