Rust Tutorial: Memory Safety and Concurrency

Last update: 04/12/2025
Author Isaac
  • Rust ensures memory safety at compilation through ownership, borrowing, and lifetimes, without using garbage collection.
  • The type system and aliasing rules allow concurrency without data races using mutexes, channels, and smart pointers.
  • Cargo, crates.io, and an active ecosystem simplify dependency management, compilation, testing, and deployment.
  • Understanding structs, enums, Option, and Result is key to handling errors and modeling safe data in concurrent applications.

C programming language vs Rust: advantages and disadvantages

Rust has become one of those languages ​​that every systems developer hears about repeatedly : fast like C and C++, but with an almost obsessive focus on memory safety and well-executed concurrency. This isn't just empty marketing: its design revolves around the compiler detecting errors at compile time—errors that in other languages ​​you only see when the system is already in production… or when it crashes.

If you're interested in understanding how Rust achieves secure memory without garbage collection and concurrency without fear of data races , this tutorial is for you. We'll cover everything from the fundamentals of the language and its ecosystem to key concepts like ownership, borrowing, composite types, tools like Cargo, and even take a look at atomicity and locking from a more accessible perspective for those new to concurrency, all with a focus on security and performance.

Rust Tutorial: Performance, Memory Safety, and Concurrency

Rust is a general-purpose, multi-paradigm programming language designed for both low-level systems programming and high-level projects , ranging from operating systems , game engines, and browsers to high-performance web services. It was originally developed at Mozilla with the goal of improving software security, especially in sensitive components such as browser engines.

Its defining characteristic is that it guarantees memory safety at compile time without using a garbage collector. Instead, Rust employs an ownership system and a borrow checker that tracks the lifetime of each value and its references. This prevents common problems such as dangling pointers, buffer overflows, and memory leaks without requiring automatic reference counting or garbage collection.

Furthermore, Rust is designed to facilitate safe concurrency . Its type and ownership model prevents data races between threads, at least while remaining in safe Rust code. This means that many dangerous situations are detected at compile time, before a single line of code is executed.

For all these reasons, major companies like Dropbox, Microsoft, Amazon, and Google have adopted Rust in critical parts of their infrastructure. And it's no coincidence that it has consistently topped Stack Overflow polls for years as one of the "most loved" languages ​​among developers: it combines C++-style performance with a modern toolset (Cargo, crates.io) and a very active community, the so-called Rustaceans.

Basic concepts: programming language, types and memory

Before getting into the specifics of memory safety and concurrency, it's worth clarifying some general concepts that come up all the time when you work with Rust, especially if you come from other languages ​​or are just starting to program.

A programming language is, ultimately, a set of rules and structures that allows you to describe algorithms and transform them into executable programs. Rust compiles to native machine code using its compiler rustcTherefore, the performance you get is usually on par with C and C++.

Memory management is the process by which a program allocates and frees blocks of memory while it is running . Errors in this area are often fatal: memory leaks (failing to free unused memory), data corruption from out-of-bounds writing, or using memory after it has been freed. Rust addresses this with a very strong type system and formal rules for ownership, borrowing, and lifetimes.

Rust also features terms like smart types and pointersA type describes what kind of data a variable stores (integers, floats, strings, structures, etc.) and how it can be manipulated. Smart pointers (for example, Box, Rc y Arc) are structures that encapsulate memory addresses and add extra logic to manage resources safely, such as counting shared references or moving values ​​to the heap.

In the field of concurrency, concepts such as race conditions, mutexes, and channels become essential: a race condition occurs when several threads access and modify a shared resource simultaneously without proper coordination; a mutex (mutual exclusion) ensures that only one thread enters the critical section at a time; and channels allow messages to be sent between threads without directly sharing memory.

Why learn Rust: Memory safety and fearless concurrency

Rust has earned its reputation by offering three invaluable pillars for modern programming : performance, security, and up-to-date tools. Let's explore why these points are so important.

In terms of performance, Rust compiles directly to native binaries without requiring a virtual machine or interpreter. The zero-cost abstractions model aims to ensure that high-level abstractions do not add overhead at runtime. Therefore, it is ideal for developing systems, video games , browser components, or low-latency microservices.

Memory safety relies on its ownership and borrowing system . There's no garbage collector, but the compiler knows exactly who owns each resource, when it's no longer needed, and when it can be freed. This prevents leaks, dangling pointers, and many of the errors that have traditionally made C and C++ programming so risky.

In the area of ​​competition, Rust pursues what is usually called “concurrency without fear”The type system itself prevents data roots from existing in secure code. If you want to share mutable data between threads, you'll need to use appropriate primitives such as Mutex, RwLock o Arc, and the compiler will ensure that the aliasing and mutability rules are respected.

  Ultimate Guide: How AI Credits Work in Microsoft 365 and Copilot (Word, Excel, PowerPoint, Outlook, Designer...)

The development experience is enhanced by modern tools like Cargo , the integrated package manager and build infrastructure, and a broad ecosystem of libraries (crates) covering everything from asynchronous networking (Tokyo) to web frameworks (Actix, Rocket, Axum). All of this is supported by an open, prolific, and remarkably patient community, especially for beginners.

Installation and essential tools: rustup, rustc and Cargo

To write and run your first programs in Rust, the usual thing to do is to start by installing the official toolchain using rustup (see the full introduction to Rust ), a simple installer and version manager that works on the main operating systems.

With rust up You can install, update, and switch between different versions of Rust (stable, beta, nightly) without breaking anything. Simply go to the official Rust tools page and follow the steps for your system. Once installed, the compiler will be available. rustc, the project manager cargo and the own rustup trust your terminal.

the compiler rustc It's what transforms your source code into executable binaries or libraries. Although you could invoke it directly with commands , the rustc main.rsIn practice, you'll almost always work through Cargo, which handles the calls to rustc with the right options.

The central tool of the workflow is RoleWith just a few commands, you can create new projects, manage dependencies, compile, run, test, and publish packages on crates.io. Some commonly used basic commands are: cargo new, cargo build, cargo run, cargo test y cargo check, which checks the code without producing the final executable, ideal for detecting errors quickly.

If you want to tinker without installing anything, Rust Playground (the official online executor) and platforms like Replit allow you to write and run small pieces of code from your browser, perfect for experimenting with memory and concurrency examples without having to set up the entire environment.

Your first program: Hello, Rust, and basic flow

The classic way to start a conversation in any language is with the famous "Hello, world." In Rust, a file main.rs at least could contain something as simple as a function main that prints a string on the screen.

The key word fn indicates that we are defining a function, and main This is the program's entry point. The function's code block goes within curly braces. To write to the console, use the macro println!, which accepts a string literal (or a template with bookmarks) and sends it to standard output ending in a newline character.

If you compile directly with rustc main.rs, you will get an executable binary (for example, main o main.exe (depending on the system). When you run it, you'll see the message in the terminal. But the idiomatic way to work with Rust is to let Cargo take the lead on the project.

With cargo new nombre_proyecto A folder structure is automatically created with a src/main.rs already prepared with a "Hello, world" and a file Cargo.toml which contains metadata and future dependencies. From there, cargo run compile and run the binaryand it only recompiles when it detects changes.

This way of working is not only convenient, but it gets you used to using the standard Rust ecosystem from the beginning, which is very useful when you start adding crates for concurrency, networking, testing, or whatever you need.

// We declare the main function: program entry point fn main() { // We use the println! macro to print text to the console println!("Hello, world!"); }

Variables, mutability, and basic data types

In Rust, variables are declared with the keyword let, and by default are immutableIn other words, once you assign them a value, you cannot modify it unless you explicitly declare it as mutable with mut.

Immutability by default helps avoid subtle logic errors, especially in concurrent programs where multiple threads might want to change the same value. If you need to change it, you write something like let mut contador = 0;From there you can reassign new values ​​to contador.

Rust also allows the so-called shadowingYou can declare a new variable with the same name within the same scope, hiding the previous one. This is not the same as mutating, because you are creating a new value (which can even be of a different type). For example, you can convert from a string to an integer using the same name, as long as it is a new declaration with let.

Rust's type system is static, which means that The type of each variable is known at compilationHowever, type inference is quite powerful: if you write let x = 5;The compiler assumes that it is a i32 Unless you tell it otherwise. You can add notes such as let x: i64 = 5; when you want to be explicit.

Among the available scalar types are the signed and unsigned integers (i8, u8, i32, etc.), the floating ones (f32, f64), the Booleans (bool) and Unicode characters (char). These simple types are usually cheap to copy and many implement the trait Copywhich means that when you assign them or pass them to a function, they are copied instead of moved.

Strings in Rust: &str and String

Text handling in Rust can be a little confusing at first because it clearly distinguishes between chain “slices” and proprietary chainsThe two key pieces are &str y String.

Un &str is a slice of immutable chainA view of a UTF-8 byte sequence stored somewhere. Typical examples include literals like "Hola"which are of the type &'static str (They exist for the entire life of the program and are embedded in the binary.) Slices do not own the data; they only point to it.

  Rufus: A complete guide to creating bootable USB drives with UEFI, Secure Boot, and GPT

String, on the other hand, is a own string, mutable and hosted in the heapIt can be resized, concatenated, passed between functions by moving its property, etc. It is often used when you want to build dynamic text or store it long-term within structures.

In many scenarios you will transform between one and the other: for example, you will create a String::from("hola") from a sliceor you'll borrow a &str a String by passing references to functions that only need to read.

This separation between owned and borrowed data is key to memory management and extends to the rest of the language: collections, structures, and enums follow the same ideas of who owns and who only looks.

Functions, control flow, and comments

Functions in Rust are defined with fn and allow the program to be organized into reusable logical units. Each function specifies the type of its parameters and its return type following an arrow ->If it returns nothing meaningful, the unitary type is assumed. ().

An important detail is that the last expression in a function (or any block) without a semicolon is taken as the implicit return value. You can use return for early returnsBut in idiomatic code, you often simply leave the final expression without. ;.

The control flow is handled with the classics if/elseloops loop, while y forIn Rust, if It is an expression that returns a valueso you can use it directly in a letprovided the branches return the same type. Loops for They typically iterate over ranges or collection iterators and are the recommended option instead of manual indexes.

To document the code and make life easier for whoever comes after (including yourself in a month), you can use line comments with // or block with /* ... */In addition, Rust offers documentation comments with /// which become generated documents, although that fits more into large projects.

Ownership, lending, and lifetimes: the foundation of memory security

Here we arrive at the heart of Rust's memory model: the system of ownership, borrowing, and lifetimes . These rules ensure that references are always valid and that memory is safely released without accumulated garbage.

The basic rules of ownership are simple to state, although they may be difficult to internalize at first: Each value has a single owner.There can only be one owner at a time; and when the owner leaves its scope, the value is destroyed and its memory is released. This applies, for example, to a String: upon completion of the block where it was declared, it is automatically invoked drop which frees up the heap memory.

When you assign a proper value to another variable or pass it by value to a function, the property is moved. This means that the original variable is no longer valid after the move . This move semantics prevents double releases, because there are never two owners trying to release the same resource.

To allow multiple parts of the program to access the same value without changing ownership, Rust introduces references and borrowing. When you borrow, you create a reference &T (immutable) or &mut T (mutable) to the value without transferring ownership. The loan is limited by the loan verifier's rules., which checks that references do not outlive the data they point to and that mutable and shared accesses are not dangerously mixed.

The borrowing rules can be summarized as follows: at any given time, you can have either multiple immutable references to a value, or a single mutable reference , but not both simultaneously. This eliminates race conditions in shared memory: either there are many readers, or there is only one writer; there can never be simultaneous readers and writers accessing the same data at the same time.

Composite types: structs, enums, and smart pointers

Rust provides several ways to group related data into richer structures, starting with structs . A struct allows you to define a custom type with named fields, for example, a user with email, name, activity status, and login counter.

To create an instance of a struct, you fill in all its fields, and you can mark the variable that contains it as mutable to modify its values ​​later. There is also the struct update syntax, which allows you to build a new instance by reusing some fields from an existing one. ..otro_struct.

The enums They are another essential pillar: they allow you to define a type that can be one of several possible variants, each with its own associated data or without it. A classic example is an enum for IP addresses, with one variant V4 which stores four octets and another V6 that stores a string with IPv6 notation.

Rust's standard library includes two very important enums: Option<T> y Result<T, E>The first represents the presence or absence of a value (something or nothing), and is used to avoid null pointers; the second models operations that can return a correct result or an error, requiring that error handling be explicit and secure.

To manage dynamic memory and share data, Rust has smart pointers , the Box<T>, which moves a value to the heap and maintains unique ownership; Rc<T>, a shared reference count for single-threaded environments; and Arc<T>, similar to Rc but safe for multiple threads. Using them correctly is crucial when combining dynamic memory with concurrency.

Cargo and the Crates Ecosystem

Cargo is the glue that holds the Rust ecosystem together: manages the compilation, dependencies, and project lifecycleEach project has a file Cargo.toml which acts as a manifest, declaring the name, version, language edition, and external dependencies.

  How to change the default browser in Windows 11 step by step

The section This file allows you to list third-party crates with their versions. When you run cargo build o cargo runCargo automatically downloads these crates from crates.io, compiles them, and links them to your project. It's that easy to add, for example, random number generators, web frameworks, or cryptographic libraries.

Among the most common commands are cargo new to start binary projects o cargo new --lib for libraries; cargo build to compile in debug mode; cargo build --release to obtain an optimized, production-oriented version; and cargo test to run the battery of tests.

cargo check It deserves special mention: it compiles the code to an intermediate point without generating a binary, which makes it be very fast at detecting compilation errorsIt's perfect for iterating quickly while the borrow checker points out problems with properties, references, and lifetimes.

Thanks to this ecosystem, it's common to structure your projects as small, well-defined crates, sharing code between them and reusing solutions created by the community. For advanced concurrency, for example, you'll have crates like Tokio for asynchronous programming or crossbeam for high-performance concurrent data structures.

Concurrency in Rust: threads, mutexes, channels, and atomics

Concurrency is one of the reasons Rust is generating so much interest: it allows you to take advantage of multi-core processors without falling into the typical pitfalls of threads and shared memory . If this is your first time encountering these topics, it's helpful to distinguish between several concepts.

Concurrency involves executing multiple tasks that overlap in time, either on one or multiple cores. In Rust, you can create system threads to perform work in parallel, and the language guides you to ensure that data sharing between them is safe. A classic error is the race condition, where two threads access and modify data simultaneously, and the result depends on the execution order—something very difficult to debug.

To coordinate access to shared data, Rust relies on primitives such as mutexwhich guarantee mutual exclusion: only one thread can enter the critical section at a time. In combination with Arc<T> To share ownership between threads, it is possible to build shared data structures that comply with the rules of ownership and borrowing.

Another common form of inter-thread communication, heavily encouraged in Rust, is message passing using channels . A channel has a sending end and a receiving end; threads pass messages (values) through it, reducing the use of mutable shared memory and simplifying reasoning about the system state.

When delving deeper into low-level concurrency, atomic types emerge . These are variables accessed through operations that are indivisible from a thread perspective. This allows for the implementation of shared counters, state flags, lock-free queues, and more. Mastering atomic types requires understanding memory models and access commands, so many developers prefer to start with mutexes and channels before diving into these details.

First steps and resources for learning concurrency and atomics

If you're entering the field with no prior experience, it's wise to build a solid foundation of general concepts before tackling advanced tools like Rust's atomic types. Books like "Programming Rust" offer a gradual introduction, but it's normal for works focused on atomic types and locks to seem dense at first.

For greater ease, it's advisable to first familiarize yourself with Traditional threads, mutual exclusion, and message passing in Rust. Play with examples of std::thread, std::sync::Mutex, std::sync::Arc and channels std::sync::mpsc It helps you understand how the compiler guides you and what errors it avoids.

In parallel, it's highly recommended to review introductory resources on concurrency in general, even if they aren't specifically focused on Rust: understanding what race conditions are, what blocking means, the implications of shared memory versus message passing, and how locks are used. Once these concepts become second nature, atomic locks cease to be "black magic" and become just another tool, albeit a very delicate one.

When you return to more advanced texts about atomics and locks in Rust, it will be much easier to follow the reasoning if you already understand what problem each construct is trying to solve: from a simple thread-safe counter to lock-free structures that minimize contention.

Ultimately, Rust offers both high-level primitives and very low-level tools, and the key is to always choose the safest level of abstraction that solves your problem, resorting to atomic code. unsafe only when it truly adds value and you fully understand its implications.

This entire ecosystem of types, ownership, borrowing, crates, tools, and concurrency primitives combines to offer a language in which you can write fast, robust, and maintainable software , minimizing many kinds of errors that have historically plagued systems programming. As you practice with small projects, exercises like Rustlings, and official documentation, these concepts will go from seeming like strict rules to becoming an ally that warns you before a bug reaches production.

Introduction to the Rust language with examples-0
Related articles:
Complete Introduction to Rust: A Practical Beginner's Guide with Examples