Tại sao một người sẽ sử dụng Tác vụ <T> trên ValueTask <T> trong C #?


168

Kể từ C # 7.0, các phương thức không đồng bộ có thể trả về ValueTask <T>. Giải thích nói rằng nó nên được sử dụng khi chúng ta có kết quả được lưu trong bộ nhớ cache hoặc mô phỏng async thông qua mã đồng bộ. Tuy nhiên, tôi vẫn không hiểu vấn đề khi sử dụng ValueTask là gì hoặc trên thực tế tại sao async / await không được tạo với loại giá trị ngay từ đầu. Khi nào ValueTask không thực hiện được công việc?


7
Tôi nghi ngờ điều đó có liên quan đến lợi ích của ValueTask<T>(về mặt phân bổ) không cụ thể hóa cho các hoạt động thực sự không đồng bộ (vì trong trường hợp ValueTask<T>đó vẫn sẽ cần phân bổ heap). Ngoài ra còn có vấn đề Task<T>có rất nhiều hỗ trợ khác trong các thư viện.
Jon Skeet

3
@JonSkeet các thư viện hiện có là một vấn đề nhưng điều này đặt ra câu hỏi Nhiệm vụ có nên là ValueTask ngay từ đầu không? Những lợi ích có thể không tồn tại khi sử dụng nó cho những thứ không đồng bộ thực tế nhưng nó có hại không?
Stilgar

8
Xem github.com/dotnet/corefx/issues/4708#issuecomment-160658188 để có nhiều sự khôn ngoan hơn tôi có thể truyền đạt :)
Jon Skeet


3
@JoelMueller cốt truyện dày lên :)
Stilgar

Câu trả lời:


245

Từ các tài liệu API (nhấn mạnh thêm):

Các phương thức có thể trả về một thể hiện của loại giá trị này khi có khả năng kết quả hoạt động của chúng sẽ có sẵn một cách đồng bộ khi phương thức được dự kiến ​​sẽ được gọi thường xuyên đến mức chi phí phân bổ mới Task<TResult>cho mỗi cuộc gọi sẽ bị cấm.

Có sự đánh đổi để sử dụng ValueTask<TResult>thay vì a Task<TResult>. Ví dụ, mặc dù ValueTask<TResult>có thể giúp tránh phân bổ trong trường hợp kết quả thành công có sẵn một cách đồng bộ, nó cũng chứa hai trường trong khi một Task<TResult>kiểu tham chiếu là một trường đơn lẻ. Điều này có nghĩa là một cuộc gọi phương thức kết thúc trả về hai trường giá trị dữ liệu thay vì một, đó là nhiều dữ liệu để sao chép. Điều đó cũng có nghĩa là nếu một phương thức trả về một trong số đó được chờ trong mộtasync phương thức, thì máy trạng thái cho asyncphương thức đó sẽ lớn hơn do cần lưu trữ cấu trúc đó là hai trường thay vì một tham chiếu.

Hơn nữa, để sử dụng ngoài việc tiêu thụ kết quả của một hoạt động không đồng bộ thông qua await, ValueTask<TResult>có thể dẫn đến một mô hình lập trình phức tạp hơn, do đó thực sự có thể dẫn đến phân bổ nhiều hơn. Ví dụ, hãy xem xét một phương thức có thể trả về một Task<TResult>với một tác vụ được lưu trong bộ nhớ cache là kết quả chung hoặc a ValueTask<TResult>. Nếu người tiêu dùng của kết quả muốn sử dụng nó như một Task<TResult>, chẳng hạn như sử dụng với các phương thức như Task.WhenAllTask.WhenAny, ValueTask<TResult>trước tiên cần phải được chuyển đổi thành Task<TResult>sử dụngAsTask , dẫn đến phân bổ có thể tránh được nếu Task<TResult>sử dụng bộ nhớ cache ở nơi đầu tiên

Như vậy, lựa chọn mặc định cho bất kỳ phương thức không đồng bộ nào sẽ là trả về một Taskhoặc Task<TResult>. Chỉ khi phân tích hiệu suất chứng minh nó đáng giá nên ValueTask<TResult>được sử dụng thay vì Task<TResult>.


7
@MattThomas: Nó tiết kiệm một Taskphân bổ duy nhất (nhỏ và rẻ trong những ngày này), nhưng với chi phí làm cho phân bổ hiện tại của người gọi lớn hơn và tăng gấp đôi kích thước của giá trị trả lại (ảnh hưởng đến phân bổ đăng ký). Mặc dù đó là một lựa chọn rõ ràng cho kịch bản đọc được đệm, mặc định áp dụng nó cho tất cả các giao diện không phải là điều tôi khuyên dùng.
Stephen Cleary

1
Phải, Taskhoặc ValueTaskcó thể được sử dụng làm loại trả về đồng bộ (với Task.FromResult). Nhưng vẫn còn giá trị (heh) ValueTasknếu bạn có thứ gì đó mà bạn mong đợi là đồng bộ. ReadByteAsynclà một ví dụ cổ điển. Tôi tin rằng ValueTask được tạo ra chủ yếu cho các "kênh" mới (luồng byte mức thấp), cũng có thể được sử dụng trong lõi ASP.NET nơi hiệu năng thực sự quan trọng.
Stephen Cleary

1
Ồ tôi biết rằng lol, chỉ tự hỏi liệu bạn có gì để thêm vào nhận xét cụ thể đó không;)
julealgon

2
Liệu PR này chuyển sự cân bằng hơn để thích ValueTask? (ref: blog.marcgravell.com/2019/08/ )
stuartd

2
@stuartd: Hiện tại, tôi vẫn sẽ khuyên bạn nên sử dụng Task<T>làm mặc định. Điều này chỉ bởi vì hầu hết các nhà phát triển không quen thuộc với các hạn chế xung quanh ValueTask<T>(cụ thể là quy tắc "chỉ tiêu thụ một lần" và quy tắc "không chặn"). Điều đó nói rằng, nếu tất cả các nhà phát triển trong nhóm của bạn đều tốt ValueTask<T>, thì tôi sẽ đề xuất một hướng dẫn ưu tiên ở cấp độ nhóm ValueTask<T>.
Stephen Cleary

104

Tuy nhiên tôi vẫn không hiểu vấn đề khi sử dụng ValueTask là gì

Các loại cấu trúc không miễn phí. Sao chép các cấu trúc lớn hơn kích thước của một tham chiếu có thể chậm hơn so với sao chép một tham chiếu. Lưu trữ các cấu trúc lớn hơn một tham chiếu chiếm nhiều bộ nhớ hơn lưu trữ một tham chiếu. Các cấu trúc lớn hơn 64 bit có thể không được đăng ký khi tham chiếu có thể được đăng ký. Những lợi ích của áp lực thu thấp hơn có thể không vượt quá chi phí.

Vấn đề hiệu suất nên được tiếp cận với một kỷ luật kỹ thuật. Đặt mục tiêu, đo lường tiến trình của bạn so với mục tiêu và sau đó quyết định cách sửa đổi chương trình nếu mục tiêu không được đáp ứng, đo lường trên đường đi để đảm bảo rằng những thay đổi của bạn thực sự là những cải tiến.

tại sao async / await không được tạo với loại giá trị ngay từ đầu.

awaitđã được thêm vào C # rất lâu sau khi Task<T>loại đã tồn tại. Nó sẽ có phần sai lầm khi phát minh ra một loại mới khi một loại đã tồn tại. Và awaitđã trải qua rất nhiều lần lặp lại thiết kế tuyệt vời trước khi giải quyết cái đã được xuất xưởng vào năm 2012. Sự hoàn hảo là kẻ thù của cái tốt; tốt hơn để gửi một giải pháp hoạt động tốt với cơ sở hạ tầng hiện có và sau đó nếu có nhu cầu của người dùng, cung cấp các cải tiến sau này.

Tôi cũng lưu ý rằng tính năng mới cho phép các loại do người dùng cung cấp là đầu ra của phương thức do trình biên dịch tạo thêm rủi ro đáng kể và gánh nặng thử nghiệm. Khi những thứ duy nhất bạn có thể trả về là void hoặc một nhiệm vụ, nhóm thử nghiệm không phải xem xét bất kỳ kịch bản nào trong đó một số loại hoàn toàn điên rồ được trả lại. Kiểm tra trình biên dịch có nghĩa là tìm ra không chỉ những chương trình mà mọi người có khả năng viết, mà cả những chương trình nào có thể viết, bởi vì chúng tôi muốn trình biên dịch biên dịch tất cả các chương trình hợp pháp, không chỉ tất cả các chương trình hợp lý. Đó là đắt tiền.

Ai đó có thể giải thích khi ValueTask sẽ không thực hiện công việc?

Mục đích của điều này là cải thiện hiệu suất. Nó không thực hiện công việc nếu nó không đo lường được và cải thiện đáng kể hiệu suất. Không có gì đảm bảo rằng nó sẽ.


23

ValueTask<T>không phải là một tập hợp con Task<T>, nó là một superset .

ValueTask<T>là một liên kết phân biệt của T và a Task<T>, làm cho nó không phân bổ ReadAsync<T>để trả về đồng bộ một giá trị T có sẵn (ngược lại với việc sử dụng Task.FromResult<T>, cần phân bổ một Task<T>thể hiện). ValueTask<T>là đáng chờ đợi, vì vậy hầu hết các trường hợp tiêu thụ sẽ không thể phân biệt được với a Task<T>.

ValueTask, là một cấu trúc, cho phép viết các phương thức không đồng bộ không phân bổ bộ nhớ khi chúng chạy đồng bộ mà không ảnh hưởng đến tính nhất quán của API. Hãy tưởng tượng có một giao diện với phương thức trả về tác vụ. Mỗi lớp thực hiện giao diện này phải trả về một Tác vụ ngay cả khi chúng xảy ra để thực thi đồng bộ (hy vọng sử dụng Task.FromResult). Tất nhiên, bạn có thể có 2 phương thức khác nhau trên giao diện, một phương thức đồng bộ và một phương thức không đồng bộ nhưng điều này đòi hỏi 2 cách triển khai khác nhau để tránh đồng bộ hóa qua async và async trên đồng bộ hóa.

Vì vậy, nó cho phép bạn viết một phương thức không đồng bộ hoặc đồng bộ, thay vào đó viết một phương thức giống hệt nhau cho mỗi phương thức. Bạn có thể sử dụng nó ở bất cứ đâu bạn sử dụng Task<T>nhưng thường không thêm bất cứ thứ gì.

Vâng, nó bổ sung thêm một điều: Nó thêm một lời hứa ngụ ý cho người gọi rằng phương thức này thực sự sử dụng chức năng bổ sung ValueTask<T>cung cấp. Cá nhân tôi thích chọn các loại tham số và trả về cho người gọi càng nhiều càng tốt. Đừng quay lại IList<T>nếu bảng liệt kê không thể cung cấp số đếm; Đừng quay lại IEnumerable<T>nếu có thể. Người tiêu dùng của bạn không cần phải tra cứu bất kỳ tài liệu nào để biết phương pháp nào của bạn có thể được gọi một cách hợp lý và phương pháp nào không thể.

Tôi không thấy những thay đổi thiết kế trong tương lai là một lập luận thuyết phục ở đó. Hoàn toàn ngược lại: Nếu một phương thức thay đổi ngữ nghĩa của nó, nó sẽ phá vỡ bản dựng cho đến khi tất cả các lệnh gọi đến nó được cập nhật tương ứng. Nếu điều đó được coi là không mong muốn (và tin tôi đi, tôi đồng cảm với mong muốn không phá vỡ bản dựng), hãy xem xét phiên bản giao diện.

Đây thực chất là những gì gõ mạnh là dành cho.

Nếu một số lập trình viên thiết kế các phương thức không đồng bộ trong cửa hàng của bạn không thể đưa ra quyết định sáng suốt, có thể hữu ích khi chỉ định một cố vấn cao cấp cho mỗi lập trình viên ít kinh nghiệm hơn và xem xét mã hàng tuần. Nếu họ đoán sai, giải thích tại sao nó nên được thực hiện khác nhau. Đó là chi phí dành cho những người đàn anh, nhưng nó sẽ giúp các đàn em tăng tốc nhanh hơn nhiều so với việc ném họ vào tận cùng và cho họ một số quy tắc tùy tiện để tuân theo.

Nếu anh chàng viết phương pháp này không biết liệu nó có thể được gọi một cách đồng bộ hay không, thì ai trên trái đất?!

Nếu bạn có nhiều lập trình viên thiếu kinh nghiệm viết các phương thức không đồng bộ, thì những người đó cũng gọi họ như vậy phải không? Họ có đủ điều kiện để tự mình tìm ra cái nào an toàn để gọi async không, hoặc họ sẽ bắt đầu áp dụng quy tắc tùy ý tương tự như cách họ gọi những thứ này?

Vấn đề ở đây không phải là kiểu trả về của bạn, đó là các lập trình viên được đặt vào vai trò mà họ chưa sẵn sàng. Điều đó đã xảy ra vì một lý do, vì vậy tôi chắc chắn rằng nó không thể tầm thường để sửa chữa. Mô tả nó chắc chắn không phải là một giải pháp. Nhưng tìm kiếm một cách để lén vấn đề qua trình biên dịch cũng không phải là một giải pháp.


4
Nếu tôi thấy một phương thức quay trở lại ValueTask<T>, tôi cho rằng anh chàng đã viết phương thức đó đã làm điều đó bởi vì phương thức đó thực sự sử dụng chức năng ValueTask<T>bổ sung. Tôi không hiểu lý do tại sao bạn nghĩ rằng tất cả các phương thức của bạn đều có cùng kiểu trả về. Mục tiêu ở đó là gì?
15ee8f99-57ff-4f92-890c-b56153

2
Mục tiêu 1 sẽ là cho phép thay đổi thực hiện. Điều gì sẽ xảy ra nếu tôi không lưu trữ kết quả ngay bây giờ nhưng thêm mã bộ đệm vào sau? Mục tiêu 2 sẽ là có một hướng dẫn rõ ràng cho một nhóm mà không phải tất cả các thành viên đều hiểu khi ValueTask hữu ích. Chỉ cần làm điều đó luôn nếu không có hại trong việc sử dụng nó.
Stilgar

6
Theo quan điểm của tôi, điều đó gần giống như việc tất cả các phương thức của bạn trở lại objectbởi vì có thể một ngày nào đó bạn sẽ muốn chúng quay trở lại intthay vì string.
15ee8f99-57ff-4f92-890c-b56153

3
Có thể nhưng nếu bạn đặt câu hỏi "tại sao bạn lại trả về một chuỗi thay vì đối tượng" thì tôi có thể dễ dàng trả lời nó chỉ ra sự thiếu an toàn kiểu. Những lý do không sử dụng ValueTask ở mọi nơi dường như tinh tế hơn nhiều.
Stilgar

3
@Stilgar Một điều tôi sẽ nói: Những vấn đề tế nhị sẽ làm bạn lo lắng hơn những vấn đề rõ ràng.
15ee8f99-57ff-4f92-890c-b56153

20

Có một số thay đổi trong .Net Core 2.1 . Bắt đầu từ lõi .net 2.1 ValueTask có thể thể hiện không chỉ các hành động đã hoàn thành đồng bộ mà cả async cũng đã hoàn thành. Ngoài ra, chúng tôi nhận được loại không chung chung ValueTask.

Tôi sẽ để lại bình luận của Stephen Toub liên quan đến câu hỏi của bạn:

Chúng tôi vẫn cần chính thức hóa hướng dẫn, nhưng tôi hy vọng nó sẽ giống như thế này đối với diện tích bề mặt API công khai:

  • Nhiệm vụ cung cấp khả năng sử dụng nhiều nhất.

  • ValueTask cung cấp hầu hết các tùy chọn để tối ưu hóa hiệu suất.

  • Nếu bạn đang viết một giao diện / phương thức ảo mà người khác sẽ ghi đè, ValueTask là lựa chọn mặc định phù hợp.

  • Nếu bạn mong đợi API sẽ được sử dụng trên các đường dẫn nóng nơi phân bổ sẽ quan trọng, ValueTask là một lựa chọn tốt.

  • Mặt khác, khi hiệu suất không quan trọng, mặc định là Nhiệm vụ, vì nó cung cấp sự đảm bảo và khả năng sử dụng tốt hơn.

Từ góc độ triển khai, nhiều phiên bản ValueTask được trả về vẫn sẽ được tác vụ hỗ trợ.

Tính năng có thể được sử dụng không chỉ trong lõi .net 2.1. Bạn sẽ có thể sử dụng nó với gói System.Threading.T task.Extensions .


1
Thông tin khác từ Stephen ngày hôm nay: blog.msdn.microsoft.com/dotnet/2018/11/07/ Kẻ
Mike Cheel
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.