Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new channels article #274

Closed
wants to merge 37 commits into from
Closed
Changes from 27 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b7e0d06
Create 2021-03-12-new-Nim-channels.md
ringabout Mar 11, 2021
4a5901e
Update 2021-03-12-new-Nim-channels.md
ringabout Mar 11, 2021
f28cebd
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 11, 2021
4bb8b90
Apply suggestions from code review
ringabout Mar 11, 2021
4183f52
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
5b87806
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
8b43a3b
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
5f11cf4
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
8db9837
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
a381873
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 12, 2021
5f11660
Update 2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
4b45bcd
Update 2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
426ce67
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
f9e5f94
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
42513d9
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
c124ead
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
9d6db08
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
01c6bf0
Apply suggestions from code review
ringabout Mar 14, 2021
965e392
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 14, 2021
fe2b67e
Apply suggestions from code review
ringabout Mar 14, 2021
accb697
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 15, 2021
84c5b0e
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 15, 2021
ef79a5d
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 15, 2021
d2383bd
Apply suggestions from code review
ringabout Mar 15, 2021
df83246
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 15, 2021
40b3d70
Update 2021-03-12-new-Nim-channels.md
ringabout Mar 15, 2021
52f3a07
update benchmark
ringabout Mar 15, 2021
5c9b9ec
Apply suggestions from code review
ringabout Mar 17, 2021
44abd9a
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 18, 2021
0d6700a
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 18, 2021
bc1bbb6
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 18, 2021
3d1169d
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 18, 2021
1aa803f
Apply suggestions from code review
ringabout Mar 18, 2021
f2a595e
Update jekyll/_posts/2021-03-12-new-Nim-channels.md
ringabout Mar 23, 2021
fefc4eb
better
ringabout Mar 31, 2021
2f9ed9a
minor whitespace changes
narimiran May 3, 2021
c112607
change the date
narimiran May 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions jekyll/_posts/2021-03-12-new-Nim-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
---

title: "The new Nim channels implementation for ORC"
author: xflywind
excerpt: "The new channels library is efficient and safe to use"

---

# The new Nim channels implementation for ORC

Version 1.4 ships with the so-called ORC memory management algorithm. ORC is the existing ARC algorithm (first shipped in version 1.2) plus a cycle collector. The Nim devel branch introduces a new module called `std/isolation`, which allows us to pass `isolated` data to threads safely and easily - it prevents data races at compile time. Another recent addition to the devel branch is `std/channels`, which is designed for ORC. It combines `isolated` data and `channels`, and is efficient and safe to use.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

**Note:** at the time of writing, to compile the below code you need to use a development version of the Nim compiler (from 2021-03-12 or later). However, it should also work in Nim 1.6.0 or later.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

## Background

A channel is a model for sharing memory via message passing. A thread is able to send or receive messages over a channel. It's like sending a letter to your friend: the postman is the channel, and your friend is the receiver. You might already know `system/channels_builtin`, which is the old channels implementation. What's better in the new implementation? With the old one, first you need to copy your letter and send the copy to your friend. Then your friend may mark something on the copied letter and it won't affect the original. This works fine, but it is not efficient. If you use the new implementation, you only need to put your letter in the mailbox. No need to copy it!

## The advantages

- Designed for ARC/ORC, no legacy code
- No need to `deepCopy`, just move data around
- No data races

## Explore the new channels

**Note:** Be sure to compile your code with `--gc:orc –-threads:on -d:ssl`.

### Let's crawl the web

**todo_urls.json**

```json
{"url": ["https://google.com", "https://nim-lang.org"]}
```

**app.nim**

The main thread prepares tasks by reading `todo_urls.json`, and then it sends JSON data to a channel. The crawl thread does the actual work - it receives URL data from the channel and downloads the contents using the `httpclient` module.

```nim
import std/channels
import std/[httpclient, isolation, json]

var ch = newChannel[JsonNode]() # we need to send JsonNode

proc download(client: HttpClient, url: string) =
let response = client.get(url)
echo "content: ", response.body[0 .. 20] # prints the results

proc crawl =
var client = newHttpClient() # the crawler
defer: client.close()
var data: JsonNode
ch.recv(data) # the JSON data
if data != nil:
ringabout marked this conversation as resolved.
Show resolved Hide resolved
for url in data["url"]:
download(client, url.getStr)

proc prepareTasks(fileWithUrls: string): seq[Isolated[JsonNode]] =
result = @[]
ringabout marked this conversation as resolved.
Show resolved Hide resolved
for line in lines(fileWithUrls):
result.add isolate(parseJson(line)) # parse JSON file

proc spawnCrawlers =
var tasks = prepareTasks("todo_urls.json")
Copy link
Member

@timotheecour timotheecour Mar 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about making this test self-contained by using a string instead of a file:

var tasks = prepareTasks("""
{"url": ["https://google.com", "https://nim-lang.org"]}
{"url": ["https://github.com/nim-lang/Nim"]}
""")

and adapting prepareTasks.

furthermore, I'd recommended making it even simpler by making 1 task = 1 url:

var tasks = prepareTasks("""
https://google.com
https://nim-lang.org
https://github.com/nim-lang/Nim
""")

(since the added complexity has nothing to do with std/channels). Simpler examples are easier to adapt to new settings.

for t in mitems tasks: # we need a mutable view of the items
ch.send move t

var thr: Thread[void]
createThread(thr, crawl) # create crawl thread

spawnCrawlers()
joinThread(thr)
ringabout marked this conversation as resolved.
Show resolved Hide resolved
```

First, we import `std/channels`.

Then we can create a channel using `newChannel`, which returns a `Channel[T]`. It uses `mpmc` internally, which stands for "multiple producer, multiple consumer". The `elements` parameter is used to specify whether a channel is buffered or not. For an unbuffered channel, the sender and the receiver block until the other side is ready. Sending data to a buffered channel blocks only when the buffer is full. Receiving data from a buffered channel blocks when the buffer is empty.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

ringabout marked this conversation as resolved.
Show resolved Hide resolved
`newChannel` is a generic proc - you can specify the types of the data you want to send or receive.

```nim
var chan1 = newChannel[int]()
# or
var chan2 = newChannel[string](elements = 1) # unbuffered channel
# or
var chan3 = newChannel[seq[string]](elements = 30) # buffered channel
```

The `send` proc takes data that we want to send to the channel. The passed data is moved around, not copied. Because `chan.send(isolate(data))` is very common to use, `template send[T](c: var Chan[T]; src: T) = chan.send(isolate(src))` is provided for convenience. For example, you can use `chan.send("Hello World")` instead of `chan.send(isolate("Hello World!"))`.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

There are two useful procs for a receiver: `recv` and `tryRecv`. `recv` blocks until something is sent to the channel. In contrast, `tryRecv` doesn't block - if no message exists in the channel, it just fails and returns `false`. We can write a while loop to call `tryRecv`and handle a message when available.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

### It is safe and convenient

The Nim compiler rejects the program below at compile time. It says that `expression cannot be isolated: s`. This is because `s` is a `ref object` - it may be modified somewhere and is not unique, so the variable cannot be isolated.
ringabout marked this conversation as resolved.
Show resolved Hide resolved


```nim
import std/[channels, json, isolation]

var chan = newChannel[JsonNode]()

proc spawnCrawlers =
var s = newJString("Hello, Nim")
chan.send isolate(s) # compile time error
chan.send unsafeIsolate(s) # ok: user's responsability to check that `s` isn't mutated
```

It is only allowed to pass a function call directly.

```nim
import std/[channels, json, isolation]

var chan = newChannel[JsonNode]()

proc spawnCrawlers =
chan.send isolate(newJString("Hello, Nim"))
```

`Isolated` data can only be moved, not copied. It is implemented as a library without bloating Nim's core type system. The `isolate` proc is used to create an isolated subgraph from the expression `value`. Whether the expression `value` is isolated is checked at compile time. The `extract` proc is used to get the internal value of `Isolated` data.

```nim
import std/isolation

var data = isolate("string")
doAssert data.extract == "string"
doAssert data.extract == ""
ringabout marked this conversation as resolved.
Show resolved Hide resolved
```

By means of `Isolated` data, the channels become safer and more convenient to use.


## Benchmark

Here is a simple benchmark. We create 40 threads that send data to the channel and 5/10/20 threads that receive it.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

```nim
# benchmark the new channel implementation with
# `nim r --threads:on --gc:orc -d:newChan -d:danger app.nim`
#
# benchmark the old channel implementation with
# `nim r --threads:on -d:oldChan -d:danger app.nim`

import std/times

var
sender: array[40, Thread[void]]
receiver: array[5, Thread[void]]
# receiver: array[10, Thread[void]] # with 10 threads
# receiver: array[20, Thread[void]] # with 20 threads

when defined(newChan):
import std/[channels, isolation]
var chan = newChannel[seq[string]](40)

proc recvHandler() =
var x: seq[string]
ringabout marked this conversation as resolved.
Show resolved Hide resolved
chan.recv(x)
discard x

elif defined(oldChan):
var chan: Channel[seq[string]]

chan.open(maxItems = 40)

proc recvHandler() =
let x = chan.recv()
discard x

proc sendHandler() =
chan.send(@["Hello, Nim"])

template benchmark() =
for t in mitems(sender):
t.createThread(sendHandler)

joinThreads(sender)
for i in 0 .. receiver.high:
createThread(receiver[i], recvHandler)

let start = now()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

joinThreads(receiver)
echo now() - start

benchmark()
ringabout marked this conversation as resolved.
Show resolved Hide resolved
```

The new implementation is much faster than the old one!


| Implementation | 5 threads | 10 threads | 20 threads |
| ------------------------------------------ | -----------: | -----------: | ----------: |
| system/channels_builtin + refc (-d:danger) | 458 μs | 859 μs | 1710 μs |
| std/channels + orc (-d:danger) | 188 μs | 258 μs | 428 μs |



## Summary

The new channels implementation makes ORC suitable for sharing data between threads. Data races are prevented at compile time by sending isolated subgraphs checked at compile time.

If you use the latest devel, you can run the example above and experiment with `std/channels` in your own programs. Please try it out and give us your feedback!

## Further information

- [Isolated data for Nim](https://github.com/nim-lang/RFCs/issues/244)
- [Introduction to ARC/ORC in Nim](https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc-in-nim.html)
- [ORC - Vorsprung durch Algorithmen](https://nim-lang.org/blog/2020/12/08/introducing-orc.html)


-------

If you like this article and how we evolve Nim, please consider a donation. You can donate via:

- [Open Collective](https://opencollective.com/nim)

- [Patreon](https://www.patreon.com/araq)

- [PayPal](https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=FLWX5V2PMAXAU)

- Bitcoin: 1BXfuKM2uvoD6mbx4g5xM3eQhLzkCK77tJ


If you are a company, we also offer commercial support. Please get in touch with us via [[email protected]](mailto:[email protected]). As a commercial backer, you can decide what features and bugfixes should be prioritized.