Concurrency

Spawning Concurrent Tasks

V’s model of concurrency is going to be very similar to Go’s. For now, go foo() runs foo() concurrently in a different thread:

  1. import math
  2. fn p(a f64, b f64) { // ordinary function without return value
  3. c := math.sqrt(a * a + b * b)
  4. println(c)
  5. }
  6. fn main() {
  7. go p(3, 4)
  8. // p will be run in parallel thread
  9. }

In V 0.4 go foo() will be automatically renamed via vfmt to spawn foo(), and there will be a way to launch a coroutine (a lightweight thread managed by the runtime).

Sometimes it is necessary to wait until a parallel thread has finished. This can be done by assigning a handle to the started thread and calling the wait() method to this handle later:

  1. import math
  2. fn p(a f64, b f64) { // ordinary function without return value
  3. c := math.sqrt(a * a + b * b)
  4. println(c) // prints `5`
  5. }
  6. fn main() {
  7. h := go p(3, 4)
  8. // p() runs in parallel thread
  9. h.wait()
  10. // p() has definitely finished
  11. }

This approach can also be used to get a return value from a function that is run in a parallel thread. There is no need to modify the function itself to be able to call it concurrently.

  1. import math { sqrt }
  2. fn get_hypot(a f64, b f64) f64 { // ordinary function returning a value
  3. c := sqrt(a * a + b * b)
  4. return c
  5. }
  6. fn main() {
  7. g := go get_hypot(54.06, 2.08) // spawn thread and get handle to it
  8. h1 := get_hypot(2.32, 16.74) // do some other calculation here
  9. h2 := g.wait() // get result from spawned thread
  10. println('Results: $h1, $h2') // prints `Results: 16.9, 54.1`
  11. }

If there is a large number of tasks, it might be easier to manage them using an array of threads.

  1. import time
  2. fn task(id int, duration int) {
  3. println('task $id begin')
  4. time.sleep(duration * time.millisecond)
  5. println('task $id end')
  6. }
  7. fn main() {
  8. mut threads := []thread{}
  9. threads << go task(1, 500)
  10. threads << go task(2, 900)
  11. threads << go task(3, 100)
  12. threads.wait()
  13. println('done')
  14. }
  15. // Output:
  16. // task 1 begin
  17. // task 2 begin
  18. // task 3 begin
  19. // task 3 end
  20. // task 1 end
  21. // task 2 end
  22. // done

Additionally for threads that return the same type, calling wait() on the thread array will return all computed values.

  1. fn expensive_computing(i int) int {
  2. return i * i
  3. }
  4. fn main() {
  5. mut threads := []thread int{}
  6. for i in 1 .. 10 {
  7. threads << go expensive_computing(i)
  8. }
  9. // Join all tasks
  10. r := threads.wait()
  11. println('All jobs finished: $r')
  12. }
  13. // Output: All jobs finished: [1, 4, 9, 16, 25, 36, 49, 64, 81]

Channels

Channels are the preferred way to communicate between threads. 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 field 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 field of the individual channel object. Channels can be passed to threads like normal variables:

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

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

  1. // make buffered channels so pushing does not block (if there is room in the buffer)
  2. ch := chan int{cap: 1}
  3. ch2 := chan f64{cap: 1}
  4. n := 5
  5. // push
  6. ch <- n
  7. ch2 <- 7.3
  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. // wip
  2. ch := chan int{}
  3. ch2 := chan f64{}
  4. // ...
  5. ch.close()
  6. // ...
  7. m := <-ch or {
  8. println('channel has been closed')
  9. }
  10. // propagate error
  11. 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. ch := chan f64{}
  4. ch2 := chan f64{}
  5. ch3 := chan f64{}
  6. mut b := 0.0
  7. c := 1.0
  8. // ... setup go threads that will send on ch/ch2
  9. go fn (the_channel chan f64) {
  10. time.sleep(5 * time.millisecond)
  11. the_channel <- 1.0
  12. }(ch)
  13. go fn (the_channel chan f64) {
  14. time.sleep(1 * time.millisecond)
  15. the_channel <- 1.0
  16. }(ch2)
  17. go fn (the_channel chan f64) {
  18. _ := <-the_channel
  19. }(ch3)
  20. select {
  21. a := <-ch {
  22. // do something with `a`
  23. eprintln('> a: $a')
  24. }
  25. b = <-ch2 {
  26. // do something with predeclared variable `b`
  27. eprintln('> b: $b')
  28. }
  29. ch3 <- c {
  30. // do something if `c` was sent
  31. time.sleep(5 * time.millisecond)
  32. eprintln('> c: $c was send on channel ch3')
  33. }
  34. 500 * time.millisecond {
  35. // do something if no channel has become ready within 0.5s
  36. eprintln('> more than 0.5s passed without a channel being ready')
  37. }
  38. }
  39. eprintln('> done')
  40. }

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. // wip
  2. if select {
  3. ch <- a {
  4. // ...
  5. }
  6. } {
  7. // channel was open
  8. } else {
  9. // channel is closed
  10. }

Special Channel Features

For special purposes there are some builtin fields and methods:

  1. struct Abc {
  2. x int
  3. }
  4. a := 2.13
  5. ch := chan f64{}
  6. res := ch.try_push(a) // try to perform `ch <- a`
  7. println(res)
  8. l := ch.len // number of elements in queue
  9. c := ch.cap // maximum queue length
  10. is_closed := ch.closed // bool flag - has `ch` been closed
  11. println(l)
  12. println(c)
  13. mut b := Abc{}
  14. ch2 := chan Abc{}
  15. 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 fields in production is not recommended - algorithms based on them are often subject to race conditions. Especially .len and .closed should not be used to make decisions. Use or branches, error propagation or select instead (see Syntax and Usage and Channel Select above).

Shared Objects

Data can be exchanged between a thread and the calling thread via a shared variable. Such variables should be created as shared and passed to the thread as such, too. The underlying struct contains a hidden mutex that allows locking concurrent access using rlock for read-only and lock for read/write access.

  1. struct St {
  2. mut:
  3. x int // data to be shared
  4. }
  5. fn (shared b St) g() {
  6. lock b {
  7. // read/modify/write b.x
  8. }
  9. }
  10. fn main() {
  11. shared a := St{
  12. x: 10
  13. }
  14. go a.g()
  15. // ...
  16. rlock a {
  17. // read a.x
  18. }
  19. }

Shared variables must be structs, arrays or maps.