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.

  1. import sync
  2. import time
  3. fn task(id int, duration int, mut wg sync.WaitGroup) {
  4. println('task $id begin')
  5. time.sleep_ms(duration)
  6. println('task $id end')
  7. wg.done()
  8. }
  9. fn main() {
  10. mut wg := sync.new_waitgroup()
  11. wg.add(3)
  12. go task(1, 500, mut wg)
  13. go task(2, 900, mut wg)
  14. go task(3, 100, mut wg)
  15. wg.wait()
  16. println('done')
  17. }
  18. // Output:
  19. // task 1 begin
  20. // task 2 begin
  21. // task 3 begin
  22. // task 3 end
  23. // task 1 end
  24. // task 2 end
  25. // 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:

  1. ch := chan int{} // unbuffered - "synchronous"
  2. 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:

  1. import sync
  2. fn f(ch chan int) {
  3. // ...
  4. }
  5. fn main() {
  6. ch := chan int{}
  7. go f(ch)
  8. // ...
  9. }

Objects can be pushed to channels using the arrow operator. The same operator can be used to pop objects from the other end:

  1. import sync
  2. mut ch := chan int{}
  3. mut ch2 := chan f64{}
  4. n := 5
  5. x := 7.3
  6. ch <- n // push
  7. ch2 <- x
  8. mut y := f64(0.0)
  9. m := <-ch // pop creating new variable
  10. 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).

  1. mut ch := chan int{}
  2. mut ch2 := chan f64{}
  3. // ...
  4. ch.close()
  5. // ...
  6. m := <-ch or {
  7. println('channel has been closed')
  8. }
  9. // propagate error
  10. 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:

  1. import time
  2. fn main () {
  3. mut c := chan f64{}
  4. mut ch := chan f64{}
  5. mut ch2 := chan f64{}
  6. mut ch3 := chan f64{}
  7. mut b := 0.0
  8. // ...
  9. select {
  10. a := <-ch {
  11. // do something with `a`
  12. }
  13. b = <-ch2 {
  14. // do something with predeclared variable `b`
  15. }
  16. ch3 <- c {
  17. // do something if `c` was sent
  18. }
  19. > 500 * time.millisecond {
  20. // do something if no channel has become ready within 0.5s
  21. }
  22. }
  23. }

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:

  1. if select {
  2. ch <- a {
  3. // ...
  4. }
  5. } {
  6. // channel was open
  7. } else {
  8. // channel is closed
  9. }

Special Channel Features

For special purposes there are some builtin properties and methods:

  1. import sync
  2. struct Abc{x int}
  3. a := 2.13
  4. mut ch := chan f64{}
  5. res := ch.try_push(a) // try to perform `ch <- a`
  6. println(res)
  7. l := ch.len // number of elements in queue
  8. c := ch.cap // maximum queue length
  9. println(l)
  10. println(c)
  11. // mut b := Abc{}
  12. // mut ch2 := chan f64{}
  13. // 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:

  1. import sync
  2. struct St {
  3. mut:
  4. x int // share data
  5. mtx &sync.Mutex
  6. }
  7. fn (mut b St) g() {
  8. b.mtx.m_lock()
  9. // read/modify/write b.x
  10. b.mtx.unlock()
  11. }
  12. fn caller() {
  13. mut a := &St{ // create as reference so it's on the heap
  14. x: 10
  15. mtx: sync.new_mutex()
  16. }
  17. go a.g()
  18. a.mtx.m_lock()
  19. // read/modify/write a.x
  20. a.mtx.unlock()
  21. }