Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fine-tune the Ordering for the atomic usages #2706

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

wang384670111
Copy link

SeqCst is overly restrictive. I believe that the ordering can be appropriately modified.

In messages model, I believe that MESSAGES, IGNORE_MESSAGES, and ERRORED are merely signals for multithreading purposes and do not synchronize with other locals. Therefore, using Relaxed ordering should suffice.

In lib and utils models, COUNTER and NEXT_ID are used for multithreading counting purposes, so using Relaxed ordering is sufficient.

In walk model, the load and store of quit_now should use Acquire/Release to ensure that the stack is up-to-date.

Copy link
Owner

@BurntSushi BurntSushi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used SeqCst in part because I didn't want to do the analysis necessary to demonstrate that a less restrictive ordering was correct, and also because I didn't perceive using SeqCst to be a particular issue in most of these cases.

With that said, I buy all of the changes to Relaxed here I think.

However, I'm unsure of the Require/Release changes in the ignore crate. I think those warrant comments explaining their correctness.

}

/// Returns true if this worker should quit immediately.
fn is_quit_now(&self) -> bool {
self.quit_now.load(AtomicOrdering::SeqCst)
self.quit_now.load(AtomicOrdering::Acquire)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and the Release above need comments justifying why this is correct.

@wang384670111
Copy link
Author

I used SeqCst in part because I didn't want to do the analysis necessary to demonstrate that a less restrictive ordering was correct, and also because I didn't perceive using SeqCst to be a particular issue in most of these cases.

With that said, I buy all of the changes to Relaxed here I think.

However, I'm unsure of the Require/Release changes in the ignore crate. I think those warrant comments explaining their correctness.

My reasoning is based on the following observations:

Line 1501 calls quit_now after calling run_one at line 1562, where run_one invokes generate_work. Inside generate_work, specifically at line 1655, the send performs a push on the stack.

fn run(mut self) {
while let Some(work) = self.get_work() {
if let WalkState::Quit = self.run_one(work) {
self.quit_now();
}
}
}

if !should_skip_filesize && !should_skip_filtered {
self.send(Work { dent, ignore: ig.clone(), root_device });
}

Then, after executing is_quit_now at line 1669, send_quit is called at line 1680 to access the stack and perform another push.
if self.is_quit_now() {
value = Some(Message::Quit)
}
match value {
Some(Message::Work(work)) => {
return Some(work);
}
Some(Message::Quit) => {
// Repeat quit message to wake up sleeping threads, if
// any. The domino effect will ensure that every thread
// will quit.
self.send_quit();
return None;
}

Based on this sequence of operations, in the absence of a definitive happen-before relationship within a concurrent environment, I believe it is necessary to use Acquire/Release to establish synchronization, in order to properly observe these modifications to stack.

@wang384670111
Copy link
Author

I used SeqCst in part because I didn't want to do the analysis necessary to demonstrate that a less restrictive ordering was correct, and also because I didn't perceive using SeqCst to be a particular issue in most of these cases.
With that said, I buy all of the changes to Relaxed here I think.
However, I'm unsure of the Require/Release changes in the ignore crate. I think those warrant comments explaining their correctness.

My reasoning is based on the following observations:

Line 1501 calls quit_now after calling run_one at line 1562, where run_one invokes generate_work. Inside generate_work, specifically at line 1655, the send performs a push on the stack.

fn run(mut self) {
while let Some(work) = self.get_work() {
if let WalkState::Quit = self.run_one(work) {
self.quit_now();
}
}
}

if !should_skip_filesize && !should_skip_filtered {
self.send(Work { dent, ignore: ig.clone(), root_device });
}

Then, after executing is_quit_now at line 1669, send_quit is called at line 1680 to access the stack and perform another push.

if self.is_quit_now() {
value = Some(Message::Quit)
}
match value {
Some(Message::Work(work)) => {
return Some(work);
}
Some(Message::Quit) => {
// Repeat quit message to wake up sleeping threads, if
// any. The domino effect will ensure that every thread
// will quit.
self.send_quit();
return None;
}

Based on this sequence of operations, in the absence of a definitive happen-before relationship within a concurrent environment, I believe it is necessary to use Acquire/Release to establish synchronization, in order to properly observe these modifications to stack.

I used SeqCst in part because I didn't want to do the analysis necessary to demonstrate that a less restrictive ordering was correct, and also because I didn't perceive using SeqCst to be a particular issue in most of these cases.
With that said, I buy all of the changes to Relaxed here I think.
However, I'm unsure of the Require/Release changes in the ignore crate. I think those warrant comments explaining their correctness.

My reasoning is based on the following observations:

Line 1501 calls quit_now after calling run_one at line 1562, where run_one invokes generate_work. Inside generate_work, specifically at line 1655, the send performs a push on the stack.

fn run(mut self) {
while let Some(work) = self.get_work() {
if let WalkState::Quit = self.run_one(work) {
self.quit_now();
}
}
}

if !should_skip_filesize && !should_skip_filtered {
self.send(Work { dent, ignore: ig.clone(), root_device });
}

Then, after executing is_quit_now at line 1669, send_quit is called at line 1680 to access the stack and perform another push.

if self.is_quit_now() {
value = Some(Message::Quit)
}
match value {
Some(Message::Work(work)) => {
return Some(work);
}
Some(Message::Quit) => {
// Repeat quit message to wake up sleeping threads, if
// any. The domino effect will ensure that every thread
// will quit.
self.send_quit();
return None;
}

Based on this sequence of operations, in the absence of a definitive happen-before relationship within a concurrent environment, I believe it is necessary to use Acquire/Release to establish synchronization, in order to properly observe these modifications to stack.

I've reconsidered my earlier deduction and realized I overlooked a crucial detail.

Before calling quit_now at 1501, we have a successful return of WalkState::Quit at 1500. This indicates that the stack push operation, occurring at line 1655, has completed. This establishes a happen-before relationship, making it unnecessary to use Release in quit_now to ensure the completion of the stack push operation. Therefore, using Relaxed for both the store in quit_now and the subsequent load should suffice.

I would greatly appreciate your feedback on this observation. I'm eager for the potential merge and am ready to provide any further clarifications as needed.

@BurntSushi
Copy link
Owner

It might take me some time to look into this. I basically need to convince myself of your argument.

If you want to get something merged more quickly, you could open a new PR with the less "interesting" changes and keep this PR focused on the changes to ignore's parallel traversal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants