Skip to content

Commit

Permalink
fixed concu according to professor
Browse files Browse the repository at this point in the history
  • Loading branch information
Pismice committed Jul 9, 2024
1 parent ad84895 commit f305cab
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 42 deletions.
25 changes: 19 additions & 6 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
Urgent:
- [ ] Images sam
- [ ] Finir la plannif
- [*] Finir la plannif
- [ ] MVP
- [ ] Epoll (ptetre lier ca a BD ?)
- [ ] Changer le _index.org qui fait qu on arrive sur Utils

N importe quand:
- [ ] Changer le _index.org qui fait qu on arrive sur Utils
- [ ] Epoll (ptetre lier ca a BD ?)
- [ ] error.CeQueJeVeux
- [ ] Corriger le feedback web et concu
- [*] Corriger le feedback web et concu
- [ ] tout relier et mettre Dans mes conclusion mettre des liens vers ma doc
- [ ] Essayer export pdf avec les nouveaux ***
- [ ] Noter tous les documents checkes et pas checks
- [ ] Page accueil Hextra, avec gh, presentation, images, ...
- [ ] Verifier que tous les premieres scripts sont exectuables
- [ ] Un script pour lancer les serveurs se serait pas mal
- [ ] https://zig.news/minosian/deciphering-many-item-types-the-skeleton-19i
- [ ] Faire joli Readme

Fin:
- [ ] sources zotero
- [ ] sources zotero
- [ ] Changer les titres, les images et les liens

Peut etre:
- [?] Websocket
Expand All @@ -26,6 +27,18 @@ Peut etre:
- [?] debug, binutils, breakpoints, debugger

Questions:
- Feedback bien dans l'ensemble ? J avais l impression conclusion un peu maigre. Structure bien aussi psq surtout remarques legeres.
- Moyen que l expert regarde le site plutot que le document PDF pour la doc ?
- Jdois faire quoi en + du rapport ? Une affiche et cest tout ?


Remarques sur feedback:
- Preemptimve multitasking: "useful" le dernier bout de taf peut etre useful pour moi pareil pour "is short lived" cest pas forcement vrai, ca peut etre une groooose tache
- Kernel threads: "it seem a repetition"
- Async/await: "officials" pour moi cest ok car cest les officiels
- Async/await: "intro to libraries??"
- Pas sur de mon graphe sur les coroutines
- std.Thread: "you cannot conclude much"
- est ce que mon lien pour prouver que allocator pour Wasi est suffisant ?
- stack_size: "standard zig stack" cest bien ca
- conclusion : "leaky ??" ??
27 changes: 15 additions & 12 deletions content/docs/concurrency/_index.org
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
#+title: Concurrency
#+weight: 30
#+weight: 15
#+hugo_cascade_type: docs
#+math: true

** Introduction
The objectives of this chapter is to go in depth about the different ways to do concurrency in ZIG.

We are first going to explore a few definitions and concepts that are going to be useful to understand the rest of the chapter.

Then we are going to explore the different ways we could achieve concurrency in Zig.

We are first going to explore a few definitions and concepts that are going to be useful to understand the rest of the chapter. Then we are going to explore the different ways we could achieve concurrency in Zig.
By the end you should be able to see the pros and cons of each solution and choose the one that fits your needs to develop your next Zig projects.

** Definitions
Before diving into the different ways to do concurrency in ZIG, let's first define some terms that are useful to understand the basics of concurrency (not related to Zig). Some terms we are going to explain here might not even be needed in the next sections, but might help you clarify your concurency concepts.
Before diving into the different ways to do concurrency in ZIG, let's first define some terms that are useful to understand the basics of concurrency that are not necessarly related to Zig. By the end of reading the definitions you should have a better understanding about the implementations we are going to see later.

It is important to note that the borders between some definitions are really blur and it is possible that you read a slightly different definition in another source.
It is important to note that the boundaries between some definitions are really blur and it is possible that you read a slightly different definition from an other source.

*** Coroutine
Courtines enable the management of concurrent tasks like callbacks do (which make them not concurrent by nature but they are a tool to achieve concurrency). Their great power lies in their ability to write concurent task like you would write sequential code. They achieve this by yielding control between them. They are used for cooperative multitasking, since the control flow is managed by themselves and not the OS. You might see them in a lot of languages like Python, Lua, Kotlin, ... with keywords like **yield**, **resume**, **suspend**, ...
Courtines enable the management of concurrent tasks like callbacks do (which make them not concurrent by nature but they are a tool to achieve concurrency). Their great power lies in their ability to write concurent tasks like you would write sequential code. They achieve this by yielding control between them. They are used for cooperative multitasking, since the control flow is managed by themselves and not the OS. You might see them in a lot of languages like [[https://docs.python.org/3/library/asyncio-task.html][Python]], [[https://www.lua.org/pil/9.1.html][Lua]], [[https://kotlinlang.org/docs/coroutines-overview.html][Kotlin]] with keywords like **yield**, **resume** and **suspend**.

Coroutines can be either stackful or stackless, we are not gonna dive deep into this concept since most of the time you are going to use stackful coroutines since they allow you to suspend from within a nested stackframe (the only strength of stackless coroutines: efficiency)

Expand All @@ -30,7 +27,13 @@ The only way to transfer the control flow is by explicitly passing control **to
**** Asymmetric coroutines (called asymmetric because the control-transfer can go both ways)
- They have two control-transfer mechanisms:
1. invoking another coroutine which is going to be the subcoroutine of the calling coroutine
2. suspending itself and giving control back to the caller
2. suspending itself and giving control back to the caller, meaning that the coroutine does not have to specifiy to which coroutine it is going to give the control back.

#+CAPTION: Asymmetric coroutines
#+NAME: fig:SED-HR4049
[[/HEIG_ZIG/images/coroutines.png]]

You can get more infromations about those 2 last concepts by reading this [[https://stackoverflow.com/questions/41891989/what-is-the-difference-between-asymmetric-and-symmetric-coroutines][StackOverflow thread]].

*** Green threads (userland threads)
Green threads, also known as userland threads are managed by a runtime or VM (userspace either way) instead of the OS scheduler (kernel space) that manages standard kernel threads.
Expand All @@ -45,7 +48,7 @@ Be careful when using green threads not to use blocking functions, because block
*** Preemptive multitasking
In preemptive multitasking it is the underlying architecture (not us, but the OS or the runtime for exemple) that is in charge of choosing which threads to execute and when. This implies that our threads can be stopped (preempted) at any time, even if it is in the middle of a task.

This method gives the advantage of not having to worry about a thread being starved, since the underlying architecture is going to make sure that everyone gets enough CPU time. As we can see in the 2 graphs for preemptive and cooperative multitasking, the preemptive multitasking might context switch our tasks too much, which can lead to a lot of overhead.
This method gives the advantage of not having to worry about a thread being starved, since the underlying architecture is going to make sure that everyone gets enough CPU time. As we can see in the 2 graphs for preemptive and cooperative multitasking, the preemptive multitasking might context switch our tasks too much, which can lead to a lot of undersiable overhead.

There is an interesting thing that is happening in this graph, at the end we see that task 1 is only doing a very small job, which cost a context switch for almost no job, but the scheduler does not know that a task remains only a small job until done and can context switch it.
#+CAPTION: Preemptive multitasking
Expand All @@ -63,7 +66,7 @@ In cooperative multitasking, we can yield the control back whenever we want, whi
[[/images/coop.svg]]

*** Kernel threads
Multithreading, it is the most basic and history way to do concurrency, it works by running the work on multiple threads that are going to be exectued in parallel (if the CPU can), each thread runs independently of the others. Unlike asynchronous event-driven programming, threads typically block until their assigned task completes.
Multithreading, it is the most basic and historical way to do concurrency, it works by running the work on multiple threads that are going to be exectued in parallel (if the CPU can), each thread runs independently of the others. Unlike asynchronous event-driven programming, threads typically block until their assigned task completes.

Threads are managed by the OS scheduler which is going to decide when to execute which thread.

Expand All @@ -74,7 +77,7 @@ However, scalability can become a concern when managing numerous threads. The ov
To avoid this overhead, thread pools are often used, which manage a set of threads that can be reused for multiple tasks. This approach reduces the overhead of creating and destroying threads for each task, making it more efficient and scalable.

*** Event-driven programming
Event-driven programming, is basically an event loop that listen for "events". This architecture. Under the hood this works by having an event loop that is going to poll for events and check regulary if an event has been emitted. Those events can be for exemple interupts or signals.
Event-driven programming, is basically an event loop that listen for "events". Under the hood this works by having an event loop that is going to poll for events and check regulary if an event has been emitted. Those events can be for exemple interupts or signals.

*** Asynchronous programming (non-blocking IO)
Asynchronous IO can be achieved by opening non-blocking sockets and the by using one of those 2 methods:
Expand Down
4 changes: 2 additions & 2 deletions content/docs/concurrency/conclusion.org
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#+weight: 100
#+hugo_cascade_type: docs

Zig is a really new language which is not very mature yet, that is why you mind find only a few ressources online about the state of concurrency in Zig. However there are already a few pioneers who have really intersting projects or talks. The first that greatly helped understand concurency especially in Zig is [[https://x.com/kingprotty?lang=en][King Protty]] who answered a few of my questions on the Zig Discord server and also made 2 nice videos [[https://www.youtube.com/watch?v=8k33ZvWYQ20][1]] [[https://www.youtube.com/watch?v=Ul8OO4vQMTw][2]]. The big project that uses concurency in Zig is [[https://tigerbeetle.com/][TigerBeetle]] who designed a highly efficient database engine using.
Zig is a really new language which is not very mature yet, that is why you mind find only a few ressources online about the state of concurrency in Zig. However there are already a few pioneers who have really intersting projects or talks. The first that greatly helped understand concurency especially in Zig is [[https://x.com/kingprotty?lang=en][King Protty]] who answered a few of my questions on the Zig Discord server and also made 2 nice videos [[https://www.youtube.com/watch?v=8k33ZvWYQ20][1]] [[https://www.youtube.com/watch?v=Ul8OO4vQMTw][2]]. The big project that uses concurency in Zig is [[https://tigerbeetle.com/][TigerBeetle]] whish is an highly efficient database engine using.

The normal way to do async IO in Zig would be to use its [[file:./async_await][async/await]]async/await feature, but since it is not supported anymore it is completly out of the picture for now. If you still find it interesting to work this way then [[file:./zigcoro][Zigocoro]] might be the best fit since it is almost the interface so when async/await will come back into the language it will be an easy migration, except if the async/await of the language release breaking changes.

The most traditional and easiest way to deal with asynchronous code would be to use [[file:./std.Thread][Kernal threads]] that are disposable in standard library making it easy to use without having to import any external library. The basic functionnalites are very equivalent to those of [[https://man7.org/linux/man-pages/man7/pthreads.7.html][POSIX threads]] making it easy to understand for any C developer.
The most traditional and easiest way to deal with asynchronous code would be to use [[file:./std.Thread][Kernel threads]] that are available in standard library making it easy to use without having to import any external library. The basic functionnalites are very equivalent to those of [[https://man7.org/linux/man-pages/man7/pthreads.7.html][POSIX threads]] making it easy to understand for any C developer.

If you want to monitor non-blocking IO operations without spawning kernel threads which have a big overhead, [[file:./libxev_libuv][libuv]] is the event loop that you should use and [[file:./epoll][epoll]] if you prefer a lower abstraction level and only need to work linux.

Expand Down
18 changes: 10 additions & 8 deletions content/docs/concurrency/std.Thread.org
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ std.debug.print("Total CPU cores = {!}\n", .{std.Thread.getCpuCount()});
#+end_src

**** Thread pool
You could also use a thread pool in order to have a few threads to multiple jobs and not 1 thread = 1 job
You could also use a thread pool in order to have a few threads to multiple jobs and not a thread per job.
#+begin_src zig :imports '(std) :main 'yes :testsuite 'no
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
Expand Down Expand Up @@ -55,9 +55,9 @@ You could also use a thread pool in order to have a few threads to multiple jobs
**** Implementation in the std
Under the hood the threads are either pthread ([[https://ziglang.org/documentation/master/std/#std.Thread.use_pthreads][if we are under linux AND linking libc]]) or it is simpy going to use native OS threads (syscalls) wrapped by a Zig implementation.

The advantage of doing multi-threading in Zig is that you don't have to worry about what is the target system going to be, since **std.Thread** implementation automatically chooses the native OS threads for the system your are compiling for (except if you want to enforce the use of pthreads).
The advantage of doing multi-threading in Zig is that you don't have to worry about what the target system is going to be, since **std.Thread** implementation automatically chooses the native OS threads for the system your are compiling for, except if you want to enforce the use of pthreads by linking the libc.

In C if you are using Windows for exemple, since **pthreads** it is not natively supported you would have to use a third-party implementation by adding a compilation tag like so:
In C if you are using Windows for exemple, since **pthreads** it is not natively supported you would have to use a third-party implementation by adding a compilation flag like so:
#+begin_src c
gcc program.c -o program -pthread
#+end_src
Expand Down Expand Up @@ -171,7 +171,7 @@ Or you could write your own wrapper kind of like the way Zig does (this is not g
***** Zig pthreads vs LinuxThreadImpl vs C pthreads
When compiling on Linux, by default your threads are going to use the [[https://github.com/ziglang/zig/blob/28476a5ee94d311319941b54e9da66210690ce70/lib/std/Thread.zig#L1042][LinuxThreadImpl]]. Which under the hood simply is a wrapper around some syscalls in order to manage threads (the code does closely the same thing as the pthread code).

You might have notice that when you are linking libc, Zig is going to use pthreads instead of the LinuxThreadImpl. This is because pthreads are more performant at the moment and since you are already linking libc it is better to take advantage of that and ue pt hreads.
You might have notice that when you are linking libc, Zig is going to use pthreads instead of the LinuxThreadImpl. This is because pthreads are more performant at the moment and since you are already linking libc it is better to take advantage of that and ue pthreads.

In order to verify that we are going to benchmarks 3 different implementations: one in Zig using LinuxThreadImpl, one in Zig using pthreads and one in C using pthreads.

Expand Down Expand Up @@ -199,12 +199,14 @@ Note that it is hard to benchmark thread implementations and you can easily end
fn goTo() void {}
#+end_src

If we run this code with hyperfine (100 runs) once while linking libc (using pthreads),once in vanilla mode (using LinuxThreadImpl) and a list time using pthreads with C, we can sometimes see that there is indeed a slight performance difference between the them:
If we run this code with hyperfine (100 runs) once while linking libc (using pthreads),once in vanilla mode (using LinuxThreadImpl) and a list time using pthreads with C, we can see that they overlap in the measure variablity, so we can conclude that the performances probably will not change between the different implementations:
- Zig pthreads = 274.4 ms += 4.7 ms
- LinuxThreadImpl = 276.7s ms += 33.9 ms
- C pthreads = 272.7 ms += 34.0 ms

Those test have been run multiple times on different days and the results can vary a bit, but all the implementations can beat each others from time to time, since it is heavily dependent on the OS scheduler and not themselves.
I tried having 100'000 threads instead of 10'000 but I ran out of memory.

Those test have been run multiple times on different days and the results can vary a bit, but all the implementations can beat each others from time to time, since it is heavily dependent on the OS scheduler and not the thread implementations themselves.

The difference is so small that even when only spawning and destroying threads we barely see it. In a real world application where this would very unlikely be the bottleneck, which thread implementation you are going to use is very likely to not change anything the way your program perform.

Expand Down Expand Up @@ -293,7 +295,7 @@ And the equivalent C code:
**** Leaky abstraction
There are 2 things you can tweak when using *std.Thread*: the stack size and the allocator.

The allocator you pass is only going to be needed only if you use the [[https://ziglang.org/documentation/master/std/#std.Thread.WasiThreadImpl][WasiThreadImpl]] (which is the default implementation for WebAssembly).
The allocator you pass is only going to be needed if you use the [[https://ziglang.org/documentation/master/std/#std.Thread.WasiThreadImpl][WasiThreadImpl]] (which is the default implementation for WebAssembly). The reasons for that is that it needs the allocator to dynamically allocate the eventuals metadata that are going to be used by the thread. Here is the [[https://github.com/ziglang/zig/blob/1824bee579fffad3f17b639ebb1a94fd890ad68d/lib/std/Thread.zig#L918][code where you can see that]].
#+begin_src zig
fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) SpawnError!WasiThreadImpl {
if (config.allocator == null) {
Expand All @@ -311,7 +313,7 @@ You wont't have to free it anyway since it is only used to be copied like we can
allocator.free(self.thread.memory);
#+end_src

However, configuring the stack size is going to be used for every implementation of the threads. This is the default stack size:
However, configuring the stack size is going to be used for every implementation of the threads. This is the [[https://github.com/ziglang/zig/blob/1824bee579fffad3f17b639ebb1a94fd890ad68d/lib/std/Thread.zig#L298][default stack size]]:
#+begin_src zig
/// Size in bytes of the Thread's stack
stack_size: usize = 16 * 1024 * 1024
Expand Down
Loading

0 comments on commit f305cab

Please sign in to comment.