Bạn tìm kiếm gì khi gỡ lỗi bế tắc?


25

Gần đây tôi đã làm việc trên các dự án sử dụng nhiều luồng. Tôi nghĩ rằng tôi ổn khi thiết kế chúng; sử dụng thiết kế không trạng thái càng nhiều càng tốt, khóa truy cập vào tất cả các tài nguyên mà nhiều hơn một luồng cần, v.v. Kinh nghiệm của tôi về lập trình chức năng đã giúp ích rất nhiều.

Tuy nhiên, khi đọc mã chủ đề của người khác, tôi bị lẫn lộn. Tôi đang gỡ lỗi bế tắc ngay bây giờ và vì phong cách và thiết kế mã hóa khác với phong cách cá nhân của tôi, tôi gặp khó khăn khi thấy các điều kiện bế tắc tiềm ẩn.

Bạn tìm kiếm gì khi gỡ lỗi bế tắc?


Tôi đang hỏi điều này ở đây thay vì SO vì tôi muốn có nhiều gợi ý chung hơn về việc gỡ lỗi bế tắc, không phải là câu trả lời cụ thể cho vấn đề của tôi.
Michael K

Các chiến lược tôi có thể nghĩ đến là ghi nhật ký (như một số chiến lược khác đã chỉ ra), thực sự đang kiểm tra biểu đồ bế tắc của những người đang chờ đợi ai đó (xem stackoverflow.com/questions/3483094/ cho một số con trỏ) và chú thích khóa (xem clang.llvm.org/docs/ThreadSquilAnalysis.html ). Ngay cả khi đó không phải là mã của bạn, bạn có thể cố gắng thuyết phục tác giả thêm chú thích-- họ có thể sẽ tìm thấy lỗi và sửa chúng (có thể bao gồm cả mã của bạn) trong quy trình.
Don nở

Câu trả lời:


23

Nếu tình huống là một bế tắc thực sự (tức là hai luồng giữ hai khóa khác nhau, nhưng ít nhất một luồng muốn khóa một luồng khác) thì trước tiên bạn cần phải từ bỏ tất cả các khái niệm trước về cách khóa lệnh. Giả sử không có gì. Bạn có thể muốn xóa tất cả các nhận xét khỏi mã bạn đang xem, vì những nhận xét đó có thể khiến bạn tin vào điều gì đó không đúng. Thật khó để nhấn mạnh điều này đủ: giả sử không có gì.

Sau đó, xác định khóa nào được giữ trong khi luồng cố gắng khóa thứ khác. Nếu bạn có thể, đảm bảo rằng một chuỗi mở khóa theo thứ tự ngược từ khóa. Thậm chí tốt hơn, đảm bảo rằng một chủ đề chỉ giữ một khóa tại một thời điểm.

Làm việc chăm chỉ thông qua việc thực hiện một luồng và kiểm tra tất cả các sự kiện khóa. Tại mỗi khóa, xác định xem một luồng có giữ các khóa khác hay không, và nếu vậy, trong trường hợp nào thì một luồng khác, thực hiện một đường dẫn thực hiện tương tự, có thể xem xét sự kiện khóa đang xem xét.

Chắc chắn là bạn sẽ không tìm thấy vấn đề trước khi hết thời gian hoặc tiền bạc.


4
+1 Wow, điều đó thật bi quan ... mặc dù đó không phải là sự thật. Đó là một điều chắc chắn rằng bạn không thể tìm thấy tất cả các lỗi. Cảm ơn những lời đề nghị!
Michael K

Bruce, sự hợp tác của bạn về "bế tắc thực sự" là đáng ngạc nhiên đối với tôi. Tôi nghĩ rằng một bế tắc giữa hai luồng là khi mỗi luồng đang chờ một khóa mà khóa kia giữ. Định nghĩa của bạn dường như cũng bao gồm trường hợp một luồng, trong khi giữ một khóa, chờ đợi để có được khóa thứ hai hiện đang được giữ bởi một luồng khác. Điều đó không có vẻ như bế tắc đối với tôi; Là nó??
Don nở

@DonHatch - Tôi phras nó kém. Tình huống bạn mô tả không bế tắc. Tôi đã hy vọng truyền đạt sự lộn xộn của việc gỡ lỗi một tình huống bao gồm khóa giữ A, sau đó cố lấy khóa B, trong khi luồng giữ khóa B đang cố lấy khóa A. Có thể. Hoặc có thể tình hình phức tạp hơn rất nhiều. Bạn chỉ cần giữ một quan điểm rất cởi mở về thứ tự mua lại khóa. Kiểm tra tất cả các giả định. Không tin tưởng gì cả.
Bruce Ediger

+1 đề nghị đọc kỹ mã và kiểm tra tất cả các hoạt động khóa một cách cô lập. Dễ dàng hơn nhiều để xem xét một biểu đồ phức tạp bằng cách kiểm tra cẩn thận một nút đơn hơn là thử và xem toàn bộ mọi thứ cùng một lúc. Đã bao nhiêu lần tôi tìm thấy vấn đề chỉ bằng cách nhìn chằm chằm vào mã và chạy các kịch bản khác nhau trong đầu.
Newtopian

11
  1. Như những người khác đã nói ... nếu bạn có thể nhận được thông tin hữu ích để đăng nhập thì hãy thử trước vì đó là cách dễ nhất để làm.

  2. Xác định các ổ khóa có liên quan. Thay đổi tất cả các mutex / semaphores chờ mãi mãi để chờ đợi thời gian chờ đợi ... một cái gì đó vô lý dài như 5 phút. Đăng nhập lỗi khi nó hết thời gian. Điều này ít nhất sẽ chỉ cho bạn theo hướng của một trong những ổ khóa có liên quan đến vấn đề. Tùy thuộc vào sự thay đổi của thời gian, bạn có thể gặp may mắn và tìm thấy cả hai khóa sau một vài lần chạy. Sử dụng mã / điều kiện của mã lỗi chức năng để ghi lại dấu vết ngăn xếp giả sau khi chờ đợi theo thời gian không xác định được cách bạn đến đó ở vị trí đầu tiên. Điều này sẽ giúp bạn xác định chủ đề có liên quan đến vấn đề.

  3. Một điều khác mà bạn có thể thử là xây dựng thư viện trình bao quanh các dịch vụ mutex / semaphore của bạn. Theo dõi những chủ đề có mỗi mutex và những chủ đề đang chờ đợi trên mutex. Xây dựng một chủ đề màn hình để kiểm tra xem các chủ đề đã bị chặn bao lâu. Kích hoạt trên một số thời lượng hợp lý và kết xuất thông tin trạng thái mà bạn đang theo dõi.

Tại một số điểm, kiểm tra mã cũ đơn giản sẽ là cần thiết.


6

Bước đầu tiên (như Péter nói) là đăng nhập. Mặc dù theo kinh nghiệm của tôi, điều này thường có vấn đề. Trong xử lý song song nặng, điều này thường là không thể. Tôi đã phải gỡ lỗi một cái gì đó tương tự với mạng thần kinh một lần, nó đã xử lý 100k nút mỗi giây. Lỗi xảy ra chỉ sau vài giờ và thậm chí một dòng đầu ra đã làm mọi thứ chậm lại rất nhiều, đến mức phải mất nhiều ngày. Nếu đăng nhập là có thể, tập trung ít hơn vào dữ liệu, nhưng nhiều hơn vào dòng chảy của chương trình, cho đến khi bạn biết phần nào nó xảy ra. Chỉ cần một dòng đơn giản ở đầu mỗi hàm và nếu bạn có thể tìm đúng hàm, hãy chia nó thành các phần nhỏ hơn.

Một tùy chọn khác là loại bỏ các phần của mã và dữ liệu để bản địa hóa lỗi. Thậm chí có thể viết một số chương trình nhỏ chỉ cần một số lớp và chỉ chạy các bài kiểm tra cơ bản nhất (tất nhiên vẫn còn trong một số chủ đề). Xóa mọi thứ liên quan đến gui, ví dụ như bất kỳ đầu ra nào về trạng thái xử lý thực tế. (Tôi thấy giao diện người dùng thường xuyên là nguồn gây ra lỗi)

Trong mã của bạn, hãy cố gắng tuân theo luồng kiểm soát logic hoàn chỉnh giữa khởi tạo khóa và giải phóng nó. Một lỗi phổ biến có thể là khóa khi bắt đầu một chức năng, mở khóa ở cuối, nhưng có một câu lệnh trả về có điều kiện ở đâu đó ở giữa. Ngoại lệ có thể ngăn chặn phát hành quá.


"Trường hợp ngoại lệ có thể ngăn chặn việc phát hành" -> Tôi thương hại những ngôn ngữ không có biến số trong phạm vi: /
Matthieu M.

1
@Matthieu: Có các biến phạm vi và thực sự sử dụng chúng đúng cách có thể là hai điều khác nhau. Và ông đã yêu cầu các vấn đề có thể nói chung, mà không đề cập đến một ngôn ngữ cụ thể. Vì vậy, đây là một điều, có thể ảnh hưởng đến dòng kiểm soát.
thorsten müller

3

Những người bạn thân nhất của tôi đã được in / ghi nhật ký tại những nơi thú vị trong mã. Chúng thường giúp tôi hiểu rõ hơn những gì thực sự xảy ra bên trong ứng dụng, mà không làm gián đoạn thời gian giữa các luồng khác nhau, điều này có thể ngăn chặn việc tái tạo lỗi.

Nếu thất bại, phương pháp duy nhất còn lại của tôi là nhìn chằm chằm vào mã và cố gắng xây dựng một mô hình tinh thần của các luồng và tương tác khác nhau, và cố gắng nghĩ ra những cách điên rồ có thể để đạt được những gì rõ ràng đã xảy ra :-) Nhưng tôi không coi mình là một kẻ giết người bế tắc rất có kinh nghiệm. Hy vọng những người khác sẽ có thể đưa ra những ý tưởng tốt hơn, từ đó tôi cũng có thể học hỏi :-)


1
Tôi đã gỡ lỗi một vài ổ khóa chết như thế này ngày hôm nay. Thủ thuật là bọc pthread_mutex_lock () bằng macro in chức năng, số dòng, tên tệp và tên biến mutex (bằng cách mã hóa nó) trước và sau khi có được khóa. Làm tương tự cho pthread_mutex_unlock (). Khi tôi thấy rằng chủ đề của tôi bị đóng băng, tôi chỉ cần nhìn vào hai tin nhắn cuối cùng, có hai luồng cố gắng khóa nhưng không bao giờ hoàn thành nó! Bây giờ tất cả những gì còn lại là thêm một cơ chế để chuyển đổi điều này khi chạy. :-)
Plumator

3

Trước hết, hãy cố gắng để có được tác giả của mã đó. Anh ta có lẽ sẽ có ý tưởng về những gì anh ta đã viết. ngay cả khi hai bạn không thể xác định chính xác vấn đề chỉ bằng cách nói chuyện, Ít nhất bạn có thể ngồi lại với anh ấy để xác định phần bế tắc, sẽ nhanh hơn nhiều so với việc bạn hiểu mã của anh ấy / cô ấy mà không cần trợ giúp.

Thất bại, như Péter Török đã nói, Logging có lẽ là cách. Theo tôi biết, Debugger đã làm một công việc tồi tệ trên môi trường đa luồng. cố gắng xác định vị trí của khóa ở đâu, lấy toàn bộ tài nguyên đang chờ và trong điều kiện tình trạng đua xảy ra.


không, đăng nhập là kẻ thù của bạn ở đây - khi bạn đăng nhập chậm, bạn thay đổi hành vi của chương trình thành điểm dễ dàng để chương trình chạy hoàn hảo với tính năng ghi nhật ký, nhưng bế tắc khi tắt đăng nhập. Đây là loại vấn đề tương tự mà bạn gặp phải khi chạy một chương trình trên một CPU chứ không phải CPU đa lõi.
gbjbaanb

@gbjbaanb, tôi nghĩ rằng đó là kẻ thù của bạn quá khắc nghiệt. Có lẽ sẽ đúng khi nói đó là người bạn thân nhất của bạn, người thỉnh thoảng làm bạn thất vọng. Tôi sẽ đồng ý với một số người khác trên trang này nói rằng việc đăng nhập là bước đầu tiên tốt để thực hiện, sau khi kiểm tra mã đã thất bại - thường (thực tế là hầu hết thời gian, theo kinh nghiệm của tôi), một chiến lược ghi nhật ký đơn giản sẽ định vị vấn đề dễ dàng, và bạn đã hoàn thành. Mặt khác, tất cả đều có nghĩa là sử dụng các phương pháp khác, nhưng tôi không nghĩ rằng đó là lời khuyên tốt để tránh thử công cụ thường xuyên nhất cho công việc chỉ vì nó không hữu ích.
Don nở

0

Câu hỏi này thu hút tôi;) Trước hết, hãy xem xét bản thân bạn may mắn vì bạn có thể tái tạo vấn đề một cách nhất quán trên mỗi lần chạy. Nếu bạn nhận được cùng một ngoại lệ với cùng một stacktrace mỗi lần thì nó sẽ khá đơn giản. Nếu không, thì đừng tin vào stacktrace nhiều như vậy, thay vào đó chỉ giám sát truy cập vào các đối tượng toàn cầu và trạng thái của nó thay đổi trong quá trình thực thi.


0

Nếu bạn phải gỡ lỗi bế tắc, bạn đã gặp rắc rối. Theo quy định, sử dụng khóa trong thời gian ngắn nhất - hoặc hoàn toàn không, nếu có thể. Bất kỳ tình huống mà bạn mất một khóa và sau đó đi đến mã không tầm thường nên được tránh.

Tất nhiên nó phụ thuộc vào môi trường lập trình của bạn, nhưng bạn nên xem xét những thứ như hàng đợi tuần tự có thể cho phép bạn truy cập tài nguyên từ một luồng duy nhất.

Và sau đó, có một chiến lược cũ nhưng không thể sai lầm: Gán "cấp độ" cho mỗi khóa, bắt đầu từ cấp 0. Nếu bạn lấy khóa cấp 0, bạn không được phép sử dụng bất kỳ khóa nào khác. Sau khi thực hiện khóa cấp 1, bạn có thể thực hiện khóa cấp 0. Sau khi thực hiện khóa cấp 10, bạn có thể khóa ở cấp 9 hoặc thấp hơn, v.v.

Nếu bạn thấy điều này là không thể, bạn cần sửa mã của mình vì bạn sẽ gặp phải bế tắc.

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.