Thắc mắc về asyn/await

  • 450 Views
  • Last Post 20 May 2019
  • Topic Is Solved
admin posted this 18 May 2019

Asyn/await

Câu hỏi:

1. Có phải asyn/await dùng ThreadPool (quản lý nhiều separate thread), nếu có thì liệu rằng nhiều thread thực thi cùng lúc như vậy chương trình có nhanh hơn không (mình đang hình dung google chrome nguốn RAM nhưng user ngày nay lại thích thực thi nhiều tác vụ cùng lúc thay vì phải chờ đợi)?

2. Trong team mình có anh kia đã từng dùng tới asyn/await khi thao tác dữ liệu lớn, nhưng sau đó a leader có nói dùng cái đó trong trường hợp này là không khả thi (mình review code thì thấy kết quả trả về không hề có sự phụ thuộc lẫn nhau (độc lập)) => vậy thì theo ý kiến cá nhân mình thấy nếu câu trả lời cho số 1 là YES thì thực thi sẽ nhanh hơn, đáp ứng tốt hơn cho user về mặt UX, còn mọi người thấy vì yếu tố nào khác mà mình không nên dùng tới asyn/await trong trường hợp này không nhỉ?

Gợi ý:

- Theo như kiến thức của mình thì việc async/await là để tối ưu hoá threadpool. Khi chương trình cần phải làm tác vụ gì đó lâu mà không phải do mình kiểm soát (truy xuất DB, lấy dữ liệu từ 3rd party) thay vì phải ngồi đợi và khiến thread đang chạy bị "giam" thì nó sẽ giải phóng thread đó về lại pool để có thể nhận request mới. 

Rồi khi có kết quả thì 1 thread mới sẽ được chỉ định để tiếp tục công việc. Việc này tối ưu hoá được threadpool vì số lượng thread có hạn. Nên nếu là mình thì mình cũng sẽ dùng await/async như anh senior của bạn

- Tưởng tượng bạn cần lấy 100k dòng trong DB, nó mất 10s. Nếu như cách bình thường List<T> a = getData() thì cái thread chạy dòng này sẽ bị block trong 10s. Và nếu có 100 request như vậy đến, 100 thread sẽ bị "block". Và request thứ 101 sẽ ko được thực thi.

Ngược lại khi dùng List<T> a = await getData() thì ngay tại thời điểm chờ 10s cho DB trả về dữ liệu, hàm chứa dòng lệnh này sẽ bị hoãn (suspend) và thread này được trả về pool, nếu có request kế tiếp đến thì thread vừa trả về vẫn có thể dùng để thực thi cho đến đoạn lấy dữ liệu ở trên. Như vậy bạn có thể serve nhiều hơn 100 request với cách này. Cái await nó buộc chương trình phải chờ kết quả rồi mới làm tiếp nên tất nhiên bạn thấy nó chờ. 

Còn khi chỉ viết Task<int> thì nó chỉ trả về 1 cái reference đến cái task thôi chứ nó không phải là kết quả. 

Cuối cùng khi bạn muốn sử dụng kết quả bạn cũng phải dùng i.Result() hoặc await i (mà nếu lúc này kết quả vẫn chưa có thì vẫn phải chờ). Việc sử dụng async/await chỉ hữu dụng và thấy rõ khi bạn test thật nhiều request cùng lúc, chứ lúc debug chỉ gửi 1 request thì bạn tất nhiên không thấy sự khác biệt rồi

- Đầu tiên hết là mình có .net threads. Để quản lý thread thì .net có threadpool. 

Theo mình biết thì số lượng thread phụ thuộc vào CPU, số lượng địa chỉ memory, hệ điều hành...nhưng chia làm 2 loại worker và I/O. Một số sách nói chia vậy là để threadpool có thể handle I/O thậm chí cả khi nó không còn worker thread đang rảnh. 

Threadpool có nhiều kỹ thuật để quản lý sử dụng thread, xem cái nào đang work, assign work cho cái nào...dùng mấy algorithm bla bla nhưng tóm lại là tự mình làm việc (một cách không hiệu quả) với thread rất là expensive - tốn tài nguyên. Nên .net cho ra đời Task. Task là một công việc. Mình có Task mình cứ giao cho ThreadPool nó sẽ tự manage và run sao cho hợp lý nhất. Mình có thể có 1 triệu Task nhưng tất cả số task đó đều được giao cho 1000 threads. Task không giống như thread rất là lightweight. 

ThreadPool sẽ đóng vai trò quản lý sao cho tối ưu nhất. Từ Task mình có mong muốn để code asynchronous một cách dễ dàng hơn (ví dụ như chain 1 loạt Task continueWith hay với các methods khác của Task), trực quan hơn thì có feature async/await. 

Nó là một phần của asynchronous programming có lợi ích là mình sử dụng tài nguyên hiệu quả hơn như anh ở trên nói, bạn có nhiêu đó threads, nếu 5 threads đểu đang busy (block) bởi tác vụ synchronous. Threadpool sẽ phải cân đối lại và (cơ bản) là tạo thêm threads để phục vụ những tác vụ khác mà như bạn biết, rất là expensive. 

Thử tưởng tượng hàng trăm ngàn request mà tất cả các thread đều busy hết thì sẽ thế nào. ThreadPool (một cách đơn giản) sẽ kiểm tra các threads hiện tại, tạo threads mới, chạy thuật toán để assign tasks, quản lý state...v..v Từ asynchronous programming (cụ thể hơn là dùng async/await) bạn có thể handle dc nhiều thứ hơn và nhanh hơn. 

Ví dụ như bạn có một service handle 100000 requests mà mỗi request gọi xuống DB hoặc HTTP sang một endpoint khác. Bạn sẽ thấy là async giúp handle được nhiều request hơn trong một cùng khoảng thời gian. Tài nguyên hữu hạn => Threadpool quản lý không dễ dãi (nhiểu thuật toán, nhiều limit) => nên dùng asynchronous. 

Xa hơn asynchronous programming không chỉ gói gọn trong process của phần mềm của mình mà còn ở tầng kiến trúc nữa. Ví dụ như trong kiến trúc phân tán, mình có nhiều VM chạy trên một cluster thì mình cũng không muốn tất cả VM đều ngồi chờ đợi I/O từ bên ngoài (giả sử mỗi máy VM chạy 5 services đều cùng gọi 10 000 requests tới DB, HTTP, FTP và ...chờ). 

Bạn có thể nhìn CPU của mình, giả sử có 4 core vật lý và thử tượng tượng 4 core ngồi không vì đang chờ máy in in 4 cuốn sách 100k trang vì một tập lệnh Wait() nào đó. Ngay cả hệ điều hành được làm ra cũng phải quản lý cân đối những core của CPU này, liên tục assign resource CPU và trả lại để mọi công việc mình giao cho nó nó đều phải hoàn tất, mà mình thì không thể tư assign thêm core cho CPU. 

Tóm lại mình nghĩ 1) là YES, async có thể làm cho program chạy nhanh hơn nhưng là do nó xử dụng hiệu quả tài nguyên hơn để đạt được khả năng concurrency tốt hơn.

Về ý số 2 của bạn. Mình nghĩ nên có sự phân biệt giữa asynchronous và concurrency. Concurrency mới phụ thuộc vào việc mỗi request có độc lập hay không. Asynchronous thì theo mình không liên quan lắm. Nếu không độc lập thì bạn dùng async/await và nếu độc lập bạn cũng có thể dùng async/await. async/await bắt đầu bằng việc mình dùng keyword async trên method: ví dụ async Task<int> GetNumber() { A(); B();}. 

Nó cho .net biết đây là một method async và có thể await (awaitable)khi gọi. Khi thực thi, control đang chạy tới dòng await thì sẽ yield (nhả) control ra lên một level và sinh ra một state machine để handle kết quả công việc đó và lấy context để sau này tiếp tục nếu cần, cứ thế nó up ra hết thì sẽ nhả thread lại vào threadpool. Khi có kết quả thì sẽ handle tiếp bằng cách lấy lại context cũ và chạy tiếp các dòng tiếp theo. Phần này có thể chạy trên thread cũ cũng có thể là một thread khác. 

Trường hợp mà mình nghĩ không nên dùng async là khi phần công việc đó là synchronous mà mình muốn wrap nó vào một async method vì như vậy không có ích lợi gì cả, còn có thể gây ra những hiểu sai về method mà mình viết và sau này maintain sẽ phức tạp hơn khi code lớn và nhiều team mà nó thì không đúng trong trường hợp 2) của bạn nên mình nghĩ dùng asynchronous là không sai cho riêng việc gọi xuống DB và lấy dữ liệu. Trừ phi trường hợp này bạn muốn cùng lúc thực hiện một hoặc nhiều những tác vụ khác không liên quan đến việc lấy data. 

Ví dụ await GetData(); DisplayLoadingScreen(); LogAuditRequest(); 2 hàm sau không dùng hoặc không dùng hết (ví dụ chỉ cần ID của một dòng trong tập data) thì cách gọi có thể sai vì lúc này cần làm concurrency nhưng bản thân việc async/await trên method láy data là không sai.

 

Tham khảo từ cộng đồng .NET Core VN

admin posted this 20 May 2019

 - Mình sẽ nói là "It depends". Vd bạn có 2 method có 2 bước gồm bước 1 đọc DB, bước 2 sử lý kết quả từ DB. Thì sử dụng Async/Await cho bước đọc từ DB cũng không nhanh hơn bước đọc theo dạng synchronous. 

- Chỉ là thread đang xử lý trong trường hợp dùng Async/await sẽ được tận dụng hiệu quả hơn, tối ưu hơn cho các task khác vì nó ko cần phải ngồi đợi kết quả từ DB trả về mà không làm gì như trong trường hợp xài kiểu synchronous. 

- Nhưng nếu tưởng tượng bước lấy kết quả từ DB là gồm việc thực hiện 3 DB query khác nhau, thế thì sử dụng Async/await cho Task.WhenAll để xử lý 3 DB tasks concurrent sẽ nhanh hơn việc execute 3 DB tasks synchronous.

- Nếu bạn await cho 1 "TRUE" I/O operation, thì sẽ không tốn thêm thread mới từ threadpool, còn ngoài ra thì chắc chắn sẽ có 1 threadpool thread được dedicate cho việc xử lý ngầm cái task. Nhiều thư viện vd như MySQL Connector for .NET, mang tiếng là có support Async task nhưng thực ra trong implementation của nó lại dùng Task.FromResult khi sử lý I/O, thì đây không phải là TRUE I/O operations, chắc chắn sẽ tốn thread.

- Bạn phải biết ưu điểm, nhược điểm của 3 cái niệm: Asynchronous, concurrent & parallel programming. Recommend nên xài Async/Await cho các task I/O để tối ưu threadpool, hoặc khi muốn thực hiện concurrent nhiều tác vụ để không ảnh hưởng nhiều đến UI context.

- Không giúp chạy nhanh hơn mà tận dụng được số luồng song song tốt hơn thay vì thread bị hold khi có task chạy tốn time. Nó sẽ nhả ra và trả thread về pool để phục vụ request khác. Khi thread cũ xong nó lại callback lại để pool cấp thread khác. 

- Không tạo thêm thread mà chỉ tối ưu thread. 

- Tối ưu luồng trên cpu để phục vụ được nhiều hơn ở 1 thời điểm. Giống như việc đưa con đi học thay vì đợi ở cổng trường chờ nó học xong đón về thì về nhà đi làm việc khác. Lúc nào học xong nó gọi callback mình ra đón.

Close