- Let's take a quick recap
of how to define and use static immutable variables.
Here's a static immutable variable, message.
I've initialized it with a compile time constant, hello.
So that initialization would happen at compile time,
and message is not mutable
so the message will always be, hello.
Okay, I've got another immutable variable here, timestamp.
The value isn't initialized at compile time,
the value is initialized lazily at run-time on first usage.
The first time you access timestamp
the code in here will be lazily evaluated, as we've seen,
and assigned the timestamp.
Thereafter timestamp is immutable.
So in both of these cases
message is initialized at compiled time and is immutable,
timestamp is initialized there at run-time
but is thereafter also immutable,
so once it's initialized it's fixed.
Okay, well, what about having mutable statics?
You can define mutable statics,
you give it the initial value but it can change afterwards.
Right, so that sounds easy.
It isn't, because thread-safety kicks in.
The Rust compiler enforces thread-safety,
it doesn't let you just change a static like this.
You could try it, you get an error.
What I tried to do here
is I've tried to define a static local,
as it happens to be local,
it could have been global, happens to be local.
I've tried to define a static variable
as mutable, local count,
so initialize it to zero initially and then increment it.
Every time you call a function
the value would be incremented.
That won't compile, you get an error on the statement.
The code isn't thread-safe,
mutable threads might be trying to access
that particular instruction at the same time,
and they could, well, coincide with each other
and then you've got chaos.
So Rust doesn't allow you to...
I mean, you can declare a mutable static
but you can only update it
if you can promise to be thread-safe,
and this isn't thread-safe.
So how do you do it?
Well, one approach to achieve thread-safety
would be to lock the variable using a mutex.
A mutex is a mutual exclusion lock,
so you write your code so that it locks a variable,
updates it, and then unlocks the variable.
So you have a mutex that protects access,
it locks access to the variable,
only one thread is allowed to update the variable at a time.
Okay, so that's the idea of a mutual exclusion lock,
similar to what you'd have in C++, you can a mutex.
And in Java you can have the lock keyword
to synchronize access to a variable.
So other threads block until the lock is released.
Thread number one locks the mutex
and has access to the object that the mutex is protecting.
If thread number two comes along
and tries to lock the mutex,
it can't because thread one already has it.
Thread two blocks and waits
until thread one finishes and releases the lock,
and then thread two can kick in.
It's like having a room
where only one person is allowed in at a time,
you have to wait for the other person to leave
before you can enter.
So we'll discuss thread-safety
in mutexes later in the course.
What about this? This is one,
bit of a hack, really,
but sometimes, you know, it can be useful.
Rust has the concept of unsafe code,
you can mark a function as unsafe
and Rust will allow you to break its protection rules.
If you have an unsafe function,
then thread-safety is not enforced by Rust any longer,
it allows you to get away with it, really.
So this would be a little bit of a cheat
but you can do it.
So this unsafe code here,
because it's unsafe, that's a key word,
Rust will allow me to do things
which would normally be unsafe.
It assumes that I know what I'm doing,
could be a risky strategy
but Rust assumes that I know what I'm doing.
The thing is, if you have an unsafe function
you can only call that from another unsafe function,
so somewhere in your application
you've gotta take thread-safety into account.
So I've got some higher level function here
which calls some unsafe function,
this block also has to be marked as unsafe.
You can have an unsafe function
where the whole function is kind of up to you,
and you can also have an unsafe block.
An unsafe block can call an unsafe function,
like I've done here.
So this would be the the easiest way
to be able to update a static,
to have it mutable and to have it updated.
Declaring it as mutable wasn't the problem,
it's when you then try to update it that it's not safe.
So you have to explicitly say to the compiler,
"Let me do it, I know what I'm doing."
And then when you call that function,
the call itself must be enclosed in an unsafe wrapper.
This is not the recommended approach,
it's the easiest way for now.
Thread safety done properly, we'll discuss later.
There is another approach, depending on what you are doing.
If all you want to do
is to just kind of increment an integer safely,
Rust has a bunch of atomic data types.
Atomic data types perform operations
in a thread-safe manner,
so for the primitive types like I8 and U8, I16, I32, bool,
there are thread-safe wrapper classes
which, when you try to change the value
of an atomic integer,
it automatically wraps it in the mutex,
effectively, to give you thread-safety.
So we have classes or structures, I should say,
such as AtomicI8, a thread-safe wrapper
around an 8-bit integer.
So AtomicI8 has methods that are thread-safe
which you can use to increment the value,
decrement the value,
and perform lots of other operations as well
on 8-bit integers or 8-bit unsigned integers,
or Booleans, and so on.
So atomic data types give you the ability
to modify effectively a primitive,
but in a type safe fashion.
Atomically in other words, so you can perform the operation
without being interrupted by another thread.
So atomic types lock the value implicitly.
When you create an AtomicI8,
it effectively wraps with the mutex, the underlying integer.
Whenever you try to do anything to the atomic value,
it ensures that the value is locked
to give you exclusive access.
So thread-safety out-of-the-box,
it means you don't have to write any mutex code yourself,
just using these data types are implicitly thread-safe.
So let's see an example
of how these atomic types might work.
So here's an example that's gonna use atomics
in conjunction with static variables.
Quite a few things I need to explain here.
AtomicI32 and all the other atomic data types
are located in the atomic module,
which is in the sync parent module
which is in the standard crate, okay?
And we also have another type called ordering,
which we'll see coming into play a bit here.
That turns out to be a bit tricky
so let me come onto that in a moment.
This function here isn't unsafe, okay,
I did not declare it as unsafe.
I've got an atomic integer,
the atomic integer itself doesn't change
but the value inside it will.
So I didn't declare the atomic integer, local count,
I didn't declare the variable as mutable
but the value that it contains inside it,
you can think of an atomic integer
as being like a wrapper over an I32, okay?
And the object itself,
the atomic object itself isn't mutable
but the value inside it is, okay,
and the atomic type I32, AtomicI32,
has various different methods you can call
to change the value inside the wrapper.
Okay, so it's like a type safe, a thread-safe wrapper
over a 32-bit integer.
So I give it the value zero, initially,
and then upon this atomic integer
there are various methods you can call basically fetch_add.
It fetches the value, which is currently zero,
and it adds one to it,
and then it does it in a relaxed order.
I'll like say a little bit more
about this ordering parameter in a minute.
When you have thread-safety
an atomic operations turns out to be quite complicated
because there are lots of optimizations
that the compiler could do, or the hardware could do,
kind of reordering statements inside your code.
So this flag here,
it specifies what type of reordering
you've allowed to happen.
So let's just ignore that for now,
and I'll say a few more words about it in a minute.
The net effect is that I had a local counter
that was zero, initially,
and here I basically said,
it would be like saying I + = 1
but in a thread-safe fashion,
and then I output the value of local count.
Atomics support debug formatting,
so you can say :?
and it'll output the value of the atomic integer.
So let's take a look at the documentation.
If you go to the documentation, it'll give you information
about all of these standard synchronized atomic types
from the standard sync atomic module.
Let's have a look at the documentation.
Okay, so here's the documentation for the atomic module
and the sync module, and the stud crate,
and there's some kind of general explanation here
which you can read.
I wouldn't recommend that you don't have to read this,
you know, it's quite dry, really quite technical.
Let's have a look at the structures, though.
These are the atomic structures,
wrappers around Boolean I8, I16, I32, I64, Isize,
Ptr we'll talk about later, unsigned equivalents like that.
Let's have a look at AtomicI32,
that's what we were using, wasn't it.
So an integer type
which can be safely shared between threads,
a thread-safe 32-bit integer.
These are all the methods you can call upon an AtomicI32,
okay, so you can fetch the value from memory
and add a value to it, that's the function we looked at.
You can subtract, you can do kind of Boolean logic,
or and and, and not,
so let's have a look at the fetch and add method.
Let's ignore the self parameter just for now.
It takes the value that you want to add,
in our example, we added one.
The second parameter is an ordering.
Ordering is quite tricky,
let's have a look at the documentation for ordering.
(mouse clicks)
Ordering is an enum.
I dunno if you were gonna guess that.
You can specify how are you going to allow the compiler
and the hardware to order
the kind of statements that need to be executed.
All right, now I'm not gonna get into the theory of this
because it's kind of out of scope.
What I will say is that the way that Rust
manages memory ordering is basically the same as in C++20.
I'm not sure if that helps, but it's the same model.
And if you want to kind of get a bit more insight
into what this actually means, there's a link here.
You can go to the nomicon, the Rustonomicon,
easy for you to say.
And this is quite detailed information, really,
about how Atomics work in Rust
and how the compiler is allowed
to reorder statements to optimize,
and how the hardware
is allowed to reorder statements, as well, to optimize.
And it's quite a good job they've actually done
of explaining this.
At the end it talks about the difference
between all the different types of reordering allowed,
sequentially consistent, release acquire, and then relaxed.
All right, so if you really want to get into this in detail,
this is the place to go.
In most cases, you can just get away
with the relaxed ordering, and that would be fine.
So then we have a static,
which is, although not declared mutable here,
the value inside the atomic can be mutated.
So I can use the atomic API to fetch the value,
add one to it, and then store the value back again,
and then output the value like so.