2.17

Xem bằng tiếng Anh

2.17 Đồng thời và song song

Tổng quan và động lực

Đồng thời là nghệ thuật cấu trúc một chương trình thành các nhiệm vụ độc lập có thể tiến triển mà không phải chờ nhau. Song song là thực sự thực thi các nhiệm vụ đó tại cùng một thời điểm trên nhiều bộ xử lý. Sự phân biệt này không phải chuyện học thuật. Đồng thời là cách tổ chức mã để một lệnh gọi mạng chậm không làm đóng băng cả chương trình; song song là cách hoàn thành một phép tính lớn nhanh hơn bằng cách trải nó qua các lõi. Nhầm lẫn hai thứ dẫn các đội thêm luồng với hy vọng tốc độ và chỉ nhận được lỗi.

Với các đội lớn, chủ đề này quan trọng vì đồng thời là nơi tính đúng đắn lặng lẽ chết. Một tác giả đơn lẻ viết mã đơn luồng có thể suy luận về nó từng dòng, nhưng khoảnh khắc nhiều tác giả chia sẻ bộ nhớ qua các luồng, số lượng các cách xen kẽ có thể bùng nổ, và một chương trình vượt qua mọi bài kiểm thử vẫn có thể thất bại một lần trong một triệu dưới tải môi trường thực, không phải bằng một sự sụp đổ ồn ào mà bằng dữ liệu bị hỏng, các yêu cầu treo, và những sự cố không ai tái hiện được. Chương này xây dựng trên trọng tâm cấp mã của chương 2.16 (kỹ thuật hiệu năng) và nền tảng tin học của chương 2.13, và nó nuôi dưỡng các bài toán điều phối của chương 3.3 (hệ thống phân tán), vốn là đồng thời qua các máy với sự tàn nhẫn thêm của một mạng không đáng tin cậy.

Với các doanh nghiệp, lỗi đồng thời là lỗi thông lượng. Các dịch vụ lưu lượng cao sống hay chết nhờ khả năng xử lý hàng nghìn yêu cầu đồng thời mà không chạy đua trên trạng thái dùng chung, và một bộ đếm không đồng bộ hóa đơn lẻ có thể làm hỏng một sổ cái dưới tải. Với chính phủ, rủi ro là tính đúng đắn và khả năng kiểm toán trong các hệ thống chạy hàng chục năm và chạm đến an toàn, trợ cấp hoặc hồ sơ công. Một điều kiện chạy đua trong hệ thống thuế hay y tế không phải là sự bất tiện; đó là một câu trả lời sai mà ai đó sau này phải giải thích với cơ quan giám sát. Ở cả hai bối cảnh mục tiêu là như nhau: biến con đường an toàn thành mặc định, để nhiều người chạm vào mã không ai phải là chuyên gia đồng thời.

Các nguyên tắc chính

  • Đồng thời là cấu trúc; song song là thực thi. Hãy quyết định bạn thực sự cần cái nào trước khi với tới luồng.
  • Trạng thái khả biến dùng chung là kẻ thù. Gần như mọi lỗi đồng thời đều truy về hai nhiệm vụ chạm vào cùng dữ liệu có thể thay đổi.
  • Ưu tiên tính bất biến và truyền thông điệp. Dữ liệu không thể thay đổi thì không thể bị chạy đua, và thông điệp thắng bộ nhớ dùng chung về độ an toàn.
  • Tính không xác định là khó khăn cốt lõi. Lỗi xuất hiện một lần chạy trong một nghìn là toàn bộ vấn đề, không phải trường hợp biên.
  • Giới hạn mọi thứ. Hàng đợi không giới hạn, số luồng và công việc đang xử lý biến một đợt tăng đột biến thành một sự cố ngừng hoạt động.
  • Mô hình cấp cao thắng khóa thô. Tác tử, kênh và đồng thời có cấu trúc cho nhiều tác giả một mặc định an toàn, và mỗi khóa đều có chi phí.
  • Kiểm thử các cách xen kẽ, không chỉ đường suôn sẻ. Kiểm thử xác định không thể bắt một lỗi mà chỉ một thứ tự hiếm mới làm lộ.

Khuyến nghị

Quyết định bạn cần đồng thời hay song song

Hãy bắt đầu bằng việc gọi tên vấn đề. Nếu dịch vụ của bạn dành phần lớn thời gian để chờ (cơ sở dữ liệu, lệnh gọi mạng hoặc đĩa), bạn có một khối lượng công việc bị giới hạn bởi I/O, và đồng thời là câu trả lời: cấu trúc mã sao cho trong khi một yêu cầu chờ, các yêu cầu khác tiếp tục. Một luồng đơn với async/await, hoặc một nhóm nhỏ, có thể phục vụ hàng nghìn yêu cầu đang chờ. Nếu thay vào đó chương trình của bạn bị giới hạn bởi CPU, miệt mài tính toán với ít chờ đợi, thì song song qua các lõi là thứ mua được tốc độ, và ở đây trần được đặt bởi định luật Amdahl (xem chương 2.16): phần tuần tự giới hạn mức tăng tốc của bạn bất kể bạn thêm bao nhiêu lõi. Hãy đo xem bạn đang ở chế độ nào trước khi thiết kế.

Coi trạng thái khả biến dùng chung là kẻ thù

Gần như mọi khiếm khuyết đồng thời quy về cùng một hình dạng: hai nhiệm vụ đọc và ghi cùng dữ liệu có thể thay đổi mà không thống nhất một thứ tự. Đây là một điều kiện chạy đua, và nó tạo ra các cập nhật bị mất, đối tượng ghi dở, và các giá trị vi phạm bất biến mà mã giả định là an toàn. Sự phòng thủ đáng tin cậy nhất là có ít trạng thái khả biến dùng chung hơn. Hãy cho mỗi nhiệm vụ dữ liệu riêng của nó, truyền bản sao thay vì tham chiếu, và giới hạn trạng thái khả biến trong một chủ sở hữu duy nhất mà những nhiệm vụ khác tiếp cận qua thông điệp. Khi bạn thực sự phải chia sẻ, hãy làm cho việc chia sẻ tường minh và nhỏ, để người đánh giá thấy được mọi nơi trạng thái bị chạm vào.

Ưu tiên tính bất biến và truyền thông điệp làm mặc định

Dữ liệu dùng chung an toàn nhất là dữ liệu không thể thay đổi. Một đối tượng bất biến, một khi được xây dựng, có thể được đọc bởi bất kỳ số luồng nào mà không cần đồng bộ hóa, vì không có gì để chạy đua. Hãy biến tính bất biến thành mặc định và tính khả biến thành ngoại lệ có chủ đích. Khi các nhiệm vụ phải điều phối, hãy ưu tiên truyền thông điệp hơn bộ nhớ dùng chung: thay vì chia sẻ một biến chung, hãy để một nhiệm vụ gửi giá trị đến nhiệm vụ kia, đó là triết lý đằng sau châm ngôn của Go “đừng giao tiếp bằng cách chia sẻ bộ nhớ; hãy chia sẻ bộ nhớ bằng cách giao tiếp.” Truyền thông điệp biến các lỗi vô hình, phụ thuộc thứ tự thành luồng dữ liệu tường minh, kiểm tra được, và sự rõ ràng đó hầu như luôn đáng chi phí mỗi thông điệp trong mã mà nhiều người bảo trì.

Với tới các mô hình cấp cao trước khóa thô

Khóa viết tay đúng về nguyên tắc và thảm họa trong thực hành, vì con người kém suy luận về mọi cách xen kẽ. Hãy ưu tiên các mô hình khiến đồng thời an toàn trở thành mặc định. Mô hình tác tử cho mỗi tác tử trạng thái riêng tư và một hộp thư: các tác tử không bao giờ chia sẻ bộ nhớ và chỉ gửi thông điệp, nên cả các loại chạy đua biến mất. Các tiến trình tuần tự giao tiếp (CSP), mô hình đằng sau kênh trong các ngôn ngữ như Go, có các tiến trình độc lập truyền giá trị qua các kênh có kiểu. Đồng thời có cấu trúc gắn tuổi thọ của các nhiệm vụ đồng thời với một phạm vi từ vựng, để các nhiệm vụ không thể tồn tại lâu hơn khối đã sinh ra chúng và lỗi lan truyền thay vì biến mất. Async/await cho phép bạn viết mã đồng thời, bị giới hạn bởi I/O theo phong cách tuần tự. Mỗi cái nâng sàn cho tác giả trung bình, đó là điều một đội lớn cần.

Hiểu mô hình bộ nhớ, tính nguyên tử và khả năng nhìn thấy

Khi bạn chia sẻ bộ nhớ, hai thuộc tính cắn bạn. Tính nguyên tử nghĩa là một thao tác xảy ra tất cả cùng lúc hoặc không xảy ra; một phép tăng đơn giản (x = x + 1) không nguyên tử, vì nó đọc, cộng và ghi thành ba bước mà luồng khác có thể ngắt, đó là cách các bộ đếm mất cập nhật. Khả năng nhìn thấy nghĩa là một lần ghi của một luồng trở nên quan sát được bởi luồng khác; không có đồng bộ hóa thích hợp, một giá trị được ghi trên một lõi có thể nằm trong bộ nhớ đệm không được lõi khác thấy, nên một luồng có thể lặp mãi trên một cờ đã được đặt. Mô hình bộ nhớ của ngôn ngữ bạn xác định khi nào các lần ghi trở nên hiển thị và trình biên dịch cùng CPU được phép sắp xếp lại những thứ tự nào, nên bạn không thể cho rằng mã chạy theo thứ tự bạn đã viết. Hãy dùng các kiểu nguyên tử và nguyên thủy đồng bộ hóa của ngôn ngữ thay vì tự phát minh lược đồ không khóa của riêng mình.

Dùng nguyên thủy đồng bộ hóa một cách có chủ đích, và thiết kế chống bế tắc

Khi chia sẻ là không thể tránh, hãy với tới nguyên thủy đúng và tôn trọng giá của nó. Một khóa hay mutex (loại trừ lẫn nhau) cho phép một luồng tại một thời điểm vào một vùng tới hạn, nhưng tuần tự hóa truy cập, nên một khóa nóng trở thành nút thắt xóa bỏ lợi ích của nhiều lõi. Một semaphore giới hạn có bao nhiêu nhiệm vụ được tiến hành cùng lúc, đó là cách bạn giới hạn một nhóm. Các thao tác nguyên tử cung cấp cập nhật không khóa cho các giá trị đơn giản như bộ đếm, rẻ hơn khóa nhưng dễ dùng sai cho bất cứ thứ gì phức hợp. Khóa mang ba chế độ thất bại kinh điển. Một bế tắc là khi các nhiệm vụ chờ nhau trong một chu trình và không cái nào tiến được, ví dụ giáo khoa là hai luồng mỗi luồng giữ một khóa và muốn khóa kia. Một livelock là khi các nhiệm vụ cứ phản ứng với nhau nhưng không có tiến triển. Chết đói là khi một nhiệm vụ không bao giờ nhận được tài nguyên vì những nhiệm vụ khác cứ chen lên. Những kỷ luật ngăn chặn chúng là cụ thể: áp đặt một thứ tự khóa toàn cục, giữ khóa ngắn, thêm thời gian chờ để một nhiệm vụ kẹt thất bại ồn ào, không bao giờ gọi mã lạ khi đang giữ khóa, và dùng lập lịch công bằng ở nơi chết đói là rủi ro. Hãy viết các quy tắc này ra, vì một tác giả mới không thể tự khám phá lại chúng chỉ từ mã.

Giới hạn hàng đợi, nhóm và công việc đang xử lý bằng áp suất ngược

Một hàng đợi không giới hạn là một quả bom hẹn giờ. Dưới một đợt tăng lưu lượng, công việc đến nhanh hơn tốc độ nó thoát, hàng đợi lớn lên không giới hạn, bộ nhớ đầy, và dịch vụ chết theo cách trông như một sự cố hết bộ nhớ bí ẩn thay vì sự quá tải thực của nó. Hãy giới hạn mọi hàng đợi, đặt trần cho mọi nhóm luồng, và áp dụng áp suất ngược: khi hệ thống đầy, hãy báo ngược lên thượng nguồn để chậm lại hoặc từ chối công việc nhanh thay vì chấp nhận công việc vô hạn mà bạn không thể hoàn thành. Định cỡ nhóm theo khối lượng công việc (xấp xỉ số lõi cho công việc bị giới hạn bởi CPU, cao hơn cho công việc bị giới hạn bởi I/O nơi các luồng chủ yếu chờ), và coi giới hạn là một quyết định năng lực có chủ đích. Điều này kết nối với các mẫu chịu lỗi của chương 3.3.

Dùng song song dữ liệu ở nơi công việc song song một cách hiển nhiên

Một số bài toán chia gọn gàng: áp dụng cùng một thao tác cho mọi phần tử của một tập dữ liệu lớn, không phần tử nào phụ thuộc vào phần tử khác. Song song dữ liệu này là loại thân thiện nhất, vì có ít trạng thái dùng chung để chạy đua và mức tăng tốc có thể tiến gần số lõi, như các đường ống map-reduce, phép toán mảng song song và mã số học vector hóa đều cho thấy. Ngay cả ở đây, hãy tôn trọng định luật Amdahl: bước hợp nhất hoặc rút gọn thường mang tính tuần tự và giới hạn lợi ích của bạn, và chi phí chia có thể chi phối với đầu vào nhỏ. Hãy với tới nó khi công việc cho mỗi phần tử đáng kể và các phần tử thực sự độc lập; nếu không, phiên bản tuần tự đơn giản nhất thường vừa đủ nhanh vừa dễ giữ đúng hơn nhiều, một điểm mà các thực hành xây dựng của chương 2.9 củng cố.

Kiểm thử và gỡ lỗi mã không xác định một cách có chủ đích

Lỗi đồng thời là không xác định, nên các bài kiểm thử thông thường, chạy một cách xen kẽ, hầu như bỏ sót chúng. Hãy tấn công vấn đề một cách có chủ đích bằng các bài kiểm thử căng thẳng và mờ chạy nhiều nhiệm vụ dưới thời điểm ngẫu nhiên để làm lộ các thứ tự hiếm. Hãy với tới các bộ phát hiện chạy đua và bộ khử độc luồng, công cụ trang bị truy cập bộ nhớ để bắt các cuộc chạy đua dữ liệu ngay cả khi cách xen kẽ lỗi không xảy ra lần chạy này. Ở nơi nền tảng của bạn cung cấp, hãy dùng mô phỏng xác định hoặc bộ lập lịch được kiểm soát phát lại một cách xen kẽ cụ thể, biến một heisenbug thành thứ tái hiện được, và thiết kế để một lần treo trên môi trường thực cho phép bạn nắm bắt trạng thái luồng và quyền sở hữu khóa, gắn với kỷ luật gỡ lỗi của chương 2.15. Trên hết, hãy ưu tiên các thiết kế (tính bất biến, truyền thông điệp, sở hữu đơn) khiến cả loại lỗi này không thể xảy ra, vì một lỗi bạn không thể tạo ra là lỗi bạn không bao giờ phải gỡ.

Đánh đổi: ưu và nhược điểm

Cách tiếp cậnƯu điểmNhược điểm
Bộ nhớ dùng chung với khóaNhanh trên mỗi thao tác; quen thuộcLỗi chạy đua, bế tắc và khả năng nhìn thấy; khó để nhiều tác giả giữ đúng
Tính bất biếnKhông cần đồng bộ hóa; đọc an toàn luồng một cách tầm thườngChi phí sao chép; vụng về với các cấu trúc khả biến lớn
Truyền thông điệp (tác tử, kênh)Luồng dữ liệu tường minh; cả loại lỗi biến mấtChi phí mỗi thông điệp; có thể che giấu áp suất ngược nếu hàng đợi không giới hạn
Async/awaitĐồng thời rẻ cho công việc bị giới hạn bởi I/O; mã trông tuần tựKhông song song cho công việc CPU; chặn một nhiệm vụ làm đình trệ các nhiệm vụ khác
Đồng thời có cấu trúcTuổi thọ nhiệm vụ rõ ràng; lỗi lan truyền; không rò rỉ nhiệm vụMới hơn, ít có sẵn trong một số hệ sinh thái
Song song dữ liệuTăng tốc gần tuyến tính trên công việc độc lậpTrần Amdahl; chi phí chi phối với đầu vào nhỏ
Nguyên tử / không khóaKhông tranh chấp khóa cho các giá trị đơn giảnCực kỳ dễ sai một cách tinh vi; khó rà soát

Căng thẳng trung tâm là an toàn so với tốc độ thô, và lời giải là mua tính đúng đắn trước và chỉ chi hiệu năng ở nơi đo lường chứng minh bạn phải làm. Khóa bộ nhớ dùng chung thô là nhanh nhất trên mỗi thao tác và nguy hiểm nhất trên mỗi dòng mã; các mô hình cấp cao tốn một chút thông lượng và trả lại rất nhiều an toàn và sự rõ ràng, và với mã được nhiều bàn tay bảo trì, sự đánh đổi đó hoàn toàn đáng làm. Hãy dành đồng thời không khóa chỉnh tay cho những điểm nóng nhỏ nơi một trình lập hồ sơ (chương 2.16) chứng minh chi phí điều phối quan trọng, và giữ ngay cả những điểm đó đằng sau một ranh giới được kiểm thử kỹ.

Câu hỏi để thảo luận với đội của bạn

  1. Với dịch vụ bận rộn nhất của bạn, khối lượng công việc bị giới hạn bởi I/O hay CPU, và thiết kế đồng thời của bạn có khớp không? Các đội thường xuyên thêm nhóm luồng vào các dịch vụ dành 95% thời gian để chờ một cơ sở dữ liệu, thu được tranh chấp nhưng không có thông lượng, hoặc họ cố song song hóa một phép tính mà phần tuần tự của nó giới hạn mọi mức tăng tốc. Thiết kế đúng đi theo chế độ: async hoặc một nhóm nhỏ cho công việc nặng về chờ đợi, song song thực sự qua các lõi cho công việc nặng về tính toán. Hãy mang một hồ sơ cho thấy thời gian thực sự đi đâu, không phải một giả định, và nếu phần lớn thời gian dành để tính toán, hãy đo phần tuần tự và để định luật Amdahl cho bạn biết trần. Câu trả lời định hình việc bạn với tới async, một nhóm có giới hạn, hay song song dữ liệu.

  2. Mặc định của đội bạn để chia sẻ trạng thái qua các nhiệm vụ là gì, và nó có an toàn theo cấu trúc không? Trong một đội lớn, mặc định quan trọng hơn các ngoại lệ, vì phần lớn mã được viết bởi những người không phải chuyên gia đồng thời và sao chép bất cứ mẫu nào đã có. Nếu mặc định là các đối tượng khả biến dùng chung được bảo vệ bằng khóa tùy nghi, bạn chỉ cách một lần quên khóa khỏi một cuộc chạy đua nổi lên nhiều tháng sau trên môi trường thực. Nếu mặc định là tính bất biến và truyền thông điệp, cả các loại lỗi không bao giờ xảy ra, và chỗ hiếm hoi thực sự cần bộ nhớ dùng chung nổi bật lên để được rà soát cẩn thận. Hãy thảo luận một kỹ sư mới hôm nay sẽ với tới cái gì, liệu việc đánh giá của bạn có bắt được một lần ghi không đồng bộ hóa không, và làm thế nào biến con đường an toàn thành con đường dễ.

  3. Bạn sẽ tìm, tái hiện và sửa một lỗi đồng thời xuất hiện một lần trong một triệu yêu cầu trên môi trường thực như thế nào? Câu trả lời trung thực của nhiều đội là họ không thể, vì lỗi biến mất khi họ nhìn và các bài kiểm thử của họ chỉ chạy một cách xen kẽ lành tính. Điều đó nên làm bạn lo, vì những lỗi này làm hỏng dữ liệu một cách lặng lẽ và xói mòn niềm tin. Hãy nói về việc bạn có chạy bộ phát hiện chạy đua và bộ khử độc luồng trong tích hợp liên tục không, bạn có kiểm thử căng thẳng với thời điểm ngẫu nhiên không, và khả năng quan sát môi trường thực của bạn có nắm bắt trạng thái luồng và khóa vào khoảnh khắc treo không. Các đội tốt nhất trả lời bằng cách làm cho hầu hết các lỗi như vậy không thể xảy ra thông qua lựa chọn mô hình, để vài lỗi còn lại hiếm và được khoanh vùng.

  4. Ở đâu trong hệ thống của bạn vẫn còn một hàng đợi không giới hạn hoặc một nhóm luồng không có trần, và điều gì xảy ra với nó dưới một đợt tăng đột ngột gấp mười lần? Điều này quan trọng vì công việc đang xử lý không giới hạn là thất bại giả dạng một sự cố hết bộ nhớ bí ẩn: công việc đến nhanh hơn tốc độ nó thoát, bộ nhớ đầy, và dịch vụ chết trông như một lỗi phần cứng thay vì sự quá tải thực của nó. Những cân nhắc đối lập là thật, vì một giới hạn đặt quá thấp từ chối lưu lượng chính đáng và một giới hạn quá cao trì hoãn sự sụp đổ thay vì ngăn chặn nó, nên con số là một quyết định năng lực, không phải phỏng đoán. Hãy mang một bản kiểm kê mọi hàng đợi và nhóm, giới hạn hiện tại của nó (hoặc sự thừa nhận rằng nó không có), hành vi áp suất ngược khi nó đầy, và bằng chứng kiểm thử tải về cách hệ thống suy giảm ở rìa. Với một đội máy doanh nghiệp, một hàng đợi không giới hạn đơn lẻ có thể lan thành sự cố ngừng hoạt động toàn đội máy, và với một nền tảng chính phủ phải luôn sẵn sàng cho công dân, việc từ chối êm ái kèm một lỗi rõ ràng là nghĩa vụ dịch vụ, nên giới hạn và đường từ chối của nó thuộc về kế hoạch năng lực và sổ tay vận hành, không phải trí nhớ của một kỹ sư.

  5. Chính sách của đội bạn về việc dùng các mô hình đồng thời cấp cao so với khóa viết tay là gì, và bạn đã cho phép ngoại lệ ở đâu? Mô hình mặc định quyết định mức độ an toàn của thay đổi trung bình, vì phần lớn tác giả không phải chuyên gia đồng thời và sẽ sao chép bất cứ mẫu nào đã có: tác tử, kênh và đồng thời có cấu trúc nâng sàn cho mọi người, trong khi khóa thô đúng về lý thuyết và là nguồn bế tắc trong thực hành. Căng thẳng là các mô hình cấp cao tốn một chút chi phí mỗi thông điệp hoặc mỗi nhiệm vụ, và một trình lập hồ sơ thỉnh thoảng sẽ chứng minh một đường nóng cần mã không khóa chỉnh tay, nên một lệnh cấm đại trà sai ngang một sự tự do hoàn toàn. Hãy mang danh sách những nơi bạn đã với xuống dưới mặc định an toàn, bằng chứng lập hồ sơ biện minh cho từng nơi, và mỗi ngoại lệ được rào lại sau một ranh giới được kiểm thử và một thứ tự khóa có tài liệu như thế nào. Trong một doanh nghiệp lớn, chính sách này giữ hàng nghìn người đóng góp khỏi việc mỗi người phát minh lại một lược đồ không an toàn, và trong một hệ thống chính phủ tồn tại lâu, nó cho phép một người đánh giá nhiều năm sau hiểu vì sao một mẫu nguy hiểm được cho phép và xác nhận nó vẫn còn chính đáng.

  6. Khi bạn quyết định song song hóa một phép tính, bạn đo phần tuần tự như thế nào, và ai chịu trách nhiệm xác nhận mức tăng tốc là thật? Các đội thường xuyên trải một phép tính qua các lõi và ăn mừng một con số mà trình lập hồ sơ sẽ không bao giờ xác nhận, vì định luật Amdahl giới hạn lợi ích ở nghịch đảo của phần tuần tự bất kể bạn thêm bao nhiêu lõi, và chi phí chia-và-hợp nhất có thể xóa sạch lợi ích với đầu vào nhỏ. Lực kéo đối lập là song song thêm độ phức tạp thực và bề mặt chạy đua mới, nên câu hỏi là liệu mức tăng tốc đo được có biện minh cho rủi ro về tính đúng đắn bạn đang gánh. Hãy mang một hồ sơ cô lập phần tuần tự, kích thước đầu vào nơi song song thực sự thắng, và một điểm chuẩn trước-sau trên phần cứng đại diện thay vì một ước tính hy vọng. Với một doanh nghiệp trả tiền cho một đội máy tính toán lớn, một phân tích phần tuần tự trung thực biến thành chi tiêu phần cứng tiết kiệm hoặc lãng phí, và với một cơ quan chính phủ chịu trách nhiệm về chi phí của một hệ thống công, người đã ký duyệt thiết kế song song nên có thể cho thấy phép đo biện minh cho nó dưới kiểm toán.

Góc nhìn theo lĩnh vực

Công ty khởi nghiệp. Với một đội nhỏ và không còn nguồn vốn dư, hãy mua tính đúng đắn bằng cấu trúc, không phải bằng một chuyên gia đồng thời mà bạn không thể tuyển. Hãy với tới mặc định an toàn duy nhất mà ngôn ngữ của bạn cho, async/await cho công việc bị giới hạn bởi I/O, một nhiệm vụ sở hữu hoặc một tác tử cho bất kỳ trạng thái dùng chung nào, và bỏ qua hoàn toàn khóa chỉnh tay. Một cuộc chạy đua mất cập nhật trong đường thanh toán có thể đánh chìm bạn nhanh hơn một tính năng bị bỏ lỡ, vì vậy hãy chi một lượng nhỏ mã thêm để làm cho loại lỗi đó không thể xảy ra và đi tiếp.

Doanh nghiệp nhỏ. Bạn không có ai có công việc là đồng thời, nên hãy ưu tiên các nền tảng và dịch vụ được quản lý xử lý nó cho bạn: một giao dịch cơ sở dữ liệu, một hàng đợi lưu trữ, hoặc mô hình yêu cầu của một khung thắng các luồng mà bạn bảo trì bằng tay. Khi bạn đánh giá một công cụ, hãy coi “cái này có làm đồng thời an toàn theo mặc định không” là một câu hỏi mua hay xây, và ưu tiên lựa chọn nơi một cách xen kẽ sai không thể âm thầm làm hỏng bản ghi của khách hàng. Hãy giữ trạng thái khả biến dùng chung ra khỏi mã của chính bạn ở bất cứ nơi nào một dịch vụ được quản lý, có giới hạn có thể nắm giữ nó thay thế.

Doanh nghiệp lớn. Qua nhiều đội mục tiêu là một mặc định nhà giữ an toàn cho hàng nghìn người đóng góp: tính bất biến và truyền thông điệp làm chuẩn mực, mô hình cấp cao hơn khóa thô, hàng đợi và nhóm có giới hạn với áp suất ngược, và một thứ tự khóa toàn cục có tài liệu. Hãy mã hóa những điều này trong tiêu chuẩn kỹ thuật, thực thi chúng bằng bộ phát hiện chạy đua và kiểm thử căng thẳng trong CI, và quản trị các ngoại lệ nơi một trình lập hồ sơ biện minh mã không khóa để mỗi cái ở lại sau một ranh giới được kiểm thử, rà soát. Hãy quản lý năng lực đồng thời như mối quan tâm toàn đội máy với giới hạn hàng đợi và kích thước nhóm gắn với tải đo được.

Chính phủ. Tính đúng đắn và khả năng kiểm toán trong các hệ thống chạy hàng chục năm quan trọng hơn thông lượng thô. Hãy yêu cầu mọi chuyển tiếp trạng thái được ghi lại và có thể phát lại, để một cuộc chạy đua nghi ngờ có thể được tái hiện và bản sửa được chứng minh với cơ quan giám sát, và giữ các đường xác định không có AI cho các quyết định chạm đến trợ cấp, an toàn hoặc hồ sơ công. Mua sắm nên yêu cầu các nhà cung cấp tiết lộ mô hình đồng thời của họ và bằng chứng về độ bao phủ bộ phát hiện chạy đua và kiểm thử căng thẳng, vì một câu trả lời sai dưới tải trong một hệ thống công không phải sự bất tiện, nó là thứ một viên chức có trách nhiệm sau này phải giải thích.

Ví dụ

Công ty khởi nghiệp. Một đội nhỏ phát hành một tính năng thanh toán và nhận thấy số dư tài khoản thỉnh thoảng lệch vài xu dưới tải. Nguyên nhân là một phép đọc-sửa-ghi đơn giản trên trường số dư từ các bộ xử lý yêu cầu đồng thời, một cuộc chạy đua mất cập nhật. Thay vì rải khóa, họ chuyển số dư của mỗi tài khoản sau một nhiệm vụ sở hữu duy nhất xử lý ghi nợ và ghi có như các thông điệp, từng cái một. Độ lệch biến mất, mã trở nên dễ suy luận, và họ thêm một bài kiểm thử căng thẳng bắn hàng nghìn lần chuyển đồng thời để bảo vệ bản sửa. Một thay đổi cấu trúc, cả một loại lỗi được loại bỏ.

Doanh nghiệp lớn. Một dịch vụ đơn hàng thông lượng cao xử lý hàng chục nghìn yêu cầu mỗi giây chịu các đỉnh độ trễ định kỳ và thỉnh thoảng sập do hết bộ nhớ trong các đợt tăng lưu lượng. Điều tra tìm thấy một hàng đợi công việc không giới hạn sau một nhóm luồng lớn lên không giới hạn khi nhu cầu vượt năng lực. Đội giới hạn hàng đợi, đặt trần cho nhóm ở kích thước gắn với số lõi, và thêm áp suất ngược từ chối nhanh tải thừa với một lỗi rõ ràng. Thông lượng trở nên dễ đoán, các lần sập dừng, và một khóa nóng trên bộ nhớ đệm dùng chung được thay bằng cấu trúc không khóa chỉ sau khi một trình lập hồ sơ chứng minh tranh chấp là thật. Mặc định an toàn cho nhiều tác giả, đồng thời được tinh chỉnh chỉ ở nơi đo được.

Chính phủ. Một nền tảng trợ cấp quốc gia chạy hàng chục năm và phải tạo ra kết quả có thể kiểm toán, đúng đắn ngay cả dưới các cập nhật hồ sơ đồng thời. Đội chọn tính bất biến và truyền thông điệp làm mặc định nhà, giới hạn mọi mẩu trạng thái khả biến trong một chủ sở hữu duy nhất, và áp đặt một thứ tự khóa toàn cục ở bất cứ nơi nào khóa còn lại, tất cả được viết vào tiêu chuẩn kỹ thuật. Họ chạy bộ khử độc luồng và kiểm thử căng thẳng ngẫu nhiên trong đường ống, và thiết kế để mọi chuyển tiếp trạng thái được ghi lại và có thể phát lại cho cơ quan giám sát, cho phép họ tái hiện và chứng minh bản sửa khi một cách xen kẽ hiếm bị nghi ngờ. Tính đúng đắn và khả năng kiểm toán được coi là yêu cầu hạng nhất, không phải chuyện hiệu năng nghĩ đến sau.

Luận chứng kinh doanh: động lực, ROI và TCO

Lợi tức của đồng thời có kỷ luật thể hiện ở những sự cố không bao giờ xảy ra. Một cuộc chạy đua trên môi trường thực đơn lẻ có thể làm hỏng dữ liệu qua hàng nghìn bản ghi, và chi phí bao gồm cả giờ kỹ thuật để tìm một lỗi ẩn khi bị quan sát lẫn chi phí lớn hơn nhiều của việc đối chiếu dữ liệu xấu, thông báo người dùng bị ảnh hưởng và xây dựng lại niềm tin. Đây là một trong những khiếm khuyết đắt nhất để chẩn đoán chính vì chúng không xác định, nên đuổi theo một heisenbug có thể làm nhỏ bé công sức chọn một mô hình an toàn ngay từ đầu.

Lợi ích cũng thể hiện ở thông lượng và chi phí. Định cỡ đồng thời phù hợp cho phép một dịch vụ xử lý nhiều tải hơn trên cùng phần cứng, một khoản tiết kiệm lặp lại cho một đội máy lớn, trong khi áp suất ngược và hàng đợi có giới hạn ngăn các sự cố ngừng hoạt động dây chuyền biến một đợt tăng lưu lượng thành sự cố công khai. Tổng chi phí sở hữu khiêm tốn và chủ yếu mang tính văn hóa: bạn đầu tư vào một phong cách nhà (tính bất biến, truyền thông điệp, đồng thời có cấu trúc), vào công cụ (bộ phát hiện chạy đua, bộ khử độc luồng, bộ khung căng thẳng trong CI), và vào các tiêu chuẩn mã hóa thứ tự khóa và giới hạn. Phương án thay thế là một cơ sở mã nơi tính đúng đắn phụ thuộc vào việc mọi tác giả phải là chuyên gia mãi mãi, điều mà không đội đang lớn nào duy trì được. Hãy trình bày lập luận với ban lãnh đạo bằng đơn vị của họ: dịch một cuộc chạy đua được ngăn thành các sự cố hỏng dữ liệu được tránh, áp suất ngược thành các sự cố ngừng hoạt động được ngăn, và một mặc định an toàn thành thời gian hội nhập tiết kiệm.

Phản mẫu và cạm bẫy

  • Thêm luồng để tăng tốc cho công việc bị giới hạn bởi I/O. Nhiều luồng hơn trên một dịch vụ nặng về chờ đợi mua tranh chấp, không phải thông lượng.
  • Trạng thái khả biến dùng chung ở khắp nơi. Bất kỳ luồng nào thay đổi bất kỳ đối tượng nào khiến tính đúng đắn thành chuyện may rủi mà không người đánh giá nào xác minh được.
  • Hàng đợi và nhóm không giới hạn. Một đợt tăng lưu lượng làm hàng đợi lớn lên cho đến khi bộ nhớ chết; sự sập trông bí ẩn nhưng chỉ là quá tải đơn thuần.
  • Khóa tùy nghi không có thứ tự toàn cục. Khóa lấy theo thứ tự khác nhau khắp cơ sở mã bế tắc dưới tải.
  • Cho rằng mã chạy theo thứ tự đã viết. Phớt lờ mô hình bộ nhớ, nên một lỗi khả năng nhìn thấy để một luồng quay vòng trên một giá trị lỗi thời.
  • Sự khéo léo không khóa tự chế. Các lược đồ không khóa tùy chỉnh hầu như luôn sai một cách tinh vi và gần như không thể rà soát.
  • Chỉ kiểm thử cách xen kẽ suôn sẻ. Kiểm thử xác định đạt trong khi thứ tự một-trong-một-triệu làm hỏng môi trường thực.
  • Gọi mã lạ khi đang giữ khóa. Một callback chặn hoặc vào lại biến một vùng tới hạn thành bế tắc.

Mô hình trưởng thành

  • Mức 1, Khởi đầu: Đồng thời tùy nghi và phản ứng. Luồng và khóa được thêm bằng bản năng, trạng thái khả biến dùng chung ở khắp nơi, và hàng đợi không giới hạn. Điều kiện chạy đua nổi lên như các sự cố môi trường thực không tái hiện được mà không ai chẩn đoán được, và không có công cụ nào để bắt chúng.
  • Mức 2, Phát triển: Một số đội đã học các thực hành cơ bản: họ dùng khóa cẩn thận hơn và giới hạn các hàng đợi hiển nhiên nhất. Có nhận thức không chính thức về chạy đua và bế tắc, và một vài đường quan trọng được xem xét thêm. Thực hành không nhất quán giữa các đội, kiểm thử vẫn chủ yếu một cách xen kẽ, và các mẫu an toàn sống ở cá nhân thay vì trong văn bản.
  • Mức 3, Chuẩn hóa: Tổ chức có một phong cách nhà được lập thành tài liệu và thực thi toàn tổ chức: tính bất biến và truyền thông điệp làm mặc định, mô hình cấp cao hơn khóa thô, hàng đợi và nhóm có giới hạn với áp suất ngược, và một thứ tự khóa toàn cục có tài liệu. Bộ phát hiện chạy đua và kiểm thử căng thẳng chạy trong CI, và các lựa chọn đồng thời đi theo việc công việc bị giới hạn bởi I/O hay CPU.
  • Mức 4, Quản lý: Tổ chức đo và kiểm soát tư thế đồng thời của mình theo các đường cơ sở. Nó theo dõi độ bao phủ bộ phát hiện chạy đua và bộ khử độc luồng qua các dịch vụ, ghi lại độ sâu hàng đợi, thời gian chờ khóa, độ bão hòa nhóm và tỷ lệ từ chối như các chỉ số được giám sát, và kiểm thử tải đường cong suy giảm để mọi giới hạn là một quyết định năng lực có dữ liệu hậu thuẫn. Các sự cố đồng thời được đếm và theo xu hướng, phần tuần tự của các khối lượng công việc song song hóa được đo đối chiếu với mức tăng tốc thực sự đạt được, và các quyết định đi hay không đi về thiết kế mới dựa trên bằng chứng đó thay vì bản năng.
  • Mức 5, Điều phối: Đồng thời an toàn là con đường ít trở ngại nhất cho mọi tác giả, và thực hành được cải tiến liên tục và tích hợp trong toàn tổ chức. Cả các loại lỗi không thể xảy ra theo cấu trúc, các điểm nóng chỉ được điều chỉnh ở nơi lập hồ sơ chứng minh, và phát lại xác định làm cho lỗi còn lại hiếm tái hiện được. Tính đúng đắn và khả năng kiểm toán là các thuộc tính được bảo vệ liên tục, giới hạn năng lực thích ứng với tải quan sát được, và các tiêu chuẩn tiến hóa khi nền tảng và khối lượng công việc dịch chuyển.

Ý tưởng để thảo luận

  1. Nếu bạn kiểm toán dịch vụ bận rộn nhất của mình hôm nay, bao nhiêu trạng thái của nó được chia sẻ và khả biến, và bao nhiêu phần của sự chia sẻ đó thực sự cần thiết?
  2. Câu trả lời mặc định của đội bạn khi ai đó cần hai nhiệm vụ điều phối là gì, và bạn muốn nó là tính bất biến hay truyền thông điệp?
  3. Hàng đợi không giới hạn hay nhóm không có trần còn ẩn ở đâu trong hệ thống của bạn, và điều gì sẽ xảy ra với chúng dưới một đợt tăng lưu lượng đột ngột gấp mười lần?
  4. Các lần chạy tích hợp liên tục của bạn có bao gồm bộ phát hiện chạy đua hay bộ khử độc luồng không, và lần cuối một cái bắt được điều gì đó trước môi trường thực là khi nào?
  5. Với khối lượng công việc song song hóa nhiều nhất của bạn, phần tuần tự là bao nhiêu, và định luật Amdahl có giới hạn mức tăng tốc bạn thực sự đang theo đuổi không?
  6. Đội bạn có thể tái hiện một lỗi xen kẽ một-trong-một-triệu theo yêu cầu không, và cần gì để đạt đến đó?

Những điểm cần ghi nhớ

  • Đồng thời cấu trúc một chương trình thành các nhiệm vụ độc lập; song song thực thi chúng cùng lúc. Hãy quyết định bạn cần cái nào trước khi thêm luồng.
  • Trạng thái khả biến dùng chung là gốc rễ của gần như mọi lỗi đồng thời; hãy ưu tiên tính bất biến và truyền thông điệp làm mặc định an toàn cho nhiều tác giả.
  • Với tới các mô hình cấp cao (tác tử, kênh, đồng thời có cấu trúc, async/await) trước khóa viết tay, vốn đúng về lý thuyết và nguy hiểm trong thực hành.
  • Hiểu tính nguyên tử, khả năng nhìn thấy và mô hình bộ nhớ của bạn; dùng nguyên thủy đúng, giữ khóa ngắn, và áp đặt thứ tự khóa toàn cục để tránh bế tắc, livelock và chết đói.
  • Giới hạn mọi hàng đợi và nhóm và áp dụng áp suất ngược, để một đợt tăng đột biến suy giảm êm ái thay vì sập (chương 3.3).
  • Kiểm thử các cách xen kẽ một cách có chủ đích bằng bộ phát hiện chạy đua, kiểm thử căng thẳng và phát lại (chương 2.15), và tôn trọng định luật Amdahl khi song song hóa (chương 2.16).
  • Với doanh nghiệp đây là thông lượng và các sự cố được ngăn; với chính phủ đó là tính đúng đắn và khả năng kiểm toán trong các hệ thống tồn tại lâu.

Tài liệu tham khảo và đọc thêm

  • Brian Goetz và cộng sự, Java Concurrency in Practice (tính nguyên tử, khả năng nhìn thấy, mô hình bộ nhớ và xuất bản an toàn).
  • Herb Sutter, “The Free Lunch Is Over” (vì sao phần mềm phải đón nhận đồng thời khi tốc độ xung nhịp chững lại).
  • Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (thứ tự và nền tảng của suy luận đồng thời).
  • C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): mô hình CSP đằng sau các kênh.
  • Carl Hewitt, Peter Bishop và Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (nguồn gốc của mô hình tác tử).
  • Edsger W. Dijkstra, “Cooperating Sequential Processes” (semaphore, loại trừ lẫn nhau và vấn đề bế tắc).
  • Maurice Herlihy và Nir Shavit, The Art of Multiprocessor Programming (khóa, nguyên tử và cấu trúc dữ liệu không khóa).
  • Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (lập luận cho đồng thời có cấu trúc).
  • Martin Kleppmann, Designing Data-Intensive Applications (đồng thời và nhất quán nơi bộ nhớ gặp hệ thống phân tán).
  • Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): nguồn gốc của định luật Amdahl.