Channels
Chào mọi người, hôm nay chúng ta sẽ nói về kênh (channels). Nếu bạn đến từ một ngôn ngữ như Golang, bạn sẽ quen với kênh vì đó là tất cả những gì họ có để giải quyết các vấn đề về đồng thời và song song. Trong C#, kênh là một tính năng khá mới và vấn đề chính mà kênh giải quyết là vấn đề nhà sản xuất/người tiêu dùng (producer/consumer), đúng không? Vậy nên nếu bạn có thể tìm thấy một mô hình tinh thần trong mã của mình rằng một cái gì đó đang sản xuất cái gì đó và sau đó một cái khác phải tiêu thụ nó, bạn hầu như muốn sử dụng một kênh vì đó là một giải pháp rất tốt cho vấn đề đó. Và nhiều nhà cung cấp đám mây cũng cung cấp các giải pháp kênh như pub/sub (publisher/subscriber queues), vốn thực chất là một kênh. Vì vậy, đây là một công nghệ được sử dụng khá thường xuyên và cách nó hoạt động thì có thể mang lại lợi ích, đúng không? Trước hết, chúng ta sẽ bắt đầu với một ví dụ nhanh về lý do tại sao bạn có thể muốn sử dụng một kênh, sau đó chúng ta sẽ nhảy vào cách nó được tạo ra, nhìn từ góc độ cao hơn vì ở mức thấp hơn, kênh được tạo ra bằng cách sử dụng các khái niệm cấp thấp hơn để làm cho nó hiệu quả hơn, nhưng chúng ta sẽ chỉ có một cái nhìn tổng quan và sau đó chúng ta sẽ vào Visual Studio trong một dự án ASP.NET Core và chúng ta sẽ xem một số trường hợp sử dụng thực tế cho nó. Được rồi, vậy hãy bắt đầu.
Một ví dụ đơn giản ở đây là chúng ta muốn gửi một thông báo và điều này có thể đòi hỏi một chút trì hoãn, đúng không? Chúng ta sẽ phải giao tiếp với một dịch vụ bên ngoài và hãy tiếp tục lấy kết quả. Vậy đây là một thao tác chặn và là một thao tác chặn không phải là tôi đang chặn một luồng, đúng không? Tôi có thể làm cho điều này không chặn bằng cách làm nó không đồng bộ, nhưng đây là một thao tác chặn theo nghĩa là người dùng gửi yêu cầu tới dịch vụ của bạn, đúng không? Vậy hãy tưởng tượng nếu một người dùng gửi yêu cầu tới dịch vụ của bạn và một phần của yêu cầu phải gửi một thông báo như là một phần trả lời cho người dùng thì người dùng phải đợi thông báo được tạo ra thì họ sẽ phải chờ, đúng không? Không quan trọng luồng hoàn thành hay không, họ sẽ phải đợi. Vậy đây là những gì chúng ta đang mô phỏng ở đây. Vì vậy, chúng ta chỉ muốn gửi thông báo và tôi rõ ràng không in gì cả, vậy hãy tiếp tục và chỉ cần đổ dữ liệu tôi ở đây. Vì bạn không quen thuộc, tôi đã sử dụng một nền tảng trực tuyến cho các bài học của mình gần đây, liên kết có trong mô tả, đó là một phần tuyệt vời của công cụ. Ồ vâng, như bạn có thể thấy, những gì xảy ra là thực thi tuần tự, đúng không? Điều này cũng chính xác những gì nên xảy ra, đúng không? Bạn gửi yêu cầu tới máy chủ, bạn cập nhật dữ liệu sản phẩm trong cơ sở dữ liệu và sau đó như một phần của đó, ai đó nhận được thông báo, đúng không? Nhưng vì nó cần phải đi tới một dịch vụ nào đó để gửi thông báo, người dùng vẫn phải đợi cho đến khi yêu cầu hoàn thành. Vậy bạn có thể đặt việc gửi thông báo này vào một nhiệm vụ fire-and-forget, đúng không? Vậy nên bạn chỉ nói, được rồi, gửi thông báo đi, tôi không quan tâm khi nào nó hoàn thành, vv. Vậy nên điều bạn sẽ bị cám dỗ là bạn sẽ chỉ đặt run ở đây, đúng không? Và mọi thứ sẽ hoàn thành khá nhanh chóng và rồi giả sử chỉ để chúng ta có thể có một cái nhìn trực quan về các nhiệm vụ đang hoàn thành, chúng ta có thể thấy rằng chúng đang được đẩy ra, đúng không? Vậy chúng ta đã mô phỏng gì ở đây? Chúng ta đã mô phỏng cơ bản là 10 người thực hiện yêu cầu hoặc bạn thực hiện yêu cầu 10 lần và tất cả các yêu cầu này hoàn thành ngay lập tức nhưng sau một thời gian, thông báo được gửi đi, được chứ? Vậy vấn đề với phương pháp fire-and-forget này là phạm vi (scope), đúng không? Vậy giả sử đây sẽ nằm trong một dịch vụ và thường thì cách bạn giải quyết dịch vụ là bạn thực hiện dependency injection, đúng không? Vậy nên bạn inject phụ thuộc vào một dịch vụ và sau đó trong dịch vụ này, hàm gửi thông báo sẽ tiêu thụ một trong các dịch vụ đã được inject vì chúng ta kết thúc phạm vi này trước khi thông báo thực sự được gửi đi, dependency injection sẽ hủy bỏ dịch vụ đã được inject. Khi dịch vụ đó bị hủy bỏ, khi hàm gửi thông báo cố gắng tiêu thụ nó, đó là khi vấn đề sẽ xảy ra, được không? Và điều này sẽ phá vỡ như dịch vụ không còn tồn tại, đúng không? Vậy nên đây là nơi bạn có thể muốn sử dụng một kênh, nơi bạn cơ bản nói, hãy đặt một thông điệp về những gì bạn muốn gửi vào một kênh và khi một dịch vụ sẵn sàng, bạn có thể lấy nó lên. Và một vấn đề khác với giải pháp này là giả sử bạn đang chạy trên một máy có một bộ xử lý, đúng không? Và bạn có giả sử 20 hoặc một trăm người dùng một ngày, đúng không? Và họ tất cả tạo ra 10 thông báo mỗi người, đúng không? Giả sử nó vì bộ xử lý chậm, nó có thể mất một giây để gửi một thông báo, bạn đang thực chất là làm ngập các nhiệm vụ với những nhiệm vụ fire-and-forget này và tại một thời điểm nào đó, máy chủ của bạn sẽ chậm phản hồi dù sao đi nữa, đúng không? Vậy nên bạn có thể muốn giới hạn số lượng nhiệm vụ được xử lý cùng một lúc và một kênh có thể giúp điều đó nữa, đúng không? Vì nó thực hiện một hàng đợi, vậy trước khi đi vào các ví dụ trong Visual Studio, những gì tôi muốn làm là tôi muốn tiếp tục và triển khai một kênh cho chính mình, đúng không? Một vấn đề thực tế mà tôi đã giải quyết khi tôi tự triển khai một kênh trước khi kênh thực sự tồn tại trong C# là vấn đề xử lý video. Vậy nên xử lý video là một nhiệm vụ dài hạn và giống như YouTube, nơi bạn tải lên một video và sau đó bạn có thể đóng trình duyệt và video của bạn vẫn đang được xử lý thành các chất lượng khác nhau, đúng không? Vậy là cơ bản những gì họ làm là họ lấy nhiệm vụ của bạn, họ đặt nó lên một kênh và sau đó một cái gì đó đọc từ hàng đợi và bắt đầu xử lý video của bạn. Vậy nên tôi cơ bản đã làm điều tương tự và vâng, tôi sẽ chỉ cho bạn cách tôi đã làm điều đó. Vậy nên điều này sẽ bao gồm kiến thức về semaphore, đúng không? Nếu bạn chưa xem video đó, hãy xem video trước của tôi, nếu bạn biết semaphore là gì, bạn đã sẵn sàng để tiếp tục. Vậy nên một kênh bao gồm hai thứ thực sự là bạn muốn có thể đọc từ một kênh và bạn muốn có thể viết vào một kênh, đúng không? Vậy nên hãy tiếp tục và tạo hai interface, đúng không? Vậy nên public interface IReader và public interface IWriter, đúng không? Vậy nên chúng ta sẽ viết một loại nào đó và chúng ta cũng muốn có thể đọc loại đó, đúng không? Vậy nên hãy tiếp tục và đẩy cái gì đó vào kênh, đúng không? Và nó thường là bất kỳ thông điệp nào, đúng không? Không quan trọng nó là gì, nó là một loại thông điệp nào đó và toàn bộ vấn đề về kênh là thông điệp này là như thế nào, chúng ta có một dịch vụ trong phạm vi của hàm chính này mà sẽ được tiêu thụ bởi hàm gửi thông báo thay vì phụ thuộc vào bộ nhớ trong dịch vụ này của việc có thêm một dịch vụ được inject vào đúng không? Vậy nên nó giống như một loại trạng thái của dịch vụ đó cần phải hoặc hàm này để có thể tiêu thụ dịch vụ đã được inject đúng không? Vậy nên nó là một bộ nhớ không trạng thái của dịch vụ đó ở đây, chúng ta không phụ thuộc vào trạng thái mà thay vào đó nói đây là những gì cần để tạo ra thông báo, thông điệp này và chúng ta đẩy nó vào kênh và sau đó cái gì đó khác sẽ đọc từ hàng đợi và tạo ra thông báo, đúng không? Vậy nên thay vì phụ thuộc vào trạng thái và chia sẻ bộ nhớ thông qua trạng thái đó, chúng ta chia sẻ bộ nhớ bằng cách truyền nó vào hàng đợi và đó là toàn bộ triết lý đằng sau kênh, chúng ta chia sẻ bộ nhớ bằng cách truyền nó qua các kênh thay vì có một trạng thái toàn cục cơ bản. Vậy nên đây là cái gì đó chúng ta muốn đẩy vào, chúng ta cũng muốn có thể đọc nó nữa, đúng không? Và hàng đợi của tôi sẽ bị giới hạn một chút, tôi cũng sẽ làm nó thành một nhiệm vụ vì đây sẽ là một thao tác không đồng bộ, chúng ta không phải lúc nào cũng có thể đọc từ kênh nếu nó trống, chúng ta không muốn chặn luồng, đúng không? Vậy nên hãy tiếp tục và import task. Đẩy là một thao tác đồng bộ, chúng ta chỉ đẩy và nó giống như thêm một mục vào danh sách, đúng không? Tiếp theo, chúng ta muốn người viết có thể hoàn thành vào một thời điểm nào đó, không thực sự cần thiết trong kịch bản ASP.NET Core nơi máy chủ của bạn chạy lâu dài, nhưng cho ví dụ này vì tôi đang ở link pad và dịch vụ của tôi không nên chạy lâu dài, tôi sẽ tiếp tục và triển khai một vài hàm thực sự tồn tại trên kênh nhưng bạn có thể không cần chúng. Tiếp theo, sẽ là void complete, đúng không? Hoặc vâng, hãy nói complete. Vậy đây là nơi cơ bản nói rằng chúng ta đã hoàn thành việc viết vào kênh và ở đây chúng ta có thể nói, oh, nó đã hoàn thành, được rồi và chúng ta có thể kiểm tra xem nó đã hoàn thành hay chưa. Được rồi, vậy ở đây chúng ta sẽ nói đẩy cái gì đó đẩy cái gì đó vào hàng đợi một khi chúng ta hoàn thành, hãy tiếp tục và nói hoàn thành ở đây, chúng ta sẽ nói chờ để đọc từ hàng đợi và khi chúng ta hoàn thành thì thoát, đúng không? Vậy nên hãy tiếp tục và triển khai kênh của chúng ta. Khi tôi thức dậy hôm nay, tôi có lớp Channel của mình, chúng ta sẽ triển khai một kênh của loại T, đúng không? Vậy nên đây là thông điệp mà chúng ta đang truyền dữ liệu mà chúng ta chia sẻ giữa các luồng, đúng không? Hoặc các nhiệm vụ, thậm chí chúng ta có và tôi đọc int và tôi viết T, đúng không? Vậy nên hãy tiếp tục và triển khai các interface này, được rồi? Vậy nên chúng ta có một vài thứ về việc đã hoàn thành, hãy tiếp tục và nhanh chóng giải quyết điều này. Chúng ta sẽ có một pool riêng tư đã hoàn thành, đúng không? Và nếu nó hoàn thành thì nó void, chúng ta sẽ đặt isComplete = true và sau đó ở đây chúng ta chỉ muốn trả về finish, được rồi? Vậy nên điều đó khá đơn giản, hãy tiếp tục và đặt ở dưới để chúng ta không bị rối với nó nữa, đúng không? Vâng, những gì chúng ta thực sự muốn giải quyết là việc đẩy, đọc và nó vẫn đang là thao tác không đồng bộ, vậy nên kênh đôi khi hoạt động như một hàng đợi, vậy đây là những gì tôi sẽ triển khai, thực chất kênh giống như một hàng đợi mà điều tiết số lượng mục bạn có thể có trong hàng đợi hoặc ai có thể đọc từ hàng đợi, đúng không? Hoặc khi nào bạn có thể đọc từ hàng đợi theo cách không đồng bộ, vậy chỉ đọc khi chúng ta có mục trên đó, đúng không? Vậy nên chúng ta sẽ có một constructor từ đó chúng ta sẽ khởi tạo một hàng đợi và vì chúng ta muốn đồng thời, nhiều luồng có thể ghi vào kênh này, chúng ta muốn đồng thời, bạn biết đấy, nó sẽ là một hàng đợi kiểu chung, đúng không? Và nó sẽ là kiểu T, đúng không? Vậy nên một lần nữa, đây là thông điệp mà chúng ta đang truyền và đây là cái mà chúng ta sẽ cần khởi tạo ConcurrentQueue ở đó, được rồi. Vậy nên chúng ta có hàng đợi của mình, thực chất đây là nơi chúng ta đặt các thông điệp, nó giống như thêm thứ gì đó vào danh sách ngay khi chúng ta thêm nó, đúng không? Chúng ta hoàn thành yêu cầu và sau đó cái gì đó khác có thể tiếp tục ở đây, đúng rồi. Vậy nên khi chúng ta đẩy thứ gì đó, chúng ta lấy hàng đợi của mình và chúng ta gọi Enqueue, đúng không? Vậy nên đây thực chất là chúng ta đẩy thứ gì đó vào hàng đợi, chúng ta sẽ đặt thông điệp vào đây, đúng không? Đơn giản thôi. Đây là một thao tác đồng bộ, rất an toàn, nhưng điều chúng ta muốn làm bây giờ là chúng ta muốn đọc thứ gì đó, đúng không? Vậy nên cách bạn đọc thứ gì đó từ hàng đợi là bạn thử Dequeue, đúng không? Vậy nó sẽ trả về một boolean nhưng nó cũng sẽ out VAR t của kết quả của bạn, đúng không? Vậy nên nếu chúng ta có cái gì đó, hãy tiếp tục và lấy biến out var message và đây sẽ là kiểu T và đây là cái chúng ta có thể trả về, đúng không? Và tôi sẽ để lỗi này ở đây vì nó sẽ là không đồng bộ trong giây lát vì đây là toàn bộ mục đích là cố gắng làm cho nó không đồng bộ, cái gì đó sẽ cố gắng tiếp tục đọc từ đây, đúng không? Vậy nên sẽ có hai nhiệm vụ, một là nhà sản xuất và một là người tiêu dùng, đúng không? Nhà sản xuất sẽ đặt thứ gì đó và người tiêu dùng sẽ đọc thứ gì đó, đúng không? Vậy nên khi người tiêu dùng đang chờ để đọc, chúng ta không muốn người tiêu dùng chặn luồng, vậy đây là lý do tại sao điều này phải là không đồng bộ, đúng không? Và đây là lý do tại sao bạn cần biết về semaphore, đây là cách chúng ta sẽ giải quyết vấn đề này, đây là nơi chúng ta cơ bản nói chỉ có một số lượng mục nhất định trên hàng đợi và đây là khi nào bạn có thể đọc, đúng không? Vậy nên nó thực chất là cổng, đúng không? Nếu một khi chúng ta đẩy một mục, chúng ta muốn cơ bản nói, đúng, bạn được phép đọc, hãy vào và đọc hoặc các thông điệp của chúng ta sẽ cơ bản là vào và ra, đúng không? Vậy nên hãy tiếp tục và lấy một semaphore và chúng ta sẽ cơ bản xem nó hoạt động như thế nào, đúng không? Và chúng ta sẽ chỉ định rằng semaphore này là semaphoreSlim, nhanh chóng, đẹp đẽ. Vậy nên một semaphore là một semaphoreSlim mới với giá trị ban đầu là 0 bởi vì khi kênh được tạo ra, chúng ta không có mục nào trong hàng đợi, đúng không? Mỗi lần chúng ta đẩy một thông điệp vào hàng đợi, chúng ta muốn nói với người tiêu dùng rằng bạn có thể đến và đọc thông điệp này, đúng không? Và hiểu rằng có thể có nhiều tiến trình, đúng không? Có thể có nhiều tiến trình hoặc nhiều người tiêu dùng cho kênh này và cũng như nhiều nhà sản xuất cho kênh này, đó là toàn bộ vấn đề về kênh, đúng không? Vậy nên những gì chúng ta thực chất làm là chúng ta đặt thứ gì đó vào hàng đợi và sau đó chúng ta nói, đúng, ai đó hãy đến và lấy thứ này ở đây. Những gì chúng ta muốn làm là chúng ta muốn cơ bản nhìn từ semaphore của mình, chúng ta sẽ viết, để tôi biến điều này thành semaphore, chúng ta không bị rối, chúng ta sẽ tiếp tục và chờ semaphore, đúng không? Và đây là những gì semaphore làm, vậy nên chúng ta sẽ để luồng vào đây và nó sẽ rời đi cho đến khi có thứ gì đó trên hàng đợi. Khi có thứ gì đó trên hàng đợi, nó sẽ tiếp tục nhiệm vụ này, nó sẽ đẩy mục lên từ cái gọi là đọc một thông điệp từ hàng đợi và sau đó chúng ta sẽ trả về null ở đây chỉ để có thể trả về cái gì đó hoặc tôi đoán một số loại T không thể là null, vậy nên chúng ta chỉ trả về default. Nếu đây là int, nó sẽ trả về zero và không thể là null, đúng không? Vậy nên đây thực chất là cho việc triển khai kênh, đúng không? Nó thực sự đơn giản ở đây, việc triển khai của Microsoft thì có phần cấp thấp hơn để làm cho nó hiệu suất hơn. Hãy tiếp tục và thử sử dụng cái này nhé.
Vậy nên chúng ta sẽ tạo ra hai nhiệm vụ, đúng không? Chúng ta sẽ tạo ra một nhà sản xuất và một người tiêu dùng. Nhà sản xuất sẽ viết vào kênh, đúng không? Vậy nên chúng ta muốn một IWriter và chúng ta sẽ có T ở đây, hãy để tôi viết, không đặt generic ở đây, tôi chỉ nói rằng tôi muốn là string vì tôi sẽ truyền thông điệp xung quanh, đúng không? Và những gì tôi sẽ làm là tôi sẽ nói, hãy lấy bốn và giả sử bốn là một trăm thông điệp, hãy có nhà viết, chúng ta sẽ tiếp tục và đẩy thông điệp của mình và I vào string có lẽ một chút ở đó, nó có, vậy nên chúng ta sẽ đẩy nó và một khi chúng ta hoàn thành việc đẩy, chúng ta chỉ tiếp tục và trong writer của mình nói rằng chúng ta đã hoàn thành và đây là một nhiệm vụ chỉ để nó có thể chạy song song với người tiêu dùng nữa, đúng không? Vậy nên hãy tiếp tục và đặt một chút trì hoãn ở đây, đúng không? Vậy nên không phải là một gì đó lớn, nhưng bạn sẽ thấy rằng đây sẽ là một thao tác chặn nhưng nó vẫn sẽ chạy song song, đúng không? Vậy nên chúng ta sẽ trì hoãn một trăm mili giây và hãy tiếp tục và lấy kết quả. Ở cuối, chúng ta chỉ muốn nói rằng nhiệm vụ này đã hoàn thành và trả về, đúng không? Vậy nên không có thực sự cần thiết phải có thao tác không đồng bộ như tôi đã giải thích trong các video async/await của mình mà chúng ta chỉ có thể muốn chạy điều này song song, đúng không? Vậy nên đây là nhà sản xuất. Hãy tiếp tục và tạo một người tiêu dùng và người tiêu dùng sẽ tiêu thụ, IReader hoặc một lần nữa, chúng ta chỉ muốn đây là string, đúng không? Và đây lại là một nhiệm vụ, vậy nên chúng ta muốn không phải là for nhưng là while và chúng ta muốn tiếp tục đọc cho đến khi nó hoàn thành, đúng không? Vậy nên chúng ta sẽ lấy isComplete và trong khi nó chưa hoàn thành, hãy tiếp tục và lấy thông điệp của bạn và chúng ta sẽ lấy người đọc và chúng ta sẽ đọc, đúng không? Và đây là một nhiệm vụ, vậy nên chúng ta thực sự muốn làm điều gì đó với điều này, vậy nên hãy làm cho nó không đồng bộ, hãy đợi ở đây và chúng ta sẽ tiếp tục và đổ thông điệp này, đúng không? Vậy nên chúng ta chỉ sẽ có một số và hai khác biệt, tôi chỉ sẽ để nó chỉ để phân biệt giữa số này, tôi sẽ đánh dấu chúng như là thông điệp. Điều chúng ta sau đó sẽ làm là chúng ta sẽ chỉ trả về hoặc không, không, chúng ta không cần trả về gì cả, đúng không? Một điều với complete là chúng ta có thể viết tất cả các thông điệp và hoàn thành trước khi chúng ta đã đọc hết mọi thứ, vậy nên khi chúng ta đã hoàn thành, chúng ta cũng muốn làm ở đây, giả sử và kiểm tra xem hàng đợi có trống không, đúng không? Nếu chúng ta đã hoàn thành và hàng đợi trống thì thực sự đã hoàn thành, đúng không? Không phải khi chúng ta đã hoàn thành việc viết, vậy nên hãy tiếp tục lấy hàng đợi của chúng ta và nói nó trống, đúng không? Vậy nên khi chúng ta hoàn thành và nó trống thì đã đến lúc đi. Vậy nên chúng ta có hai nhiệm vụ này, một là nhà sản xuất, một là người tiêu dùng, hãy tiếp tục và tạo một kênh, đúng không? Nó sẽ là kênh của string, đúng không? Đơn giản thôi. Bây giờ chúng ta muốn đợi tất cả các nhiệm vụ của mình, nơi chúng ta sẽ có nhà sản xuất và chúng ta sẽ truyền kênh vào đây và sau đó chúng ta sẽ có người tiêu dùng và chúng ta sẽ truyền kênh vào đây nữa, đúng không? Vậy nên cơ bản là vậy, hãy tiếp tục và chạy nó và xem điều gì xảy ra. Vậy nên tôi đã thấy hành vi khá không mong đợi, tôi kỳ vọng chúng sẽ đi từng cái một cơ bản nhưng những gì đã xảy ra là chúng ta đã tạo ra nhiệm vụ này trước và nó là tuần tự, vậy nên những gì chúng ta muốn làm là khởi chạy người tiêu dùng trước vì nó thực sự sẽ giải phóng luồng, vậy nên hãy tiếp tục và đặt người tiêu dùng trước và sau đó chúng ta sẽ khởi chạy nhà sản xuất nữa, đúng không? Được rồi, chúng ta thấy ở đây rằng cơ bản chúng ta đang đẩy vào hàng đợi, đúng không? Vào kênh này và sau đó người tiêu dùng cơ bản đọc và xử lý các thông điệp khi chúng đến, không có gì ngăn chúng ta có nhiều nhà sản xuất như vậy, đúng không? Vậy nên hãy tiếp tục và thay đổi điều này một chút, chúng ta sẽ tiếp tục và đợi trên delay và chúng ta sẽ làm cho nó không đồng bộ, vậy nên sẽ có một vấn đề với việc write complete mà chúng ta thực sự không quá bận tâm trong ví dụ này, tuy nhiên trong môi trường sản xuất bạn muốn sử dụng các kỹ thuật đặc biệt để kết hợp việc viết kênh hoặc đọc kênh, đúng không? Vậy nên nếu hai người đang viết vào một hoặc hai người đang đọc từ một, bạn cơ bản muốn làm điều gì đó đặc biệt để thích ứng với điều đó, đúng không? Vậy nên hãy tiếp tục và tạo một vài nhiệm vụ nữa, đúng không? Vậy nên đây sẽ là bốn kênh nữa, vậy nên khoảng 40 thông điệp nhưng bạn sẽ thấy rằng có một lượng trì hoãn khá tốt và người tiêu dùng đang xé tan qua các mục này, đúng không? Vậy nên đó cơ bản là kênh và vinh quang của nó, bạn có thể có nhiều nhà sản xuất và nhiều người tiêu dùng, đúng không? Vì những người viết kênh và người tiêu dùng đọc từ kênh và mục tiêu chính là không chia sẻ chuỗi này trong một biến toàn cục ở đây, dữ liệu, đúng không? Chúng ta không muốn chia sẻ dữ liệu này toàn cục giữa hai nhiệm vụ, chúng ta muốn truyền dữ liệu này qua lại giữa các nhiệm vụ, đúng không? Vậy nên đó là toàn bộ lý do của đoạn này. Vậy nên ở đây trong Visual Studio, tôi có một cấu hình nhỏ mà tôi sẽ đi qua nhanh. Có một vài thành phần nhưng nếu bạn đã sử dụng .NET Core một thời gian, cơ bản là tôi muốn chứng minh mục đích tại sao bạn muốn sử dụng một kênh (channel) và khái niệm về việc các dịch vụ bị hủy sau khi bạn đã khởi tạo fire-and-forget, đúng không? Vậy nên những gì tôi đang thêm vào chỉ là một API với các controller, đúng không? Vậy nên mặc định là controller, định tuyến với các controller. Tôi có một dịch vụ thông báo mà tôi sẽ sử dụng sau này. Tôi có một cơ sở dữ liệu trong bộ nhớ với Entity Framework, và tôi có một HTTP client mà tôi cũng có thể inject. Vậy nên, đóng phần khởi động (startup), nơi chúng ta sẽ quay lại. Tôi có một controller đơn giản home với một action gửi (send).
Được rồi, hãy để tôi khởi chạy nó. Tôi sẽ gỡ lỗi điều này. Vậy nên khi nó đang chạy ở bên kia, tôi sẽ mở Postman và tôi sẽ nhấn gửi và bạn có thể thấy nó mất khoảng sáu mili giây để hoàn thành, đúng không? Vậy nên hiện tại nó khá nhanh. Giả sử một số yêu cầu sẽ mất lâu hơn những yêu cầu khác, đúng không? Vậy nên chúng ta sẽ có một số dòng như delay, chúng ta sẽ có 100 mili giây ở đó và ở đây. Vậy nên hãy tiếp tục và chờ ở đây và thêm một 200 mili giây nữa ở đây. Giả sử chúng ta đang thực hiện một số cuộc gọi, nơi hai cuộc gọi này cơ bản là chúng ta đang cố gắng gửi một thông báo, đúng không? Và những gì có thể liên quan, chúng ta sẽ thấy trong dịch vụ sau một chút, nhưng hãy tiếp tục và chạy nó và xem cách chúng ta sẽ tạo ra offset, chúng ta sẽ tăng thời gian mà yêu cầu này mất và sau đó chúng ta sẽ lại phân tán và xem vấn đề gì xảy ra với việc phân tán này, được rồi?
Vậy nên hãy tiếp tục và chạy nó, bạn có thể thấy nó tăng lên đến 400 mili giây và sẽ giảm xuống sau yêu cầu đầu tiên, đúng không? Có thể do cache hoặc gì đó, nhưng chúng ta thường lấy 300 mili giây mà chúng ta đã thêm ở đây. Vậy nên hãy tiếp tục và offset chúng, nếu chúng ta có mười nghìn người dùng thì thời gian này sẽ nhanh chóng tăng lên, đúng không? Vậy nên chúng ta chỉ muốn tiếp tục và chạy nó trong một hàm khác, được rồi? Nó sẽ là một nhiệm vụ fire-and-forget, một thứ gì đó giống như vậy sẽ xảy ra. Vậy nên hãy tiếp tục và chạy nó và ở đây nếu chúng ta gửi, chúng ta có thể thấy rằng chúng ta quay lại cơ bản là bảy mili giây, sáu mili giây, tám. Chúng ta hiện đang offset, chúng ta đang bắt đầu một nhiệm vụ khác nên nó chạy song song. Vậy nên cơ bản là chúng ta không đợi, chúng ta cơ bản nói rằng, đúng, tôi làm việc này ở đó.
Vậy vấn đề với điều này theo thời gian tôi đã rút gọn trước khi hướng dẫn để chúng ta có thể nhanh chóng đi qua và quay lại với các kênh, tôi có một dịch vụ ở đây mà tôi đã đăng ký có tên là notifications, nó nằm trong thư mục services. Tôi có một mô hình dữ liệu ở đây là chỉ một người dùng đơn giản và tôi có cơ sở dữ liệu, đúng không? Tôi kế thừa từ DbContext và tôi chỉ có một bảng người dùng. Đây là một cơ sở dữ liệu trong bộ nhớ và dịch vụ thông báo này làm gì? Nó tiêu thụ cơ sở dữ liệu này và nó cũng có HTTP client factory. Vậy nên những gì tôi làm ở đây là tôi đang thực hiện một chút seed cho người dùng, đúng không? Vậy nếu người dùng không tồn tại, đây chỉ là một biện pháp an toàn, tôi thêm một người dùng nếu không có. Vậy nên những gì tôi làm là tôi có thể lấy một số thông tin về những người dùng mà tôi muốn thông báo: email của họ, ID thiết bị di động của họ, nơi tôi sẽ gửi những thông báo này, những thông báo nào họ đã bật, họ có muốn xem những thông báo này không, vv. Sau đó tôi muốn tạo một client, tôi muốn có khả năng gửi thông báo đó, tôi muốn lưu lại những gì tôi đã gửi, đã gửi thông báo này tới đâu, và tôi muốn lưu nó vào cơ sở dữ liệu, đúng không? Vậy nên việc gửi thông báo không đơn giản như chỉ gửi thông báo, việc gửi thông báo liên quan đến rất nhiều cài đặt thông báo khác nhau: bạn được phép xem những thông báo nào, bạn có client nào đã bật cho thông báo, đó là trình duyệt, đó là app, đó là email, vv. Vậy nên đây là một quá trình phức tạp và giả sử rằng quá trình này có thể mất một giây, dù nó không phải nhưng có thể, được không?
Vậy nên những gì tôi sẽ làm là tôi sẽ vào controller home của mình, là hãy tiếp tục và tôi sẽ để lại đây cho ví dụ, chúng ta sẽ dùng send a và từ services tôi sẽ inject dịch vụ notifications, đúng không? Notifications và những gì tôi sẽ làm là tôi sẽ lấy notifications và tôi sẽ gọi send, đúng không? Và vì đây là một nhiệm vụ fire-and-forget, tôi sẽ nói rằng đây là một nhiệm vụ boolean, tôi sẽ chỉ trả về nó từ action controller đó. Vâng, hãy tiếp tục và chạy nó, chúng ta sẽ chỉ có một so sánh về thời gian nó mất bao lâu. Vậy nên bây giờ trở lại Postman, tôi muốn vào send a, bây giờ tôi sẽ gửi nó và lần đầu tiên nó mất khoảng một giây, lần sau 88, 60, 80, 46:44, 33, 37, đúng không? Vậy bạn có thể có một cảm nhận về nó ở đâu đó. Vậy giả sử quá trình này bởi vì chúng ta đang sử dụng một cơ sở dữ liệu trong bộ nhớ khá nhanh và nếu chúng ta sẽ sử dụng một cơ sở dữ liệu SQL mà nhiều dịch vụ sẽ giao tiếp với nó, chúng ta sẽ gặp phải một tình huống khá phức tạp, đúng không?
Vậy nên hãy tiếp tục, tôi hy vọng tôi sẽ sao chép hàm này, nó đang trở nên hơi phức tạp, xin lỗi. Vậy nên hãy tiếp tục và thêm một send 8 nữa, chúng ta sẽ sắp xếp nó và những gì chúng ta sẽ làm là vâng, hãy thử bao quanh nó trong một try-catch để chúng ta có thể thấy lỗi rõ ràng, chúng ta sẽ bắt ngoại lệ, import beep này và chỉ để tôi có thể thấy nó, hãy tiếp tục và ký tên nó ở đây và tôi sẽ đặt một breakpoint ở đây, được rồi? Những gì tôi sẽ làm là tôi sẽ làm điều tương tự, tôi chỉ sẽ tiếp tục và đặt trong một try-catch để hy vọng giảm thời gian mà dịch vụ này đang mất, được rồi? Như vậy, được rồi. Một điều đã xảy ra ở đây là lambda này không phải là không đồng bộ, vậy nên chúng ta không thể làm gì đó và chúng ta không thực sự phải đợi cho dịch vụ này không còn thao tác không đồng bộ nữa, vậy nên hãy tiếp tục và thay đổi điều này thành boolean và nó đã được viết là true ở đó, được rồi. Vậy nên một lần nữa đây là một thao tác đồng bộ, chúng ta tạo một nhiệm vụ sẽ hoàn thành sau một thời gian và sau đó chúng ta thoát khỏi hàm, chúng ta sau đó sẽ thoát khỏi action, thoát khỏi controller và những gì sẽ xảy ra là chúng ta không còn truy cập vào những dịch vụ này nữa và khi nhiệm vụ này được thực hiện và gặp lỗi, đúng không? Vậy nên một lần nữa, hãy tiếp tục và có lẽ nên đặt tên nó là send beep, vâng, sent B sẽ gọi, giả sử đây là sent B sẽ gọi send a trên send a, đúng không? Rối loạn giao tiếp, đúng không? Đây không còn là fire-and-forget, nó sẽ trả về một boolean ở đây, tôi sẽ gọi send a một lần nữa và chúng ta sẽ có thể quan sát lỗi mà tôi đã nói đến.
Hey, hãy vào Postman, hãy tiếp tục và kích hoạt nó và đây chúng ta ở vị trí ngoại lệ, đúng không? Vậy nó nói gì, tôi sẽ tiếp tục và chúng ta ở đâu kính lúp của tôi, bạn tôi, nó ở đó, đúng không? Được rồi, đối tượng đã được dispose. Một nguyên nhân phổ biến của lỗi này là dispose một context được kết quả từ dependency injection và sau đó cố gắng làm tương tự với context ở nơi khác trong ứng dụng của bạn, đúng không? Vậy nên vâng, cơ bản là nó không giữ được trạng thái của dịch vụ này và đây là điều mà tôi đã đề cập ở đầu khi tôi giải thích rằng cơ bản là các dịch vụ mà chúng ta inject ở đây không còn có sẵn cho nhiệm vụ này nữa bởi vì chúng ta thực sự đã hoàn thành việc thoát khỏi phạm vi, đúng không? Vậy nên đây là vấn đề với giải pháp fire-and-forget này và cũng như chúng ta cơ bản đang tạo ra một ngăn xếp các nhiệm vụ có thể theo thời gian kiểm soát quá trình của chúng ta và làm nghẽn các luồng khác, đúng không?
Vậy nên hãy đóng mọi thứ và hãy nghĩ về cách chúng ta có thể offset, đúng không? Cơ bản là chúng ta muốn có dịch vụ thông báo này như một producer hoặc notification, đúng không? Và sau đó chúng ta có một consumer như một dispatcher của notification, đúng không? Vậy nên chúng ta sẽ phải có một tiến trình cố gắng đọc từ một kênh và đây là nơi bạn có thể sử dụng một hosted service hoặc một background service, đúng không? Vậy nên đây là nơi chúng ta sẽ đi vào services và chúng ta sẽ thêm một hosted service, được rồi? Tôi hiểu tại thời điểm này có thể nếu bạn đang tự hỏi kênh nằm ở đâu thì chúng ta chưa thấy triển khai của các kênh, việc hiểu vấn đề mà các kênh giải quyết quan trọng hơn so với kênh bản thân vì kênh rất đơn giản, nó có một vài chức năng dễ hiểu, bạn chỉ cần biết đặt kênh vào bức tranh lớn hơn thế nào, hãy kiên nhẫn nhé.
Vậy nên một hosted service, hãy tiếp tục và tạo một hosted service và đây chỉ chạy trong nền của ứng dụng ASP.NET Core, nó sẽ không thể bị hit bởi một yêu cầu, nó là thứ mà chúng ta có thể cắm kênh vào và gửi dữ liệu vào nó và chúng ta sẽ xử lý nó trong nền, đúng không? Vậy nên hãy tiếp tục và thực sự tạo một notification dispatcher, đúng không? Chúng ta sẽ làm nó như một background service, đúng không? Vậy nên background service nó kế thừa từ IHostedService và sau đó trong startup, hosted service chấp nhận một T HostedService, nơi T HostedService là IHostedService, được rồi? Vậy nên notification dispatcher là thứ chúng ta sẽ đặt ở đây và đây là những gì nó cơ bản sẽ bắt đầu và nó sẽ bắt đầu vòng lặp. Vậy nên hãy tiếp tục và triển khai lớp trừu tượng và ở đây chúng ta có một hàm WriteAsync, ExecuteAsync sẽ bắt đầu và nó sẽ không dừng chạy, đúng không? Vậy nên đây là nhiệm vụ consumer. Vậy nên notification dispatcher là một consumer và nó sẽ cố gắng đọc từ kênh notifications, đúng không? Oh, ở đây chúng ta muốn vòng lặp while và nếu không hoàn thành thì đọc từ kênh và gửi thông báo, đúng không? Rất đơn giản.
Bây giờ chúng ta cơ bản chỉ cần nối kênh từ dịch vụ thông báo và một lần nữa dịch vụ thông báo này có thể là thứ khác như tạo đơn hàng, đăng ký website, vv. Vậy nên gửi email, lấy dữ liệu từ cơ sở dữ liệu, viết vào cơ sở dữ liệu, tất cả đều là các nhiệm vụ tốn thời gian. Liên lạc như thế nào, oh, những gì chúng ta muốn làm là cơ bản lấy các dịch vụ này và dán chúng vào đây, đúng không? Oh, HTTP client, đúng rồi, tôi chỉ copy, có thể tôi sẽ lấy chức năng này ở đây và tôi sẽ làm Sentinel, đúng không? Đây thực chất là những gì cần phải xảy ra để gửi thông báo net, đúng không? Vậy nên điều này sẽ phải là không đồng bộ và bất kỳ thao tác đồng bộ nào sẽ cần namespace Entity Framework, đúng không?
Vậy nên một khi mọi thứ được biên dịch nhưng để tôi mở Solution Explorer và nhân tiện, tôi lấy string, tôi chỉ lấy một cái gì đó từ tài liệu của Microsoft. Vậy nên đổi tên constructor để chúng ta có cơ sở dữ liệu, chúng ta có HTTP client factory, một điều với cơ sở dữ liệu, chúng ta có thể cần lấy nó từ service provider và đây là thứ chúng ta sẽ xử lý nếu lỗi xảy ra và tiếp tục, thường những gì bạn sẽ làm là bạn sẽ bắt toàn bộ thứ này, đúng không? Như thế này và bạn sẽ ghi log nó. Hãy tiếp tục và thêm một logger thật, vậy nên tôi logger notification dispatcher logger, đúng không? Và hãy tạo trường logger này, được rồi? Vậy nên chúng ta không chạm vào các kênh ngay bây giờ, nó đang thiết lập, ghi log error notification failed exception e, hãy tiếp tục và sử dụng System, đúng không? Vậy nên log error cho phép bạn ghi một exception và sau đó thông điệp, đúng không? Và sau đó bất kỳ tham số nào bạn muốn đưa vào, vâng, đó là nơi chúng ta muốn một breakpoint nếu có gì đó sai sót nhưng chúng ta không bao giờ muốn phá vỡ vòng lặp của notification dispatcher, đúng không?
Vậy nên kênh, hãy tiếp tục và kết nối kênh này, đúng không? Trong startup, những gì tôi muốn làm là thêm một kênh, đúng không? Vậy nên services.AddSingleton và nó sẽ là một singleton vì tôi muốn kênh này tồn tại và nó chỉ có một kênh, đúng không? Không bao giờ dispose và chúng ta không cần loại nào cả. Những gì chúng ta muốn làm là chúng ta muốn lấy kênh, đúng không? Vì vậy, nó là một Channel<T> trong System.Threading.Channels, thêm namespace channels ở đó, nó ở đó đúng không? Và kênh ở đây là gì?
Vậy nên hãy tiếp tục và nhanh chóng khám phá nó một chút, chúng ta có lớp Channels tự nó và một bộ sưu tập các hàm tĩnh sẽ tạo kênh cho bạn. Vậy nên chúng ta có một channel của loại T và nó là một lớp trừu tượng, đúng không? Vậy nên bạn có thể kế thừa từ nó, nó kế thừa từ Channel<T>, đúng không? ChannelReader<T>, vậy nên chúng ta sẽ viết một loại, chúng ta sẽ đọc một loại và channel tiêu chuẩn mà chúng ta đang lấy ở đây là Channel<T> và ChannelWriter<T>, đúng không? Vậy nên trong kênh này, chúng ta có reader và writer, cùng tình huống và chủ đề chính, những khác biệt nhỏ tinh tế chỉ nằm ở các hàm, các tùy chọn mà các hàm này cung cấp cho bạn, các nhiệm vụ completion chỉ là cách nó hoạt động, nhưng tất cả các khái niệm về read là done và có sẵn và cùng điều tương tự cho ChannelWriter, mark complete hoặc try write vào nó, đúng không? Và tôi sẽ nhanh chóng lướt qua các hàm này một khi chúng ta bắt đầu viết, nhưng chơi với chúng sẽ cho bạn một ý tưởng tốt về chúng là gì, đúng không? Tôi chỉ muốn hoàn thành ví dụ thực tế về nơi sử dụng channel.
Vậy nên những gì chúng ta muốn làm là vì chúng ta sẽ truyền xung quanh một string, tôi chỉ sẽ tạo một unbounded channel có nghĩa là không giới hạn số lượng thông điệp có thể đi vào Q, kênh này là, nếu bạn muốn, sẽ thiếu bộ nhớ, điều này hiếm khi xảy ra ngày nay. Bạn muốn đặt giới hạn trên Q của bạn và sau đó bạn sẽ không nhận được ngoại lệ tràn bộ nhớ hay gì đó nữa. Vậy nên tạo unbounded channel của loại string, đây sẽ trả về một channel của loại, đúng không? Vậy nên ở đây bạn có thể thấy kiểu Channel<string> và đây không phải là dịch vụ, đây bây giờ là một dịch vụ singleton mà chúng ta có thể inject, đúng không? Và còn có các link khác mà bạn có thể sử dụng theo cách hướng đối tượng rõ ràng, bạn có thể bọc nó, bạn có thể cơ bản tạo kênh này và đặt các triển khai của reader và writer và sau đó tách reader và writer thành các dịch vụ riêng biệt khác, nhưng ở đây những gì chúng ta sẽ làm là hãy tiếp tục và chỉ inject kênh này vào một action và sau đó vào dispatcher và làm cho action giao tiếp với dispatcher, đúng không?
Vậy nên hãy tiếp tục và lấy controller home của chúng ta ở dưới đây, những gì tôi sẽ làm là tôi sẽ tạo một public async Task<IActionResult> SendAsync() per channel và từ services chúng ta sẽ tiếp tục và lấy một channel của loại string vì đó là những gì chúng ta đã inject và hãy tiếp tục và lấy kênh này, chúng ta sẽ cần namespace, hãy tiếp tục và trả về bulk a troop ở đây và những gì tôi sẽ làm là tôi sẽ lấy một nhiệm vụ từ Bulk vì đây sẽ cần điều này sẽ cần phải là không đồng bộ vì kênh của chúng ta đang nhanh chóng đi qua, chúng ta sẽ sử dụng writer vì chúng ta sẽ viết vào kênh và chúng ta chức năng complete, chúng ta đánh dấu nó là complete, không gì sẽ được viết vào kênh này nữa, cơ bản là chúng ta sau đó có try complete sẽ ném ngoại lệ nếu nó không thể complete, đúng không? Với ngoại lệ mà bạn đã cung cấp, try rồi và sau đó wait to write, cùng loại công việc và in hand vì try write là synchronous và wait to write là synchronous, cho phép bạn biết khi nào bạn có thể viết, đúng không? Vậy nên đây chủ yếu hơn nếu bạn có một bounded channel, bạn chờ để nó viết sync và sau đó bạn cố gắng viết, bạn nghĩ nếu bạn không thành công, bạn rất chờ để viết sync lại, đúng không? Vậy nên nếu bạn chọn viết sync, bạn cơ bản kết hợp try và wait to write sync khi bạn có thể viết, bạn sẽ viết, đúng không?
Vậy nên đó cơ bản là những gì chúng ta sẽ sử dụng, hãy tiếp tục và chỉ nói hello, đúng không? Thông điệp đơn giản, hãy tiếp tục và chờ nó, được rồi? Và sau đó chúng ta sẽ trả về, vậy nên chúng ta có một dịch vụ và điều chính cần hiểu là chúng ta đang cố gắng đạt được kết quả offset công việc này và nếu bạn cuộn lại video nhớ các số, nó ở đâu đó khoảng 30-30-40 mili giây mà chúng ta nhận được kết quả với toàn bộ mớ hỗn độn này, vậy đây là những gì chúng ta đang cố gắng làm, chúng ta đang cố gắng vượt qua 30-40, chúng ta sẽ cố gắng quay lại 6-7 mili giây mà chúng ta đã nhận được từ đầu, đúng không?
Vậy nên kênh riêng của tôi, hãy tiếp tục và inject nó vào notification dispatcher của chúng ta và một vài điều với những dịch vụ này là cơ sở dữ liệu này thực sự sẽ bị hỏng vì chỉ vì nó là một singleton và cơ sở dữ liệu là một scope, tôi đoán là chỉ về dependency injection và thời gian sống, chúng ta sẽ sửa điều đó, hãy đầu tiên xử lý kênh, đúng không? Trong khi chúng ta đang trên luồng của kênh, rubber channel, khởi tạo trường, vậy nên nếu không hoàn thành, bạn sử dụng reader này như thế nào? Đúng không? Nơi đây là một consumer, chúng ta sẽ sử dụng reader, chúng ta đang đọc từ kênh. Channel consumer đang đọc, producer đang viết, mark complete, nó sẽ trả về một nhiệm vụ mà chúng ta chỉ cần dựa vào nếu nó hoàn thành, bạn người đã hoàn thành nhiệm vụ này, nếu không tiếp tục vòng lặp, đúng không? Vậy nên hãy để các comment, chúng ta muốn lấy thông điệp, hãy lấy kênh, chúng ta sẽ đi đến reader, đúng không? Và tình huống tương tự như với các hàm trên writer, nó cũng giống như vậy, chúng ta muốn đọc hoặc viết, đúng không? Nhưng chỉ các tùy chọn mà chúng cung cấp, đúng không? Vậy nên đọc tất cả những thứ này, bạn muốn đọc tất cả các thông điệp hay chỉ đọc một thông điệp, bạn muốn làm một synchronous try read và wait để read sink, mặc dù đây đều được bao gồm trong read async, vậy nên bạn cố gắng đọc, nếu thành công bạn chờ read async và khi bạn sẵn sàng để đọc bạn cố gắng đọc, đúng không? Nếu không thành công bạn trở lại chờ ở đây, bạn cơ bản sẵn sàng để đọc, bạn đọc, đúng rồi? Đó là những gì chúng ta sẽ sử dụng, hãy tiếp tục và đọc thông điệp một lần nữa, đây là không đồng bộ, chúng ta sẽ cần chờ nó và bây giờ hãy sắp xếp provider của cơ sở dữ liệu, đúng không? Cơ bản nó sẽ crash vì nó là một scope và notification dispatcher là một singleton, đúng không? Tôi tự nhắc lại ở đây những gì chúng ta muốn là một service provider, vậy nên hãy lấy provider và hãy tạo một trường ở đây và nơi chúng ta sử dụng cơ sở dữ liệu này, chúng ta muốn lấy một using statement và chúng ta muốn tạo một scope từ provider, đúng không? Và khi scope kết thúc chúng ta muốn dispose nó, bằng provider, vậy nên database = ... Vâng, chúng ta muốn lấy service brighter, get required scope và chúng ta sẽ lấy cơ sở dữ liệu mà chúng ta đã inject và vâng, cơ bản là chúng ta đang xử lý thông điệp, chúng ta thực sự không tiêu thụ nó nhưng nếu cần chúng ta có thể.
Hãy tiếp tục và đặt một breakpoint ở đây để chúng ta đạt đến ngoại lệ, chúng ta có thể không. Hãy tiếp tục và cũng đặt một breakpoint ở đây để chúng ta biết tại điểm nào chúng ta bắt đầu đọc hoặc bắt đầu chờ đọc và thường thì đây sẽ được kích hoạt ngay khi ứng dụng khởi động và sau đó luồng sẽ đi làm gì đó khác vì dịch vụ nền này bắt đầu ngay khi server khởi động, đúng không?
Vậy nên tổng kết trước khi đi, chúng ta thêm một singleton Channel, đây chỉ là một kênh tồn tại ở đâu đó trong bộ nhớ và sau đó chúng ta có một đống dịch vụ mà chúng ta inject vào notification dispatcher của chúng ta, cái này sẽ đọc từ kênh này và nó sẽ làm việc của mình, đúng không? Không quan trọng nó làm gì, tất cả là về việc đọc. Sau đó chúng ta sẽ kích hoạt việc viết vào kênh bằng cách gọi action controller nơi chúng ta có thể inject Channel của mình và chỉ để lưu ý và inject kênh này vào bất kỳ nơi nào chúng ta muốn dịch vụ thông báo, chúng ta có thể inject nó vào đó nếu muốn, chúng ta chỉ chọn để inject vào action controller, đúng không?
Được rồi, oh, hãy tiếp tục và chạy nó xem gì xảy ra. Vậy nên chúng ta gặp breakpoint đầu tiên ngay khi ứng dụng khởi động, bạn có thể thấy chúng ta thực sự chưa vào trang home, tôi nghĩ chúng ta thực sự chưa vào trang home hoặc thậm chí không có trang home, vậy không sao. Hãy tiếp tục và chỉ F5 ở đây, điều này sẽ trả về luồng và khi ứng dụng có thể hoạt động như một lần để viết. Vậy nên những gì chúng ta muốn làm bây giờ là chúng ta muốn gửi một yêu cầu tới home send, đúng không? Vậy nên chúng ta gửi nó, được rồi, và ở đây chúng ta đang vào trong scope này, vậy nên tôi sẽ loại bỏ breakpoint này bây giờ, chỉ để bạn biết là ngay khi chúng ta đẩy thứ gì đó vào hàng đợi nó bắt đầu thực thi trong nền, đúng không? Chúng ta sẽ tiếp tục và ở đây cơ bản chúng ta lặp lại và chúng ta bắt đầu chờ đọc từ kênh, dịch vụ đã quay trở lại, nó bị chặn vì tôi đã chờ trên breakpoint nhưng chúng ta sẽ thấy nó chạy với tốc độ đầy đủ trong một giây nữa, đúng không? Và sau đó chúng ta cơ bản đang chờ lại, một lần nữa khi tôi tiếp tục, luồng đã từ bỏ quyền kiểm soát ở đây, tôi sẽ loại bỏ breakpoint này và bạn thấy chúng ta chưa gặp ngoại lệ nào, vậy nên mọi thứ đang hoạt động như mong đợi.
Những gì tôi sẽ làm bây giờ là trong home controller không có breakpoint ở đây, tôi muốn spam một số request, đúng không? Vậy nên tôi sẽ gửi 40 mili giây, gửi lại 1024, 97, vậy nên bạn có thể thấy chúng ta đang nhận được offset của tải công việc và chúng ta trở lại với thực thi nhanh nhẹn, đúng không? Và một lần nữa tôi nghĩ chúng ta có thể quay lại send b để có một so sánh, vậy nên gửi tôi 64, 59, 46, 47, 33-62, 36, 33, như một loại bình luận thể thao, nhưng bạn có thể thấy rằng với dispatcher và kênh, chúng ta có thể cơ bản có một nhiệm vụ riêng biệt và chúng ta cơ bản off load, chúng ta tách một nhiệm vụ ở đây và một Tasker ở đây, vậy nên bây giờ chúng ta có hai nhiệm vụ đang chạy và notification dispatcher là nhiệm vụ nền này và cách chúng ta kết nối chúng là kênh này, đúng không? Và bất cứ khi nào có thứ gì đó đặt vào kênh, dispatcher sẽ đọc nó và xử lý nó, đúng không? Và kênh có thời gian sống riêng của nó, xin lỗi, dispatcher - consumer có thời gian sống riêng của nó, nó có các dịch vụ được inject và nó quản lý trạng thái riêng của nó và tất cả những gì bạn đang làm là truyền các thông điệp xung quanh, đúng không? Vậy nên vâng, đây cơ bản là một sử dụng thực tế của kênh trong một ứng dụng đơn lẻ.
Một cách lớn, thực sự là một cách lớn mà kênh được sử dụng trong ngành là phương tiện của sự phân tách các mối quan tâm (separation of concerns), vậy nên bạn có hệ thống phân tán, bạn có microservices, cách bạn giao tiếp giữa các microservices là thông qua các dịch vụ kênh, vậy nên bạn sẽ có một dịch vụ đặt một thông điệp lên kênh dịch vụ trên đám mây và sau đó một máy khác bạn sẽ có một cluster, vậy nên bạn sẽ có nhiều máy, một trong số đó sẽ đọc từ kênh cơ bản nếu quá nhiều mục bắt đầu tích tụ trên kênh bạn mở thêm nhiều máy và điều đó cơ bản là scaling ngang (horizontal scaling), nếu bạn không xử lý yêu cầu nhanh đủ bạn sẽ có thêm nhiều máy ảo, đúng không? Vậy là đó là nơi mà kênh cũng có thể được sử dụng trong tính toán phân tán, đó là một khái niệm khá quan trọng, bạn biết đấy.
Vâng, đó sẽ là kết thúc cho tập này. Tất cả mã có sẵn ở liên kết trong mô tả. Nếu bạn có bất kỳ câu hỏi nào, hãy bình luận. Thích video này, đăng ký, đừng quên tham gia máy chủ Discord của tôi. Tôi cũng stream vào các ngày Chủ Nhật và Thứ Tư nên hãy đảm bảo bạn theo dõi để không bỏ lỡ. Hy vọng tôi sẽ gặp bạn trong các video khác của tôi.
Nhận xét
Đăng nhận xét