Data Synchronization in Concurrent tasks using locks in V Language

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.

Data Synchronization in Concurrent tasks using locks in V Language - TutLinks
Data Synchronization in Concurrent tasks using locks in V Language – TutLinks

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

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 structmap, 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.

Sharing data between the main thread and concurrent tasks using locks in V Programming Language - TutLinks
Sharing data between the main thread and concurrent tasks using locks in V Programming Language – TutLinks

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.

Navule Pavan Kumar Rao

I am a Full Stack Software Engineer with the Product Development experience in Banking, Finance, Corporate Tax and Automobile domains. I use SOLID Programming Principles and Design Patterns and Architect Software Solutions that scale using C#, .NET, Python, PHP and TDD. I am an expert in deployment of the Software Applications to Cloud Platforms such as Azure, GCP and non cloud On-Premise Infrastructures using shell scripts that become a part of CI/CD. I pursued Executive M.Tech in Data Science from IIT, Hyderabad (Indian Institute of Technology, Hyderabad) and hold B.Tech in Electonics and Communications Engineering from Vaagdevi Institute of Technology & Science.

This Post Has One Comment

Leave a Reply