You are on page 1of 25

Eraser: Một công cụ phát hiện cuộc đua dữ liệu

động cho các chương trình đa luồng


Lập trình đa luồng rất khó và dễ xảy ra lỗi. Rất dễ mắc lỗi trong quá trình đồng bộ
hóa tạo ra một race data, tuy nhiên có thể cực kỳ khó tìm ra lỗi này trong quá trình
gỡ lỗi. Bài viết này mô tả một công cụ mới, được gọi là Eraser, để phát hiện động
các chủng tộc dữ liệu trong các chương trình đa luồng dựa trên khóa. Eraser sử
dụng kỹ thuật viết lại nhị phân để giám sát mọi tham chiếu bộ nhớ dùng chung và
xác minh rằng hành vi khóa nhất quán được quan sát. Chúng tôi trình bày một số
nghiên cứu điển hình, bao gồm các môn học đại học và công cụ tìm kiếm Web đa
luồng, chứng minh tính hiệu quả của phương pháp này.
1. INTRODUCTION
Đa luồng đã trở thành một kỹ thuật lập trình phổ biến. Hầu hết các hệ
điều hành thương mại hỗ trợ luồng và các ứng dụng phổ biến như Microsoft
Word và Netscape Navigator là đa luồng.
Thật không may, việc gỡ lỗi một chương trình đa luồng có thể khó khăn.
Các lỗi đơn giản trong quá trình đồng bộ hóa có thể tạo ra các races data phụ
thuộc vào thời gian có thể mất hàng tuần hoặc hàng tháng để theo dõi. Vì lý
do này, nhiều lập trình viên đã chống lại việc sử dụng các luồng. Những khó
khăn với việc sử dụng các luồng đã được John Ousterhout tóm tắt.
Trong bài viết này, chúng tôi mô tả một công cụ, được gọi là Eraser, tự
động phát hiện các race data trong các chương trình đa luồng. Chúng tôi đã
triển khai Eraser cho Digital Unix và sử dụng nó để phát hiện các chủng tộc
dữ liệu trong một số chương trình, từ công cụ tìm kiếm Web AltaVista đến
các bài tập lập trình giới thiệu do sinh viên chưa tốt nghiệp viết.
Công việc trước đây trong phát hiện dynamic races dựa trên mối quan hệ
xảy ra trước khi xảy ra của Lamport [Lamport 1978] và kiểm tra xem các
truy cập bộ nhớ xung đột từ các luồng khác nhau có được phân tách bằng
các sự kiện đồng bộ hóa hay không. Trước khi các thuật toán xử lý nhiều
kiểu đồng bộ hóa, nhưng tính tổng quát này phải trả giá. Chúng tôi đã nhắm
mục tiêu Eraser đặc biệt vào đồng bộ hóa dựa trên khóa được sử dụng trong
các chương trình đa luồng hiện đại. Eraser chỉ cần kiểm tra xem tất cả các
quyền truy cập bộ nhớ dùng chung có tuân theo một nguyên tắc khóa nhất
quán hay không. A locking discipline là một chính sách lập trình đảm bảo sự
vắng mặt của các race data. Ví dụ, một kỷ luật khóa đơn giản là yêu cầu mọi
biến được chia sẻ giữa các luồng được bảo vệ bằng một khóa loại trừ lẫn
nhau. Chúng tôi sẽ tranh luận rằng đối với nhiều chương trình, phương pháp
thực thi kỷ luật khóa của Eraser đơn giản hơn, hiệu quả hơn và triệt để hơn
trong việc bắt các race so với phương pháp dựa trên sự kiện xảy ra trước đó.
Theo những gì chúng tôi biết, Eraser là công cụ phát hiện dynamic race đầu
tiên được áp dụng cho các máy chủ sản xuất đa luồng.
Phần còn lại của bài viết này được tổ chức như sau. Sau khi xem xét cuộc
đua dữ liệu là gì và mô tả công việc trước đây trong việc phát hiện cuộc đua,
chúng tôi trình bày thuật toán Lockset được Eraser sử dụng, đầu tiên ở cấp
độ cao và sau đó ở cấp độ đủ thấp để tiết lộ các kỹ thuật triển khai quan
trọng về hiệu suất chính. Cuối cùng, chúng tôi mô tả trải nghiệm chúng tôi
đã có khi sử dụng Eraser với một số chương trình đa luồng.
Eraser không có mối quan hệ nào với công cụ có cùng tên do Mellor-
Crummey [1993] xây dựng để phát hiện các race data trong các chương trình
Fortran song song bộ nhớ dùng chung như một phần của Môi trường Lập
trình ParaScope.
1.1Definitions
Khóa là một đối tượng đồng bộ hóa đơn giản được sử dụng để loại trừ lẫn
nhau; nó có sẵn hoặc thuộc sở hữu của một chủ đề. Các thao tác trên
khóa mu là lock (mu) và unlock (mu). Vì vậy, về cơ bản nó là một
semaphore nhị phân được sử dụng để loại trừ lẫn nhau, nhưng khác với
semaphore ở chỗ chỉ chủ sở hữu của một khóa mới được phép giải phóng
nó.
Một cuộc chạy đua dữ liệu xảy ra khi hai luồng đồng thời truy cập vào
một biến được chia sẻ và khi
—Có ít nhất một quyền truy cập là viết và
—Các luồng không sử dụng cơ chế rõ ràng nào để ngăn các truy cập
đồng thời.
Nếu một chương trình có khả năng chạy đua dữ liệu, thì ảnh hưởng
của các truy cập xung đột tới biến được chia sẻ sẽ phụ thuộc vào sự xen
kẽ của các lần thực thi luồng. Mặc dù các lập trình viên đôi khi cố tình
cho phép một cuộc chạy đua dữ liệu khi thuyết không xác định dường
như vô hại, nhưng thông thường cuộc chạy đua dữ liệu tiềm ẩn là một lỗi
nghiêm trọng do không đồng bộ hóa đúng cách.
1.2Công việc liên quan
Một nỗ lực ban đầu để tránh các cuộc đua dữ liệu là khái niệm tiên phong
về monitors, được đưa ra bởi Hoare [1974]. Màn hình là một nhóm các
biến được chia sẻ cùng với các thủ tục được phép truy cập chúng, tất cả
được đóng gói cùng với một khóa ẩn danh duy nhất được tự động thu
thập và giải phóng khi nhập và xuất thủ tục. Các biến được chia sẻ trong
monitor nằm ngoài phạm vi (tức là không nhìn thấy) bên ngoài monitor;
do đó, chúng chỉ có thể được truy cập từ trong các quy trình của monitor,
nơi khóa được giữ. Do đó, các monitors cung cấp một đảm bảo tĩnh, thời
gian biên dịch rằng các truy cập vào các biến được chia sẻ được tuần tự
hóa và do đó không bị chạy đua dữ liệu. Monitors là một cách hiệu quả
để tránh các cuộc chạy đua dữ liệu nếu tất cả các biến được chia sẻ đều là
các khối cầu tĩnh, nhưng chúng không bảo vệ khỏi các cuộc chạy đua dữ
liệu trong các chương trình có các biến chia sẻ được phân bổ động, một
hạn chế mà người dùng ban đầu nhận thấy là đáng kể [Lampson và
Redell 1980]. Bằng cách thay thế kiểm tra động cho kiểm tra tĩnh, công
việc của chúng tôi nhằm mục đích cho phép dữ liệu được chia sẻ được
phân bổ động trong khi vẫn giữ được mức độ an toàn của monitors càng
nhiều càng tốt.
Một số nỗ lực đã được thực hiện để tạo ra các hệ thống phát hiện race
hoàn toàn tĩnh (tức là thời gian biên dịch) hoạt động với sự hiện diện của
dữ liệu được chia sẻ được phân bổ động: ví dụ: dây khóa của Sun
[SunSoft 1994] và Bộ kiểm tra tĩnh mở rộng cho Modula-3 [ Detlefs và
cộng sự. 1997] .2 Nhưng những cách tiếp cận này có vẻ có vấn đề, vì
chúng yêu cầu lý luận tĩnh về ngữ nghĩa của chương trình.
Hầu hết các công việc trước đây trong việc phát hiện dynamic races đã
được thực hiện bởi cộng đồng lập trình song song khoa học [Dinning và
Schonberg 1990; Mellor-Crummey 1991; Netzer năm 1991; Perkovic và
Keleher 1996] và dựa trên mối quan hệ xảy ra trước khi xảy ra của
Lamport, mà bây giờ chúng ta mô tả.
Thứ tự xảy ra trước là thứ tự từng phần trên tất cả các sự kiện của tất
cả các luồng trong một thực thi đồng thời. Trong bất kỳ chuỗi đơn lẻ nào,
các sự kiện được sắp xếp theo thứ tự mà chúng đã xảy ra. Giữa các luồng,
các sự kiện được sắp xếp theo thuộc tính của các đối tượng đồng bộ mà
chúng truy cập. Nếu một luồng truy cập vào một đối tượng đồng bộ hóa
và lần truy cập tiếp theo vào đối tượng là bởi một luồng khác, thì lần truy
cập đầu tiên được xác định sẽ xảy ra trước luồng thứ hai nếu ngữ nghĩa
của đối tượng đồng bộ hóa cấm một lịch trình trong đó hai tương tác này
được trao đổi trong thời gian. Ví dụ, Hình 1 cho thấy một thứ tự có thể có
của hai luồng thực thi cùng một đoạn mã. Ba câu lệnh chương trình được
thực thi bởi Luồng 1 được sắp xếp theo thứ tự xảy ra trước đó vì chúng
được thực thi tuần tự trong cùng một luồng. Khóa mu của Chủ đề 2 được
đặt hàng trước với việc mở khóa mu bởi Chủ đề 1 vì không thể có được ổ
khóa trước khi chủ sở hữu trước của nó phát hành. Cuối cùng, ba câu
lệnh được thực thi bởi Luồng 2 được sắp xếp theo thứ tự xảy ra trước đó
vì chúng được thực thi tuần tự trong luồng đó.
Nếu cả hai luồng đều truy cập vào một biến được chia sẻ và các truy
cập không được sắp xếp theo quan hệ xảy ra trước, thì trong một lần thực
thi chương trình khác, trong đó luồng chậm hơn chạy nhanh hơn và /
hoặc luồng nhanh hơn chạy chậm hơn, hai quyền truy cập có thể có xảy
ra đồng thời; nghĩa là, một cuộc chạy đua dữ liệu có thể đã xảy ra, cho dù
nó có thực sự xảy ra hay không. Tất cả các công cụ phát hiện dynamic
races trước đây mà chúng ta biết đều dựa trên quan sát này. Các trình
phát hiện cuộc đua này giám sát mọi tham chiếu dữ liệu và hoạt động
đồng bộ hóa và kiểm tra các truy cập xung đột vào các biến được chia sẻ
không liên quan đến mối quan hệ xảy ra trước đối với việc thực thi cụ thể
mà chúng đang theo dõi.
Thật không may, các công cụ dựa trên việc xảy ra trước đó có hai
nhược điểm đáng kể. Đầu tiên, chúng khó triển khai hiệu quả vì chúng
yêu cầu thông tin trên mỗi luồng về các truy cập đồng thời đến từng vị trí
bộ nhớ dùng chung. Quan trọng hơn, hiệu quả của các công cụ dựa trên
tình huống xảy ra trước đó phụ thuộc nhiều vào sự đan xen được tạo ra
bởi bộ lập lịch.
Hình 2 cho thấy một ví dụ đơn giản trong đó cách tiếp cận xảy ra
trước có thể bỏ lỡ một cuộc đua dữ liệu. Mặc dù có một cuộc chạy đua dữ
liệu tiềm ẩn trên các truy cập không được bảo vệ tới y, nó sẽ không được
phát hiện trong quá trình thực thi được hiển thị trong hình, bởi vì Luồng
1 giữ khóa trước Luồng 2, và do đó, các truy cập vào y được sắp xếp theo
thứ tự xen kẽ này do xảy ra. -trước. Một công cụ dựa trên wouldbefore sẽ
chỉ phát hiện ra lỗi nếu bộ lập lịch trình tạo ra một sự xen kẽ trong đó
đoạn mã cho Thread 2 xảy ra trước đoạn mã cho Thread 1. Do đó, để có
hiệu quả, một bộ phát hiện race dựa trên xảy ra trước đó cần một số
lượng các trường hợp kiểm thử để kiểm tra nhiều khả năng đan xen.
Ngược lại, lỗi lập trình trong Hình 2 sẽ được phát hiện bởi Eraser với bất
kỳ trường hợp kiểm thử nào thực hiện hai đường dẫn mã, bởi vì các
đường dẫn vi phạm kỷ luật khóa đối với y bất kể sự xen kẽ được tạo ra
bởi bộ lập lịch. Mặc dù Eraser là một công cụ kiểm tra và do đó không
thể đảm bảo rằng một chương trình không có race , nhưng nó có thể phát
hiện ra nhiều race hơn so với các công cụ dựa trên sự kiện xảy ra trước
đó.
Kỹ thuật bao phủ khóa của Dinning và Schonberg là một cải tiến đối
với phương pháp tiếp cận xảy ra trước cho các chương trình sử dụng
nhiều khóa [Dinning và Schonberg 1991]. Thật vậy, một cách để mô tả
cách tiếp cận của chúng tôi là chúng tôi mở rộng sự cải tiến của Dinning
và Schonberg và loại bỏ bộ máy cơ bản đã xảy ra trước đó mà chúng
đang cải tiến.
2. THUẬT TOÁN VỊ TRÍ
Trong phần này, chúng tôi mô tả cách thuật toán Lockset phát hiện các race.
Thảo luận ở mức khá cao; các kỹ thuật được sử dụng để triển khai thuật toán
một cách hiệu quả sẽ được mô tả trong phần sau.
Phiên bản đầu tiên và đơn giản nhất của thuật toán Lockset thực thi kỷ
luật khóa đơn giản rằng mọi biến được chia sẻ đều được bảo vệ bởi một số
khóa, theo nghĩa là khóa được giữ bởi bất kỳ luồng nào bất cứ khi nào nó
truy cập vào biến. Eraser kiểm tra xem chương trình có tôn trọng kỷ luật này
hay không bằng cách giám sát tất cả các lần đọc và ghi khi chương trình
thực thi. Vì Eraser không có cách nào biết được ổ khóa nào được dùng để
bảo vệ biến nào, nên nó phải suy ra mối quan hệ bảo vệ từ lịch sử thực thi.
Đối với mỗi biến được chia sẻ v, Eraser duy trì tập C (v) các khóa ứng
viên cho v. Tập hợp này chứa các khóa đã bảo vệ v cho tính toán cho đến
nay. Nghĩa là, một khóa l nằm trong C (v) nếu, trong tính toán cho đến thời
điểm đó, mọi luồng đã truy cập v đang giữ l tại thời điểm truy cập. Khi một
biến mới v được khởi tạo, tập ứng cử viên C (v) của nó được coi là chứa tất
cả các khóa có thể có. Khi biến được truy cập, Eraser cập nhật C (v) với giao
điểm của C (v) và tập hợp các khóa được giữ bởi luồng hiện tại. Quá trình
này, được gọi là tinh chỉnh ổ khóa, đảm bảo rằng bất kỳ khóa nào bảo vệ v
nhất quán đều được chứa trong C (v). Nếu một số khóa l luôn bảo vệ v, nó sẽ
vẫn ở trong C (v) vì C (v) được tinh chế. Nếu C (v) trở nên trống, điều này
cho thấy rằng không có khóa nào bảo vệ v.
Tóm lại, đây là phiên bản đầu tiên của thuật toán Lockset:
Hình 3 minh họa cách một cuộc đua dữ liệu tiềm năng được phát hiện
thông qua tinh chỉnh ổ khóa. Cột bên trái chứa các câu lệnh của chương
trình, được thực hiện theo thứ tự từ trên xuống dưới. Cột bên phải phản ánh
tập hợp các khóa ứng viên, C (v), sau mỗi câu lệnh được thực thi. Ví dụ này
có hai khóa, vì vậy C (v) bắt đầu chứa cả hai khóa. Sau khi v được truy cập
trong khi giữ mu1, C (v) được tinh chỉnh để chứa khóa đó. Sau đó, v được
truy cập lại, chỉ có mu2 được giữ lại. Giao điểm của các tập singleton {mu1}
và {mu2} là tập rỗng, chỉ ra một cách chính xác rằng không có khóa nào bảo
vệ v.
2.1Cải thiện kỷ luật khóa
Kỷ luật khóa đơn giản mà chúng tôi sử dụng cho đến nay là quá nghiêm
ngặt. Có ba phương pháp lập trình rất phổ biến vi phạm kỷ luật, nhưng
không có bất kỳ cuộc đua dữ liệu nào:
— Khởi tạo: Các biến được chia sẻ thường xuyên được khởi tạo mà
không cần giữ khóa.
—Dữ liệu được chia sẻ đã đọc: Một số biến được chia sẻ chỉ được ghi
trong quá trình khởi tạo và chỉ đọc sau đó. Chúng có thể được truy cập
một cách an toàn mà không cần khóa.
—Khóa đọc-ghi: Khóa đọc-ghi cho phép nhiều người đọc truy cập vào
một biến được chia sẻ, nhưng chỉ cho phép một người duy nhất có thể
ghi.
Trong phần còn lại của phần này, chúng tôi sẽ mở rộng thuật toán
Lockset để phù hợp với việc khởi tạo và dữ liệu được chia sẻ đọc, và sau
đó mở rộng nó hơn nữa để phù hợp với các khóa đọc-ghi.
2.2Khởi tạo và Đọc-Chia sẻ
Không cần một luồng để khóa những người khác nếu không có luồng nào
khác có thể giữ một tham chiếu đến dữ liệu đang được truy cập. Các lập
trình viên thường tận dụng sự quan sát này khi khởi tạo dữ liệu mới được
cấp phát. Để tránh các cảnh báo sai do các lần ghi khởi tạo mở khóa này
gây ra, chúng tôi trì hoãn việc sàng lọc nhóm ứng cử viên của vị trí cho
đến khi nó được khởi chạy. Thật không may, chúng tôi không có cách
nào dễ dàng để biết khi nào quá trình khởi tạo hoàn tất. Do đó, Eraser coi
một biến chia sẻ được khởi tạo khi nó được truy cập lần đầu tiên bởi một
luồng thứ hai. Miễn là một biến chỉ được truy cập bởi một luồng duy
nhất, việc đọc và ghi sẽ không ảnh hưởng đến tập ứng viên.
Vì việc đọc đồng thời một biến được chia sẻ bởi nhiều luồng không
phải là chạy đua, nên cũng không cần phải bảo vệ một biến nếu nó ở chế
độ chỉ đọc. Để hỗ trợ chia sẻ đọc đã mở khóa cho những dữ liệu như vậy,
chúng tôi báo cáo các cuộc đua chỉ sau khi một biến khởi tạo đã được
nhiều hơn một chuỗi chia sẻ ghi.

Hình 4 minh họa các chuyển đổi trạng thái kiểm soát thời điểm tinh chỉnh
ổ khóa xảy ra và khi các cuộc đua được báo cáo. Khi một biến được cấp
phát lần đầu, nó được đặt ở trạng thái Virgin, cho biết rằng dữ liệu là mới
và chưa được tham chiếu bởi bất kỳ luồng nào. Khi dữ liệu được truy
cập, nó sẽ chuyển sang trạng thái Exclusive, biểu thị rằng nó đã được truy
cập, nhưng chỉ bởi một luồng. Trong trạng thái này, các lần đọc và ghi
tiếp theo bởi cùng một luồng không thay đổi trạng thái của biến và không
cập nhật C (v). Điều này giải quyết vấn đề khởi tạo, vì luồng đầu tiên có
thể khởi tạo biến mà không khiến C (v) được tinh chỉnh. Khi nào và nếu
một luồng khác truy cập vào biến, thì trạng thái sẽ thay đổi. Quyền truy
cập đã đọc thay đổi trạng thái thành Được chia sẻ. Ở trạng thái Chia sẻ, C
(v) được cập nhật, nhưng các cuộc đua dữ liệu không được báo cáo, ngay
cả khi C (v) trở nên trống. Điều này giải quyết vấn đề dữ liệu được chia
sẻ đã đọc, vì nhiều luồng có thể đọc một biến mà không gây ra một cuộc
đua được báo cáo. Truy cập ghi từ một luồng mới sẽ thay đổi trạng thái
từ Độc quyền hoặc Được chia sẻ sang trạng thái Chia sẻ-Sửa đổi, trong
đó C (v) được cập nhật và các cuộc đua được báo cáo, giống như được
mô tả trong phiên bản ban đầu, đơn giản của thuật toán.
Sự hỗ trợ của chúng tôi cho việc khởi chạy khiến việc kiểm tra của
Eraser phụ thuộc nhiều hơn vào công cụ lập lịch so với những gì chúng
tôi muốn. Giả sử rằng một luồng cấp phát và khởi tạo một biến được chia
sẻ mà không có khóa và làm cho biến đó có thể truy cập nhầm vào luồng
thứ hai trước khi nó hoàn tất quá trình khởi tạo. Sau đó, Eraser sẽ phát
hiện ra lỗi nếu bất kỳ truy cập nào của luồng thứ hai xảy ra trước các
hành động khởi tạo cuối cùng của luồng đầu tiên, nhưng nếu không thì
Eraser sẽ bỏ sót lỗi. Chúng tôi không nghĩ rằng đây là một vấn đề, nhưng
chúng tôi không có cách nào để biết chắc chắn.
2.3Read-Write Locks
Nhiều chương trình sử dụng khóa một đầu đọc, nhiều đầu đọc cũng như
khóa đơn giản. Để phù hợp với phong cách này, chúng tôi giới thiệu lần
cải tiến cuối cùng của chúng tôi về kỷ luật khóa: chúng tôi yêu cầu rằng
đối với mỗi biến v, một số khóa m bảo vệ v, nghĩa là m được giữ ở chế
độ ghi cho mỗi lần ghi v và m được giữ ở một số chế độ (đọc hoặc viết)
cho mỗi lần đọc v.
Chúng tôi tiếp tục sử dụng các chuyển đổi trạng thái của Hình 4,
nhưng khi biến chuyển sang trạng thái Chia sẻ-Sửa đổi, việc kiểm tra hơi
khác một chút:
Có nghĩa là, các khóa được giữ hoàn toàn ở chế độ đọc sẽ bị loại bỏ khỏi
tập ứng cử viên khi ghi xảy ra, vì các khóa được giữ bởi người viết không
bảo vệ chống lại cuộc chạy đua dữ liệu giữa người viết và một số chuỗi
trình đọc khác.
3. THỰC HIỆN LỖI
Eraser được triển khai cho hệ điều hành Digital Unix trên bộ xử lý Alpha, sử
dụng hệ thống sửa đổi nhị phân ATOM [Srivastava và Eustace 1994]. Eraser
lấy một chương trình nhị phân chưa sửa đổi làm đầu vào và thêm thiết bị đo
đạc để tạo ra một nhị phân mới giống hệt nhau về mặt chức năng, nhưng bao
gồm các lệnh gọi đến thời gian chạy Eraser để triển khai thuật toán Lockset
Để duy trì C (v), Eraser công cụ mỗi lần tải và lưu trữ trong chương trình.
Để duy trì lock_held(t) cho mỗi luồng t, Eraser công cụ mỗi lệnh gọi để lấy
hoặc giải phóng một khóa, cũng như các sơ khai quản lý việc khởi tạo và
hoàn thiện luồng. Để khởi tạo C (v) cho dữ liệu được cấp phát động, Eraser
công cụ mỗi cuộc gọi đến bộ cấp phát lưu trữ.
Eraser coi mỗi từ 32 bit trong dữ liệu heap hoặc toàn cục là một biến có
thể được chia sẻ, vì trên nền tảng của chúng tôi, từ 32 bit là đơn vị kết hợp
bộ nhớ nhỏ nhất. Eraser không tải và lưu trữ thiết bị có chế độ địa chỉ gián
tiếp ra khỏi con trỏ ngăn xếp, vì chúng được giả định là các tham chiếu ngăn
xếp và các biến được chia sẻ được giả định là ở các vị trí toàn cục hoặc trong
heap. Eraser sẽ duy trì các tập ứng cử viên cho các vị trí ngăn xếp được truy
cập thông qua các thanh ghi không phải là con trỏ ngăn xếp, nhưng đây là
một phần tạo tác của việc triển khai chứ không phải là một kế hoạch có chủ
ý để hỗ trợ các chương trình chia sẻ vị trí ngăn xếp giữa các luồng.
Khi một cuộc đua được báo cáo, Eraser cho biết tệp và số dòng tại đó nó
được phát hiện và danh sách dấu vết của tất cả các khung ngăn xếp đang
hoạt động. Báo cáo cũng bao gồm ID luồng, địa chỉ bộ nhớ, kiểu truy cập bộ
nhớ và các giá trị thanh ghi quan trọng như bộ đếm chương trình và con trỏ
ngăn xếp. Khi được sử dụng cùng với mã nguồn của chương trình, chúng tôi
nhận thấy rằng thông tin này thường đủ để xác định nguồn gốc của cuộc đua.
Nếu nguyên nhân của một cuộc đua vẫn chưa rõ ràng, người dùng có thể chỉ
đạo Eraser ghi lại tất cả các quyền truy cập vào một biến cụ thể dẫn đến thay
đổi bộ khóa ứng viên của nó.
3.1Đại diện cho các bộ khóa ứng viên
Việc triển khai các bộ khóa một cách ngây thơ sẽ lưu trữ một danh sách
các khóa ứng viên cho mỗi vị trí bộ nhớ, có khả năng tiêu tốn nhiều lần
bộ nhớ được cấp phát của chương trình. Chúng ta có thể tránh được
khoản chi phí này bằng cách khai thác một thực tế may mắn là số lượng
các bộ khóa khác nhau được quan sát trong thực tế là khá ít. Trên thực tế,
chúng tôi chưa bao giờ quan sát thấy hơn 10.000 bộ khóa khác nhau xuất
hiện trong bất kỳ quá trình thực thi nào của thuật toán giám sát Lockset.
Do đó, chúng tôi biểu diễn mỗi tập khóa bằng một số nguyên nhỏ, một
chỉ mục ổ khóa vào một bảng mà các mục nhập của nó đại diện cho tập
hợp khóa dưới dạng vectơ được sắp xếp của địa chỉ khóa. Các mục nhập
trong bảng không bao giờ được phân bổ hoặc sửa đổi thỏa thuận, vì vậy
mỗi chỉ mục ổ khóa vẫn có giá trị trong suốt thời gian của chương trình.
Các chỉ mục ổ khóa mới được tạo ra do kết quả của việc mua lại khóa,
phát hành khóa hoặc thông qua ứng dụng của hoạt động giao nhau. Để
đảm bảo rằng mỗi chỉ mục ổ khóa đại diện cho một bộ khóa duy nhất,
chúng tôi duy trì một bảng băm của các vectơ khóa hoàn chỉnh được tìm
kiếm trước khi một chỉ mục ổ khóa mới được tạo. Eraser cũng lưu trữ kết
quả của mỗi giao điểm, do đó, trường hợp nhanh cho giao điểm được
thiết lập chỉ đơn giản là tra cứu bảng. Mỗi vectơ khóa trong bảng được
sắp xếp, do đó khi bộ nhớ cache bị lỗi, trường hợp chậm của hoạt động
giao nhau có thể được thực hiện bằng cách so sánh đơn giản giữa hai
vectơ đã sắp xếp.
Đối với mỗi từ 32 bit trong phân đoạn dữ liệu và heap, có một từ bóng
tương ứng được sử dụng để chứa chỉ mục ổ khóa 30 bit và điều kiện
trạng thái 2 bit. Ở trạng thái Exclusive, 30 bit không được sử dụng để lưu
trữ chỉ mục ổ khóa mà được sử dụng để lưu trữ ID của luồng có quyền
truy cập độc quyền.
Tất cả các quy trình cấp phát bộ nhớ tiêu chuẩn đều được thiết kế để
cấp phát và khởi tạo một từ bóng cho mỗi từ được chương trình cấp phát.
Khi một chuỗi truy cập vào vị trí bộ nhớ, Eraser sẽ tìm từ bóng bằng cách
thêm một vị trí cố định vào địa chỉ của vị trí. Hình 5 minh họa cách bộ
nhớ bóng và biểu diễn chỉ mục ổ khóa được sử dụng để liên kết mỗi biến
được chia sẻ với một tập hợp các khóa ứng viên tương ứng.
3.2Performance
Hiệu suất không phải là mục tiêu chính trong việc triển khai Eraser của
chúng tôi và do đó, có nhiều cơ hội để tối ưu hóa. Các ứng dụng thường
chậm đi từ 10 đến 30 trong khi sử dụng Eraser. Sự giãn nở thời gian này
có thể thay đổi thứ tự các luồng được lên lịch và có thể ảnh hưởng đến
hoạt động của các ứng dụng nhạy cảm với thời gian. Kinh nghiệm của
chúng tôi cho thấy rằng sự khác biệt trong lập lịch chuỗi ít ảnh hưởng
đến kết quả của Eraser. Chúng tôi có ít kinh nghiệm hơn với các ứng
dụng rất nhạy cảm về thời gian và có thể chúng sẽ được hưởng lợi từ kỹ
thuật giám sát hiệu quả hơn.
Chúng tôi ước tính rằng một nửa sự chậm lại trong quá trình triển khai
hiện tại là do chi phí thực hiện lệnh gọi thủ tục ở mỗi lần tải và lệnh lưu
trữ. Chi phí này có thể được loại bỏ bằng cách sử dụng phiên bản ATOM
có thể mã giám sát nội tuyến [Scales et al. Năm 1996]. Ngoài ra, có nhiều
cơ hội để sử dụng phân tích tĩnh để giảm chi phí của mã giám sát; nhưng
chúng tôi đã không khám phá chúng.
Mặc dù điều chỉnh hiệu suất hạn chế của chúng tôi, chúng tôi nhận
thấy rằng Eraser đủ nhanh để gỡ lỗi hầu hết các chương trình và do đó
đáp ứng tiêu chí hiệu suất thiết yếu nhất.
3.3Chú thích chương trình
Đúng như dự đoán, kinh nghiệm của chúng tôi với Eraser cho thấy nó có
thể tạo ra cảnh báo sai. Một phần trong nghiên cứu của chúng tôi là nhằm
tìm ra các chú thích hiệu quả để ngăn chặn các cảnh báo giả mà không vô
tình làm mất các cảnh báo hữu ích. Đây là một chìa khóa để làm cho một
công cụ như Eraser trở nên hữu ích. Nếu các cảnh báo sai bị loại bỏ bằng
các chú thích chính xác và cụ thể, thì khi một chương trình được sửa đổi
và chương trình đã sửa đổi được kiểm tra, chỉ những cảnh báo mới và có
liên quan sẽ được tạo ra.
Theo kinh nghiệm của chúng tôi, các cảnh báo sai chủ yếu được chia
thành ba loại chính:
—Sử dụng lại bộ nhớ: Đã báo cáo sai cảnh báo vì bộ nhớ được sử dụng
lại mà không đặt lại bộ nhớ bóng. Eraser cung cấp tất cả các quy trình
cấp phát bộ nhớ C, C11 và Unix tiêu chuẩn. Tuy nhiên, nhiều chương
trình triển khai danh sách miễn phí hoặc trình cấp phát riêng và Eraser
không có cách nào biết rằng phần bộ nhớ được tái chế riêng được bảo vệ
bởi một bộ khóa mới.
—Khóa riêng tư: Các cảnh báo sai đã được báo cáo vì khóa được thực
hiện mà không truyền thông tin này đến Eraser trong thời gian chạy. Điều
này thường là do triển khai riêng của nhiều khóa đầu đọc / ghi đơn,
không phải là một phần của giao diện pthreads tiêu chuẩn mà Eraser công
cụ.
—Benign Races: Các cuộc đua dữ liệu thực được tìm thấy không ảnh
hưởng đến tính đúng đắn của chương trình. Một số trong số này là cố ý,
và những người khác là vô tình.
Đối với mỗi danh mục này, chúng tôi đã phát triển chú thích chương trình
để cho phép người dùng Eraser loại bỏ báo cáo sai. Đối với các cuộc đua
lành tính, chúng tôi đã thêm
EraserIgnoreOn( )
EraserIgnoreOff( )
thông báo cho bộ dò cuộc đua rằng nó không nên báo cáo bất kỳ cuộc
đua nào trong mã được đánh dấu ngoặc. Để ngăn chặn các cuộc đua sử
dụng lại bộ nhớ bị báo cáo, chúng tôi đã thêm
EraserReuse(address, size)
hướng dẫn Eraser đặt lại bộ nhớ bóng tương ứng với phạm vi bộ nhớ
được chỉ định về trạng thái Virgin. Cuối cùng, sự tồn tại của các triển
khai khóa riêng tư có thể được giao tiếp bằng cách chú thích chúng bằng
EraserReadLock(lock)
EraserReadUnlock(lock)
EraserWriteLock(lock)
EraserWriteUnlock(lock)
Chúng tôi nhận thấy rằng một số ít các chú thích này thường đủ để
loại bỏ tất cả các cảnh báo sai.
3.4Race Detection in an OS Kernel
Chúng tôi đã bắt đầu sửa đổi Eraser để phát hiện races trong hệ điều hành
SPIN [Bershad et al. 1995]. Một số tính năng của SPIN, chẳng hạn như
tạo mã thời gian chạy và liên kết mã muộn, làm phức tạp quá trình đo đạc
và do đó Eraser chưa hoạt động trong môi trường này. Tuy nhiên, trong
khi chúng tôi không có kết quả về các cuộc đua dữ liệu được tìm thấy,
chúng tôi đã có được một số kinh nghiệm hữu ích về việc triển khai một
công cụ như vậy ở cấp nhân, khác với cấp người dùng theo một số cách.
Đầu tiên, SPIN (giống như nhiều hệ điều hành) thường tăng mức ngắt
của bộ xử lý để loại trừ lẫn nhau đối với các cấu trúc dữ liệu được chia sẻ
được truy cập bởi trình điều khiển thiết bị và mã mức ngắt khác. Trong
hầu hết các hệ thống, việc nâng mức ngắt lên n đảm bảo rằng chỉ các ngắt
có mức ưu tiên lớn hơn n mới được phục vụ cho đến khi mức ngắt được
hạ xuống. Nâng cao và sau đó khôi phục mức ngắt có thể được sử dụng
thay vì khóa, như sau:

Tuy nhiên, không giống như khóa, một mức ngắt cụ thể bảo vệ toàn bộ
dữ liệu được bảo vệ bởi mức ngắt thấp hơn. Chúng tôi đã kết hợp sự khác
biệt này vào Eraser bằng cách gán khóa cho từng mức ngắt riêng lẻ. Khi
hạt nhân đặt mức ngắt thành n, Eraser xử lý hoạt động này như thể n
khóa ngắt đầu tiên đã được thực hiện. Chúng tôi mong đợi kỹ thuật này
cho phép chúng tôi phát hiện các chủng tộc giữa mã bằng cách sử dụng
khóa tiêu chuẩn và mã sử dụng mức ngắt.
Một sự khác biệt nữa là hệ điều hành sử dụng nhiều hơn việc đồng bộ
hóa kiểu post / wait. Ví dụ phổ biến nhất là việc sử dụng các semaphores
để đồng bộ hóa việc thực thi giữa một luồng và một trình điều khiển thiết
bị I / O. Khi nhận được dữ liệu, trình điều khiển thiết bị sẽ thực hiện một
số xử lý tối thiểu và sau đó sử dụng thao tác V để báo hiệu một luồng
đang chờ hoạt động P, chẳng hạn, để đánh thức một luồng đang chờ hoàn
thành I / O. Điều này có thể gây ra sự cố cho Eraser nếu dữ liệu được
chia sẻ giữa trình điều khiển thiết bị và luồng. Bởi vì các semaphores
không được “sở hữu” nên Eraser khó có thể suy ra dữ liệu nào chúng
đang được sử dụng để bảo vệ, dẫn đến việc đưa ra các cảnh báo sai. Các
hệ thống tích hợp luồng và xử lý ngắt [Kleiman và Eykholt 1995] có thể
gặp ít rắc rối hơn với vấn đề này.
4. EXPERIENCE
Chúng tôi đã hiệu chỉnh Eraser trên một số chương trình đơn giản có các lỗi
đồng bộ hóa phổ biến (ví dụ: quên khóa, sử dụng khóa sai, v.v.) và các phiên
bản của các chương trình đó đã sửa lỗi. Trong khi lập trình các thử nghiệm
này, chúng tôi đã vô tình giới thiệu một cuộc đua và đáng mừng là Eraser đã
phát hiện ra nó. Những bài kiểm tra đơn giản này cực kỳ hữu ích để tìm lỗi
trong Eraser. Sau khi thuyết phục bản thân rằng công cụ hoạt động, chúng
tôi đã giải quyết một số máy chủ đa luồng lớn được viết bởi các nhà nghiên
cứu giàu kinh nghiệm tại Trung tâm nghiên cứu hệ thống của Digital
Equipment Corporation: máy chủ HTTP và công cụ lập chỉ mục từ
AltaVista, máy chủ bộ đệm Vesta và hệ thống đĩa phân tán Petal. Chúng tôi
cũng áp dụng Eraser cho một số bài tập về nhà do các lập trình viên đại học
tại Đại học Washington viết.
Như được mô tả chi tiết bên dưới, Eraser đã tìm thấy các điều kiện chạy
đua không mong muốn trong ba trong số bốn chương trình máy chủ và trong
nhiều bài tập về nhà ở bậc đại học. Nó cũng tạo ra các cảnh báo sai mà
chúng tôi có thể ngăn chặn bằng các chú thích. Khi chúng tôi tìm thấy điều
kiện cuộc đua hoặc cảnh báo sai, chúng tôi đã sửa đổi chương trình một cách
thích hợp và sau đó điều chỉnh lại Eraser để xác định các vấn đề còn lại.
Mười lần lặp lại của quy trình này thường là đủ để giải quyết tất cả các cuộc
đua được báo cáo của chương trình.
Các lập trình viên của máy chủ mà chúng tôi đã kiểm tra Eraser đã không
bắt đầu với kế hoạch kiểm tra Eraser hoặc thậm chí sử dụng kỷ luật khóa của
Eraser. Thực tế là Eraser hoạt động tốt trên các máy chủ là bằng chứng cho
thấy các lập trình viên có kinh nghiệm có xu hướng tuân theo kỷ luật khóa
đơn giản ngay cả trong môi trường cung cấp nhiều nguyên thủy đồng bộ hóa
phức tạp hơn.
Trong phần còn lại của phần này, chúng tôi báo cáo chi tiết về trải
nghiệm của chúng tôi với từng chương trình.
4.1AltaVista
Chúng tôi đã kiểm tra hai thành phần của dịch vụ lập chỉ mục Web
AltaVista phổ biến: mhttpd và Ni2.
Chương trình mhttpd là một máy chủ HTTP nhẹ được thiết kế để hỗ
trợ tải máy chủ cực cao mà AltaVista đã trải qua. Mỗi yêu cầu tìm kiếm
được xử lý bởi một luồng riêng biệt và dựa vào khóa để đồng bộ hóa
quyền truy cập bằng các yêu cầu đồng thời tới cấu trúc dữ liệu được chia
sẻ. Ngoài ra, mhttpd sử dụng một số luồng bổ sung để quản lý các tác vụ
nền như cấu hình và quản lý bộ đệm tên. Máy chủ bao gồm khoảng 5000
dòng mã nguồn C. Chúng tôi đã thử nghiệm mhttpd bằng cách gọi một
loạt các tập lệnh thử nghiệm từ ba trình duyệt Web riêng biệt. Thử
nghiệm mhttpd đã sử dụng khoảng 100 ổ khóa khác nhau tạo thành
khoảng 250 bộ khóa khác nhau.
Công cụ lập chỉ mục Ni2 được sử dụng để tra cứu thông tin theo các
truy vấn chỉ mục. Cấu trúc dữ liệu chỉ mục được chia sẻ giữa tất cả các
luồng máy chủ và sử dụng khóa một cách rõ ràng để đảm bảo rằng các
bản cập nhật được tuần tự hóa. Các thư viện Ni2 cơ bản chứa khoảng
20.000 dòng mã nguồn C. Chúng tôi đã thử nghiệm Ni2 một cách riêng
biệt bằng cách sử dụng một tiện ích có tên ft gửi một loạt các yêu cầu
ngẫu nhiên bằng cách sử dụng một số chủ đề cụ thể (chúng tôi đã sử
dụng 10). Thử nghiệm ft đã sử dụng khoảng 900 khóa tạo thành khoảng
3600 bộ khóa riêng biệt.
Chúng tôi đã tìm thấy một số lượng lớn các cuộc đua được báo cáo,
hầu hết trong số đó là báo động sai. Nguyên nhân chủ yếu là do tái sử
dụng bộ nhớ, sau đó là các khóa riêng và các cuộc đua lành tính. Các
chủng tộc lành tính được tìm thấy trong Ni2 đặc biệt thú vị, bởi vì chúng
minh họa cho việc sử dụng các chủng tộc một cách có chủ đích để giảm
thiểu chi phí khóa. Ví dụ: hãy xem xét đoạn mã sau:
khóa ip lock giữ. Cuộc đua đã được cố ý lập trình như một sự tối ưu hóa
để tránh bị khóa chi phí trong trường hợp phổ biến là ip fp đã được thiết
lập. Chương trình chính xác ngay cả với cuộc đua, vì trường ip fp không
bao giờ chuyển từ khác 0 sang 0 trong phạm vi nhiều luồng và chương
trình lặp lại kiểm tra bên trong khóa trong trường hợp trường được kiểm
tra bằng 0 (do đó tránh được cuộc đua trong đó hai luồng tìm trường bằng
không và cả hai sau đó khởi tạo nó).
Loại mã này rất phức tạp. Ví dụ: có thể an toàn khi truy cập trường p-
> ip fp trong phần còn lại của quy trình (các dòng được thay thế bằng dấu
chấm lửng trong đoạn mã). Nhưng trên thực tế, đây sẽ là một sai lầm, bởi
vì mô hình nhất quán bộ nhớ của Alpha cho phép bộ xử lý xem các hoạt
động của bộ nhớ không theo thứ tự nếu không có đồng bộ hóa can thiệp.
Mặc dù mã Ni2 là đúng, sau khi sử dụng Eraser, lập trình viên đã quyết
định lập trình lại phần này của nó để đối số về độ đúng của nó đơn giản
hơn.
Chúng tôi cũng tìm thấy một cuộc chạy đua lành tính trong chương
trình khai thác thử nghiệm Ni2, nơi nhiều luồng chạy đua đọc và ghi vào
một biến toàn cục được gọi là truy vấn giết. Biến này được khởi tạo thành
false và được đặt thành true để chỉ ra rằng tất cả các luồng sẽ thoát. Mỗi
luồng định kỳ thăm dò biến và thoát ra khi nó được đặt thành true. Các
mã hoàn thiện khác có các cuộc đua lành tính tương tự. Để ngăn trình dò
cuộc đua báo cáo những cuộc đua như vậy, chúng tôi đã sử dụng chú
thích EraserIgnoreOn / Off (). Tương tự, mhttpd bỏ qua các khóa khi cập
nhật định kỳ dữ liệu và thống kê cấu hình toàn cầu. Đây thực sự là những
lỗi đồng bộ hóa, nhưng ảnh hưởng của chúng tương đối nhỏ, đó có lẽ là
lý do tại sao chúng không được phát hiện trong thời gian dài.
Việc chèn chín chú thích trong thư viện Ni2, năm chú thích trong bộ
khai thác thử nghiệm ft và 10 chú thích trong máy chủ mhttpd đã giảm số
lượng cuộc đua được báo cáo từ hơn một trăm xuống còn không.
4.2Máy chủ bộ nhớ đệm Vesta
Vesta là một hệ thống quản lý cấu hình phần mềm tiên tiến. 4 Các cấu
hình được viết bằng ngôn ngữ chức năng chuyên dụng mô tả các phần
phụ thuộc và các quy tắc được sử dụng để suy ra trạng thái hiện tại của
phần mềm. Kết quả một phần, chẳng hạn như tệp “.o” được tạo bởi trình
biên dịch C, được lưu vào bộ nhớ đệm trong máy chủ bộ đệm Vesta và
được sử dụng bởi trình tạo Vesta để tạo một cấu hình cụ thể. Máy chủ bộ
nhớ cache bao gồm khoảng 30.000 dòng mã C11. Chúng tôi đã kiểm tra
máy chủ bộ nhớ cache bằng cách sử dụng tiện ích TestCache đưa ra một
luồng yêu cầu ngẫu nhiên đồng thời. Máy chủ bộ nhớ đệm đã sử dụng 10
luồng, có 26 khóa riêng biệt và khởi tạo 70 bộ khóa khác nhau.
Trong quá trình thử nghiệm máy chủ bộ nhớ cache, Eraser đã báo cáo
một số cuộc đua, chủ yếu xoay quanh ba cấu trúc dữ liệu. Tập hợp chủng
tộc đầu tiên được phát hiện trong mã duy trì dấu vân tay trong các mục
nhập bộ nhớ cache. Bởi vì tính toán một dấu vân tay có thể tốn kém, máy
chủ bộ nhớ cache duy trì một trường boolean trong mục nhập bộ nhớ
cache ghi lại xem dấu vân tay đó có hợp lệ hay không. Tệp tham chiếu
chỉ được tính nếu giá trị thực của nó là cần thiết và giá trị hiện tại của nó
không hợp lệ. Thật không may, boolean đã được truy cập mà không có
khóa bảo vệ, trong mã như thế này:

Đây là một cuộc chạy đua dữ liệu nghiêm trọng, vì trong trường hợp
không có rào cản bộ nhớ, ngữ nghĩa Alpha không đảm bảo rằng nội dung
của trường validFP nhất quán với trường fp.
Một tập hợp các cuộc đua khác xoay quanh danh sách miễn phí trong
đối tượng CacheS. Đối tượng CacheS duy trì một danh sách miễn phí các
loại mục nhật ký khác nhau. Phản hồi đầu tiên của chúng tôi là sử dụng
chú thích EraserReuse () trong đó các phần tử được phân bổ khỏi danh
sách miễn phí này. Tuy nhiên, điều này không làm cho tất cả các cảnh
báo biến mất; các cuộc gọi để xóa nhật ký vẫn gây ra các cuộc đua. Kiểm
tra cho thấy phần đầu của mỗi khúc gỗ được bảo vệ bằng khóa, nhưng
không phải các mục riêng lẻ. Các quy trình Flush khóa phần đầu của nhật
ký, lưu trữ giá trị của nó trong một biến ngăn xếp, đặt phần đầu thành 0
và nhả khóa. Sau đó, họ truy cập các mục riêng lẻ mà không có bất kỳ ổ
khóa nào, cuối cùng đưa chúng vào danh sách miễn phí. Điều này đúng
vì các luồng khác truy cập các mục nhật ký với khóa đầu nhật ký được
giữ và các luồng không duy trì các con trỏ vào nhật ký. Do đó, Flush làm
cho dữ liệu trở nên riêng tư một cách hiệu quả đối với luồng mà Flush
được gọi. Chúng tôi đã loại bỏ báo cáo về các cuộc đua này bằng cách
chuyển các chú thích EraserReuse () sang ba quy trình Flush.
Cuối cùng, đã có một số cảnh báo sai liên quan đến đối tượng TCP
sock và SRPC được sử dụng để triển khai RPC phía máy chủ. Máy chủ
bộ đệm sử dụng một chuỗi máy chủ chính để chờ các yêu cầu RPC đến.
Khi nhận được yêu cầu, luồng này sẽ chuyển các cấu trúc dữ liệu RPC và
socket hiện tại tới một luồng công nhân chịu trách nhiệm xử lý phần còn
lại của RPC. Vì luồng chính và luồng công nhân sẽ không bao giờ truy
cập đồng thời các cấu trúc dữ liệu nên chúng không cần sử dụng khóa để
tuần tự hóa quyền truy cập. Đối với Eraser, điều này giống như vi phạm
kỷ luật khóa và được gắn cờ là một cuộc đua. Với một số nỗ lực, có thể
sửa đổi Eraser để nhận ra kỷ luật khóa này, nhưng chúng tôi đã có thể đạt
được hiệu quả tương tự với hai chú thích EraserReuse ().
Tổng cộng, 10 chú thích và một lần sửa lỗi là đủ để giảm các báo cáo
cuộc đua từ vài trăm xuống 0.
4.3Peta
Petal là một hệ thống lưu trữ phân tán cung cấp cho khách hàng một đĩa
ảo khổng lồ được thực hiện bởi một cụm máy chủ và đĩa vật lý [Lee và
Thekkath 1996]. Petal thực hiện một thuật toán đồng thuận phân tán cũng
như các cơ chế phát hiện và khôi phục lỗi. Máy chủ Petal có khoảng
25.000 dòng mã C và chúng tôi đã sử dụng 64 luồng công nhân đồng thời
trong các thử nghiệm của mình. Chúng tôi đã thử nghiệm Petal bằng một
tiện ích đưa ra các yêu cầu đọc và ghi ngẫu nhiên.
Chúng tôi đã tìm thấy một số cảnh báo sai do triển khai khóa người
đọc-người viết riêng tư. Chúng dễ dàng bị loại bỏ bằng cách sử dụng chú
thích. Chúng tôi cũng phát hiện ra một cuộc đua thực sự trong GMapCh
CheckServerThread () thông thường. Quy trình này được chạy bởi một
luồng duy nhất và kiểm tra định kỳ để đảm bảo rằng các máy chủ lân cận
đang chạy. Tuy nhiên, khi làm như vậy, nó đọc trường trạng thái gmap->
mà không giữ khóa gmapState (tất cả các luồng khác giữ trước khi viết
gmap-> trạng thái).
Chúng tôi đã tìm thấy hai cuộc đua trong đó các biến toàn cầu chứa số
liệu thống kê đã được sửa đổi mà không cần khóa. Những cuộc đua này
là có chủ đích, dựa trên cơ sở rằng việc khóa là tốn kém và số liệu thống
kê của máy chủ chỉ cần gần đúng.
Cuối cùng, chúng tôi đã tìm thấy một cảnh báo sai mà chúng tôi
không thể chú thích. Hàm GmapCh_Write2 () chia một số luồng và
chuyển từng tham chiếu tới một thành phần của khung ngăn xếp của
GmapCh_Write2. GmapCh_Write2 () triển khai một cấu trúc giống như
phép nối để giữ cho khung ngăn xếp hoạt động cho đến khi các luồng
quay trở lại. Nhưng Eraser không khởi động lại bộ nhớ bóng cho mỗi
khung ngăn xếp mới; do đó, việc sử dụng lại bộ nhớ ngăn xếp cho các
trường hợp khác nhau của khung ngăn xếp dẫn đến báo động giả.
4.4Bài tập đại học
Đối lập với kinh nghiệm của chúng tôi với các chương trình máy chủ đa
luồng trưởng thành, hai đồng nghiệp của chúng tôi tại Đại học
Washington đã sử dụng Eraser để kiểm tra các loại lỗi đồng bộ hóa được
tìm thấy trong các bài tập về nhà do lớp hệ điều hành đại học của họ tạo
ra (giao tiếp cá nhân, SE Choi và EC Lewis, 1997). Chúng tôi báo cáo kết
quả của họ ở đây để chứng minh cách Eraser hoạt động với cơ sở mã ít
phức tạp hơn.
Lớp được yêu cầu hoàn thành bốn bài tập đa luồng tiêu chuẩn. Các
nhiệm vụ này có thể được phân loại đại khái là cấp thấp (xây dựng khóa
từ test-and-set), cấp luồng (xây dựng một gói luồng nhỏ), cấp đồng bộ
hóa (xây dựng semaphores và mutexes) và cấp ứng dụng (kiểu nhà sản
xuất / người tiêu dùng các vấn đề). Mỗi nhiệm vụ được xây dựng dựa
trên việc thực hiện nhiệm vụ trước đó. Các đồng nghiệp của chúng tôi đã
sử dụng Eraser để kiểm tra từng bài tập này cho khoảng 40 nhóm; tổng
cộng khoảng 100 bài tập có thể chạy được đã được nộp (không phải tất cả
các nhóm đều hoàn thành tất cả các bài tập; một số không biên dịch; và
một số nhóm ngay lập tức bị khóa). Trong số các nhiệm vụ “đang hoạt
động” này, 10% có các cuộc đua dữ liệu được tìm thấy bởi Eraser. Những
nguyên nhân này là do quên lấy khóa, lấy khóa trong khi ghi nhưng
không dùng để đọc, sử dụng các khóa khác nhau để bảo vệ cùng một cấu
trúc dữ liệu vào những thời điểm khác nhau và quên yêu cầu lại các khóa
đã được phát hành trong một vòng lặp.
Eraser cũng báo cáo một cảnh báo giả được kích hoạt bởi một hàng
đợi được bảo vệ ngầm các phần tử bằng cách truy cập hàng đợi thông qua
các trường đầu và đuôi bị khóa (giống như đối tượng CacheS của Vesta).
4.5Hiệu quả và Độ nhạy
Vì Eraser sử dụng một phương pháp kiểm tra nên nó không thể chứng
minh rằng một chương trình không có các cuộc đua dữ liệu. Nhưng
chúng tôi tin rằng Eraser hoạt động tốt, so với kiểm tra và gỡ lỗi thủ
công, và kiểm tra của Eraser không nhạy cảm lắm với việc xen kẽ bộ lập
lịch. Để kiểm tra những niềm tin này, chúng tôi đã thực hiện hai thí
nghiệm bổ sung.
Chúng tôi đã tham khảo lịch sử chương trình của Ni2 và giới thiệu lại
hai chủng tộc dữ liệu đã tồn tại trong các phiên bản trước. Lỗi đầu tiên là
quyền truy cập không khóa vào số lượng tham chiếu được sử dụng để thu
thập rác cấu trúc dữ liệu tệp. Cuộc đua khác là do không thực hiện được
một khóa bổ sung cần thiết để bảo vệ cấu trúc dữ liệu của một chương
trình con được gọi ở giữa một thủ tục lớn. Các chủng tộc này đã tồn tại
trong mã nguồn Ni2 trong vài tháng trước khi chúng được tác giả chương
trình tìm thấy và sửa theo cách thủ công. Sử dụng Eraser, một người
trong chúng tôi có thể xác định vị trí của cả hai cuộc đua trong vài phút
mà không được cung cấp bất kỳ thông tin nào về vị trí của các cuộc đua
hoặc cách chúng được gây ra. Phải mất 30 phút để sửa cả hai lỗi và xác
minh sự vắng mặt của báo cáo cuộc đua tiếp theo.
Chúng tôi đã kiểm tra vấn đề độ nhạy bằng cách chạy lại Ni2 và Vesta
thử nghiệm, nhưng chỉ sử dụng hai luồng đồng thời thay vì 10. Nếu
Eraser nhạy cảm với sự khác biệt trong việc đan xen luồng thì chúng tôi
sẽ tìm thấy một tập hợp các báo cáo chủng tộc khác nhau. Trên thực tế,
chúng tôi đã tìm thấy các báo cáo cuộc đua giống nhau (mặc dù đôi khi
theo thứ tự khác nhau) trên nhiều lần chạy bằng cách sử dụng hai chủ đề
hoặc 10.
5. RÚT KINH NGHIỆM BỔ SUNG
Trong phần này, chúng tôi đề cập ngắn gọn đến hai chủ đề khác, mỗi chủ đề
liên quan đến hình thức kiểm tra động để tìm lỗi đồng bộ hóa trong các
chương trình đa luồng mà chúng tôi đã thử nghiệm và tin rằng nó quan trọng
và đầy hứa hẹn, nhưng chúng tôi đã không triển khai trong Eraser.
Chủ đề đầu tiên là bảo vệ bằng nhiều khóa. Một số chương trình bảo vệ
một số biến được chia sẻ bằng nhiều khóa thay vì một khóa duy nhất. Trong
trường hợp này, quy tắc là mọi luồng ghi biến phải giữ tất cả các khóa bảo
vệ và mọi luồng đọc biến phải giữ ít nhất một khóa bảo vệ. Chính sách này
chỉ cho phép một cặp truy cập đồng thời nếu cả hai quyền truy cập đều được
đọc và do đó ngăn chặn các cuộc chạy đua dữ liệu.
Sử dụng nhiều khóa bảo vệ theo một số cách tương tự như sử dụng khóa
đầu đọc và khóa ghi, nhưng nó không nhằm mục đích tăng tính đồng thời
cũng như tránh bế tắc trong một chương trình có chứa cuộc gọi lên.
Sử dụng phiên bản Eraser trước đó đã phát hiện điều kiện chủng tộc trong
các chương trình Modula-3 đa luồng, chúng tôi nhận thấy rằng thuật toán
Lockset đã báo cáo cảnh báo sai cho các chương trình Trestle [Manasse và
Nelson 1991] bảo vệ các vị trí được chia sẻ bằng nhiều khóa, vì mỗi trong số
hai trình đọc có thể truy cập vị trí trong khi giữ hai ổ khóa khác nhau. Như
một thử nghiệm, chúng tôi đã xử lý vấn đề bằng cách sửa đổi thuật toán
Lockset để tinh chỉnh bộ ứng viên chỉ để ghi, trong khi kiểm tra nó cho cả
lần đọc và ghi, như sau:

Điều này đã ngăn chặn các cảnh báo sai, nhưng việc sửa đổi này có thể
gây ra các âm tính giả. Ví dụ: nếu một luồng t1 đọc v trong khi giữ khóa m1
và một luồng t2 ghi v trong khi giữ khóa m2, thì việc vi phạm kỷ luật khóa
sẽ chỉ được báo cáo nếu lần ghi trước lần đọc. Nói chung, phiên bản sửa đổi
sẽ hoạt động tốt chỉ khi trường hợp thử nghiệm gây ra đủ số lần đọc biến
được chia sẻ để thực hiện theo các lần ghi tương ứng.
Về mặt lý thuyết, có thể xử lý nhiều khóa bảo vệ mà không có bất kỳ
nguy cơ âm tính giả nào, nhưng cấu trúc dữ liệu được yêu cầu (bộ khóa thay
vì chỉ bộ khóa) dường như có chi phí phức tạp vượt quá khả năng thu được.
Vì chúng tôi không thoải mái với âm tính giả và vì kỹ thuật khóa nhiều bảo
vệ không phổ biến, nên phiên bản Eraser hiện tại bỏ qua kỹ thuật này, tạo ra
cảnh báo sai cho các chương trình sử dụng nó.
Chủ đề thứ hai là bế tắc. Nếu cuộc đua dữ liệu là Scylla, thì bế tắc là
Charybdis
Một kỷ luật đơn giản để tránh bế tắc là chọn thứ tự từng phần trong số tất
cả các khóa và lập trình từng luồng sao cho bất cứ khi nào nó giữ nhiều hơn
một khóa, nó sẽ thu thập chúng theo thứ tự tăng dần. Kỷ luật này tương tự
như kỷ luật khóa để tránh chạy đua dữ liệu: nó phù hợp để kiểm tra bằng
giám sát động và dễ dàng tạo ra một trường hợp thử nghiệm cho thấy sự vi
phạm kỷ luật hơn là tạo ra một trường hợp thử nghiệm thực sự gây ra bế tắc.
Đối với một thử nghiệm độc lập, chúng tôi đã chọn một ứng dụng Trestle
lớn được biết là có đồng bộ hóa phức tạp (formedit, trình chỉnh sửa giao diện
người dùng chế độ xem kép), ghi lại tất cả các lần nhận khóa và kiểm tra
xem liệu đơn đặt hàng có tồn tại trên các ổ khóa hay không được tôn trọng
bởi mọi chủ đề. Một vài giây sau khi khởi động formedit, màn hình thử
nghiệm của chúng tôi đã phát hiện ra một chu kỳ khóa, cho thấy rằng không
tồn tại thứ tự từng phần. Việc kiểm tra chu kỳ chặt chẽ cho thấy một sự bế
tắc tiềm ẩn trong formedit. Chúng tôi coi đây là một kết quả đầy hứa hẹn và
phỏng đoán rằng kiểm tra bế tắc dọc theo những dòng này sẽ là một bổ sung
hữu ích cho Eraser. Tuy nhiên, cần phải làm việc nhiều hơn để lập danh mục
các biến thể âm thanh và hữu ích trên kỷ luật thứ tự từng phần và phát triển
các chú thích để ngăn chặn các cảnh báo sai.
6. CONCLUSION
Các nhà thiết kế phần cứng đã học cách thiết kế để có thể kiểm tra được. Các
lập trình viên sử dụng các luồng cũng phải học như vậy. Nó là không đủ để
viết một chương trình chính xác; Tính đúng đắn phải được chứng minh, lý
tưởng nhất là bằng cách kiểm tra tĩnh, thực tế bằng sự kết hợp của kiểm tra
tĩnh từng phần, sau đó là kiểm tra động có kỷ luật.
Bài viết này đã mô tả những ưu điểm của việc thực thi một kỷ luật khóa
đơn giản thay vì kiểm tra các chủng tộc trong các chương trình song song
nói chung sử dụng nhiều nguyên thủy đồng bộ hóa khác nhau và đã chứng
minh rằng với kỹ thuật này, việc kiểm tra động các chương trình đa luồng
sản xuất cho các chủng tộc dữ liệu là rất thực tế.
Các lập trình viên trong lĩnh vực hệ điều hành dường như xem các công
cụ phát hiện chủng tộc động là bí truyền và không thực tế. Thay vào đó, kinh
nghiệm của chúng tôi khiến chúng tôi tin rằng chúng là một cách thực tế và
hiệu quả để tránh các cuộc chạy đua dữ liệu và việc phát hiện cuộc đua động
phải là một quy trình tiêu chuẩn trong bất kỳ nỗ lực kiểm tra có kỷ luật nào
đối với một chương trình đa luồng. Khi việc sử dụng đa luồng mở rộng, sự
không đáng tin cậy do các cuộc đua dữ liệu gây ra cũng sẽ tăng lên, trừ khi
các phương pháp tốt hơn được sử dụng để loại bỏ chúng. Chúng tôi tin rằng
phương pháp Lockset được triển khai trong Eraser là đầy hứa hẹn.
ACKNOWLEDGMENTS
Chúng tôi xin cảm ơn những cá nhân sau đây đã đóng góp cho dự án này.
Sung-Eun Choi và E. Christoper Lewis chịu trách nhiệm về tất cả các thí
nghiệm ở bậc đại học. Alan Heydon, Dave Detlefs, Chandu Thekkath và
Edward Lee đã đưa ra lời khuyên chuyên môn về Vesta và Petal. Puneet
Kumar đã làm việc trên một phiên bản trước đó của Eraser. Cynthia
Hibbard, Brian Bershad, Michael Ernst, Paulo Guedes, Wilson Hsieh, Terri
Watson, và những người đánh giá SOSP và TOCS đã cung cấp phản hồi hữu
ích về các bản thảo trước đó của bài viết này.
Question
Eraser chỉ đảm bảo rằng dữ liệu được bảo vệ bởi một bộ khóa nhất quán.
Đưa ra một bản phác thảo trực quan về một loại điều kiện race mà nó sẽ bỏ
lỡ, một ví dụ và giải thích cách Eraser có thể được mở rộng để xử lý chúng.

https://blog.acolyer.org/2015/01/28/eraser-a-dynamic-data-race-detector-for-
multi-threaded-programs/
http://www.cs.cmu.edu/afs/cs/academic/class/15712-f15/www/lectures/06-
eraser.pdf
https://dl.acm.org/doi/10.1145/265924.265927
http://pages.cs.wisc.edu/~swift/classes/cs736-
fa12/blog/2012/10/eraser_a_dynamic_data_race_det.html
https://slidetodoc.com/eraser-a-dynamic-data-race-detector-for-
multithreaded-2/

You might also like