Порядок блокировки мьютекса не гарантируется;первый поток может получить блокировку 100% времени, в то время как второй поток 0%
. Потоки запланированы ОС, и вполне возможен следующий сценарий:
- ОС отдает процессорное время первому потоку и получает блокировку
- ОС отдает процессорное время второму потоку, но блокировка снята, следовательно, она переходит в спящий режим
- Первый поток снимает блокировку, но ОС по-прежнему может запускаться.Он идет на другую итерацию цикла и повторно получает блокировку
- Другой поток не может продолжить, потому что блокировка все еще снята.
Если вы дадите второму потоку больше временичтобы получить блокировку, вы увидите ожидаемую схему пинг-понга, хотя нет никакой гарантии (плохая ОС может решить никогда не выделять процессорное время некоторым вашим потокам):
use std::sync::{Arc, Mutex};
use std::{thread, time};
fn main() {
let data1 = Arc::new(Mutex::new(1));
let data2 = data1.clone();
let ten_millis = time::Duration::from_millis(10);
let a = thread::spawn(move || loop {
let mut data = data1.lock().unwrap();
*data += 1;
if *data > 10 {
break;
}
drop(data);
thread::sleep(ten_millis);
println!("ping ");
});
let b = thread::spawn(move || loop {
let mut data = data2.lock().unwrap();
*data += 1;
if *data > 10 {
break;
}
drop(data);
thread::sleep(ten_millis);
println!("pong ");
});
a.join().unwrap();
b.join().unwrap();
}
Вы можете проверить, чтоиграя со временем сна.Чем меньше время сна, тем более нерегулярным будет чередование пинг-понга, и при значениях, меньших 10 мс, вы можете увидеть пинг-пинг-понг и т. Д.
По сути, решение, основанное на времени, - этоплохо по дизайну.Вы можете гарантировать, что за «ping» последует «pong», улучшив алгоритм.Например, вы можете напечатать «ping» для нечетных чисел и «pong» для четных чисел:
use std::sync::{Arc, Mutex};
use std::{thread, time};
const MAX_ITER: i32 = 10;
fn main() {
let data1 = Arc::new(Mutex::new(1));
let data2 = data1.clone();
let ten_millis = time::Duration::from_millis(10);
let a = thread::spawn(move || 'outer: loop {
loop {
thread::sleep(ten_millis);
let mut data = data1.lock().unwrap();
if *data > MAX_ITER {
break 'outer;
}
if *data & 1 == 1 {
*data += 1;
println!("ping ");
break;
}
}
});
let b = thread::spawn(move || 'outer: loop {
loop {
thread::sleep(ten_millis);
let mut data = data2.lock().unwrap();
if *data > MAX_ITER {
break 'outer;
}
if *data & 1 == 0 {
*data += 1;
println!("pong ");
break;
}
}
});
a.join().unwrap();
b.join().unwrap();
}
Это не лучшая реализация, но я попытался сделать это с минимальным количеством модификаций дляоригинальный код.
Вы также можете рассмотреть реализацию с Condvar
, на мой взгляд, лучшее решение, так как оно позволяет избежать занятого ожидания мьютекса (ps: также устранено дублирование кода):
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
const MAX_ITER: i32 = 10;
fn main() {
let cv1 = Arc::new((Condvar::new(), Mutex::new(1)));
let cv2 = cv1.clone();
let a = thread::spawn(ping_pong_task("ping", cv1, |x| x & 1 == 1));
let b = thread::spawn(ping_pong_task("pong", cv2, |x| x & 1 == 0));
a.join().unwrap();
b.join().unwrap();
}
fn ping_pong_task<S: Into<String>>(
msg: S,
cv: Arc<(Condvar, Mutex<i32>)>,
check: impl Fn(i32) -> bool) -> impl Fn()
{
let message = msg.into();
move || {
let (condvar, mutex) = &*cv;
let mut value = mutex.lock().unwrap();
loop {
if check(*value) {
println!("{} ", message);
*value += 1;
condvar.notify_all();
}
if *value > MAX_ITER {
break;
}
value = condvar.wait(value).unwrap();
}
}
}