Sometimes it is essential to share data and have data synchronization between the concurrent tasks. With the help of shared objects and locks, V allows you to synchronize the data between the tasks that are running concurrently. In this tutorial, we will see by example, on how to leverage shared objects and locks to synchronize data across concurrent routines using V Programming language.
This tutorial is a slightly abridged version of the section ‘Sharing data between the main thread and concurrent tasks using locks in V Language’ from the book ‘Getting Started with V Programming‘ authored by myself.
Rao, Navule Pavan Kumar. Getting Started with V Programming: An End-to-end Guide to Adopting the V Language from Basic Variables and Modules to Advanced Concurrency. N.p., Packt Publishing, 2021.
Table of Contents
- Brief introduction to V Programming Language
- Data Synchronization among Concurrent tasks
- Implementing Fund collection to demonstrate Data Synchronization in V
- Defining a Struct
- Implementing a method to collect funds
- Initializing a shared object
- Implementing Donations
- Collecting donations and adding it to the fund
- Full Code
- Conclusion
Brief introduction to V Programming Language
V is statically typed compiled programming language to build maintainable and fast software. V programming comes with high performance and simplicity, which allows software programmers to do quick and rapid prototyping of applications at scale.
You can learn more about V in my book Getting Started with V Programming or on the official website vlang.io. You can visit this page to know more about what is included in this book and how I wrote this book.
We will now proceed to the tutorial to learn about data synchronization among concurrent tasks in V programming language.
Data Synchronization among Concurrent tasks
You can share or exchange the data from the main thread with the tasks that have been spawned to run concurrently. V allows you to share data between the main thread and the tasks it spawns, but only using the variables that are of the struct
, map
, or array
type. These variables need to be specified using the shared
keyword in such cases. Variables marked using the shared
keyword need to be accessed using rlock
when they are being read or using lock
when we want to read/write/modify those variables.
Implementing Fund collection to demonstrate Data Synchronization in V
We will demonstrate the data synchronization using locks and shared objects with the help of a real-time use case in this tutorial.
Let’s consider a scenario where a fundraiser is raising money for a noble cause. A donor or multiple donors, if they wish to contribute to the fund, can contribute some amount to a fund manager (who represents the main function in our code). When the donations reach the target set by the fund, the fund manager stops collecting money. Assuming that this happens concurrently until the amount that’s received is greater than or equal to the target amount, afterward, the fund manager will stop collecting further funds.
So let us proceed and implement this functionality using concurrent tasks, shared objects and locks in V.
Defining a Struct
Since data sharing can only happen using structs, maps, or array types, we will define a struct that represents a Fund, as follows:
struct Fund {
name string
target f32
mut:
total f32
num_donors int
}
The preceding code contains four fields. Two of them are name and target, where name represents the fund name and target represents the amount that must be achieved to fulfill the cause.The fields name and target will be set by the fund manager (the main program). The other two fields are total and num_donors. The struct field total is used to represent the amount that’s been accumulated in the fund via donations, while num_donors field is used to indicate the total number of donors that have contributed to this fund so far.
Implementing a method to collect funds
Now, let’s define a method called collect for the Fund struct that accepts an input argument called amt, which represents the amount that’s been collected from any generous donor, as follows:
fn (shared f Fund) collect(amt f32) {
lock f { // read - write lock
if f.total < f.target {
f.num_donors += 1
f.total += amt
println('$f.num_donors \t before: ${f.total - amt} \t funds received: $amt \t total: $f.total')
}
}
}
The collect method has a receiver argument, f, for the Fund struct that’s marked using the shared
keyword. We marked the receiver argument as shared
because the collect method will be accessed concurrently by multiple threads. In such cases, it is essential to avoid collisions, so we should use either rlock
or lock
to acquire the lock on the instance of fundbeing accessed by a concurrent task. In the collect method, we are reading and updating the total and num_donors mutable fields, so it is essential to acquire a read-write lock using lock
, as shown in the preceding code.
Initializing a shared object
Now, let’s move on and see what the fund manager (main function) will be doing to collect funds from concurrent sources. The first thing to do is to create a shared variable for Fund whose name is of the cause that the funds are being raised for, along with the target amount needed to fulfill the amount needed for the fund. This can be represented programmatically as follows:
shared fund := Fund{
name: 'A noble cause'
target: 1000.00
}
Here, we can see that A noble cause requires a minimum target amount of 1000.00 USD.
Implementing Donations
Having defined the fund for a noble cause, let’s say there is a minimum and maximum amount that the donors can donate in the range of 100 to 250 USD. This can be seen in the following code:
fn donation() f32 {
return rand.f32_in_range(100.00, 250.00)
}
Every time the the donation function is spawned to run concurrently, we can think of it as a donor who donates the amount in the range 100.00 to 250.00 USD.
Notice the usage of the f32_in_range(100.00,250.00)
function from the rand
module. The donation function returns the random amount that represents the amount that was contributed by a generous donor in USD. The f32_in_range
function, which is available in the rand
module, returns a uniformly distributed 32-bit floating-point value that is greater than or equal to the starting value, but less than the ending value in the range specified.
The
rand
module in the V programming has a significant contribution from Subhomoy Haldar and his team of V enthusiasts. Check out his musings on Reorganizing V’s Random Library.
Collecting donations and adding it to the fund
Next, the fund manager keeps seeking donations and updating the total amount that’s been collected by calling the collect method of the Fund struct, as follows:
for {
rlock fund {
if fund.total >= fund.target {
break
}
}
h := go donation()
go fund.collect(h.wait())
}
From the preceding code snippet, the fund.collect(amt)
process, which is used to collect donations, is spawned across various threads. At the same time, the fund manager (main program) has shared access to the fund data, so the fund manager keeps collecting donations until the total amount, fund.total, is greater than or equal to the target amount, fund.target by acquiring rlock
to verify this condition is met.
Full Code
Putting all the pieces of code we’ve looked at together, the full source code will appear as follows:
module main
import rand
struct Fund {
name string
target f32
mut:
total f32
num_donors int
}
fn (shared f Fund) collect(amt f32) {
lock f { // read - write lock
if f.total < f.target {
f.num_donors += 1
f.total += amt
println('$f.num_donors \t before: ${f.total - amt} \t funds received: $amt \t total: $f.total')
}
}
}
fn donation() f32 {
return rand.f32_in_range(100.00, 250.00)
}
fn main() {
shared fund := Fund{
name: 'A noble cause'
target: 1000.00
}
for {
rlock fund {
if fund.total >= fund.target {
break
}
}
h := go donation()
go fund.collect(h.wait())
}
rlock fund { // acquire read lock
println('$fund.num_donors donors donated for $fund.name')
println('$fund.name raised total fund amount: \$ $fund.total')
}
}
In the preceding code, we can see that the main thread, which we assumed to be the fund manager, created a fund as a shared
object and collected donations before summarizing the fund details, after having acquired rlock
(read lock on funds). The output of the preceding code is as follows:
1 before: 0 funds received: 196.1572 total: 196.1572
2 before: 196.1572 funds received: 155.6391 total: 351.7963
3 before: 351.7963 funds received: 156.4043 total: 508.2006
4 before: 508.2006 funds received: 182.9023 total: 691.1028
5 before: 691.1028 funds received: 185.7090 total: 876.8118
6 before: 876.8119 funds received: 190.2294 total: 1067.041
6 donors donated for A noble cause
A noble cause raised total fund amount: $ 1067.041
From the preceding output, we can see that the fund manager – in our case, the main
method – has collected donations from 6 donors for fund initiated for A noble cause. The total fund amount raised with 6 donors was $ 1067.041. Soon after the fund.total >= fund.target
condition was met, the fund manager stopped collecting further donations by breaking the infinite for loop
.
Note that you might see a different number of donors and total fund amount as we used a random amount that was generated using the rand.f32_in_range()
function.
Conclusion
We learned that we can communicate between the coroutines with the help of shared objects. In V, these can be structs, arrays, or maps. But the problem with this approach is that you need to take explicit care of concurrency synchronization techniques such as protecting the shared objects using locks such as the read-only rlock
or the read/write lock
to prevent data races. So, V has the concept of channels. Channels are advanced concurrency patterns in V that solve the problem of explicitly handling data synchronization techniques. An in depth detail about channels and working with buffered and unbuffered channels is covered in Chapter 11 Channels – An Advanced Concurrency Pattern from my book Getting Started with V Programming.
If you like this tutorial, please do bookmark 🔖 (Ctrl +D) it and spread the word 📢 by sharing it across your friends and colleagues.
Pingback: Getting Started with V Programming – TutLinks