mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-05-19 09:40:22 -07:00
ignore: rework inter-thread messaging
Change the meaning of `Quit` message. Now it means terminate. The final "dance" is unnecessary, because by the time quitting begins, no thread will ever spawn a new `Work`. The trick was to replace the heuristic spin-loop with blocking receive. Closes #1337
This commit is contained in:
parent
52d7f47420
commit
3d59bd98aa
@ -6,11 +6,9 @@ use std::io;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
use channel;
|
use channel::{self, TryRecvError};
|
||||||
use same_file::Handle;
|
use same_file::Handle;
|
||||||
use walkdir::{self, WalkDir};
|
use walkdir::{self, WalkDir};
|
||||||
|
|
||||||
@ -1242,8 +1240,7 @@ impl WalkParallel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Create the workers and then wait for them to finish.
|
// Create the workers and then wait for them to finish.
|
||||||
let num_waiting = Arc::new(AtomicUsize::new(0));
|
let num_running = Arc::new(AtomicUsize::new(threads));
|
||||||
let num_quitting = Arc::new(AtomicUsize::new(0));
|
|
||||||
let quit_now = Arc::new(AtomicBool::new(false));
|
let quit_now = Arc::new(AtomicBool::new(false));
|
||||||
crossbeam_utils::thread::scope(|s| {
|
crossbeam_utils::thread::scope(|s| {
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
@ -1253,11 +1250,7 @@ impl WalkParallel {
|
|||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
rx: rx.clone(),
|
rx: rx.clone(),
|
||||||
quit_now: quit_now.clone(),
|
quit_now: quit_now.clone(),
|
||||||
is_waiting: false,
|
num_running: num_running.clone(),
|
||||||
is_quitting: false,
|
|
||||||
num_waiting: num_waiting.clone(),
|
|
||||||
num_quitting: num_quitting.clone(),
|
|
||||||
threads: threads,
|
|
||||||
max_depth: self.max_depth,
|
max_depth: self.max_depth,
|
||||||
max_filesize: self.max_filesize,
|
max_filesize: self.max_filesize,
|
||||||
follow_links: self.follow_links,
|
follow_links: self.follow_links,
|
||||||
@ -1270,8 +1263,7 @@ impl WalkParallel {
|
|||||||
for handle in handles {
|
for handle in handles {
|
||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
})
|
}).unwrap(); // Pass along panics from threads
|
||||||
.unwrap(); // Pass along panics from threads
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn threads(&self) -> usize {
|
fn threads(&self) -> usize {
|
||||||
@ -1289,7 +1281,7 @@ enum Message {
|
|||||||
/// Work items for entries that should be skipped or ignored should not
|
/// Work items for entries that should be skipped or ignored should not
|
||||||
/// be produced.
|
/// be produced.
|
||||||
Work(Work),
|
Work(Work),
|
||||||
/// This instruction indicates that the worker should start quitting.
|
/// This instruction indicates that the worker should quit.
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1368,21 +1360,12 @@ struct Worker<'s> {
|
|||||||
tx: channel::Sender<Message>,
|
tx: channel::Sender<Message>,
|
||||||
/// The receive side of our mpmc queue.
|
/// The receive side of our mpmc queue.
|
||||||
rx: channel::Receiver<Message>,
|
rx: channel::Receiver<Message>,
|
||||||
/// Whether all workers should quit at the next opportunity. Note that
|
/// Whether all workers should terminate at the next opportunity. Note
|
||||||
/// this is distinct from quitting because of exhausting the contents of
|
/// that we need this because we don't want other `Work` to be done after
|
||||||
/// a directory. Instead, this is used when the caller's callback indicates
|
/// we quit. We wouldn't need this if have a priority channel.
|
||||||
/// that the iterator should quit immediately.
|
|
||||||
quit_now: Arc<AtomicBool>,
|
quit_now: Arc<AtomicBool>,
|
||||||
/// Whether this worker is waiting for more work.
|
|
||||||
is_waiting: bool,
|
|
||||||
/// Whether this worker has started to quit.
|
|
||||||
is_quitting: bool,
|
|
||||||
/// The number of workers waiting for more work.
|
/// The number of workers waiting for more work.
|
||||||
num_waiting: Arc<AtomicUsize>,
|
num_running: Arc<AtomicUsize>,
|
||||||
/// The number of workers waiting to quit.
|
|
||||||
num_quitting: Arc<AtomicUsize>,
|
|
||||||
/// The total number of workers.
|
|
||||||
threads: usize,
|
|
||||||
/// The maximum depth of directories to descend. A value of `0` means no
|
/// The maximum depth of directories to descend. A value of `0` means no
|
||||||
/// descension at all.
|
/// descension at all.
|
||||||
max_depth: Option<usize>,
|
max_depth: Option<usize>,
|
||||||
@ -1403,20 +1386,19 @@ impl<'s> Worker<'s> {
|
|||||||
/// The worker will call the caller's callback for all entries that aren't
|
/// The worker will call the caller's callback for all entries that aren't
|
||||||
/// skipped by the ignore matcher.
|
/// skipped by the ignore matcher.
|
||||||
fn run(mut self) {
|
fn run(mut self) {
|
||||||
while let Some(mut work) = self.get_work() {
|
'get_work: while let Some(mut work) = self.get_work() {
|
||||||
// If the work is not a directory, then we can just execute the
|
// If the work is not a directory, then we can just execute the
|
||||||
// caller's callback immediately and move on.
|
// caller's callback immediately and move on.
|
||||||
if work.is_symlink() || !work.is_dir() {
|
if work.is_symlink() || !work.is_dir() {
|
||||||
if self.visitor.visit(Ok(work.dent)).is_quit() {
|
if self.visitor.visit(Ok(work.dent)).is_quit() {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(err) = work.add_parents() {
|
if let Some(err) = work.add_parents() {
|
||||||
if self.visitor.visit(Err(err)).is_quit() {
|
if self.visitor.visit(Err(err)).is_quit() {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1427,7 +1409,7 @@ impl<'s> Worker<'s> {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
if self.visitor.visit(Err(err)).is_quit() {
|
if self.visitor.visit(Err(err)).is_quit() {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -1449,7 +1431,7 @@ impl<'s> Worker<'s> {
|
|||||||
WalkState::Skip => continue,
|
WalkState::Skip => continue,
|
||||||
WalkState::Quit => {
|
WalkState::Quit => {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !descend {
|
if !descend {
|
||||||
@ -1461,7 +1443,6 @@ impl<'s> Worker<'s> {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
if self.visitor.visit(Err(err)).is_quit() {
|
if self.visitor.visit(Err(err)).is_quit() {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1479,7 +1460,7 @@ impl<'s> Worker<'s> {
|
|||||||
);
|
);
|
||||||
if state.is_quit() {
|
if state.is_quit() {
|
||||||
self.quit_now();
|
self.quit_now();
|
||||||
return;
|
continue 'get_work;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1568,64 +1549,43 @@ impl<'s> Worker<'s> {
|
|||||||
/// If all work has been exhausted, then this returns None. The worker
|
/// If all work has been exhausted, then this returns None. The worker
|
||||||
/// should then subsequently quit.
|
/// should then subsequently quit.
|
||||||
fn get_work(&mut self) -> Option<Work> {
|
fn get_work(&mut self) -> Option<Work> {
|
||||||
|
let mut value = self.rx.try_recv();
|
||||||
loop {
|
loop {
|
||||||
|
// Simulate a priority channel: If quit_now flag is set, we can
|
||||||
|
// receive only quit messages.
|
||||||
if self.is_quit_now() {
|
if self.is_quit_now() {
|
||||||
return None;
|
value = Ok(Message::Quit)
|
||||||
}
|
}
|
||||||
match self.rx.try_recv() {
|
match value {
|
||||||
Ok(Message::Work(work)) => {
|
Ok(Message::Work(work)) => {
|
||||||
self.waiting(false);
|
|
||||||
self.quitting(false);
|
|
||||||
return Some(work);
|
return Some(work);
|
||||||
}
|
}
|
||||||
Ok(Message::Quit) => {
|
Ok(Message::Quit) => {
|
||||||
// We can't just quit because a Message::Quit could be
|
// Repeat quit message to wake up sleeping threads, if
|
||||||
// spurious. For example, it's possible to observe that
|
// any. The domino effect will ensure that every thread
|
||||||
// all workers are waiting even if there's more work to
|
// will quit.
|
||||||
// be done.
|
self.waiting();
|
||||||
//
|
self.tx.send(Message::Quit).unwrap();
|
||||||
// Therefore, we do a bit of a dance to wait until all
|
return None;
|
||||||
// workers have signaled that they're ready to quit before
|
|
||||||
// actually quitting.
|
|
||||||
//
|
|
||||||
// If the Quit message turns out to be spurious, then the
|
|
||||||
// loop below will break and we'll go back to looking for
|
|
||||||
// more work.
|
|
||||||
self.waiting(true);
|
|
||||||
self.quitting(true);
|
|
||||||
while !self.is_quit_now() {
|
|
||||||
let nwait = self.num_waiting();
|
|
||||||
let nquit = self.num_quitting();
|
|
||||||
// If the number of waiting workers dropped, then
|
|
||||||
// abort our attempt to quit.
|
|
||||||
if nwait < self.threads {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// If all workers are in this quit loop, then we
|
|
||||||
// can stop.
|
|
||||||
if nquit == self.threads {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// Otherwise, spin.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(TryRecvError::Empty) => {
|
||||||
self.waiting(true);
|
// If it was the last running thread, then no more work can
|
||||||
self.quitting(false);
|
// arrive, thus we can safely start quitting. Otherwise, a
|
||||||
if self.num_waiting() == self.threads {
|
// thread may spawn new work to be done.
|
||||||
for _ in 0..self.threads {
|
if self.waiting() == 1 {
|
||||||
self.tx.send(Message::Quit).unwrap();
|
// Every other thread is blocked at the next recv().
|
||||||
}
|
// Send the initial quit message and quit.
|
||||||
} else {
|
self.tx.send(Message::Quit).unwrap();
|
||||||
// You're right to consider this suspicious, but it's
|
return None;
|
||||||
// a useful heuristic to permit producers to catch up
|
|
||||||
// to consumers without burning the CPU. It is also
|
|
||||||
// useful as a means to prevent burning the CPU if only
|
|
||||||
// one worker is left doing actual work. It's not
|
|
||||||
// perfect and it doesn't leave the CPU completely
|
|
||||||
// idle, but it's not clear what else we can do. :-/
|
|
||||||
thread::sleep(Duration::from_millis(1));
|
|
||||||
}
|
}
|
||||||
|
// Wait for next `Work` or `Quit` message.
|
||||||
|
value = Ok(self.rx.recv().expect(
|
||||||
|
"channel disconnected while worker is alive",
|
||||||
|
));
|
||||||
|
self.resume();
|
||||||
|
},
|
||||||
|
Err(TryRecvError::Disconnected) => {
|
||||||
|
unreachable!("channel disconnected while worker is alive");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1641,44 +1601,15 @@ impl<'s> Worker<'s> {
|
|||||||
self.quit_now.load(Ordering::SeqCst)
|
self.quit_now.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the total number of workers waiting for work.
|
/// Sets this worker's "running" state to false. Returns the previous
|
||||||
fn num_waiting(&self) -> usize {
|
/// number of running workers.
|
||||||
self.num_waiting.load(Ordering::SeqCst)
|
fn waiting(&self) -> usize {
|
||||||
|
self.num_running.fetch_sub(1, Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the total number of workers ready to quit.
|
/// Sets this worker's "running" state to true.
|
||||||
fn num_quitting(&self) -> usize {
|
fn resume(&self) {
|
||||||
self.num_quitting.load(Ordering::SeqCst)
|
self.num_running.fetch_add(1, Ordering::SeqCst);
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets this worker's "quitting" state to the value of `yes`.
|
|
||||||
fn quitting(&mut self, yes: bool) {
|
|
||||||
if yes {
|
|
||||||
if !self.is_quitting {
|
|
||||||
self.is_quitting = true;
|
|
||||||
self.num_quitting.fetch_add(1, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if self.is_quitting {
|
|
||||||
self.is_quitting = false;
|
|
||||||
self.num_quitting.fetch_sub(1, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets this worker's "waiting" state to the value of `yes`.
|
|
||||||
fn waiting(&mut self, yes: bool) {
|
|
||||||
if yes {
|
|
||||||
if !self.is_waiting {
|
|
||||||
self.is_waiting = true;
|
|
||||||
self.num_waiting.fetch_add(1, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if self.is_waiting {
|
|
||||||
self.is_waiting = false;
|
|
||||||
self.num_waiting.fetch_sub(1, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user