How to create a "general mutex" for several operations

Hello fellas!

I have some sort of "numb brain" state today. I know, how to solve the following problem, but I wonder, if there is a more elegant way to do it.

There is a function to be used in multi-threaded environment that has possibly three operations:

  1. write a message to a file (using a mutex)
  2. and / or(!) write the same message to stderr (using internal locking of writeln! macro or explicit locking)
  3. increment an atomic error counter

There are at least two and mostly three operations that will be executed. All operations should be performed at the same time, while another thread is waiting for its own operations to be executed. So, I need to wrap the ops in a sort of transaction. I can realize this by creating a Mutex before the first write, create a lockguard and releasing the lock after all work is done. My question is: Is there a more elegant way to do this? Because the program does not know at compile time, whether it will write to a file or to stderr or to both writers, I cannot use one of the locks, which a file mutex or a console mutex provide.

Thanks !! :slight_smile:

I assume you're saying that all three operations should be performed atomically as a single unit, by at most one thread at a time.

No, for the reasons you describe. If you're looking for confirmation, I can confirm it.

If you do use a new mutex to protect these three as a unit, you may be able to get rid of the other two mutexes (if they're under your control), and the counter could be a regular variable protected by your new mutex rather than an atomic. But this is only possible if you always access the file and the counter under protection of your new mutex.


Another thing to know when using multiple mutexes, assuming you have to do that: Always lock mutexes in the same order to avoid deadlocks, and unlock as soon as you've done the protected operation(s). So, always lock the new mutex before the other two, and for each of the other two be sure to drop the guard right after the nested operation. This allows you to use the other two mutexes separately, if necessary, and also avoids deadlocks.

2 Likes

Thank you very much. I was in doubt, if there is a better solution. Sometimes - when I think, I've found an elegant way to perform some operation - Rust comes with a better one (I fell in love with this language).

Yes, you're absolutely right. When using a "general" mutex that guards several operations, the other mutexes and atomic types are unnecessary.

Thanks again!

Your data structure for this might be like:

struct Logger(Mutex<Inner>);

struct Inner {
    log_file: Option<std::fs::File>,
    should_write_to_stderr: bool,
    error_counter: u64,
}

impl Logger {
    fn log(&self, ...) {
        let Ok(inner) = self.inner.lock() else {
            return;
        };
        
        // Now do all three operations within the lock
    }
}

This is an example of the power of Rust’s locks containing data rather than being separate from it, beyond just memory safety: the type communicates (some of) how it should be used correctly.

5 Likes

Hi kpreid,

yes if have something like this in my mind. I develop my own logger crate and want to be prepared for all peculiarities, as my old programming tutor told me "Always write your programs with belts and garters!!". This logging lib is also safe for creating a static logger instance by several threads (who the heck will do this? I don't know, but I want to exhaust all of Rust's capabilities. I am still in my learning phase).

Thanks to all!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.