Tại sao coroutines trở lại? [đóng cửa]


19

Hầu hết các nền tảng cho coroutines xảy ra trong thập niên 60/70 và sau đó dừng lại để ủng hộ các lựa chọn thay thế (ví dụ, chủ đề)

Có bất kỳ chất nào cho sự quan tâm đổi mới trong coroutines đã xảy ra trong python và các ngôn ngữ khác?



9
Tôi không chắc họ đã rời đi.
Blrfl

Câu trả lời:


26

Quân đoàn không bao giờ rời đi, trong khi đó họ chỉ bị lu mờ bởi những thứ khác. Sự quan tâm ngày càng tăng gần đây trong lập trình không đồng bộ và do đó, phần lớn là do ba yếu tố: tăng sự chấp nhận các kỹ thuật lập trình chức năng, các bộ công cụ hỗ trợ kém cho tính song song thực sự (JavaScript! Python!) Và quan trọng nhất là: sự đánh đổi khác nhau giữa các luồng và coroutines. Đối với một số trường hợp sử dụng, coroutines là khách quan tốt hơn.

Một trong những mô hình lập trình lớn nhất của thập niên 80, 90 và ngày nay là OOP. Nếu chúng ta nhìn vào lịch sử của OOP và đặc biệt là sự phát triển của ngôn ngữ Simula, chúng ta sẽ thấy rằng các lớp học phát triển từ các coroutines. Simula được dự định để mô phỏng các hệ thống với các sự kiện riêng biệt. Mỗi phần tử của hệ thống là một quy trình riêng biệt sẽ thực hiện theo các sự kiện trong thời gian của một bước mô phỏng, sau đó mang lại kết quả để cho các quy trình khác thực hiện công việc của chúng. Trong quá trình phát triển Simula 67, khái niệm lớp học đã được giới thiệu. Bây giờ trạng thái bền bỉ của coroutine được lưu trữ trong các thành viên đối tượng và các sự kiện được kích hoạt bằng cách gọi một phương thức. Để biết thêm chi tiết, hãy xem xét việc đọc bài viết Sự phát triển của các ngôn ngữ SIMULA của Nygaard & Dahl.

Vì vậy, trong một vòng xoắn vui nhộn, chúng tôi đã sử dụng coroutines suốt, chúng tôi chỉ gọi chúng là các đối tượng và lập trình hướng sự kiện.

Liên quan đến song song, có hai loại ngôn ngữ: những ngôn ngữ có mô hình bộ nhớ phù hợp và những ngôn ngữ không có. Một mô hình bộ nhớ thảo luận về những thứ giống như nếu tôi viết vào một biến và sau đó đọc từ biến đó trong một luồng khác, tôi có thấy giá trị cũ hoặc giá trị mới hoặc có lẽ là một giá trị không hợp lệ không? "Trước" và "sau" nghĩa là gì? Những hoạt động được đảm bảo là nguyên tử?

Tạo một mô hình bộ nhớ tốt rất khó, do đó, nỗ lực này đơn giản chưa bao giờ được thực hiện đối với hầu hết các ngôn ngữ nguồn mở động không xác định, được xác định theo triển khai: Perl, JavaScript, Python, Ruby, PHP. Tất nhiên, tất cả những ngôn ngữ đó đã phát triển vượt xa so với kịch bản ban đầu mà chúng được xây dựng cho. Chà, một số ngôn ngữ này có một số loại tài liệu mô hình bộ nhớ, nhưng những ngôn ngữ đó là không đủ. Thay vào đó, chúng tôi có hack:

  • Perl có thể được biên dịch với sự hỗ trợ luồng, nhưng mỗi luồng chứa một bản sao riêng biệt của trạng thái trình thông dịch hoàn chỉnh, làm cho các luồng trở nên đắt đỏ. Là lợi ích duy nhất, phương pháp không chia sẻ này tránh được các cuộc đua dữ liệu và buộc các lập trình viên chỉ giao tiếp thông qua hàng đợi / tín hiệu / IPC. Perl không có một câu chuyện mạnh mẽ để xử lý async.

  • JavaScript luôn có sự hỗ trợ phong phú cho lập trình chức năng, vì vậy các lập trình viên sẽ mã hóa thủ công / cuộc gọi lại theo cách thủ công trong các chương trình của họ, nơi họ cần các hoạt động không đồng bộ. Ví dụ, với các yêu cầu Ajax hoặc độ trễ hoạt hình. Vì web vốn dĩ không đồng bộ, nên có rất nhiều mã JavaScript không đồng bộ và việc quản lý tất cả các cuộc gọi lại này vô cùng đau đớn. Do đó, chúng tôi thấy nhiều nỗ lực để tổ chức các cuộc gọi lại tốt hơn (Lời hứa) hoặc loại bỏ chúng hoàn toàn.

  • Python có tính năng không may này được gọi là Khóa phiên dịch toàn cầu. Về cơ bản, mô hình bộ nhớ Python là tất cả các hiệu ứng xuất hiện tuần tự vì không có sự song song. Mỗi lần chỉ có một luồng sẽ chạy mã Python. Trong khi đó Python có các luồng, chúng chỉ mạnh như coroutines. [1] Python có thể mã hóa nhiều coroutines thông qua các hàm tạo yield. Nếu được sử dụng đúng cách, điều này một mình có thể tránh được hầu hết địa ngục gọi lại từ JavaScript. Hệ thống async / await gần đây hơn từ Python 3.5 giúp cho các thành ngữ không đồng bộ thuận tiện hơn trong Python và tích hợp một vòng lặp sự kiện.

    [1]: Về mặt kỹ thuật những hạn chế này chỉ áp dụng cho CPython, triển khai tham chiếu Python. Các triển khai khác như Jython cung cấp các luồng thực sự có thể thực thi song song, nhưng phải trải qua thời gian dài để thực hiện hành vi tương đương. Về cơ bản: mỗi biến hoặc thành viên đối tượng là một biến dễ bay hơi sao cho tất cả các thay đổi là nguyên tử và được nhìn thấy ngay lập tức trong tất cả các luồng. Tất nhiên, sử dụng các biến dễ bay hơi đắt hơn nhiều so với sử dụng các biến thông thường.

  • Tôi không biết đủ về Ruby và PHP để rang chúng đúng cách.

Tóm lại: một số ngôn ngữ này có các quyết định thiết kế cơ bản làm cho việc đa luồng không mong muốn hoặc không thể, dẫn đến sự tập trung mạnh mẽ hơn vào các lựa chọn thay thế như coroutines và về cách làm cho việc lập trình async thuận tiện hơn.

Cuối cùng, hãy nói về sự khác biệt giữa coroutines và chủ đề:

Các luồng về cơ bản giống như các tiến trình, ngoại trừ việc nhiều luồng trong một tiến trình chia sẻ một không gian bộ nhớ. Điều này có nghĩa là các chủ đề không có nghĩa là trọng lượng nhẹ ánh sáng về mặt bộ nhớ. Chủ đề được lên lịch trước bởi hệ điều hành. Điều này có nghĩa là các công tắc nhiệm vụ có chi phí hoạt động cao và có thể xảy ra vào những thời điểm bất tiện. Chi phí hoạt động này có hai thành phần: chi phí tạm dừng trạng thái của luồng và chi phí chuyển đổi giữa chế độ người dùng (đối với luồng) và chế độ lõi (đối với trình lập lịch biểu).

Nếu một quá trình lên lịch các luồng của chính nó trực tiếp và hợp tác, thì việc chuyển ngữ cảnh sang chế độ kernel là không cần thiết và việc chuyển đổi các tác vụ tương đối tốn kém cho một lệnh gọi hàm gián tiếp, như trong: khá rẻ. Những sợi có trọng lượng nhẹ này có thể được gọi là sợi màu xanh lá cây, sợi hoặc coroutines tùy thuộc vào các chi tiết khác nhau. Người dùng đáng chú ý của các sợi / sợi màu xanh lá cây là các triển khai Java ban đầu và gần đây là Goroutines ở Golang. Một lợi thế về mặt khái niệm của coroutines là việc thực hiện chúng có thể được hiểu theo dòng chảy kiểm soát một cách rõ ràng qua lại giữa các coroutines. Tuy nhiên, các coroutine này không đạt được sự song song thực sự trừ khi chúng được lên lịch trên nhiều luồng hệ điều hành.

Trường hợp giá rẻ coroutines hữu ích? Hầu hết các phần mềm không cần một luồng gazillion, vì vậy các luồng đắt tiền thông thường thường ổn. Tuy nhiên, lập trình async đôi khi có thể đơn giản hóa mã của bạn. Để được sử dụng tự do, sự trừu tượng này phải đủ rẻ.

Và sau đó là web. Như đã đề cập ở trên, web vốn không đồng bộ. Yêu cầu mạng chỉ đơn giản là mất một thời gian dài. Nhiều máy chủ web duy trì một nhóm luồng đầy đủ các luồng công nhân. Tuy nhiên, hầu hết thời gian của các luồng này sẽ không hoạt động vì chúng đang chờ một số tài nguyên, có phải là đang chờ một sự kiện I / O khi tải tệp từ đĩa, đợi cho đến khi máy khách nhận ra một phần của phản hồi hoặc đợi cho đến khi cơ sở dữ liệu truy vấn hoàn thành. NodeJS đã chứng minh một cách phi thường rằng một thiết kế máy chủ không đồng bộ và dựa trên sự kiện do đó hoạt động rất tốt. Rõ ràng JavaScript không phải là ngôn ngữ duy nhất được sử dụng cho các ứng dụng web, do đó, cũng có một sự khích lệ lớn đối với các ngôn ngữ khác (đáng chú ý trong Python và C #) để giúp lập trình web không đồng bộ dễ dàng hơn.


Tôi khuyên bạn nên tìm nguồn từ đoạn thứ tư đến đoạn cuối để tránh rủi ro đạo văn, nó gần giống hệt như một nguồn khác mà tôi đã đọc. Ngoài ra, trong khi có các đơn đặt hàng có cường độ nhỏ hơn so với các luồng, hiệu năng của coroutines có thể được đơn giản hóa thành "một lệnh gọi hàm gián tiếp". Xem chi tiết Boosts về triển khai coroutine tại đâytại đây .
whn

1
@snb Về chỉnh sửa được đề xuất của bạn: GIL có thể là chi tiết triển khai CPython, nhưng vấn đề cơ bản là ngôn ngữ Python không có mô hình bộ nhớ rõ ràng chỉ định đột biến dữ liệu song song. GIL là một hack để vượt qua những vấn đề này. Nhưng việc triển khai Python với tính song song thực sự phải trải qua thời gian dài để cung cấp ngữ nghĩa tương đương, ví dụ như được thảo luận trong cuốn sách Jython . Về cơ bản: mỗi biến hoặc trường đối tượng phải là một biến dễ bay hơi đắt tiền .
amon

3
@snb Về đạo văn: Đạo văn là trình bày sai ý tưởng như của riêng bạn, đặc biệt là trong một bối cảnh học thuật. Đây là một cáo buộc nghiêm trọng , nhưng tôi chắc chắn rằng bạn không có ý như thế. Các chủ đề trên cơ bản giống như các quy trình Đoạn văn chỉ đơn thuần nhắc lại các sự kiện nổi tiếng như được dạy trong bất kỳ bài giảng hoặc sách giáo khoa nào về các hệ điều hành. Vì chỉ có rất nhiều cách để diễn đạt chính xác những sự thật này, tôi không ngạc nhiên khi đoạn văn nghe có vẻ quen thuộc với bạn.
amon

Tôi đã không thay đổi ý nghĩa để ám chỉ rằng Python đã có một mô hình bộ nhớ. Ngoài ra, việc sử dụng biến động không tự giảm hiệu suất dễ bay hơi đơn giản có nghĩa là trình biên dịch không thể tối ưu hóa biến theo cách mà nó có thể giả định biến sẽ không thay đổi với các hoạt động rõ ràng trong bối cảnh hiện tại. Trong thế giới Jython điều này thực sự có thể quan trọng, vì nó sẽ sử dụng trình biên dịch VM JIT, nhưng trong thế giới CPython, bạn không lo lắng về tối ưu hóa JIT, các biến dễ bay hơi của bạn sẽ tồn tại trong không gian thời gian chạy trình thông dịch, nơi không thể tối ưu hóa .
whn

7

Coroutines từng là hữu ích vì hệ điều hành không thực hiện trước công phủ đầu lập kế hoạch. Khi họ bắt đầu cung cấp lịch trình ưu tiên, việc từ bỏ kiểm soát định kỳ trong chương trình của bạn là lâu hơn.

Khi các bộ xử lý đa lõi trở nên phổ biến hơn, các coroutine được sử dụng để đạt được tính song song của nhiệm vụ và / hoặc giữ mức độ sử dụng của hệ thống cao (khi một luồng thực thi phải chờ trên một tài nguyên, một luồng khác có thể bắt đầu chạy ở vị trí của nó).

NodeJS là một trường hợp đặc biệt, trong đó coroutines được sử dụng có quyền truy cập song song vào IO. Nghĩa là, nhiều luồng được sử dụng để phục vụ các yêu cầu IO, nhưng một luồng duy nhất được sử dụng để thực thi mã javascript. Mục đích của việc thực thi mã người dùng trong một chuỗi ký hiệu là để tránh sự cần thiết phải sử dụng mutexes. Điều này thuộc danh mục cố gắng duy trì mức sử dụng của hệ thống cao như đã đề cập ở trên.


4
Nhưng coroutines không được quản lý hệ điều hành. Hệ điều hành không biết coroutine là gì, không giống như các sợi C ++
trao đổi quá mức vào

Nhiều hệ điều hành có coroutines.
Jörg W Mittag

coroutines như python và Javascript ES6 + không phải là đa xử lý? Làm thế nào để những người đạt được song song nhiệm vụ?
whn

1
@Mael Sự "hồi sinh" gần đây của coroutines đến từ python và javascript, cả hai đều không đạt được sự song song với các coroutines của họ như tôi hiểu. Điều đó có nghĩa là câu trả lời này là không chính xác, vì sự hoang tưởng trong nhiệm vụ không phải là lý do khiến các coroutines "quay trở lại". Ngoài ra Luas cũng không đa xử lý? EDIT: Tôi mới nhận ra rằng bạn không nói về sự song song, nhưng tại sao bạn lại trả lời tôi ngay từ đầu? Trả lời dlasalle, vì rõ ràng họ đã sai về điều này.
whn

3
@dlasalle Không, họ không thể mặc dù thực tế là nó nói "chạy song song" không có nghĩa là bất kỳ mã nào được chạy cùng một lúc. GIL sẽ dừng nó và async không sinh ra các quy trình riêng biệt cần thiết cho đa xử lý trong CPython (GILs riêng biệt). Async hoạt động với sản lượng trên một chủ đề duy nhất. Khi họ nói "song song" họ thực sự có nghĩa là một số chức năng yeilding chức năng công việc khác và interleving chức năng thực hiện. Các tiến trình async của Python không thể chạy song song vì impl. Bây giờ tôi có ba ngôn ngữ không sử dụng coroutines, Lua, Javascript và Python.
whn

5

Các hệ thống ban đầu đã sử dụng coroutines để cung cấp đồng thời chủ yếu vì chúng là cách đơn giản nhất để làm điều đó. Các luồng yêu cầu một lượng hỗ trợ khá lớn từ hệ điều hành (bạn có thể triển khai chúng ở cấp độ người dùng, nhưng bạn sẽ cần một số cách sắp xếp để hệ thống làm gián đoạn quá trình của bạn) và khó thực hiện hơn ngay cả khi bạn có hỗ trợ .

Các chủ đề bắt đầu tiếp quản sau đó bởi vì, vào thập niên 70 hoặc 80, tất cả các hệ điều hành nghiêm túc đều hỗ trợ chúng (và, vào thập niên 90, thậm chí là Windows!), Và chúng phổ biến hơn. Và chúng dễ sử dụng hơn. Đột nhiên mọi người nghĩ chủ đề là điều lớn tiếp theo.

Vào cuối những năm 90, các vết nứt bắt đầu xuất hiện và vào đầu những năm 2000, rõ ràng là có vấn đề nghiêm trọng với các luồng:

  1. họ tiêu thụ rất nhiều tài nguyên
  2. chuyển đổi ngữ cảnh mất rất nhiều thời gian, tương đối, và thường không cần thiết
  3. họ phá hủy địa phương của tài liệu tham khảo
  4. viết mã chính xác để điều phối nhiều tài nguyên có thể cần quyền truy cập độc quyền là khó khăn bất ngờ

Theo thời gian, số lượng các chương trình nhiệm vụ thường cần thực hiện bất cứ lúc nào đã tăng lên nhanh chóng, làm tăng các vấn đề gây ra bởi (1) và (2) ở trên. Sự chênh lệch giữa tốc độ xử lý và thời gian truy cập bộ nhớ ngày càng tăng, làm trầm trọng thêm vấn đề (3). Và sự phức tạp của các chương trình về số lượng và loại tài nguyên khác nhau mà chúng yêu cầu đã tăng lên, làm tăng sự liên quan của vấn đề (4).

Nhưng bằng cách mất một chút tính tổng quát và đặt thêm một chút trách nhiệm cho lập trình viên để suy nghĩ về cách các quy trình của họ có thể hoạt động cùng nhau, các coroutine có thể giải quyết tất cả các vấn đề này.

  1. Các coroutines đòi hỏi ít tài nguyên hơn một chút các trang để xếp chồng, ít hơn nhiều so với hầu hết các triển khai của các luồng.
  2. Coroutines chỉ chuyển đổi bối cảnh tại các điểm do lập trình viên xác định, điều này hy vọng chỉ có nghĩa khi cần thiết. Họ cũng thường không cần lưu giữ nhiều thông tin ngữ cảnh (ví dụ như các giá trị đăng ký) như các luồng, nghĩa là mỗi chuyển đổi thường nhanh hơn cũng như cần ít hơn chúng.
  3. Các mẫu coroutines phổ biến bao gồm các hoạt động loại nhà sản xuất / người tiêu dùng xử lý dữ liệu giữa các thói quen theo cách tích cực tăng địa phương. Hơn nữa, các chuyển đổi ngữ cảnh thường chỉ xảy ra giữa các đơn vị công việc không nằm trong chúng, tức là tại thời điểm mà địa phương thường được giảm thiểu.
  4. Khóa tài nguyên ít có khả năng là cần thiết khi các thói quen biết rằng chúng không thể bị gián đoạn một cách tùy tiện ở giữa một hoạt động, cho phép các triển khai đơn giản hơn hoạt động chính xác.

5

Lời nói đầu

Tôi muốn bắt đầu bằng cách nêu rõ lý do tại sao các xác chết không được hồi sinh, song song. Nhìn chung, các coroutines hiện đại không phải là một phương tiện để đạt được sự song song dựa trên nhiệm vụ, vì các triển khai hiện đại không sử dụng chức năng đa xử lý. Thứ gần nhất bạn có được là những thứ như sợi .

Cách sử dụng hiện đại (tại sao họ quay lại)

Các coroutine hiện đại là một cách để đạt được sự đánh giá lười biếng , một thứ rất hữu ích trong các ngôn ngữ chức năng như haskell, thay vì lặp đi lặp lại toàn bộ một tập hợp để thực hiện một thao tác, bạn sẽ có thể thực hiện một thao tác chỉ đánh giá khi cần thiết ( hữu ích cho các tập hợp vô hạn của các mục hoặc các tập hợp lớn khác có kết thúc sớm và tập hợp con).

Với việc sử dụng các từ khóa Năng suất để tạo ra máy phát điện (mà trong bản thân đáp ứng một phần nhu cầu đánh giá lười biếng) trong các ngôn ngữ như Python và C #, coroutines, trong việc thực hiện hiện đại không chỉ là có thể, nhưng có thể với ra cú pháp đặc biệt bằng ngôn ngữ riêng của mình (mặc dù python cuối cùng đã thêm một vài bit để giúp đỡ). Co-thói quen giúp đỡ với evaulation lười biếng với ý tưởng tương lai của nơi mà nếu bạn không cần giá trị của một biến vào thời điểm đó, bạn có thể trì hoãn thực sự mua nó cho đến khi bạn rõ ràng yêu cầu cho giá trị đó (cho phép bạn sử dụng các giá trị và lười biếng đánh giá nó tại một thời điểm khác với khởi tạo).

Tuy nhiên, ngoài việc đánh giá lười biếng, đặc biệt là trong thế giới web, các thói quen chung này giúp khắc phục cuộc gọi lại địa ngục . Coroutines trở nên hữu ích trong việc truy cập cơ sở dữ liệu, giao dịch trực tuyến, ui, v.v., trong đó thời gian xử lý trên máy khách sẽ không dẫn đến việc truy cập nhanh hơn những gì bạn cần. Việc xâu chuỗi có thể hoàn thành điều tương tự, nhưng đòi hỏi nhiều chi phí hơn trong lĩnh vực này, và ngược lại với coroutines, thực sự hữu ích cho việc xử lý song song .

Nói tóm lại, khi sự phát triển web phát triển và các mô hình chức năng hợp nhất nhiều hơn với các ngôn ngữ bắt buộc, coroutines đã trở thành một giải pháp cho các vấn đề không đồng bộ và đánh giá lười biếng. Các coroutines đến các không gian có vấn đề trong đó việc phân luồng và xử lý đa luồng nói chung là không cần thiết, bất tiện hoặc không thể.

Ví dụ hiện đại

Các coroutines trong các ngôn ngữ như Javascript, Lua, C # và Python đều xuất phát từ việc thực hiện các chức năng riêng lẻ từ bỏ quyền kiểm soát luồng chính sang các chức năng khác (không liên quan gì đến các cuộc gọi của hệ điều hành).

Trong ví dụ về con trăn này , chúng ta có một hàm python vui nhộn với một cái gì đó được gọi là awaitbên trong nó. Về cơ bản, đây là một sản lượng, mang lại sự thực thi để loopsau đó cho phép một chức năng khác chạy (trong trường hợp này là một factorialchức năng khác ). Lưu ý rằng khi nó nói "Thực thi song song các nhiệm vụ" là một cách viết sai, thì thực tế nó không thực thi song song, việc thực thi chức năng xen kẽ của nó thông qua việc sử dụng từ khóa await (mà hãy nhớ chỉ là một loại năng suất đặc biệt)

Chúng cho phép duy nhất, không song song, sản lượng điều khiển cho đồng thời các quy trình mà không phải là nhiệm vụ song song , theo nghĩa rằng những nhiệm vụ không hoạt động bao giờ cùng một lúc. Coroutines không phải là chủ đề trong việc thực hiện ngôn ngữ hiện đại. Tất cả các ngôn ngữ này thực hiện các thường trình đồng đều bắt nguồn từ các lệnh gọi năng suất hàm này (mà lập trình viên phải thực sự đưa vào thủ công vào các thường trình chung của bạn).

EDIT: C ++ Boost coroutine2 hoạt động theo cách tương tự, và lời giải thích chặt chẽ hơn sẽ cho hình dung rõ hơn về những gì tôi đang nói với các anh em, xem tại đây . Như bạn có thể thấy, không có "trường hợp đặc biệt" nào với việc triển khai, những thứ như sợi tăng cường là ngoại lệ của quy tắc và thậm chí sau đó yêu cầu đồng bộ hóa rõ ràng.

EDIT2: vì ai đó nghĩ rằng tôi đang nói về hệ thống dựa trên nhiệm vụ c #, nên tôi đã không làm vậy. Tôi đã nói về hệ thống của Unity và các triển khai c # ngây thơ


@ T.Sar Tôi chưa bao giờ tuyên bố C # có bất kỳ coroutines "tự nhiên" nào, C ++ (có thể thay đổi) cũng không có python (và nó vẫn có chúng) và cả ba đều có các triển khai thường xuyên. Nhưng tất cả các triển khai C # của coroutines (như những người trong sự thống nhất) đều dựa trên năng suất như tôi mô tả. Ngoài ra việc bạn sử dụng "hack" ở đây là vô nghĩa, tôi đoán mọi chương trình đều là hack vì nó không được xác định trong ngôn ngữ. Tôi không có cách nào trộn lẫn "hệ thống dựa trên nhiệm vụ" C # với bất cứ điều gì, tôi thậm chí không đề cập đến nó.
whn

Tôi sẽ đề nghị làm cho câu trả lời của bạn rõ ràng hơn một chút. C # có cả khái niệm về hướng dẫn chờ đợi và hệ thống song song dựa trên nhiệm vụ - sử dụng C # và những từ đó trong khi đưa ra ví dụ về python về cách python không thực sự song song có thể gây ra nhiều nhầm lẫn khó khăn. Ngoài ra, hãy xóa câu đầu tiên của bạn - không cần thiết phải tấn công trực tiếp người dùng khác vào câu trả lời như vậy.
T. Sar - Tái lập Monica
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.