Trong nhóm mới mà tôi quản lý, phần lớn mã của chúng tôi là nền tảng, ổ cắm TCP và mã mạng http. Tất cả C ++. Hầu hết trong số đó có nguồn gốc từ các nhà phát triển khác đã rời khỏi nhóm. Các nhà phát triển hiện tại trong nhóm rất thông minh, nhưng chủ yếu là thiếu niên về kinh nghiệm.
Vấn đề lớn nhất của chúng tôi: lỗi đồng thời đa luồng. Hầu hết các thư viện lớp của chúng tôi được viết là không đồng bộ bằng cách sử dụng một số lớp nhóm luồng. Các phương thức trên các thư viện lớp thường ghi lại các bước chạy dài trên nhóm luồng từ một luồng và sau đó các phương thức gọi lại của lớp đó được gọi trên một luồng khác. Kết quả là, chúng ta có rất nhiều lỗi trường hợp cạnh liên quan đến các giả định luồng không chính xác. Điều này dẫn đến các lỗi tinh vi vượt ra ngoài việc chỉ có các phần quan trọng và khóa để bảo vệ chống lại các vấn đề tương tranh.
Điều làm cho những vấn đề này trở nên khó khăn hơn là những nỗ lực khắc phục thường không chính xác. Một số lỗi tôi đã quan sát thấy nhóm đang cố gắng (hoặc trong chính mã kế thừa) bao gồm một số điều như sau:
Lỗi phổ biến số 1 - Khắc phục sự cố đồng thời chỉ bằng cách khóa một dữ liệu được chia sẻ, nhưng quên đi những gì xảy ra khi các phương thức không được gọi theo thứ tự dự kiến. Đây là một ví dụ rất đơn giản:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Vì vậy, bây giờ chúng tôi có một lỗi trong đó Shutdown có thể được gọi trong khi OnHttpNetworkRequestComplete đang xảy ra. Một người kiểm tra tìm thấy lỗi, ghi lại bãi chứa sự cố và gán lỗi cho nhà phát triển. Anh ta lần lượt sửa lỗi như thế này.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Các sửa chữa ở trên có vẻ tốt cho đến khi bạn nhận ra có một trường hợp cạnh thậm chí tinh tế hơn. Điều gì xảy ra nếu Shutdown được gọi trước khi OnHttpRequestComplete được gọi lại? Các ví dụ trong thế giới thực mà nhóm của tôi thậm chí còn phức tạp hơn và các trường hợp cạnh thậm chí còn khó phát hiện hơn trong quá trình xem xét mã.
Lỗi thường gặp # 2 - khắc phục các sự cố bế tắc bằng cách thoát khỏi khóa một cách mù quáng, đợi luồng khác kết thúc, sau đó nhập lại khóa - nhưng không xử lý trường hợp đối tượng vừa được cập nhật bởi luồng khác!
Sai lầm phổ biến # 3 - Mặc dù các đối tượng được tính tham chiếu, trình tự tắt máy "giải phóng" con trỏ của nó. Nhưng quên chờ đợi chủ đề vẫn đang chạy để phát hành phiên bản của nó. Như vậy, các thành phần được tắt sạch, sau đó các cuộc gọi lại giả hoặc trễ được gọi trên một đối tượng trong trạng thái không mong đợi bất kỳ cuộc gọi nào nữa.
Có các trường hợp cạnh khác, nhưng điểm mấu chốt là đây:
Lập trình đa luồng chỉ đơn giản là khó, ngay cả đối với những người thông minh.
Khi tôi mắc phải những lỗi này, tôi dành thời gian thảo luận về các lỗi với từng nhà phát triển để phát triển một bản sửa lỗi phù hợp hơn. Nhưng tôi nghi ngờ họ thường nhầm lẫn về cách giải quyết từng vấn đề vì số lượng mã kế thừa khổng lồ mà bản sửa lỗi "đúng" sẽ liên quan đến việc chạm vào.
Chúng tôi sẽ sớm giao hàng và tôi chắc chắn rằng các bản vá mà chúng tôi đang áp dụng sẽ giữ cho bản phát hành sắp tới. Sau đó, chúng tôi sẽ có thời gian để cải thiện cơ sở mã và cấu trúc lại mã khi cần thiết. Chúng tôi sẽ không có thời gian để viết lại mọi thứ. Và phần lớn mã không tệ lắm. Nhưng tôi đang tìm cách cấu trúc lại mã sao cho các vấn đề luồng có thể tránh được hoàn toàn.
Một cách tiếp cận tôi đang xem xét là điều này. Đối với mỗi tính năng nền tảng quan trọng, có một luồng chuyên dụng trong đó tất cả các sự kiện và cuộc gọi lại mạng được sắp xếp theo thứ tự. Tương tự như luồng căn hộ COM trong Windows với việc sử dụng vòng lặp tin nhắn. Các hoạt động chặn dài vẫn có thể được gửi đến một luồng nhóm công việc, nhưng cuộc gọi lại hoàn thành được gọi trên luồng của thành phần. Các thành phần thậm chí có thể chia sẻ cùng một chủ đề. Sau đó, tất cả các thư viện lớp chạy bên trong luồng có thể được viết theo giả định của một thế giới luồng đơn.
Trước khi tôi đi vào con đường đó, tôi cũng rất quan tâm nếu có các kỹ thuật tiêu chuẩn hoặc các mẫu thiết kế khác để xử lý các vấn đề đa luồng. Và tôi phải nhấn mạnh - một cái gì đó vượt ra ngoài một cuốn sách mô tả những điều cơ bản của mutexes và semaphores. Bạn nghĩ sao?
Tôi cũng quan tâm đến bất kỳ phương pháp nào khác để tiến tới quá trình tái cấu trúc. Bao gồm bất kỳ điều nào sau đây:
Văn học hoặc giấy tờ về các mẫu thiết kế xung quanh chủ đề. Một cái gì đó vượt quá giới thiệu về mutexes và semaphores. Chúng ta cũng không cần song song lớn, chỉ là cách thiết kế mô hình đối tượng để xử lý các sự kiện không đồng bộ từ các luồng khác một cách chính xác .
Các cách để lập sơ đồ phân luồng của các thành phần khác nhau, do đó sẽ dễ dàng nghiên cứu và phát triển các giải pháp cho. (Đó là, một UML tương đương để thảo luận về các luồng trên các đối tượng và các lớp)
Giáo dục nhóm phát triển của bạn về các vấn đề với mã đa luồng.
Bạn sẽ làm gì?