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 3 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
233 changes: 233 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,233 @@
---

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 also introduces a new module called `std/isolation`. With it we can pass `isolated` data to threads safely and easily. It prevents data races at compile time. Recently `std/channels` is merged to the devel branch 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
ringabout marked this conversation as resolved.
Show resolved Hide resolved

**Note:** you need the Nim devel branch to compile the code below.
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. Your friend is the receiver. You may know `system/channels` already exists. What’s the difference between new channels implementation and the old one? If you use the old one, you need to copy your letter by hand first and send the copied one to your friend instead. Then your friend may mark something on the copied letter, it won’t affect the original letter. It works fine, however it is not efficient. If you use the new one, you only need to put your letter in the mailbox. No need to copy your letter!
ringabout marked this conversation as resolved.
Show resolved Hide resolved

## The advantages

- Designed for ARC/ORC, no legacy code
- No need to `deepcopy`, just move data around
ringabout marked this conversation as resolved.
Show resolved Hide resolved
- No data races
- Based on [Michael & Scott lock-based queues](https://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf)
ringabout marked this conversation as resolved.
Show resolved Hide resolved
ringabout marked this conversation as resolved.
Show resolved Hide resolved

## Explore the new channels

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

### Let's crawl the web

**todo_urls.json**

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

**app.nim**

The main thread prepares tasks via reading `todo_urls.txt`. Then it sends JSON data to a channel. The crawl thread does the crawlers’ work. It receives URL data from the channel and downloads the contents using `httpclient`.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

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


ringabout marked this conversation as resolved.
Show resolved Hide resolved
var ch = initChan[JsonNode]() # we need to send JsonNode

proc download(client: HttpClient, url: string) =
let response = client.get(url)
echo "content: "
echo response.body[0 .. 20] # prints the results
ringabout marked this conversation as resolved.
Show resolved Hide resolved

proc crawl =
var client = newHttpClient() # the crawler
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)
client.close()
ringabout marked this conversation as resolved.
Show resolved Hide resolved

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 =
ringabout marked this conversation as resolved.
Show resolved Hide resolved
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"
ringabout marked this conversation as resolved.
Show resolved Hide resolved
ch.send move t

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

spawnCrawlers()
joinThread(thr)
```

First you need to import `std/channels`.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

Then you can create a channel using `initChan`. 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 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

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

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

`send` proc takes something 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
ringabout marked this conversation as resolved.
Show resolved Hide resolved

The Nim compiler rejects the program below at compile time. It says that `expression cannot be isolated: s`. Because s is a ref object, 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 = initChan[JsonNode]()

proc spawnCrawlers =
ringabout marked this conversation as resolved.
Show resolved Hide resolved
var s = newJString("Hello, Nim")
chan.send isolate(s)
ringabout marked this conversation as resolved.
Show resolved Hide resolved
```

It is only allowed to pass a function call directly.

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

var chan = initChan[JsonNode]()

proc spawnCrawlers =
ringabout marked this conversation as resolved.
Show resolved Hide resolved
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`. The expression `value` is checked at compile time . The `extract` proc is used to get the internal value of `Isolated` data.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

```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 safe and convenient to use.
ringabout marked this conversation as resolved.
Show resolved Hide resolved


## Benchmark

Here is a simple benchmark. We create 10 threads for sending data to the channel and 5 threads for receiving data from the channel.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

```nim
# benchmark the new channel implementation with
# `nim c -r --threads:on --gc:orc -d:newChan -d:danger app.nim`
ringabout marked this conversation as resolved.
Show resolved Hide resolved
#
# benchmark the old channel implementation with
# `nim c -r --threads:on -d:oldChan -d:danger app.nim`
ringabout marked this conversation as resolved.
Show resolved Hide resolved

import std/[os, times, isolation]
ringabout marked this conversation as resolved.
Show resolved Hide resolved

var
sender: array[0 .. 9, Thread[void]]
receiver: array[0 .. 4, Thread[void]]
ringabout marked this conversation as resolved.
Show resolved Hide resolved


ringabout marked this conversation as resolved.
Show resolved Hide resolved
when defined(newChan):
import std/channels
ringabout marked this conversation as resolved.
Show resolved Hide resolved
var chan = initChan[seq[string]](40)

proc sendHandler() =
chan.send(isolate(@["Hello, Nim"]))
ringabout marked this conversation as resolved.
Show resolved Hide resolved

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 sendHandler() =
chan.send(@["Hello, Nim"])


ringabout marked this conversation as resolved.
Show resolved Hide resolved
proc recvHandler() =
let x = chan.recv()
discard x

template benchmark() =
for i in 0 .. sender.high:
createThread(sender[i], sendHandler)
ringabout marked this conversation as resolved.
Show resolved Hide resolved

joinThreads(sender)


ringabout marked this conversation as resolved.
Show resolved Hide resolved
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()
```

The new implementation is much faster than the old one!


| Implementation | Time |
| --------------------------------- | ------------------------------------ |
| system/channels + refc(-d:danger) | 433 microseconds and 590 nanoseconds |
| std/channels + orc(-d:danger) | 137 microseconds and 522 nanoseconds |
ringabout marked this conversation as resolved.
Show resolved Hide resolved



## Summary

The new channels implementation makes ORC suitable for sharing data between threads. Data races are detected at compile time.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

If you use latest Nim version, you can run the example above and experiment `std/channels` with your own programs. Please try it out and give us your feedback!
ringabout marked this conversation as resolved.
Show resolved Hide resolved

## 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 [support@nim-lang.org](mailto:support@nim-lang.org). As a commercial backer, you can decide what features and bugfixes should be prioritized.