Where are the knowledge gaps

What kinds of topics, tutorials, questions, areas of interest do you have?

Looking for known unknowns.

2 Likes

Why it takes Coding Studio so long to update anything

8 Likes

Exactly why and how are semaphores and mutexes used? I know it has something to do with preventing race conditions in multithreading, but I’ve never had any issues without them and thus have never really learned how to use them.

3 Likes

+1. To my understanding they lock a variable so it can’t be changed while a thread is working on it, but I don’t really understand when / how to use them. I too have never had issues without them, but I wonder if my variable manipulations are simple enough that they all get done in one cycle of the thread and locking them isn’t nescessary. I’m guessing they’re only really required in higher level programs where you’re doing complex operations of variables in threads, but I’m not sure. I might even be dead wrong on how I think they’re used as well.

A mutex is more useful for systems that should not be interfered with until everything in the mutex locked section has been completed. You don’t need to worry too much about variables being changed, unless you’re storing temporary values in a global variable in more than one place.

One reason to use a mutex with V5 would be to manage the 50ms blackout window on sending messages to the controller with RMS or PROS (if I recall correctly, VCS just delays until the window is over). You could lock the controller screen mutex, send your message, wait 50ms, then unlock the controller screen mutex. Any other thread would then have a way of knowing whether the controller’s timeout window was ongoing or not because their attempts to lock the controller screen mutex would fail.

1 Like

In a true multitthreaded system it matters a lot more. Or in a real embedded system with a lot of hardware interrupts. In vex the processor only yields in specific circumstances so it is less of an issue. It’s never a bad idea to use them.

3 Likes

For auton/skills: Probably the biggest one is motion planning as a topic. Also, things like how not to get knocked off course, how to detect it, and what to do when it happens.

2 Likes

Imagine you had a resource called balance which stores the amount of money in a bank account. It’s a busy system so multiple threads are needed for parallelism.

Whenever a transaction is being considered, let’s say:

  1. The value of balance is retrieved.
  2. Money is subtracted or added to balance.
  3. Balance is updated iff the transaction would still result in a positive balance else an error is returned to whoever initiated the transaction

This is a non-atomic operation as checking and updating will both take more than one cycle.

This is where a lock is useful. Instead we do the following:

  1. Get a lock on the resource (or wait for one to be available)
  2. The value of balance is retrieved.
  3. Money is subtracted or added to balance.
  4. Balance is updated iff the transaction would still result in a positive balance else an error is returned to whoever initiated the transaction
  5. Release the lock

Semaphores are basically lists of locks with a special counter that keeps track of which locks are available at any given time. Useful here and there. All of these ideas come from operating systems concepts mostly. At least in my CS undergrad this si where they showed up.

5 Likes

Thanks. So semaphores are essentially lists of mutexes? And mutexes are signals used to inform other threads not to proceed until the resource is no longer in use?

What distinguishes a mutex from, say, having a global variable locked and doing this?

while(locked){
    sleep(1);
}
locked = true;
<math vulnerable to race conditions>
locked = false;

Does a mutex do something to the scheduler to ensure that the thread is not interrupted while it holds the mutex?

Typically no. A race condition is still possible with two locks. There is a way around it but it gets complicated. Look into priority inversion schedulers.

However, what you describe is a busy wait.

More complex schedulers know when a thread / task is waiting and basically sleeps them indefinitely until the thread acquires the mutex.

Semaphores are powerful in that they can basically give out up to X mutexes and guarantee that no more than X will ever be in use.

A semaphore of size 1 basically is a mutex and some systems will implement locks in this way.

3 Likes

In addition to the great answers here, I’d recommend checking out this page from FreeRTOS’s documentation (and the other documentation on that site- I think they do a good job of explaining the fundamentals). Of particular note are the sections on mutexes, semaphores, and task notifications (which are essentially a cool specialized version of semaphores).

1 Like

Thanks for the explanations and links.
So, would this be a correct use of mutexes in PROS?

Code
#include "main.h"
#include <sstream>

int totalTicks;

pros::Mutex mutex1;
#define leftDrive "0"
#define rightDrive "1"

std::vector<int> values {
  0,
  0
};

std::vector<pros::ADIEncoder> encoders {
  pros::ADIEncoder(1,2),
  pros::ADIEncoder(3,4)
};
void counter(void* param){
  //Convert string input to an int
  int port;
  std::stringstream temp((char*)param);
  temp >> port;

  while(true){
    //get the abs of the delta in ticks that have happened since the last update
    int currentTicks = encoders[port].get_value();
    int previousValue = values[port];
    int absDelta = abs(currentTicks - previousValue);

    //add that number of ticks to the total
    mutex1.take(TIMEOUT_MAX);
    totalTicks += absDelta;
    mutex1.give();

    //try not to completely hog the cpu
    pros::delay(1);
  }
}

void opcontrol(){
  //create a thread for each encoder that adds the encoder's ticks to the total
  pros::Task counter1(counter, (void*)leftDrive);
  pros::Task counter2(counter, (void*)rightDrive);

  while(true){
    //print the total number of ticks to the console
    std::cout << totalTicks;
    pros::delay(50);
  }
}

(I do realize this could be done without separate threads, and it probably would be better to do so, but this is just an example I came up with that isn’t completely arbitrary)

1 Like

Yes that works fine. An important thing to notice is that you didn’t wrap your access to totalTicks in opcontrol with mutexes and that is OK.

Some extra details as to what’s going on:

Reading totalTicks is performed in one instruction, so it can’t be interrupted. totalTicks += absDelta, on the other hand, needs a few instructions to 1) load from memory, 2) add absDelta, and 3) store it back into memory. Suppose you’re running the leftDrive task. You could feasibly switch to the rightDrive task after you’ve loaded totalTicks from memory but before you store the updated result. The rightDrive task will perform its totalTicks update. Now, when you switch back to the leftDrive, it’s going to finish adding absDelta to whatever it had loaded from memory from before the context switch and store the result. The result is now incorrect because you lost the addition that rightDrive task performed. The mutex guards against this by only allowing one task to “own” it. You’ve added meaning to the mutex and say “if a task owns mutex1, it can modify totalTicks.”

1 Like

Takes 8 weeks to ship the next firmware update

2 Likes