Sử dụng cấu trúc dữ liệu liên tục trong các ngôn ngữ phi chức năng


17

Các ngôn ngữ hoàn toàn có chức năng hoặc gần như hoàn toàn là chức năng được hưởng lợi từ các cấu trúc dữ liệu liên tục vì chúng không thay đổi và phù hợp với phong cách lập trình chức năng.

Nhưng theo thời gian, chúng ta thấy các thư viện cấu trúc dữ liệu liên tục cho các ngôn ngữ (dựa trên trạng thái, OOP) như Java. Một tuyên bố thường được nghe có lợi cho các cấu trúc dữ liệu liên tục là bởi vì chúng không thay đổi, chúng an toàn cho chuỗi .

Tuy nhiên, lý do mà các cấu trúc dữ liệu liên tục là an toàn luồng là vì nếu một luồng là "thêm" một phần tử vào bộ sưu tập liên tục, thì hoạt động trả về một bộ sưu tập mới như ban đầu nhưng có thêm phần tử. Do đó, các chủ đề khác xem bộ sưu tập ban đầu. Hai bộ sưu tập chia sẻ rất nhiều trạng thái nội bộ, tất nhiên - đó là lý do tại sao các cấu trúc bền bỉ này có hiệu quả.

Nhưng vì các luồng khác nhau nhìn thấy các trạng thái dữ liệu khác nhau, nên dường như các cấu trúc dữ liệu liên tục không đủ để xử lý các tình huống trong đó một luồng tạo ra sự thay đổi hiển thị cho các luồng khác. Đối với điều này, dường như chúng ta phải sử dụng các thiết bị như nguyên tử, tài liệu tham khảo, bộ nhớ giao dịch phần mềm hoặc thậm chí các khóa cổ điển và cơ chế đồng bộ hóa.

Tại sao sau đó, tính bất biến của PDS được quảng cáo là một cái gì đó có lợi cho "an toàn luồng"? Có bất kỳ ví dụ thực tế nào mà PDS giúp đồng bộ hóa hoặc giải quyết các vấn đề tương tranh không? Hay PDS chỉ đơn giản là một cách để cung cấp giao diện phi trạng thái cho một đối tượng hỗ trợ cho phong cách lập trình chức năng?


3
Bạn cứ nói "cố chấp". Bạn có thực sự có nghĩa là "kiên trì" như trong "có thể sống sót khi khởi động lại chương trình" hay chỉ là "bất biến" như trong "không bao giờ thay đổi sau khi tạo ra nó"?
Kilian Foth

17
@KilianFoth Cấu trúc dữ liệu liên tục có định nghĩa rõ ràng : "cấu trúc dữ liệu bền vững là cấu trúc dữ liệu luôn bảo tồn phiên bản trước của chính nó khi được sửa đổi". Vì vậy, đó là về việc sử dụng lại cấu trúc trước đó khi một cấu trúc mới dựa trên cấu trúc đó được tạo ra thay vì kiên trì như trong "có thể tồn tại khi khởi động lại chương trình".
Michał Kosmulski

3
Câu hỏi của bạn dường như ít về việc sử dụng các cấu trúc dữ liệu liên tục trong các ngôn ngữ phi chức năng và nhiều hơn về phần nào của sự tương tranh và song song không được giải quyết bởi chúng, bất kể mô hình.

Lỗi của tôi. Tôi không biết rằng "cấu trúc dữ liệu liên tục" là một thuật ngữ kỹ thuật khác biệt với sự bền bỉ đơn thuần.
Kilian Foth

@delnan Đúng vậy.
Ray Toal

Câu trả lời:


15

Các cấu trúc dữ liệu liên tục / không thay đổi không tự giải quyết được các vấn đề tương tranh, nhưng chúng giúp giải quyết chúng dễ dàng hơn nhiều.

Hãy xem xét một luồng T1 chuyển một tập S sang một luồng khác T2. Nếu S có thể thay đổi, T1 có vấn đề: Nó mất quyền kiểm soát những gì xảy ra với S. Chủ đề T2 có thể sửa đổi nó, do đó, T1 không thể dựa hoàn toàn vào nội dung của S. Và ngược lại - T2 không thể chắc chắn rằng T1 không sửa đổi S trong khi T2 hoạt động trên nó.

Một giải pháp là thêm một số loại hợp đồng vào giao tiếp của T1 và T2 để chỉ một trong các luồng được phép sửa đổi S. Đây là lỗi dễ xảy ra và gánh nặng cả thiết kế và thực hiện.

Một giải pháp khác là T1 hoặc T2 nhân bản cấu trúc dữ liệu (hoặc cả hai, nếu chúng không được phối hợp). Tuy nhiên, nếu S không liên tục, đây là một hoạt động O (n) đắt tiền .

Nếu bạn có cấu trúc dữ liệu liên tục, bạn sẽ không phải chịu gánh nặng này. Bạn có thể chuyển một cấu trúc cho một luồng khác và bạn không cần phải quan tâm nó làm gì với nó. Cả hai luồng đều có quyền truy cập vào phiên bản gốc và có thể thực hiện các thao tác tùy ý trên nó - nó không ảnh hưởng đến những gì các luồng khác nhìn thấy.

Xem thêm: cấu trúc dữ liệu liên tục và bất biến .


2
À, vì vậy "an toàn luồng" trong ngữ cảnh này chỉ có nghĩa là một luồng không phải lo lắng về các luồng khác phá hủy dữ liệu họ nhìn thấy, nhưng không liên quan gì đến việc đồng bộ hóa và xử lý dữ liệu mà chúng tôi muốn chia sẻ giữa các luồng. Điều đó phù hợp với những gì tôi nghĩ, nhưng +1 vì đã nói một cách tao nhã "đừng tự mình giải quyết vấn đề tương lai."
Ray Toal

2
@RayToal Có, trong ngữ cảnh này, "thread safe" có nghĩa chính xác là như vậy. Làm thế nào dữ liệu được chia sẻ giữa các luồng là một vấn đề khác nhau, có nhiều giải pháp, như bạn đã đề cập (cá nhân tôi thích STM vì khả năng kết hợp của nó). An toàn luồng đảm bảo rằng bạn không phải lo lắng điều gì xảy ra với dữ liệu sau khi được chia sẻ. Đây thực sự là một vấn đề lớn, bởi vì các luồng không cần phải đồng bộ hóa ai làm việc trên cấu trúc dữ liệu và khi nào.
Petr Pudlák

@RayToal Điều này cho phép các mô hình đồng thời thanh lịch như các tác nhân , các nhà phát triển dự phòng không phải đối phó với việc quản lý luồng và khóa rõ ràng và dựa vào tính không thay đổi của tin nhắn - bạn không biết khi nào tin nhắn được gửi và xử lý, hoặc cho những gì khác diễn viên được chuyển tiếp đến.
Petr Pudlák

Cảm ơn Petr, tôi sẽ cho các diễn viên một cái nhìn khác. Tôi quen thuộc với tất cả các cơ chế Clojure và đã lưu ý rằng Rich Hickey rõ ràng đã chọn không sử dụng mô hình diễn viên , ít nhất là như được minh họa trong Erlang. Tuy nhiên, bạn càng biết nhiều thì càng tốt.
Ray Toal

@RayToal Một liên kết thú vị, cảm ơn. Tôi chỉ sử dụng các diễn viên làm ví dụ, không phải tôi đang nói đó là giải pháp tốt nhất. Tôi chưa sử dụng Clojure, nhưng có vẻ như giải pháp ưa thích của nó là STM, thứ mà tôi chắc chắn thích hơn các diễn viên. STM cũng dựa vào tính bền bỉ / bất biến - sẽ không thể khởi động lại một giao dịch nếu nó không thể sửa đổi cấu trúc dữ liệu.
Petr Pudlák

5

Tại sao sau đó, tính bất biến của PDS được quảng cáo là một cái gì đó có lợi cho "an toàn luồng"? Có bất kỳ ví dụ thực tế nào mà PDS giúp đồng bộ hóa hoặc giải quyết các vấn đề tương tranh không?

Lợi ích chính của PDS trong trường hợp đó là bạn có thể sửa đổi một phần dữ liệu mà không làm cho mọi thứ trở nên độc đáo (không cần sao chép sâu mọi thứ, có thể nói). Điều đó có nhiều lợi ích tiềm năng bên cạnh việc cho phép bạn viết các chức năng giá rẻ mà không có tác dụng phụ: sao chép và sao chép dữ liệu, hệ thống hoàn tác tầm thường, tính năng phát lại tầm thường trong trò chơi, chỉnh sửa không phá hủy tầm thường, an toàn ngoại lệ tầm thường, v.v.


2

Người ta có thể tưởng tượng một cấu trúc dữ liệu sẽ bền bỉ nhưng có thể thay đổi. Ví dụ: bạn có thể lấy một danh sách được liên kết, được biểu thị bằng một con trỏ tới nút đầu tiên và thao tác trả trước sẽ trả về một danh sách mới, bao gồm một nút đầu mới cộng với danh sách trước đó. Vì bạn vẫn có tham chiếu đến phần đầu trước đó, bạn có thể truy cập và sửa đổi danh sách này, điều này đồng thời cũng được nhúng trong danh sách mới. Mặc dù có thể, một mô hình như vậy không cung cấp lợi ích của các cấu trúc dữ liệu liên tục và bất biến, ví dụ, nó chắc chắn không phải là luồng an toàn theo mặc định. Tuy nhiên, nó có thể có công dụng của nó miễn là nhà phát triển biết họ đang làm gì, ví dụ như về hiệu quả không gian. Cũng lưu ý rằng mặc dù cấu trúc có thể thay đổi ở cấp độ ngôn ngữ ở chỗ không có gì ngăn cản mã sửa đổi nó,

Vì vậy, câu chuyện dài, không có sự bất biến (được thực thi bởi ngôn ngữ hoặc theo quy ước), cấu trúc dữ liệu od tồn tại mất một số lợi ích của nó (an toàn luồng) nhưng không phải là khác (hiệu quả không gian cho một số tình huống).

Đối với các ví dụ từ các ngôn ngữ phi chức năng, Java String.substring()sử dụng cái mà tôi gọi là cấu trúc dữ liệu liên tục. Chuỗi được đại diện bởi một mảng các ký tự cộng với các điểm bắt đầu và kết thúc của phạm vi của mảng thực sự được sử dụng. Khi một chuỗi con được tạo, đối tượng mới sử dụng lại cùng một mảng ký tự, chỉ với các độ lệch bắt đầu và kết thúc được sửa đổi. Vì Stringlà bất biến, nên (đối với substring()hoạt động, không phải là khác) là một cấu trúc dữ liệu liên tục bất biến.

Tính bất biến của cấu trúc dữ liệu là phần liên quan đến an toàn luồng. Sự kiên trì của họ (sử dụng lại các khối hiện có khi một cấu trúc mới được tạo ra) có liên quan đến hiệu quả khi làm việc với các bộ sưu tập đó. Vì chúng là bất biến, một hoạt động như thêm một mục không sửa đổi cấu trúc hiện có mà trả về một cấu trúc mới, với phần tử bổ sung được thêm vào. Nếu mỗi lần toàn bộ cấu trúc được sao chép, bắt đầu với một bộ sưu tập trống và thêm 1000 phần tử từng cái một để kết thúc với bộ sưu tập 1000 phần tử, sẽ tạo các đối tượng tạm thời với 0 + 1 + 2 + ... + 999 = Tổng số 500000 yếu tố sẽ là một sự lãng phí rất lớn. Với các cấu trúc dữ liệu liên tục, điều này có thể tránh được vì bộ sưu tập 1 phần tử được sử dụng lại trong phần tử 2 phần tử, được sử dụng lại trong phần tử 3 phần tử, v.v.


Đôi khi thật hữu ích khi có các đối tượng gần như bất biến trong đó tất cả ngoại trừ một khía cạnh của trạng thái là bất biến: khả năng tạo ra một đối tượng có trạng thái gần giống như một đối tượng nhất định. Ví dụ, một AppendOnlyList<T>mảng được hỗ trợ bởi hai mảng tăng trưởng có thể tạo ra các ảnh chụp nhanh bất biến mà không phải sao chép bất kỳ dữ liệu nào cho mỗi ảnh chụp nhanh, nhưng người ta không thể tạo một danh sách chứa nội dung của ảnh chụp nhanh đó, cộng với một mục mới, mà không cần sao chép tất cả mọi thứ đến một mảng mới.
supercat

0

Tôi thừa nhận thiên vị khi áp dụng các khái niệm như vậy trong C ++ bởi ngôn ngữ và bản chất của nó, cũng như miền của tôi và thậm chí cả cách chúng ta sử dụng ngôn ngữ. Nhưng với những điều này, tôi nghĩ rằng các thiết kế bất biến là khía cạnh ít thú vị nhất khi nói đến việc gặt hái một số lợi ích liên quan đến lập trình chức năng, như an toàn luồng, dễ suy luận về hệ thống, tìm cách sử dụng lại nhiều hơn cho các chức năng (và tìm kiếm chúng ta có thể kết hợp chúng theo bất kỳ thứ tự nào mà không có những bất ngờ khó chịu), v.v.

Lấy ví dụ đơn giản về C ++ này (phải thừa nhận là không được tối ưu hóa cho đơn giản để tránh lúng túng trước bất kỳ chuyên gia xử lý ảnh nào ngoài đó):

// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
     Image dst(new_w, new_h);
     for (int y=0; y < new_h; ++y)
     {
         for (int x=0; x < new_w; ++x)
              dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
     }
     return dst;
}

Mặc dù việc thực hiện chức năng đó làm thay đổi trạng thái cục bộ (và tạm thời) dưới dạng hai biến truy cập và hình ảnh cục bộ tạm thời thành đầu ra, nhưng nó không có tác dụng phụ bên ngoài. Nó nhập một hình ảnh và xuất ra một hình ảnh mới. Chúng ta có thể đa luồng nó với nội dung trái tim của chúng ta. Thật dễ dàng để lý do về, dễ dàng để kiểm tra kỹ lưỡng. Nó an toàn ngoại lệ vì nếu có bất cứ thứ gì ném, hình ảnh mới sẽ tự động bị loại bỏ và chúng tôi không phải lo lắng về việc khôi phục các tác dụng phụ bên ngoài (không có hình ảnh bên ngoài nào được sửa đổi ngoài phạm vi của chức năng, có thể nói).

Tôi thấy rất ít để đạt được, và có khả năng bị mất nhiều, bằng cách làm cho Imagebất biến trong bối cảnh trên, trong C ++, ngoại trừ có khả năng làm cho chức năng trên trở nên khó sử dụng hơn và có thể kém hiệu quả hơn một chút.

Độ tinh khiết

Vì vậy, các hàm thuần túy (không có tác dụng phụ bên ngoài ) rất thú vị đối với tôi và tôi nhấn mạnh tầm quan trọng của việc thường xuyên ưu tiên chúng cho các thành viên trong nhóm ngay cả trong C ++. Nhưng các thiết kế bất biến, được áp dụng chỉ là bối cảnh và sắc thái thường không có gì thú vị đối với tôi, vì tính chất bắt buộc của ngôn ngữ, nó thường hữu ích và thiết thực để có thể biến đổi một số đối tượng tạm thời cục bộ trong quá trình hiệu quả (cả hai cho nhà phát triển và phần cứng) thực hiện một chức năng thuần túy.

Sao chép giá rẻ của cấu trúc khổng lồ

Thuộc tính hữu ích thứ hai mà tôi tìm thấy là khả năng sao chép một cách rẻ tiền các cấu trúc dữ liệu thực sự khổng lồ xung quanh khi chi phí thực hiện, như thường được phát sinh để làm cho các hàm thuần túy có tính chất đầu vào / đầu ra nghiêm ngặt của chúng, sẽ không tầm thường. Đây sẽ không phải là cấu trúc nhỏ có thể phù hợp với ngăn xếp. Chúng sẽ là những cấu trúc to lớn, giống như toàn bộ Scenecho một trò chơi video.

Trong trường hợp đó, việc sao chép có thể ngăn chặn các cơ hội song song hiệu quả, bởi vì có thể khó song song hóa vật lý và kết xuất hiệu quả mà không khóa và tắc nghẽn lẫn nhau nếu vật lý đang làm biến đổi cảnh mà trình kết xuất đang cố gắng vẽ, đồng thời có vật lý sâu sao chép toàn bộ cảnh trò chơi xung quanh chỉ để xuất một khung hình với vật lý được áp dụng có thể không hiệu quả như nhau. Tuy nhiên, nếu hệ thống vật lý là 'thuần khiết' theo nghĩa là nó chỉ nhập vào một cảnh và xuất ra một cảnh mới với vật lý được áp dụng, và độ tinh khiết như vậy không phải trả giá bằng việc sao chép thiên văn trên cao, nó có thể hoạt động một cách an toàn song song với renderer mà không cần chờ đợi người khác.

Vì vậy, khả năng sao chép giá rẻ dữ liệu thực sự khổng lồ của trạng thái ứng dụng của bạn xung quanh và xuất ra các phiên bản mới, được sửa đổi với chi phí tối thiểu để xử lý và sử dụng bộ nhớ có thể thực sự mở ra những cánh cửa mới cho sự song song và hiệu quả, và tôi tìm thấy nhiều bài học để học từ cách cấu trúc dữ liệu liên tục được thực hiện. Nhưng bất cứ điều gì chúng ta tạo ra bằng những bài học như vậy không cần phải hoàn toàn bền bỉ, hoặc cung cấp các giao diện bất biến (ví dụ, nó có thể sử dụng bản sao trên văn bản, hoặc "trình tạo / tạm thời"), để đạt được khả năng này là rẻ mạt để sao chép xung quanh và sửa đổi chỉ các phần của bản sao mà không tăng gấp đôi sử dụng bộ nhớ và truy cập bộ nhớ trong nhiệm vụ tìm kiếm sự song song và tinh khiết trong các chức năng / hệ thống / đường ống của chúng tôi.

Bất biến

Cuối cùng, có sự bất biến mà tôi cho là ít thú vị nhất trong ba thứ này, nhưng nó có thể thực thi, bằng nắm đấm sắt, khi các thiết kế đối tượng nhất định không được sử dụng làm tạm thời cục bộ cho một chức năng thuần túy, và thay vào đó trong một bối cảnh rộng lớn hơn, có giá trị loại "độ tinh khiết ở mức đối tượng", vì trong tất cả các phương thức không còn gây ra tác dụng phụ bên ngoài (không còn biến đổi các biến thành viên bên ngoài phạm vi cục bộ ngay lập tức của phương thức).

Và trong khi tôi coi nó là thứ ít thú vị nhất trong ba ngôn ngữ như C ++, thì nó chắc chắn có thể đơn giản hóa việc kiểm tra và an toàn luồng và lý luận của các đối tượng không tầm thường. Chẳng hạn, nó có thể giảm tải để đảm bảo rằng một đối tượng không thể được cung cấp bất kỳ kết hợp trạng thái duy nhất nào bên ngoài hàm tạo của nó, và chúng ta có thể tự do chuyển nó xung quanh, thậm chí bằng tham chiếu / con trỏ mà không cần dựa vào hằng số và đọc chỉ các trình lặp và xử lý và như vậy, trong khi đảm bảo (tốt, ít nhất là nhiều nhất có thể trong ngôn ngữ) rằng nội dung ban đầu của nó sẽ không bị thay đổi.

Nhưng tôi thấy đây là thuộc tính ít thú vị nhất vì hầu hết các đối tượng tôi thấy có lợi khi được sử dụng tạm thời, ở dạng có thể thay đổi, để thực hiện một hàm thuần túy (hoặc thậm chí là một khái niệm rộng hơn, như một "hệ thống thuần túy" có thể là một đối tượng hoặc chuỗi Các chức năng với hiệu quả cuối cùng chỉ đơn thuần là nhập một cái gì đó và xuất ra một cái gì đó mới mà không chạm vào bất cứ thứ gì khác), và tôi nghĩ rằng sự bất biến được đưa đến cực đoan trong một ngôn ngữ chủ yếu là một mục tiêu khá phản tác dụng. Tôi sẽ áp dụng nó một cách tiết kiệm cho các phần của codebase, nơi nó thực sự giúp ích nhiều nhất.

Cuối cùng:

[...] Dường như các cấu trúc dữ liệu liên tục không đủ để xử lý các tình huống trong đó một luồng thực hiện thay đổi có thể nhìn thấy đối với các luồng khác. Đối với điều này, dường như chúng ta phải sử dụng các thiết bị như nguyên tử, tài liệu tham khảo, bộ nhớ giao dịch phần mềm hoặc thậm chí các khóa cổ điển và cơ chế đồng bộ hóa.

Đương nhiên nếu thiết kế của bạn yêu cầu sửa đổi (theo nghĩa thiết kế cuối người dùng) để hiển thị đồng thời nhiều luồng khi chúng xảy ra, chúng tôi sẽ quay lại đồng bộ hóa hoặc ít nhất là bảng vẽ để tìm ra một số cách tinh vi để xử lý vấn đề này ( Tôi đã thấy một số ví dụ rất công phu được sử dụng bởi các chuyên gia xử lý các loại vấn đề này trong lập trình chức năng).

Nhưng tôi đã tìm thấy, một khi bạn có được kiểu sao chép và khả năng tạo ra các phiên bản sửa đổi một phần của các cấu trúc khổng lồ bẩn thỉu, như bạn có thể nhận được với các cấu trúc dữ liệu liên tục như một ví dụ, nó thường mở ra rất nhiều cánh cửa và cơ hội bạn có thể trước đây không nghĩ đến việc song song mã có thể chạy hoàn toàn độc lập với nhau trong một đường ống song song I / O nghiêm ngặt. Ngay cả khi một số phần của thuật toán phải có bản chất nối tiếp, bạn có thể trì hoãn việc xử lý thành một luồng duy nhất nhưng thấy rằng việc dựa vào các khái niệm này đã mở ra cánh cửa dễ dàng, và không phải lo lắng, song song 90% công việc nặng nhọc, ví dụ

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.