Concurrency
V’s model of concurrency is very similar to Go’s. To run foo()
concurrently, just call it with go foo()
. Right now, it launches the function on a new system thread. Soon coroutines and a scheduler will be implemented.
import sync
import time
fn task(id int, duration int, mut wg sync.WaitGroup) {
println('task $id begin')
time.sleep_ms(duration)
println('task $id end')
wg.done()
}
fn main() {
mut wg := sync.new_waitgroup()
wg.add(3)
go task(1, 500, mut wg)
go task(2, 900, mut wg)
go task(3, 100, mut wg)
wg.wait()
println('done')
}
// Output:
// task 1 begin
// task 2 begin
// task 3 begin
// task 3 end
// task 1 end
// task 2 end
// done
Channels
Channels are the preferred way to communicate between coroutines. V’s channels work basically like those in Go. You can push objects into a channel on one end and pop objects from the other end. Channels can be buffered or unbuffered and it is possible to select
from multiple channels.
Syntax and Usage
Channels have the type chan objtype
. An optional buffer length can specified as the cap
property in the declaration:
ch := chan int{} // unbuffered - "synchronous"
ch2 := chan f64{cap: 100} // buffer length 100
Channels do not have to be declared as mut
. The buffer length is not part of the type but a property of the individual channel object. Channels can be passed to coroutines like normal variables:
import sync
fn f(ch chan int) {
// ...
}
fn main() {
ch := chan int{}
go f(ch)
// ...
}
Objects can be pushed to channels using the arrow operator. The same operator can be used to pop objects from the other end:
import sync
mut ch := chan int{}
mut ch2 := chan f64{}
n := 5
x := 7.3
ch <- n // push
ch2 <- x
mut y := f64(0.0)
m := <-ch // pop creating new variable
y = <-ch2 // pop into existing variable
A channel can be closed to indicate that no further objects can be pushed. Any attempt to do so will then result in a runtime panic (with the exception of select
and try_push()
- see below). Attempts to pop will return immediately if the associated channel has been closed and the buffer is empty. This situation can be handled using an or branch (see Handling Optionals).
mut ch := chan int{}
mut ch2 := chan f64{}
// ...
ch.close()
// ...
m := <-ch or {
println('channel has been closed')
}
// propagate error
y := <-ch2 ?
Channel Select
The select
command allows monitoring several channels at the same time without noticeable CPU load. It consists of a list of possible transfers and associated branches of statements - similar to the match command:
import time
fn main () {
mut c := chan f64{}
mut ch := chan f64{}
mut ch2 := chan f64{}
mut ch3 := chan f64{}
mut b := 0.0
// ...
select {
a := <-ch {
// do something with `a`
}
b = <-ch2 {
// do something with predeclared variable `b`
}
ch3 <- c {
// do something if `c` was sent
}
> 500 * time.millisecond {
// do something if no channel has become ready within 0.5s
}
}
}
The timeout branch is optional. If it is absent select
waits for an unlimited amount of time. It is also possible to proceed immediately if no channel is ready in the moment select
is called by adding an else { ... }
branch. else
and > timeout
are mutually exclusive.
The select
command can be used as an expression of type bool
that becomes false
if all channels are closed:
if select {
ch <- a {
// ...
}
} {
// channel was open
} else {
// channel is closed
}
Special Channel Features
For special purposes there are some builtin properties and methods:
import sync
struct Abc{x int}
a := 2.13
mut ch := chan f64{}
res := ch.try_push(a) // try to perform `ch <- a`
println(res)
l := ch.len // number of elements in queue
c := ch.cap // maximum queue length
println(l)
println(c)
// mut b := Abc{}
// mut ch2 := chan f64{}
// res2 := ch2.try_pop(mut b) // try to perform `b = <-ch2
The try_push/pop()
methods will return immediately with one of the results .success
, .not_ready
or .closed
- dependent on whether the object has been transferred or the reason why not. Usage of these methods and properties in production is not recommended - algorithms based on them are often subject to race conditions. Use select
instead.
Data can be exchanged between a coroutine and the calling thread via a shared variable. Such variables should be created as references and passed to the coroutine as mut
. The underlying struct
should also contain a mutex
to lock concurrent access:
import sync
struct St {
mut:
x int // share data
mtx &sync.Mutex
}
fn (mut b St) g() {
b.mtx.m_lock()
// read/modify/write b.x
b.mtx.unlock()
}
fn caller() {
mut a := &St{ // create as reference so it's on the heap
x: 10
mtx: sync.new_mutex()
}
go a.g()
a.mtx.m_lock()
// read/modify/write a.x
a.mtx.unlock()
}