Understanding Concurrency in Rust
Time to read: 6 minutes
Because Rust is statically typed and compiled, does not use garbage collection, and has zero-cost abstractions; application speed is a given. However, you can do even more by integrating concurrency into your application designs.
Concurrency is the ability to execute multiple tasks simultaneously, and this lies at the heart of high-performance applications, enabling them to handle complex operations efficiently. Rust makes working with concurrency a more pleasant experience.
By nature, concurrent operations are non-deterministic. As a result, debugging errors can be a nightmare. Rust resolves this by applying ownership and type checking rules at compile-time, instead of runtime. This makes it easier to detect and resolve errors.
In this article, I will show you how to take advantage of concurrency to reduce the execution time of your applications. To do this, I will show you how to build a Rust application which generates a usage report for your Twilio account.
Prerequisites
To follow this tutorial, you will need the following:
- A basic understanding of Rust
- Rust 1.67 or above and Cargo
- A Twilio account. If you don't have one, you can sign up for a free trial account
What you will build
You will be building an application that generates a usage report for your Twilio account. This report will be a spreadsheet with four sheets containing the following information:
- Account details
- All-time usage records
- Message records
- Call records
Let's get started
Where you create your Rust projects, create a new Rust project and change into it using the following commands.
Add the project dependencies
Update the dependencies
section of the Cargo.toml file, in the project's top-level directory, to match the following.
Here’s what each added crate does:
- Dotenvy: Dotenvy helps with loading environment variables. It is a well-maintained version of the
dotenv
crate. - Reqwest: This package will simplify sending API requests to Twilio.
- rust_xlsxwriter: This package helps with writing Excel files in the xlsx format.
- serde, serde_derive, and serde_json: These packages reduce the complexity of deserialising JSON responses from Twilio, so that they can be more easily used.
Set the required environment variables
Now, create a new file called .env in the project's top-level directory, and paste the following code into it.
After that, retrieve your Twilio Auth Token and Account SID from the Twilio Console Dashboard, and insert them in place of the respective placeholders.
Build the application
Now that you have your Twilio credentials, you’re set to start building the application. To make your code easy to follow, you’ll build separate modules to handle separate concerns.
Handle errors
This application will use the Result type to manage errors. There are three possible errors that could occur in the application, in the following scenarios:
- Processing an API request
- Deserializing the API response
- Writing the results to a spreadsheet
To handle these errors, you will create an enum with variants for each error. Your enum will also implement the Error and Display traits.
In the src folder at the root of your project, create a new file named error.rs and add the following code to it.
In this code, you started by creating an enum named ReportGenerationError
which encapsulates the possible errors that can occur. Next, you added implementations for the From
trait, which allows you convert the library specific errors to your custom error type. Finally, you implemented the Error
and Display
traits.
With this, you can declare the return type of your function signatures as Result<T, ReportGenerationError>
where T
is a generic type. This will simplify your error handling as you can use the ?
keyword after your function calls.
Declare the models
In the src folder, create a new file named model.rs and add the following code to it.
These structs are modelled after the structure of responses from the Twilio API, which will be written to the spreadsheet.
Retrieve your usage details from Twilio
At the moment, there’s no Twilio SDK available for Rust. However, using Twilio’s Programmable Messaging API, it is possible to send JSON requests for the purpose of retrieving account information. The next module you will add will contain the relevant functions to simplify the process of retrieving your account information from the Twilio API.
In the src folder, create a new file named twilio.rs and add the following code to it.
The make_request
function is used to send requests to the Twilio API. The API response will be returned as a string (in the absence of any error). This function takes one argument which is a string slice named endpoint
. This string is appended to the base URL for the Twilio API in order to get the specified resource.
Next, the get_account_details()
function makes a request to the base endpoint which returns the account details for the provided Twilio Auth Token and Account SID. The returned string is parsed into an AccountDetails
struct and returned accordingly.
The get_call_records()
, get_message_records()
, and get_usage_records()
functions work in a similar manner. First, a request is made using the make_request()
function (providing the appropriate endpoint). Next, instead of parsing the returned string directly, the returned string is converted to a Value object. Then the appropriate key in the value is retrieved and parsed to get the required vector of objects.
Write the results to a spreadsheet
The next thing you’ll implement is the functionality to write the retrieved records to a spreadsheet. In the src folder, create a new file named writer.rs and add the following code to it.
The write_account_details()
,write_usage_records()
, write_message_records()
, and write_call_records()
functions are used to write the corresponding records to a separate sheet in the Excel file. Each function follows the following pattern:
- Create a new sheet and give it a name.
- Write the headers for the sheet and format them using the return value of the
get_header_format()
function. - Iterate through the records, write the appropriate cell value for each record, and format using the return value of the
get_row_format()
function. - Adjust the cell sizes to make sure that the content is displayed properly. This is done using the
autofit()
function.
The only public function in this module is the write_results()
, which takes all the records (account details, usage records, message records, and call records), creates a new file, and passes each record to the appropriate function to be written accordingly. Once this is done, the file is saved and closed.
Generate a report synchronously
To generate the report synchronously, the records are retrieved one after the other via the previously declared helper functions. Once the records are available, pass them to the write_results()
function you declared earlier.
To do this, add the following function to the main.rs file in the src folder.
Remember to update your imports and module declarations, as shown below.
Generate a report concurrently
The concurrent version will use threads to make the relevant API calls. These threads will update placeholders for the respective values. Once all threads have run successfully, the records are written to the spreadsheet. To do this, add the following function to the main.rs file in the src folder.
Update the imports to match the following.
This function starts off by declaring empty state values for the records you want to get. However, these values are wrapped with some structs. The Arc and Mutex structs are provided by Rust to guard against two key pitfalls of concurrency: race conditions and deadlocks. Arc is an atomic reference counter, which allows multiple threads to update a shared value while Mutex guards the wrapped resource and ensures that only one thread can access the resource at a time.
Because of the handle.join().unwrap();
call, the main thread will wait for the secondary threads to run before proceeding.
Finally, the updated values are retrieved and passed to the write_results()
function, as usual.
Compare the results
Rust’s built-in benchmark tests are only available on Nightly Rust because they use unstable features, so you will compare the results of both functions by measuring how long they take to execute.
To do this, update the main()
function in src/main.rs
to match the following.
Remember to add the following to the imports list at the top of main.rs.
Before calling either function, the application environment variables are loaded. Next, before each function call, the current time is captured using an Instant. After the function runs, the execution time is printed by calling the elapsed()
function.
Run your application using the following command.
You should see something similar to the following as a response
While the results will vary from yours (depending on internet speed and the processing power of your workstation), you will see that the concurrent execution always performs considerably better.
You will also see a new Excel spreadsheet in the project's top-level directory, named Usage report.xlsx, containing the results returned from your Twilio account.
Now you understand the essentials of concurrency in Rust.
Well done for coming this far!! Rust has a bad rep for being verbose and difficult to learn, but once you understand what is going on, you can see that it’s not that difficult.
This is just a tip of the iceberg in terms of what Rust has to offer for concurrent applications, you can go even further by taking advantage of channels.
The entire codebase is available on GitHub, should you get stuck at any point. I’m excited to see what else you come up with. Until next time, make peace not war ✌🏾
Joseph Udonsak is a software engineer with a passion for solving challenges – be it building applications, or conquering new frontiers on Candy Crush. When he’s not staring at his screens, he enjoys a cold beer and laughs with his family and friends. Find him at LinkedIn, Medium, and Dev.to.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.