Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alexis.durgnat/homepage
  • orestis.malaspin/homepage
2 results
Show changes
Commits on Source (242)
Showing
with 124 additions and 5314 deletions
# image: omalaspinas/hakyll-test:latest
image: omalaspinas/pandoc_website:latest
variables:
......@@ -31,8 +32,8 @@ before_script:
build_and_deploy:
script:
- make
- rsync -avzz css ur1bg_malas@ur1bg.ftp.infomaniak.com:web/malaspinas/
- rsync -avzz figs ur1bg_malas@ur1bg.ftp.infomaniak.com:web/malaspinas/
- rsync -avzz index.html ur1bg_malas@ur1bg.ftp.infomaniak.com:web/malaspinas/
- blc https://malaspinas.academy -ro --exclude *.pdf --filter-level 3
- pandoc -v
- make update SHELL=bash
- make deploy SHELL=bash
- rsync -avzz site/* ur1bg_malas@ur1bg.ftp.infomaniak.com:web/malaspinas/
# - blc https://malaspinas.academy/index.html -ro --exclude *.pdf --filter-level 3 --exclude https://bbb.hesge.ch*
STYLES := css/tufte-css/tufte.css \
css/pandoc.css \
css/pandoc-solarized.css \
css/tufte-extra.css
all: index.html
all: index.html
index.html: index.md Makefile
pandoc -o $@ $< -t html5 --css css/tufte-css/tufte.css -s
deploy: all
make -C archives
mkdir -p site
mkdir -p site/archives
cp index.html site/
cp -r css site/
cp -r figs site/
cp -r archives/index.html site/archives/
update:
git submodule foreach 'git pull origin master || true'
clean:
rm -rf index.html
rm -rf index.html site
all: index.html
index.html: index.md Makefile
pandoc -o $@ $< -t html5 --css ../css/tufte-css/tufte.css -s
clean:
rm -rf index.html
# Cours d'années précédentes
# Physique appliquée
<!-- Chaîne *BBB* du cours <https://bbb.hesge.ch/rooms/ty7-tww-afq-lql/join> -->
---
## [Polycopié](https://malaspinas.academy/phys/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/isc_physics), [pdf](https://malaspinas.academy/phys/cours.pdf)
## Exercices sur les Lois de Newton, [HTML](phys/exercices/newton.html), [PDF](phys/exercices/newton.pdf)
## Travail pratique: vecteurs, [HTML](https://malaspinas.academy/phys/practical_work/tp_vec), [PDF](https://malaspinas.academy/phys/practical_work/tp_vec.pdf), [tp_vec2.tar](https://malaspinas.academy/phys/practical_work/tp_vec2.tar)
## Travail pratique: planètes, [HTML](https://malaspinas.academy/phys/planets/enonce.html), [PDF](https://malaspinas.academy/phys/planets/enonce.pdf), [skeleton.tar](https://malaspinas.academy/phys/planets/skeleton.tar)
## Travail pratique: lignes de champs, [HTML](https://malaspinas.academy/phys/field_lines/enonce.html), [PDF](https://malaspinas.academy/phys/field_lines/enonce.pdf), [utils.tar.gz](https://malaspinas.academy/phys/field_lines/utils.tar.gz)
## Travail pratique: circuit RC, [HTML](https://malaspinas.academy/phys/rc_circuit/enonce.html), [PDF](https://malaspinas.academy/phys/rc_circuit/enonce.pdf)
# Mathématiques en technologie de l'information
## [Polycopié](https://malaspinas.academy/mti/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/math_tech_info), [pdf](https://malaspinas.academy/mti/cours.pdf)
## [Travail pratique: optimisation](https://malaspinas.academy/mti/tpOptimisation/index.html), [git repo](https://gitedu.hesge.ch/orestis.malaspin/math_tech_info/-/tree/master/travaux_pratiques/tpOptimisation), [pdf](https://malaspinas.academy/mti/tpOptimisation/tpOptimisation.pdf)
## [Travail pratique: intégrales](https://malaspinas.academy/mti/tpIntegrales/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/math_tech_info/-/tree/master/travaux_pratiques/tpIntegrales), [pdf](https://malaspinas.academy/mti/tpIntegrales/tp_integrales_conv.pdf)
## [Travail pratique: EDO](https://malaspinas.academy/mti/tpEdo/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/math_tech_info/tree/master/tpEdo), [pdf](https://malaspinas.academy/mti/tpEdo/tpEquadiffs.pdf)
# Programmation concurrente (2019-2020)
## [Polycopié](https://malaspinas.academy/concurrence/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/cours_prog_conc)
# Exercices de programmation concurrente
## TP1: L'ensemble de Julia [PDF](https://malaspinas.academy/tp_concurrence/julia.pdf), [HTML](https://malaspinas.academy/tp_concurrence/julia.html)
## TP2: Les threads et verrous [PDF](https://malaspinas.academy/tp_concurrence/threads.pdf), [HTML](https://malaspinas.academy/tp_concurrence/threads.html)
## TP3: Structures de données concurrentes [PDF](https://malaspinas.academy/tp_concurrence/synchro.pdf), [HTML](https://malaspinas.academy/tp_concurrence/synchro.html)
## TP4: File et workflow [PDF](https://malaspinas.academy/tp_concurrence/file_workflow.pdf), [HTML](https://malaspinas.academy/tp_concurrence/file_workflow.html)
## TP5: Variables de condition [PDF](https://malaspinas.academy/tp_concurrence/variables_condition.pdf), [HTML](https://malaspinas.academy/tp_concurrence/variables_condition.html)
## Projet: Asteroids [PDF](https://malaspinas.academy/tp_concurrence/asteroids.pdf), [HTML](https://malaspinas.academy/tp_concurrence/asteroids.html)
## TP6: Sémaphores [PDF](https://malaspinas.academy/tp_concurrence/semaphores.pdf), [HTML](https://malaspinas.academy/tp_concurrence/semaphores.html)
## Projet: Asteroids concurrents [PDF](https://malaspinas.academy/tp_concurrence/multithreaded_asteroids.pdf), [HTML](https://malaspinas.academy/tp_concurrence/multithreaded_asteroids.html)
# Anciens exercices en C (2019-20)
## Tableaux unidimensionnels, [HTML](https://malaspinas.academy/prog_seq_c_tp/tableaux_unidimensionnels/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/tableaux_unidimensionnels/tableaux_unidimensionnels.pdf), [GFX](https://malaspinas.academy/prog_seq_c_tp/tableaux_unidimensionnels/gfx_example.tar.gz)
## Les matrices, [HTML](https://malaspinas.academy/prog_seq_c_tp/matrices_intro/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/matrices_intro/matrices_intro.pdf)
## Les chaînes de caractères, [HTML](https://malaspinas.academy/prog_seq_c_tp/chaines_caracteres/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/chaines_caracteres/chaines_caracteres.pdf)
## Traitement d'images, [HTML](https://malaspinas.academy/prog_seq_c_tp/transformations/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/transformations/transformations.pdf)
## Vector, [HTML](https://malaspinas.academy/prog_seq_c_tp/vector/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/vector/vector.pdf)
## Fichier LAS, [HTML](https://malaspinas.academy/prog_seq_c_tp/lidar/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/lidar/read.pdf)
## Fichier STL, [HTML](https://malaspinas.academy/prog_seq_c_tp/stl/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/stl/stl.pdf)
## List-Vector, [HTML](https://malaspinas.academy/prog_seq_c_tp/lst_vector/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/lst_vector/linked_vector.pdf)
## Delaunay, [HTML](https://malaspinas.academy/prog_seq_c_tp/delaunay/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/delaunay/delaunay.pdf)
## TP noté 2: triangulation, [HTML](https://malaspinas.academy/prog_seq_c_tp/lidar_triangulation/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/lidar_triangulation/triangulation.pdf)
## Code Morse, [HTML](https://malaspinas.academy/prog_seq_c_tp/morse/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/morse/morse.pdf)
## Arbres quaternaires, [HTML](https://malaspinas.academy/prog_seq_c_tp/quadtree/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/quadtree/quadtree.pdf)
## File de priorité, [HTML](https://malaspinas.academy/prog_seq_c_tp/priority_queue/index.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/priority_queue/linked_list.pdf)
## Graphes, [HTML](https://malaspinas.academy/prog_seq_c_tp/shortest_path/graphes.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/shortest_path/graphes.pdf), [xml_parser.h](https://malaspinas.academy/prog_seq_c_tp/shortest_path/xml_parser.h), [xml_parser.c](https://malaspinas.academy/prog_seq_c_tp/shortest_path/xml_parser.c), [main.c](https://malaspinas.academy/prog_seq_c_tp/shortest_path/main.c), [villes.xml](https://malaspinas.academy/prog_seq_c_tp/shortest_path/villes.xml), [suisse.txt](https://malaspinas.academy/prog_seq_c_tp/shortest_path/suisse.txt)
## Floyd-Dijkstra, [HTML](https://malaspinas.academy/prog_seq_c_tp/shortest_path/df.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/shortest_path/df.pdf), [squelette.c](https://malaspinas.academy/prog_seq_c_tp/shortest_path/squelette/c/main.c), [Makefile](https://malaspinas.academy/prog_seq_c_tp/shortest_path/squelette/c/Makefile), [commandes_a_tester.txt](https://malaspinas.academy/prog_seq_c_tp/shortest_path/squelette/commandes_de_test/cmd_a_tester.txt), [output](https://malaspinas.academy/prog_seq_c_tp/shortest_path/squelette/results_out/output)
## Tests, [HTML](https://malaspinas.academy/prog_seq_c_tp/shortest_path/test.html), [PDF](https://malaspinas.academy/prog_seq_c_tp/shortest_path/test.pdf)
# Programmation séquentielle en Rust (2018-2019)
* [Slides du cours](https://malaspinas.academy/prog_seq/index.html), [git repo](https://githepia.hesge.ch/orestis.malaspin/rust)
# Exercices programmation séquentielle en Rust (2018-19)
## TP1 Rust: Nombre secret, [HTML](https://malaspinas.academy/prog_seq/exercices/01_nombre_secret/), [PDF](https://malaspinas.academy/prog_seq/exercices/01_nombre_secret/index.pdf)
## TP2 Rust: Calcul de $\pi$, [HTML](https://malaspinas.academy/prog_seq/exercices/02_calcul_pi/), [PDF](https://malaspinas.academy/prog_seq/exercices/02_calcul_pi/index.pdf)
## TP3 Rust: Tableaux, [HTML](https://malaspinas.academy/prog_seq/exercices/03_tableaux/), [PDF](https://malaspinas.academy/prog_seq/exercices/03_tableaux/index.pdf)
## TP4 Rust: Couverture de la reine, [HTML](https://malaspinas.academy/prog_seq/exercices/04_reine/), [PDF](https://malaspinas.academy/prog_seq/exercices/04_reine/index.pdf)
## TP5 Rust: Puissance 4, [HTML](https://malaspinas.academy/prog_seq/exercices/05_puissance4/), [PDF](https://malaspinas.academy/prog_seq/exercices/05_puissance4/index.pdf)
## TP6 Rust: Listes, [HTML](https://malaspinas.academy/prog_seq/exercices/06_listes/), [PDF](https://malaspinas.academy/prog_seq/exercices/06_listes/index.pdf)
## TP7 Rust: Reed-Solomon, [HTML](https://malaspinas.academy/prog_seq/exercices/07_reed_solomon/index.html), [PDF](https://malaspinas.academy/prog_seq/exercices/07_reed_solomon/index.pdf)
## TP8 Rust: Transformation d'images, [HTML](https://malaspinas.academy/prog_seq/exercices/08_transformation_images/index.html), [PDF](https://malaspinas.academy/prog_seq/exercices/08_transformation_images/index.pdf)
## TP9 Rust: Problème du voyageur de commerce, [HTML](https://malaspinas.academy/prog_seq/exercices/09_voyageur_commerce/index.html), [PDF](https://malaspinas.academy/prog_seq/exercices/09_voyageur_commerce/index.pdf)
Subproject commit c414b9117d5b39bdb747a172822333fc1043aee3
Subproject commit e225f3a0e5f8f42a1d28416c1c85962411711907
dist
cabal-dev
*.o
*.hi
*.chi
*.chs.h
*.swp
_cache
_site
cabal.sandbox.config
.cabal-sandbox/
.stack-work
The MIT License (MIT)
Copyright (c) 2013-2017 Stephen Diehl
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{-# LANGUAGE OverloadedStrings #-}
--------------------------------------------------------------------
-- |
-- Copyright : (c) Stephen Diehl 2013
-- License : MIT
-- Maintainer: stephen.m.diehl@gmail.com
-- Stability : experimental
-- Portability: non-portable
--
--------------------------------------------------------------------
module Main where
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid (mappend)
import Hakyll
import Text.Pandoc
import qualified Data.Map as M
import Data.Maybe (isJust)
import Text.Pandoc.Highlighting
import Hakyll.Images ( loadImage
, scaleImageCompiler
)
--------------------------------------------------------------------
-- Contexts
--------------------------------------------------------------------
postCtx :: Context String
postCtx =
dateField "date" "%B %e, %Y"
`mappend` mathCtx
`mappend` defaultContext
mathCtx :: Context String
mathCtx = field "mathjax" $ \item -> do
metadata <- getMetadata $ itemIdentifier item
return ""
return $ if isJust $ lookupString "mathjax" metadata
then "<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS_HTML\"></script>"
else ""
archiveCtx posts =
listField "posts" postCtx (return posts)
`mappend` constField "title" "Archives"
`mappend` defaultContext
indexCtx posts =
listField "posts" postCtx (return posts)
`mappend` constField "title" "Home"
`mappend` defaultContext
--------------------------------------------------------------------
-- Rules
--------------------------------------------------------------------
static :: Rules ()
static = do
match "fonts/*" $ do
route idRoute
compile $ copyFileCompiler
match "img/*" $ do
route idRoute
compile $ copyFileCompiler
match "css/*" $ do
route idRoute
compile compressCssCompiler
match "js/*" $ do
route idRoute
compile $ copyFileCompiler
resize :: Rules ()
resize = do
match "img/**.png" $ do
route idRoute
compile $ loadImage
>>= scaleImageCompiler 140 140
pages :: Rules ()
pages = do
match "pages/*" $ do
route $ setExtension "html"
compile $ getResourceBody
>>= loadAndApplyTemplate "templates/page.html" postCtx
>>= relativizeUrls
posts :: Rules ()
posts = do
match "posts/*" $ do
route $ setExtension "html"
-- compile $ myPandocCompiler
compile $ bibtexCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= relativizeUrls
archive :: Rules ()
archive = do
create ["archive.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" (archiveCtx posts)
>>= relativizeUrls
cours :: Rules ()
cours = do
match "cours/*" $ do
route $ setExtension "html"
-- compile $ myPandocCompiler
compile $ bibtexCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= relativizeUrls
conc :: Rules ()
conc = do
create ["prog_conc.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "cours/*"
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" (archiveCtx posts)
>>= relativizeUrls
index :: Rules ()
index = do
match "index.html" $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
getResourceBody
>>= applyAsTemplate (indexCtx posts)
>>= relativizeUrls
templates :: Rules ()
templates = match "templates/*" $ compile templateCompiler
--------------------------------------------------------------------
-- Configuration
--------------------------------------------------------------------
myPandocCompiler :: Compiler (Item String)
myPandocCompiler = pandocCompilerWith defaultHakyllReaderOptions pandocOptions
pandocOptions :: WriterOptions
pandocOptions = defaultHakyllWriterOptions
{
writerExtensions = defaultPandocExtensions
, writerHTMLMathMethod = MathJax ""
}
-- Pandoc extensions used by the myPandocCompiler
defaultPandocExtensions :: Extensions
defaultPandocExtensions =
let extensions = [
-- Pandoc Extensions: http://pandoc.org/MANUAL.html#extensions
-- Math extensions
Ext_tex_math_dollars
, Ext_tex_math_double_backslash
, Ext_latex_macros
-- Code extensions
, Ext_fenced_code_blocks
, Ext_backtick_code_blocks
, Ext_fenced_code_attributes
, Ext_inline_code_attributes -- Inline code attributes (e.g. `<$>`{.haskell})
-- Markdown extensions
, Ext_implicit_header_references -- We also allow implicit header references (instead of inserting <a> tags)
, Ext_definition_lists -- Definition lists based on PHP Markdown
, Ext_yaml_metadata_block -- Allow metadata to be speficied by YAML syntax
, Ext_superscript -- Superscripts (2^10^ is 1024)
, Ext_subscript -- Subscripts (H~2~O is water)
, Ext_footnotes -- Footnotes ([^1]: Here is a footnote)
]
defaultExtensions = writerExtensions defaultHakyllWriterOptions
in foldr enableExtension defaultExtensions extensions
bibtexCompiler :: Compiler (Item String)
bibtexCompiler = do
getResourceBody
>>= withItemBody (unixFilter "pandoc" ["-F"
, "pandoc-numbering"
, "-F"
, "pandoc-crossref"
, "-t"
, "markdown"
])
>>= readPandocWith defaultHakyllReaderOptions
>>= return . writePandocWith pandocOptions
cfg :: Configuration
cfg = defaultConfiguration
main :: IO ()
main = hakyllWith cfg $ do
pages
posts
cours
conc
archive
index
templates
static
hakyll-bootstrap
================
<p align="center" style="padding: 20px; width: 50%">
<img src="https://raw.github.com/sdiehl/hakyll-bootstrap/master/sample.png">
</p>
A template for a small corporate Hakyll site.
**Using stack**
```bash
$ stack build
$ stack exec blog -- preview
```
**Using cabal**
To get started run:
```shell
$ cabal sandbox init
$ cabal install --only-dependencies
$ cabal run preview
```
The default static pages are renderd with plain HTML with mixins
from the ``/templates`` folder..
```
index.html
pages/
about.html
contact.html
privacy.html
signup.html
team.html
tos.html
```
Blog posts are placed under the ``/posts`` folder and are
generated from Markdown.
Inline math is enabled via setting the ``mathjax`` metadata to
``on``, by default MathJax is disabled.
```text
---
title: Example Blog Post
author: Stephen Diehl
date: 2013-11-13
mathjax: on
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam
non est in neque luctus eleifend. Sed tincidunt vestibulum
facilisis. Aenean ut pulvinar massa.
```
License
--------
Released under MIT License.
import Distribution.Simple
main = defaultMain
---
author:
- Orestis Malaspinas, Steven Liatti
title: Les bugs
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Les bugs les plus courants
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^2].
Dans ce chapitre, nous allons discuter certains des bugs les
plus fréquents qu'on retrouve dans les codes concurrents.
## Quels sont les bugs possibles
Un certain nombre d'études ont été faites sur les bugs les
plus fréquents
lors de l'écriture de codes concurrents. Ce que nous allons
raconter ici se base sur: *Learning from Mistakes - A Comprehensive Study on Real World Concurrency Bug Characteristics*, par Shan Lu, Soyeon Park, Eunsoo Seo,
et Yuanyuan Zhou. **ASPLOS 2008**, Mars 2008, Seattle,
Washington. Cette étude est basée sur un certain nombre de
logiciels open source populaires (MySQL, OpenOffice, Apache, et Mozilla le butineur). L'avantage
de ce genre de codes est que des gens externes peuvent voir
et analyser les modifications faites et ainsi examiner
en particulier les bugs liés à la concurrence. Il est ainsi
possible de comprendre quels types d'erreurs les gens font et
tenter d'empêcher le répétition.
Il ressort qu'une grande partie des bugs sont des interblocages (ou deadlocks).
En effet, sur 74 bugs de concurrence, 31 étaient
dûs à des deadlocks et les 43 restants autre chose[^1].
On va d'abord commencer par étudier les bugs "non-deadlocks",
car ils sont plus nombreux et "plus simples" à analyser.
Puis nous passerons aux bugs "deadlocks".
## Les bugs pas dû à de l'interblocage
Selon l'étude de *Lu et al.* deux classes de bugs principales:
la **violation d'atomicité** et la **violation d'ordre**.
### La violation d'atomicité
La violation d'atomicité est quand un thread écrit
et un autre lit des données partagées sans
que celles-ci soient protégées par une primitive
d'exclusion mutuelle. Par exemple le pseudo-c suivant
```language-c
void *t1() {
if (thd->info) {
// read thd->info
}
}
void *t2() {
thd->info = NULL;
}
```
Ici `t1()`{.language-c} vérifie si `thd->info`{.language-c}
est non-`NULL`{.language-c} avant de lire sa valeur et de
faire des opérations avec éventuellement. Dans le même temps
`t2()`{.language-c} assigne `thd->info`{.language-c}
à `NULL`{.language-c}. Si entre la vérification
de `thd->info`{.language-c} et son utilisation `t2()`{.language-c} prend la main et fait son office,
`t1()`{.language-c} va ensuite tenter de déréférencer un pointeur `NULL`{.language-c}
ce qui va causer un plantage magistral.
Ce genre de bugs se corrige assez simplement.
---
Question #
Comment?
---
Dans ce cas il suffit bien souvent d'insérer un verrou
et de protéger la section critique où on
assigne `ths->info`{.language-c} à `NULL`{.language-c}
et l'endroit où on le lit.
```language-c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *t1() {
pthread_mutex_lock(&mutex);
if (thd->info) {
// read thd->info
}
pthread_mutex_unlock(&mutex);
}
void *t2() {
pthread_mutex_lock(&mutex);
thd->info = NULL;
pthread_mutex_unlock(&mutex);
}
```
### Les bugs de violation d'ordre
Une violation d'ordre se produit lorsque l'ordre dans
lequel deux accès mémoires sont inversés: on veut
que $A$ soit toujours exécuté avant $B$, mais
que cela n'est pas garantit à l'exécution.
Un exemple de ce genre d'exécution (en pseudo-c) serait:
```language-c
// initialize something here
void *t1() {
// do things
thread = create_thread_do_things_and_return(other_thread, ...);
// do more things
}
void *t2() {
// do things
state = thread->state;
// do even more things
}
```
Ici, dans `t1()`{.language-c} on crée un thread effectue un certain nombre d'opérations et on retourne une valeur
stockée dans `thread`{.language-c}. Dans le thread `t2()`{.language-c},
on suppose que `thread`{.language-c} est initialisé et on l'utilise.
En fait, comme nous l'avons déjà vu, au moment où `t2()`{.language-c}
arrive à la ligne `state = thread->state`{.language-c}, la variable `thread`{.language-c}
n'a pas forcément de valeur, car la fonction `create_thread_do_things_and_return()`{.language-c}
n'a pas encore retourné. L'ordre dans lequel on aimerait que soient
exécutées les instructions n'est pas celui qui est imposé à l'exécution.
---
Question #
Comment corriger ce code?
---
Une façon de corriger ce code est d'utiliser une variable de condition et signaler
à `t2()`{.language-c} que `thread`{.language-c} contient une valeur
```language-c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool ini_done = false;
// initialize something here
void *t1() {
// do things
thread = create_thread_do_things_and_return(other_thread, ...);
// we signal the initialization
pthread_mutex_lock(&mutex);
done = true;
pthread_mutex_signal(&cond);
pthread_mutex_unlock(&mutex);
// do more things
}
void *t2() {
// do things
// we wait on the initialization
pthread_mutex_lock(&mutex);
done = true;
while (!ini_done) {
pthread_mutex_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);
state = thread->state;
// do even more things
}
```
On a donc forcé l'ordre d'exécution de l'initialisation de `thread`{.language-c}
et de son utilisation à l'aide d'un booléen, d'une variable de condition et d'un verrou.
## Les bugs d'interblocage
Nous avons déjà vu un exemple typique de bug d'interblocage. Ce genre de bug peut survenir
lorsqu'on a deux threads qui vérouillent deux verrous dans des ordres différents. Imaginons la situation
où deux threads $T_1$ et $T_2$ verrouillent/déverrouillent deux verrous
$V_1$ et $V_2$.
| $T_1$ | $T_2$ |
| ----------------------|---------------|
| `pthread_mutex_lock(&V_1)`{.language-c} | `pthread_mutex_lock(&V_2)`{.language-c} |
| `pthread_mutex_lock(&V_2)`{.language-c} | `pthread_mutex_lock(&V_1)`{.language-c} |
Le tableau ci-dessus montre une situation où un interblocage **peut** se produire.
En effet, si $T_1$ verrouille $V_1$ et qu'un changement de contexte se produit
et $T_2$ verrouille $V_2$, les deux fils d'exécution attendent
l'un sur l'autre et un interblocage survient.
Une façon de voir cette situation est via le schéma de la @fig:interblocage.
On constate sur le schéma que les demandent de verrouillage et l'acquisition
des verrous forment un cycle. Ce genre de cycle est en général l'indication qu'un interblocage est possible.
![Schéma de l'interblocage de deux thread verrouillant et
tentant de verrouiller deux verrous dans un ordre différent.
On voit un cycle se former ce qui peut indiquer
la présence d'un interblocage potentiel.](figs/interblocage.svg){#fig:interblocage width=40%}
Dans ce qui suit, nous allons voir quelles sont les conditions
qui doivent être réunies pour l'existence des interblocages, et
comment essayer les éviter.
### Pourquoi surviennent les interblocages?
Les constructions amenant à des interblocages peuvent être beaucoup plus complexes
que dans l'exemple que nous venons de voir. En fait, dans de grands projets,
des dépendances très complexes entre différentes parties du programme peuvent survenir.
Une autre raison est **l'encapsulation**. Le développement
logiciel encourage l'encapsulation pour cacher au
maximum les détails d'implémentation et ainsi construire
les logiciels aussi modulaires et faciles à maintenir que
possible. Hélas, cela a aussi pour effet de
cacher certaines parties critiques comme les verrous.
Imaginons la situation où nous avons une fonction,
`add_vectors_in_place(v1, v2)`{.language-c},
qui additionne les valeurs de deux tableaux
de nombres `v1` et `v2`. Afin que cette fonction soit
thread-safe il faut verrouiller `v1` et `v2`.
Si le verrou est acquis dans l'ordre
`v1` puis `v2`, et que dans un autre thread
on appelle la même fonction avec les arguments
inversés, `add_vectors_in_place(v2, v1)`{.language-c},
on peut se retrouver dans une situation
d'interblocage sans qu'on ait commis une erreur explicite!
### Conditions pour l'interblocage
Il y a quatre conditions distinctes pour l'apparition
d'un interblocage:
- *L'exclusion mutuelle*: plsuieurs threads essaient d'avoir le contrôle exclusif d'une ressource (un thread acquière un verrou).
- *Hold-and-wait*: plusieurs threads possèdent des ressources (ont déjà acquis un verrou) et attendent d'en obtenir d'autres (attendent sur un autre verrou par exemple).
- *Pas de préemption*: Les ressources (un verrou par exemple) ne peuvent pas être libérées des fils qui les possèdent.
- *Une attente circulaire*: Il existe une chaîne de fils d'exécution telle que chaque fil possède une ressource qui est attendu pour verrouillage par le prochain thread dans la chaîne.
Si une de ces condition n'est pas remplie, il ne peut y avoir de deadlock. On va voir maintenant quelques techniques
pour éviter les interblocages en évitant chacune des ces conditions.
### L'attente circulaire
La façon la plus courante d'éviter les interblocages est
d'écrire le verrouillage en évitant les attentes circulaires. La façon la plus simple est d'utilser
un **ordre global** pour l'acquisition des verrous.
Par exemple, si nous avons deux verrous dans notre code, $V_1$ et $V_2$, les verrous seront toujours
acquis dans l'ordre $V_1$, puis $V_2$ et jamais l'inverse.
De cette façon, nous garantissons qu'il n'y aura jamais d'attente circulaire et donc jamais d'interblocage.
Il est commun que dans des systèmes plus complexes, il y a plus de verrous et donc un tel ordre ne peut pas toujours être garantit. On utilise alors un **ordre partiel**. Un exemple
d'un tel ordre partiel peut se lire dans les commentaires du [code suivant](https://elixir.bootlin.com/linux/latest/source/mm/filemap.c). Dans cet exemple, il y a un grand nombre
d'acquisition de verrous, qui vont du peu complexe (deux verrous) à de beaucoup plus complexes,
allant jusqu'à dix verrous différents. Il est évident, que ce genre de documentation
est très utile pour le bon fonctionnement du code. Il est également certain que c'est très
difficile pour n'importe quel programmeur ne connaissant pas parfaitement
la structure d'un code de ce genre d'éviter le bugs, même s'il fait très attention.
Une façon un peu plus systématique de procéder est d'utiliser les adresses
des verrous pour les ordonner. Imaginons une fonction prenant deux verrous en argument,
`foo(mutex_t *m1, mutex_t *m2)`{.language-c}. Si l'ordre d'acquisition des verrous
dépend de l'ordre dans lequel on passe les arguments à la fonction,
un deadlock est très vite arrivé si on appelle une fois `foo(m1, m2)`{.language-c}
et une fois `foo(m2, m1)`{.language-c}. Si en revanche on utilise
les adresses des verrous comme dans le code ci-dessous, on s'affranchit de ce problème
```language-c
void foo(mutex_t *m1, mutex_t m2) {
if (m1 > m2) { // on verrouille d'abord le verrou avec l'adresse la plus élevée
pthread_mutex_lock(m1);
pthread_mutex_lock(m2);
} else if (m2 > m1) {
pthread_mutex_lock(m2);
pthread_mutex_lock(m1);
} else {
assert(false && "Les deux verrous ne peuvent pa avoir la même adresse");
}
// do stuff
}
```
### Hold-and-wait
Le condition de détenir un verrou et attendre pour en acquérir un autre
peut être prévenue en acquérant les verrous de façon atomique: en mettant un verrou autour de la séquence de verrous à acquérir.
```language-c
mutex_t global, v1, v2;
pthread_mutex_lock(&global); // début de l'acquisition des verrous
pthread_mutex_lock(&v1);
pthread_mutex_lock(&v2);
// ...
pthread_mutex_unlock(&global); // fin
```
De cette façon, comme le verrou `global`{.language-c}
protège l'acquisition de tous les autres verrous, l'interblocage
est certainement évité. Si un autre thread tentait
d'acquérir les verrous `v1`{.language-c} et `v2`{.language-c}
dans un ordre différent, cela ne poserait pas problème
car il devrait attendre la libération du verrou `global`{.language-c}.
Cette méthode n'est pas idéale. En effet, l'encapsulation
possible de l'acquisition des verrous, nous oblige à
connaître en détail la structure et l'ordre de l'acquisition
des verrous pour pouvoir mettre ce genre de solution en place.
De plus, cette façon de faire pourrait diminuer de façon non négligeable la concurrence de notre application.
### Pas de préemption
Les verrous sont en général vus comme détenus jusqu'à ce qu'ils
soient déverrouillés (on ait appelé la fonction `unlock()`{.language-c}). Plusieurs problèmes peuvent apparaître
lorsqu'on détient un verrou et qu'on attend d'en acquérir un autre. Une interface plus flexible peut nous aider dans ce cas.
La fonction `pthread_mutex_trylock()`{.language-c} verrouille
le verrou si possible et retourne `0`{.language-c} ou retourne
un code d'erreur si le verrou est déjà acquis. On peut donc
réessayer de verrouiller les verrous plus tard
avec le code suivant
```language-c
before:
pthread_mutex_lock(&v1);
if (pthread_mutex_trylock(&v2) != 0) { // si on peut pas verrouiller
pthread_mutex_unlock(&v1); // on déverouille
goto before; // on retourne à la première tentative d'acquisition
}
```
De cette façon, même si un fil essaie de verrouiller `v2`{.language-c} avant `v1`{.language-c} on a pas de problème
d'interblocage. Néanmoins, un autre problème peut survenir,
celui de **l'interblocage actif** (ou **livelock**). Imaginons
que deux threads tentent en même temps cette séquence
d'acquisition et ratent dans leurs tentatives d'acquisition.
Le programme ne serait pas bloqué (contrairement à l'interblocage "passif", les fils s'exécuteraient toujours) mais
serait dans une boucle infinie de tentatives d'acquisition. Pour
se prémunir de ce problème on pourrait imaginer ajouter un délai aléatoire entre les tentatives d'acquisition
pour minimiser les probabilités d'avoir un interblocage actif.
Cette méthode pour éviter l'interblocage n'est évidemment
pas idéale, car encore une fois, l'encapsulation ne nous aide pas: le `goto`{.language-c} peut être **très** compliqué à implémenter... De plus si plus d'un verrou était acquis "en chemin", il faudrait également le déverrouiller après
le `trylock()`{.language-c}.
### Exclusion mutuelle
Finalement, on peut aussi essayer de se débarrasser de
l'exclusion mutuelle. Cela peut être difficile à concevoir,
mais à l'aide des instructions matériel, on peut construire
des structure de données qui sont sans verrou explicite
(lock-free).
On se rappelle qu'on a vu certaines instructions permettant
de construire des verrous à attente active. On avait par exemple
la fonction atomique `compute_and_exchange()` qui pouvait s'écrire
de la façon suivante
```language-c
int compare_and_exchange(int *addr, int expected, int new) {
if (*adress == expected) {
*adress = new;
return 1; // succès
}
return 0; // erreur
}
```
A l'aide de cette fonction on pourrait construire une fonction
qui incrémenterait une valeur, `value`{.language-c}, d'une
certaine quantité, `amount`{.language-c} de façon atomique
sans explicitement avoir à utiliser un verrou.
```language-c
void atomic_increment(int *value, int amount) {
do {
int old = *value;
} while (compare_and_exchange(value, old, old + amount) == 0);
}
```
De cette façon, comme nous n'utilisons pas de verrou, un
interblocage "passif" ne peut pas se produire (on peut avoir
un interblocage actif, mais ils sont beaucoup plus difficiles
à obtenir).
On peut également considérer un exemple plus complexe: l'insertion en tête d'une liste.
```language-c
typedef struct __node_t {
int value;
__node_t *next;
} node_t;
node_t *head;
void insert(int value) {
node_t *node = malloc(sizeof(node_t));
assert(node != NULL);
node->value = value;
node->next = head;
head = node;
}
```
En utilisant un verrou, on ferait un `lock()`{.language-c}
avant de modifier le pointeur `next`{.language-c} et un `unlock()`{.language-c} après avoir
modifié `head`{.language-c}.
```language-c
pthread_mutex_t list_lock = PTHREAD_MUTEX_INITIALIZER;
void insert(int value) {
node_t *node = malloc(sizeof(node_t)); // on sait que malloc est thread safe
assert(node != NULL);
node->value = value;
pthread_mutex_lock(&list_lock); // début de section critique
node->next = head;
head = node;
pthread_mutex_unlock(&list_lock); // fin de section critique
}
```
---
Question +.#
Comment modifier ce code pour avoir une insertion sans verrou?
---
De façon similaire à l'incrémentation, on peut modifier ce code en ajoutant un
`compare_and_exchange()`{.language-c}.
```language-c
pthread_mutex_t list_lock = PTHREAD_MUTEX_INITIALIZER;
void insert(int value) {
node_t *node = malloc(sizeof(node_t)); // on sait que malloc est thread safe
assert(node != NULL);
node->value = value;
do {
node->next = head;
} while (compare_and_exchange(&head, node->next, node) == 0);
}
```
Ce code essaie de façon atomique d'échanger la tête avec un noeud nouvellement créé.
Il est possible qu'un autre thread ait modifié la tête pendant la création du noeud,
et donc la condition de la boucle `do ... while`{.language-c} ne va pas être
remplie et entraîner un nouveau tour de boucle.
Les autres fonctions pour manipuler des listes peuvent être bien plus complexes.
Il existe une grande littérature sur le sujet des structures de données concurrentes
qui fonctionnent sans verrous (les structures **lock-free** ou **wait-free**).
## Éviter les interblocages avec le scheduling
Plutôt que de prévenir les interblocages on peut également les éviter totalement.
Cela se fait en ayant une connaissance approfondie des mécanismes de verrouillage
du programme. On peut ainsi ordonnancer les threads de façon appropriée "à la main".
Imaginons qu'on ait deux processeurs, $P_1$ et $P_2$, et quatre fils d'exécution, $T_{1-4}$, qui peuvent s'ordonnancer sur ces processeurs.
Supposons que le thread $T_1$ tente de vérouiller $V_1$ et $V_2$ à un moment donné de l'exécution,
$T_2$ également $V_1$ et $V_2$, $T_3$ seuelement $V_2$ et $T_4$ n'essaie d'acquérir aucun verrou.
On peut résumer cela dans le tableau suivant:
| | $T_1$ | $T_2$ | $T_3$ | $T_4$ |
| --------------|---------------|---------------|---------------|---------------|
| $V_1$ | oui | oui | non | non |
| $V_2$ | oui | oui | oui | non |
De ce tableau, on peut déduire que si $T_1$ et $T_2$ ne sont jamais ordonnancés en même temps,
on ne peut pas avoir d'interblocage. Une possibilité serait par exemple d'ordonnancer
$T_1$ et $T_2$ à la suite sur $P_1$ et $T_3$ et $T_4$ à la suite sur $P_2$.
---
Question +.#
Comment ordonnancer le tableau suivant?
| | $T_1$ | $T_2$ | $T_3$ | $T_4$ |
| --------------|---------------|---------------|---------------|---------------|
| $V_1$ | oui | oui | oui | non |
| $V_2$ | oui | oui | oui | non |
---
Bien que ces méthodes existent, elles sont très difficiles à mettre en place et sont
assez rares en pratique. Sachez simplement qu'elles existent et que parfois
elles peuvent être utiles.
[^1]: Après un calcul savant on voit que $31/74\cdot 100\cong 42\%$ des bugs sont dû à des interblocages. 42... coincidence je ne crois pas.
[^2]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
---
author:
- Orestis Malaspinas, Steven Liatti
title: Introduction aux variables de condition
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Les variables de condition
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^4].
Après avoir discuté les verrous, et leur construction, nous allons
discuter d'une autre structure qui est nécessaire
pour construire des applications concurrentes: les **variables de conditions**. Les variables de condition sont utilisées
lorsque qu'un fil d'exécution doit d'abord vérifier
une condition avant de continuer.
Le problème peut s'illustrer de la façon suivante
```language-c
void *foo() {
printf("Nous y sommes.\n");
// On veut signaler qu'on a terminé:
// mais comment faire?
return NULL;
}
int main() {
printf("Début du programme.\n");
pthread_t tid;
pthread_create(&tid, NULL, foo, NULL);
// On aimerait attendre sur la fonction foo
// mais comment faire?
printf("Fin du programme.\n");
return EXIT_SUCCESS;
}
```
Une solution serait d'utiliser une variable partagée
entre le thread principal et le thread enfant comme
dans l'exemple ci-dessous
```language-c
bool done = false;
void *foo() {
printf("Nous y sommes.\n");
done = true;
return NULL;
}
int main() {
printf("Début du programme.\n");
pthread_t tid;
pthread_create(&tid, NULL, foo, NULL);
while (!done) {
// do nothing, just wait
}
printf("Fin du programme.\n");
return EXIT_SUCCESS;
}
```
Cette solution, bien que très simple, est très inefficace.
En effet, le thread principal va tourner dans la boucle `while`{.language-c} et gaspiller des ressources pour rien.
Par ailleurs, il est assez simple d'imaginer des situations
où on peut produire du code qui sera simplement faux.
## Définition
Pour pallier aux problèmes d'inefficacité et de justesse que nous venons de discuter, on utilise les **variables de condition**.
Une variable de condition est une file d'attente, dans laquelle les
fils d'exécution peuvent se mettre lorsqu'une condition n'est pas satisfaite. Ils y attendront que la dite condition soit remplie.
Un autre thread peut alors, lorsque l'état de la condition change,
réveiller un ou plusieurs fils qui sont en attente pour leur permettre
de continuer. Il va **signaler** aux threads en attente de se réveiller.
Les variables de conditions dans la librairie POSIX se déclare
via le type `pthread_cond_t`{.labguage-c}. Une telle
variable se manipule via les fonctions[^1]
```language-c
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
pthread_cond_signal(pthread_cond_t *c);
```
Ces deux fonctions, sont les appels fondamentaux des variables de
condition. La fonction `cond_signal()`{.language-c} (on va utiliser ici
les raccourcis `cond_wait()`{.language-c} et `cond_signal()`{.language-c}
car je suis flemmard) est utilisée pour signaler qu'une variable a
changé d'état et réveille un thread. La fonction `cond_wait()`{.language-c}
a la responsabilité de mettre le thread qui l'appelle en attente.
Il faut noter que cette fonction prend un `mutex`{.language-c} en
paramètre. Par ailleurs, `cond_wait()`{.language-c}
fait l'hypothèse que le `mutex`{.language-c} est verrouillé
et a la responsabilité de libérer le verrou et de mettre
le thread en attente. Puis, lorsque le dit thread doit se réveiller
(parce qu'un autre fil d'exécution le lui a signalé)
`cond_wait()`{.language-c} doit acquérir le verrou à nouveau
et continuer l'exécution du thread. Cette partie est quelque peu
complexe. Pour la comprendre considérons le code ci-dessous
```language-c
bool done = false;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void cond_wait() {
pthread_mutex_lock(&mutex);
while (!done) {
pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);
}
void cond_signal() {
pthread_mutex_lock(&mutex);
done = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
void *foo() {
printf("Nous y sommes.\n");
cond_signal();
return NULL;
}
int main() {
printf("Début du programme.\n");
pthread_t tid;
pthread_create(&tid, NULL, foo, NULL);
cond_wait();
printf("Fin du programme.\n");
return EXIT_SUCCESS;
}
```
Il y a deux cas distincts:
1. Le thread principal crée un thread enfant et continue son exécution.
Il acquière le `mutex`{.language-c} et est mis en attente
lorsqu'il appelle `cond_wait()`{.language-c} et libère le verrou.
A un moment ou un autre, le fil enfant s'exécutera.
En appelant la fonction `cond_signal()`{.language-c}, il
verrouillera le `mutex`{.language-c}, assignera
la valeur `true`{.language-c} à la variable `done`{.language-c}
et appellera `pthread_cond_signal()`{.language-c}, ce qui aura pour effet
de réveiller le thread principal. Le thread principal
sera réveillé et aura le `mutex`{.language-c} dans un état
verrouillé, sortira de la boucle `while`{.language-c}
déverrouillera le `mutex`{.language-c} et terminera son exécution.
2. Le thread enfant est exécuté immédiatement, il appelle `cond_signal()`{.language-c}
et il modifie `done`{.language-c}. Comme
il n'y a pas de thread en attente, rien ne se passe. Le thread
principal peut donc finir son exécution tranquillement.
---
Remarque #
Comme nous allons le voir plus bas, il est très important d'utiliser la boucle `while`{.language-c} bien qu'ici cela ne soit
pas strictement nécessaire.
---
Il se pourrait que vous soyez tenté·e·s de modifier quelque peu
l'implémentation ci-dessus. N'en faites rien, cela pourrait causer
des dommages irréversibles à vos codes. Changeons un peu les
différentes parties du programme pour voir ce qui peut se passer.
Si nous remplaçons les fonctions `cond_wait()`{.language-c} et
`cond_signal()`{.language-c} par les codes ci-dessous (il va y avoir plusieurs
exemples faux)
```language-c
void cond_wait() {
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
}
void cond_signal() {
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
```
---
Question #
Pourquoi cette façon de faire est-elle problématique?
---
Dans ce cas, on utilise pas la variable `done`{.language-c}.
Hors, si on imagine qu'on est dans le cas où le thread enfant
est exécuté immédiatement, il va effectuer la signalisation et
retourner immédiatement. Le thread principal lui va ensuite
attendre indéfiniment, car plus aucun autre thread ne lui signalera
jamais de se réveiller.
L'exemple suivant est également faux. Nous nous débarrassons
cette fois du verrou
```language-c
void cond_wait() {
if (!done) {
pthread_cond_wait(&cond, &mutex);
}
}
void cond_signal() {
done = true;
pthread_cond_signal(&cond);
}
```
---
Question #
Pourquoi cette façon de faire est-elle également problématique?
---
Il y a ici un problème d'accès concurrent. Si le thread enfant
appelle `cond_wait()`{.language-c} et voit que
`done == false`{.language-c} il voudra se mettre en attente.
Hors, si à ce moment précis il y a un changement de contexte
et `cond_signal()`{.language-c} est appelé, il signalera
la condition, mais comme aucun fil n'est en attente, ce signal
sera perdu. Le thread enfant sera à un moment ou un autre
ré-ordonnancé et sera mis en attente. Comme aucun autre
signal ne sera jamais émis il restera endormi à jamais.
De ces deux mauvais exemples, on peut tirer un enseignement.
Même si cela n'est pas toujours nécessaire, il faut **toujours**
détenir le verrou avant de signaler une condition. A l'inverse,
il est toujours nécessaire d'avoir un verrou lors de l'appel
de `pthread_cond_wait()`{.language-c}. La fonction `pthread_cond_wait()`{.language-c}
présuppose que le verrou est détenu lors de son appel.
La règle générale est finalement: quoi qu'il arrive avant
d'attendre ou de signaler acquérir le verrou (et le libérer ensuite).
## Le problème producteurs/consommateurs
Le problème des **producteurs/consommateurs** a été à l'origine du
développement des variables de condition par Dijkstra.
On peut imaginer un ou plusieurs fils d'exécution
produisant des données et les plaçant dans une mémoire
tampon (buffer). Le ou les consommateurs vont chercher
les données dans la mémoire tampon et les utilisent.
Un exemple typique est la fonction `pipe` d'un programme dans un autre dans les systèmes UNIX:
```language-bash
grep hop file.txt | wc -l
```
Ici un processus exécute la fonction `grep`, qui écrit les lignes
du fichier `file.txt``hop` est présent sur la sortie standard
(enfin c'est ce que `grep` croit). Le système d'exploitation
redirige en fait la sortie dans un "tuyau" (*pipe*) UNIX. L'autre côté
du *pipe* est connecté à l'entrée du processus `wc` qui compte les
lignes dans le flot de données en entrée et affiche le résultat.
On a donc que `grep` produit des données et `wc` les consomme
avec un buffer entre deux.
---
Question #
Pouvez-vous imaginer une autre application de ce type?
---
Comme la mémoire tampon est une ressource partagée, il faut trouver un mécanisme de synchronisation pour éviter
les accès concurrents.
Pour simplifier, représentons le buffer par un simple entier[^2].
On a ensuite deux fonctions:
* la fonction `put()`{.language-c} qui va insérer une valeur
dans la mémoire tampon.
* la fonction `get()`{.language-c} qui va récupérer une valeur
de la mémoire tampon.
Finalement, on a également besoin de savoir combien de données
sont stockées dans le buffer. Pour cela on utilise un simple compteur.
```language-c
int buffer;
int counter = 0; // le compteur
void put(int value) {
assert(counter == 0 && "trying to put into a full buffer");
counter = 1;
buffer = value;
}
int get() {
assert(counter == 1 && "trying to get from empty buffer");
counter = 0;
return buffer;
}
```
La fonction `put()`{.language-c} suppose que le `buffer`
ne contient rien (`counter == 0`{.language-c}) et vérifie l'état
avec une `assert()`{.language-c}. Ensuite elle incrémente la valeur
du compteur à 1 et assigne la valeur `value`{.language-c} au
`buffer`{.language-c}. A l'inverse la fonction `get()`{.language-c}
fait l'hypothèse que le `buffer` contient une valeur
(`counter == 1`{.language-c}) et vérifie également cet état avec
une `assert()`{.language-c}.
A présent, il faut des fonctions qui auront la sagesse nécessaire
pour appeler les fonctions `put()`{.language-c} et `get()`{.language-c} au moments opportuns (quand `counter` a la bonne valeur, zéro ou un respectivement). Grâce au design astucieux
de `put()`{.language-c} et `get()`{.language-c} on saura très
vite si on fait quelque chose de faux, car les assertions
vont faire planter le programme.
Ces actions seront effectuées par deux types de threads:
les threads **producteurs** et les threads **consommateurs**.
Pour simplifier, écrivons d'abord une version complètement fausse
et naive mais qui illustre ce que ferait un thread producteur qui
met `loops`{.language-c} valeurs dans le buffer et un
thread consommateur qui récupère à l'infini les valeurs
stockées dans le buffer.
```language-c
// rien n'est initialisé c'est normal c'est une illustration
// complètement absurde
void *producer(void *arg) {
int loops = (int)*arg;
for (int i = 0; i < loops; ++i) {
put(i);
}
}
void *consumer(void *arg) {
while (1) {
int i = get();
printf("%d\n", i);
}
}
```
Il est évident que ce code ne peut pas fonctionner.
Les `assert()`{.language-c} passeraient le temps à tout
arrêter. On a des sections critiques dans `put()`{.language-c}
et `get()`{.language-c} qui ne sont pas du tout gérées.
Mais vous voyez l'idée de ce qu'on essaie de faire.
Juste utiliser un verrou pour protéger,
`put()`{.language-c} et `get()`{.language-c}
comme on l'a fait jusqu'ici ne peut pas fonctionner.
---
Question #
Pourquoi?
---
Il nous faut en fait un moyen de prévenir chacun des threads
de l'état du buffer afin qu'il puisse effectuer une action
appropriée.
### Une solution, fausse
La solution la plus simple serait d'ajouter une variable de condition et son verrou associé. Un code du genre de celui
se trouvant ci-dessous par exemple
```language-c
int loops = 10; // une initialisation au hasard
pthread_cond_t cond;
pthread_mutex_t mutex
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
if (count == 1) {
pthread_cond_wait(&cond, &mutex);
}
put(i);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
void *consumer(*void arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
if (count == 0) {
pthread_cond_wait(&cond, &mutex);
}
int tmp = get();
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
printf("%d\n", tmp);
}
}
```
Si nous avons un thread producteur et un thread consommateur
ce code fonctionne. Par contre, si nous ajoutons plus de threads
cela ne fonctionne plus.
---
Question #
Que se passe-t-il dans le cas où nous avons deux threads consommateurs pour un producteur?
---
Le premier problème est relié à l'instruction `if`{.language-c}
avant l'appel à `wait()`{.language-c}.
Le second est relié à l'utilisation d'une unique variable
de condition. Voyons à présent ce qui peut se passer.
Soient les threads consommateurs $T_{c_1}$ et $T_{c_2}$ et le
thread producteur $T_p$. Supposons que $T_{c_1}$ s'exécute.
Il acquière le verrou et vérifie s'il y a quelque chose à consommer dans le frigo... euh... dans le buffer. Comme il
n'y a rien il se met en attente et libère le verrou.
Ensuite le thread $T_p$ s'exécute. Il acquière le verrou
et vérifie si le buffer est vide. Comme il l'est il peut y déposer
une valeur. Il va ensuite signaler que le buffer est rempli.
Cela va mettre $T_{c_1}$ en état de réveil et prêt à être exécuté
(il ne s'exécute pas encore). Finalement pour $T_p$,
il se rend compte que le buffer est plein et va se mettre
en sommeil. A ce moment précis, $T_{c_2}$ est ordonnancé.
Il acquière le verrou, lit la valeur dans le buffer, signale qu'il
a lu
et libère le verrou. A ce moment précis, si $T_{c_1}$ est
ordonnancé, il va acquérir le verrou et continuer son exécution
là où elle c'était arrêtée. Il va donc tenter de lire à son tour
dans le buffer, mais celui-ci sera vide! L'assertion dans
`get()`{.language-c} nous sautera au visage et arrêtera
l'exécution. Au moins, nous sommes sauvés d'un comportement
erroné que nous croyions correct.
Pour prévenir cette erreur, nous aurions dû empêcher $T_{c_1}$
de tenter de consommer une valeur déjà consommée. En fait le signal
envoyé par $T_p$ doit être considéré comme une indication
que quelque chose a changé. Il n'y a en revanche aucune garantie
que cela sera toujours le cas lorsque le thread mis en sommeil
sera réveillé. La solution ici est de remplacer le
`if`{.language-c} par un `while`{.language-c}. De cette façon
$T_{c_1}$ aurait vérifié que la condition a bien changé après
son réveil et avoir acquis le verrou. Ainsi il n'aurait pas essayé
de consommer une valeur déjà consommée.
---
Règle #
Toujours utiliser une boucle `while`{.language-c} et jamais de `if`{.language-c}.[^3]
---
Voilà un premier problème de réglé, mais nous ne sommes pas sortis d'affaire.
En effet, un autre problème existe. Imaginons que $T_{c_1}$ et
$T_{c_2}$ s'exécutent en premier et sont mis en sommeil. $T_p$
s'exécute alors, met une valeur dans le buffer et
réveille un thread, disons $T_{c_1}$. $T_p$ refait un tour de
boucle, libère et réacquière le verrou et réalise que le buffer est plein. Il se met en sommeil. $T_{c_1}$ peut s'exécuter
(alors que $T_{c_2}$ et $T_p$ sont endormis). Il refait un tour
dans la boucle `while`{.language-c}, trouve le buffer plein et le
vide. Il signale qu'il a fini et va se rendormir. Si à présent
c'est $T_{c_2}$ qui se réveille (on a aucune garantie sur l'ordre
de l'exécution), il va trouver le buffer vide et se rendormir.
On a donc les trois threads en sommeil avec aucun moyen de les
réveiller (pas de prince·esse charmant·e, ni rien)!
On a besoin d'un autre signal qui soit dirigé: un consommateur
doit réveiller un producteur et vice-versa. Pour corriger
ce bug, nous pouvons simplement utiliser **deux** variables
de condition: une relié au remplissage du buffer, l'autre à sa
vidange comme dans le code ci-dessous.
```language-c
pthread_cond_t fill, empty; // pas oublier d'initialiser hein
pthread_mutex_t mutex;
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
while (count == 1) {
pthread_cond_wait(&empty, &mutex);
}
put(i);
pthread_cond_signal(&fill);
pthread_mutex_unlock(&mutex);
}
}
vois *consumer(*void arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&fill, &mutex);
}
int tmp = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n", tmp);
}
}
```
Dans ce code, on voit que les threads producteurs attendent
sur la condition `empty`{.language-c} et signalent la
condition `fill`{.language-c}, alors que les threads
consommateurs attendent sur la condition `fill`{.language-c}
et signalent la condition `empty`{.language-c}.
### Aller plus haut
On vient de voir comment résoudre le problème producteurs/consommateurs
pour un buffer unique. Cette solution est généralisable
pour permettre des applications plus concurrentes et efficaces.
Pour ce faire, on va créer un buffer avec plus d'espaces.
Ainsi il sera possible de produire plus de valeurs
et de les consommer avant que les threads se mettent en sommeil.
Pour ce faire, nous créons un buffer qui sera cette fois
un tableau statique. On doit garder une trace du taux de remplissage,
ainsi que de la position à laquelle nous devons lire.
On modifie donc les fonctions `put()`{.language-c} et
`get()`{.language-c} de la façon suivante
```language-c
#define MAX 100;
int buffer[MAX];
int fill_pos = 0;
int read_pos = 0;
int count = 0;
void put(int value) {
buffer[fill_pos] = value;
fill_pos = (fill_pos + 1) % MAX;
count += 1;
}
int get() {
int tmp = buffer[read_pos];
read_pos = (read_pos + 1) % MAX;
count -= 1;
return tmp;
}
```
Une toute petite adaptation à la vérification nécessaire avant
le `wait()`{.language-c} ou `signal()`{.language-c}
et le tour est joué
```{.language-c}
pthread_cond_t empty, fill;
pthread_mutex_t mutex;
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
while (count == MAX) {
pthread_cond_wait(&empty, &mutex);
}
put(i);
pthread_cond_signal(&fill);
pthread_mutex_unlock(&mutex);
}
}
void *consumer(void *arg) {
for (int i = 0; i < loops; ++i) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&fill, &mutex);
}
int value = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n", value);
}
}
```
On voit que le producteur ne se met en sommeil que lorsque
le buffer est plein et à l'inverse le consommateur n'appelle
`wait()`{.language-c} que lorsque le buffer est vide.
## Signalisation globale
Dans certains cas, on aimerait être un peu plus large lorsqu'on
réveille des threads. En particulier, on aimerait pouvoir réveiller
tous les threads endormis à la fois. Afin d'illustrer le problème
considérons l'exemple d'une application d'allocation
de mémoire sur la pile. La pile a une taille maximale
`MAX_HEAP_SIZE`{.language-c}. Nos threads vont allouer
un certain nombre d'octets
`void *allocate(int size)`{.language-c} et libérer
un certain nombre de bytes de la mémoire
`void free(void *ptr, int size)`{.language-c}.
Lorsque la mémoire est pleine, les fils d'exécution
qui sont en charge de l'allocation doivent se mettre en sommeil.
Lorsque de la mémoire se libère il faut signaler que de la place
est à nouveau disponible. Une approche simple serait le code
suivant
```language-c
int avail_bytes = MAX_HEAP_SIZE; // nombre d'octets disponibles
// comme d'habitude, une variable de condition et son verrou
pthread_cont_t cond;
pthread_mutex_t mutex;
void *allocate(int size) {
pthread_mutex_lock(&mutex);
while (avail_bytes < size) { // on doit avoir assez de mémoire
pthread_cond_wait(&cond, &mutex);
}
void *ptr = ...; // on récupère la mémoire depuis le tas
avail_bytes -= size;
pthread_mutex_unlock(&mutex);
return ptr;
}
void free(void *ptr, int size) {
pthread_mutex_lock(&mutex);
avail_bytes += size;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
```
---
Question #
Ce code a un problème. Lequel?
---
Imaginons qu'à un moment donné
on ait plus de place en mémoire (`avail_bytes == 0`{.language-c}). Un premier thread $T_1$
appelle `allocate(100)`{.language-c} et un deuxième thread $T_2$
appelle `allocate(10)`{.language-c}. Chacun de ces threads
va appeler `wait()`{.language-c} et se mettre en sommeil.
A présent, un troisième thread appelle `free(40)`{.language-c}, il libère 40 bytes et signale qu'il y a de la mémoire disponible. Si $T_1$ est réveillé, il retournera en sommeil
car il n'y a pas la place suffisante pour allouer et le programme
se retrouve à ne rien faire alors qu'il pourrait. Il suffisait
de réveiller $T_2$ bon sang! La solution la plus simple ici est
de réveiller **tous** les threads grâce à la fonction
`pthread_cond_broadcast()`{.language-c}
```language-c
int pthread_cond_broadcast(pthread_cond_t *cond);
```
Tous les threads attendant après l'appel de `wait()`{.language-c}
seront réveillés, ils vérifieront le nombre de bytes disponibles
et se remettront en sommeil immédiatement si l'espace n'est pas
disponible.
[^1]: Il ne faut pas oublier qu'il faut également initialiser les variables de condition et éventuellement les détruire aussi.
[^2]: Cela pourrait aussi être un pointeur vers une structure de
donnée plus complexe.
[^3]: C'est pas toujours nécessaire, mais ça ne fait pas de mal!
[^4]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
\ No newline at end of file
---
author:
- Orestis Malaspinas, Steven Liatti
title: Introduction à l'API des `pthreads`
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
\newcommand{\dd}{\mathrm{d}}
\newcommand{\real}{\mathbb{R}}
\newcommand{\integer}{\mathbb{Z}}
\renewcommand{\natural}{\mathbb{N}}
# API des `pthreads`
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^2].
Dans ce chapitre, nous allons brièvement introduire l'api de base de la gestion de threads en `C`
via la librairie POSIX, `pthreads`{.language-c}.
Toutes les fonctions introduites ici sont déclarées dans le fichier `pthread.h`{.language-c}. Il est donc
sous-entendu que nous inclurons toujours ce fichier dans nos codes de cette manière :
```language-c
#include <pthread.h>
```
Par ailleurs, il faut également faire l'édition des liens avec la librairie `pthread`{.language-c}, option `-lpthread`.
Je vous recommande d'utiliser les options suivantes pour compiler votre code :
```language-bash
gcc -g -Wall -Wextra -std=gnu11 -fsanitize=address -fsanitize=leak
-fsanitize=undefined -o main main.c -lpthread
```
Il faut noter qu'on utilise l'option `-std=gnu11` afin que certaines fonctionnalités comme les barrières de synchronisation soient reconnues.
L'autre solution est d'ajouter la ligne préprocesseur suivante **avant** l'inclusion des headers
```language-c
#define __GNU_SOURCE
#include <pthread.h>
```
Finalement, il se peut que toute la documentation ne soit pas installée par défaut sur votre système (concernant les barrières par exemple).
Pour les installer, il faut le package `manpages-posix-dev` pour les distribution dérivées de "Debian".
# Création et terminaison de threads
## Création de threads
Afin de créer un thread il faut appeler la fonction `pthread_create()`
dont l'entête est reproduite ci-dessous :
```language-c
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
```
Cette fonction aura pour effet de créer un thread et exécuter la fonction `start_routine()`{.language-c} à l'intérieur de celui-ci.
Elle prend en paramètres :
1. Un pointeur vers un type `pthread_t`{.language-c}, qui est un identifiant
du thread, qui va permettre d'interagir avec ledit thread.
2. Une structure `pthread_attr_t`{.language-c} qui contient un certain nombre d'attributs d'un thread.
Si ce pointeur vaut `NULL`{.language-c}, alors les attributs ont une valeur par défaut (cela fera presque toujours l'affaire dans
ce cours).
3. Un pointeur vers une fonction qui a une signature un peu compliquée :
- elle retourne un pointeur `void *`;
- elle prend en paramètre un pointeur `void *`;
4. Un pointeur `void *`{.language-c} qui représente les paramètres de la fonction `start_routine()`{.language-c}.
Le but étant de pouvoir passer n'importe quelle fonction, retournant une valeur quelconque et prenant des paramètres
d'un type quelconque.
Par ailleurs, la fonction `pthread_create()`{.language-c} retourne un entier qui donne une information
sur la réussite ou non de la création du thread. Si le retour de la fonction est 0 tout s'est bien passé,
sinon il y a eu une erreur.
**Pensez à toujours bien vérifier le retour de `pthread_create()` !**
---
Exemple (Création de thread) #
Dans cet exemple[^3], nous créons un thread, qui appelle la fonction
`func()`{.language-c} qui a pour argument. Nous souhaitons
que cette fonction ait un argument `char *`{.language-c} et devons donc
explicitement caster le pointeur `void *arg`{.language-c}.
```language-c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *func(void *arg) {
char *msg = (char *) arg; // type casting of the arg
printf("Message = %s\n", msg);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t t;
char *msg = "My first thread!";
if (pthread_create(&t, NULL, func, msg) != 0) {
perror("Thread creation error."); // affiche le message sur le canal d'erreur
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
```
On note aussi que nous testons explicitement que la création du thread s'est
bien passée. Si une erreur se produit (`pthread_create()`{.language-c} retourne autre chose que 0), nous afficherons le message
`Thread creation error`{.language-c}.
---
---
Question #
Que va afficher ce code?
---
## Terminaison de threads
Maintenant que nous avons créé un thread, nous devons aussi pouvoir en gérer la terminaison.
Cette gestion se fait avec l'appel à la fonction `pthread_join()`{.language-c} dont l'entête est reproduite ci-dessous :
```language-c
int pthread_join(pthread_t thread, void **value_ptr);
```
Lors de l'appel de cette fonction, le thread principal *attend* la terminaison du thread `thread`
créé précédemment. Elle prend en paramètre l'identifiant du thread dont on attend la fin,
et un pointeur vers un pointeur `void`{.language-c}. Ce pointeur est en fait un pointeur vers le type de retour
de la fonction `start_routine()`{.language-c}. Il est très important que `value_ptr`{.language-c} soit un pointeur de pointeur,
car on **change** la valeur passée en argument. Ainsi si cette valeur est un pointeur, la seule façon de la modifier est
d'avoir une indirection supplémentaire et donc d'avoir un double pointeur. De plus,
le type de ces pointeurs est `void`{.language-c}, afin de pouvoir retourner n'importe quel type. Si nous
ne souhaitons rien retourner, nous pouvons simplement appeler cette fonction avec `NULL`
en second paramètre.
Il faut également noter que comme dans le cas de la création d'un thread, la fonction `pthread_join()`
retourne un entier qui nous dira si la terminaison du thread s'est bien passée.
**Pensez à toujours bien vérifier le retour de `pthread_join()` !**
---
Exemple (Jointure de thread) #
Dans l'exemple précédent, nous constatons que très souvent
(presque toujours) le thread créé n'avait jamais le temps de s'exécuter avant la fin du programme. Nous corrigeons ce problème ici, en appelant la fonction
`pthread_join()`{.language-c} depuis le thread principal de notre programme.
```language-c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *func(void *arg) {
char *msg = (char *) arg; // type casting of the arg
printf("Message = %s\n", msg);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t t;
char *msg = "My first thread!";
if (pthread_create(&t, NULL, func, msg) != 0) {
perror("Thread creation error."); // affiche le message sur le canal d'erreur
return EXIT_FAILURE;
}
if (pthread_join(t, NULL) != 0) { // attente que le thread se termine
perror("Thread join error");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
```
---
## Exercices : création/terminaison
1. Écrire une librairie de création et terminaison des threads, vérifiant le retour
des fonctions `pthread_create()`{.language-c} et `pthread_join()`{.language-c} : ce seront des wrappers que vous réutiliserez un peu tout
le temps dans le reste du cours, **faites les consciencieusement !**
2. Écrire un petit programme utilisant votre librairie, créant 10 threads et affichant :
```
Hello World from thread %d.
```
où `%d`{.language-c} est le numéro de votre thread. Puis le thread principal affichera le message :
```
Hello from main thread.
```
3. Finalement, avant que le thread principal n'affiche son message,
garantissez que tous les autres threads soient terminés. Que constatez-vous comme différence?
4. Écrivez un petit programme qui crée 10 threads. Chaque thread va incrémenter 10'000 fois une variable entière
`n`{.language-c} définie dans le thread principal. Quelle devrait être la valeur de `n`{.language-c} à la fin de notre programme
s'il s'exécutait de façon séquentielle ? Que constatez-vous ici ? Quelle solution proposeriez-vous
pour résoudre ce problème ?
5. Écrivez un petit programme qui crée 10 threads. Chaque thread va incrémenter 10'000 fois une variable entière
`n`{.language-c} définie à l'intérieur de lui-même, puis retournera cette valeur. Quelle devrait être la valeur de `n`{.language-c} à la fin de notre programme
s'il s'exécutait de façon séquentielle ? Que constatez-vous ici ?
## Attributs de threads
Comme nous l'avons vu plus haut, lorsque nous créons un thread, il faut
passer en argument un `pthread_attr_t *` à la fonction `pthread_create()`.
Cette structure est initialisée/détuite à l'aide des fonctions
```language-c
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
```
Il existe plusieurs attributs configurables (voir `man pthread_attr_init` pour une liste exhaustive). A titre d'exemple, nous pouvons configurer
l'attribut `PTHREAD_CREATE_DETACHED` à l'aide de la fonction
```language-c
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
```
Cet attribut permet d'éviter à avoir à *joindre* le thread créé (cela devient impossible de joindre un thread détaché). Le système libère automatiquement les ressources une fois le thread terminé.
---
Exemple (Thread détaché) #
```language-c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *func() {
printf("I am a detached thread, although I don't know it.\n");
return NULL;
}
int main() {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_t t;
pthread_create(&t, &attr, func, NULL);
pthread_join(t, NULL); // cela ne sert à rien...
pthread_attr_destroy(&attr);
return EXIT_SUCCESS;
}
```
---
Une autre façon de détacher un thread est d'appeler la fonction
```language-c
int pthread_detach(pthread_t thread);
```
## Terminaison de threads alternatives
Nous avons vu qu'un thread peut se terminer quand la fonction qu'il exécute
retourne. Il existe deux autres façon pour un thread de se terminer.
1. Il se termine lui même (auto-terminaison) avec la fonction
```language-c
void pthread_exit(void *retval);
```
2. Un autre thread le termine (annulation) avec la fonction
```language-c
int pthread_cancel(pthread_t thread);
```
### L'auto-terminaison
Lors de l'appel à `pthread_exit()` le thread se termine et retourne la
valeur `retval` (cette donction est appelée implicitement lorsqu'un thread se termine avec `return`) au thread effectuant la jointure.
Un cas particulier estlorsque le thread principal appelle `pthread_exit()`. Dans ce cas-là, le programme bloque et attend que tous les threads seterminent
avant de terminer le processus.
### L'annulation
L'annulation d'un thread est un processus un peu plus complexe.
En effet, la fonction
```language-c
int pthread_cancel(pthread_t thread);
```
annule le thread `thread` depuis un autre thread. Cette fonction retourne
`0` en cas de succès. Si le thread est déjà terminé, un code d'erreur sera retourné. Bien que cela soit le comportement par défaut, tout thread n'est pas "annulable". Pour fixer la politique d'annulation d'un thread,
il faut qu'il appelle la fonction
```language-c
int pthread_setcancelstate(int state, int *oldstate);
```
`sate` est `PTHREAD_CANCEL_ENABLE` (valeur par défaut, autorise l'annulation) et `PTHREAD_CANCEL_DISABLE` (interdit l'annulation).
Comme l'annulation d'un thread peut-être particulièrement dangereuse (le thread appelant `pthread_cancel()` n'a aucune idée de l'état dans lequel
le thread à terminer se trouve), on définit aussi un type d'annulation avec
la fonction
```language-c
int pthread_setcanceltype(int type, int *oldtype);
```
La valeur de `type` peut avoir deux valeur différentes
* `PTHREAD_CANCEL_DEFERRED`, l'annulation peut se faire en des points d'annulation précis (défaut).
* `PTHREAD_CANCEL_ASYNCHRONOUS`, l'annulation peut se faire n'importe quand.
Finalement, un point d'annulation se définit avec la fonction
```language-c
void pthread_testcancel(void);
```
Lorsqu'un thread appelle cette fonction, son annulation sera permise aux points où elle est appelée.
---
Remarque #
Évidemment, comme rien ne peutetre simple, certains appels système (`fopen`, `write`, ...) agissent comme des points d'annulation. La liste complète se trouve sur `man pthreads`.
---
## Autres fonctions
Il existe encore d'autres fonctions potentiellement utiles, mais nous n'allons pas toutes lesvoir en détail. Vous trouverez une liste non-exhaustive ci-dessous.
* `pthread_t pthread_self();` retourne l'identifiant du thread.
* `int pthread_equal(pthread_t t1, pthread_t t2);` vérifie l'égalité entre les identifiants de 2 threads (seule façon portable de vérifier l'égalité).
# Les verrous
Le verrou permet de résoudre le problème de non *atomicité* rencontré ci-dessus.
Dans le cas de l'incrémentation de notre entier `n`{.language-c}, nous avons une **section critique** :
l'opération `n = n + 1`{.language-c} peut être interrompue à tout moment par l'exécution d'un autre thread.
Pour protéger les sections critiques de nos codes, le plus simple des mécanismes est
**l'exclusion mutuelle**. Ce mécanisme est représenté à l'aide de **mutex**-es (*mutual exclusion*)
dans la librairie POSIX. De façon simplifiée, la syntaxe pour protéger notre section critique serait :
```language-c
pthread_mutex_t lock;
pthread_mutex_lock(&lock); // acquisition du verrou
n = n + 1; // section critique
pthread_mutex_unlock(&lock); // libération du verrou
```
On définit d'abord notre verrou `lock`{.language-c}, de type `pthread_mutex_t`{.language-c} (typiquement c'est une variable globale),
puis on acquiert le verrou. À ce moment-là, aucun autre thread ne peut l'acquérir, la section critique est effectuée sur le thread
ayant acquis le verrou pendant que les autres attendent. Dès que le verrou est libéré, un autre thread peut l'acquérir, et ainsi de suite.
Afin que votre code marche et soit correct, il manque cependant deux ou trois choses :
1. Le mutex doit toujours être initialisé :
- Soit de façon statique :
```language-c
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
```
- Soit de façon dynamique :
```language-c
int rc = pthread_mutex_init(&lock, NULL); // attention à vérifier le retour
```
2. La vérification du retour lors de l'acquisition du mutex.
3. Vous devez détruire votre mutex et le mettre dans un état non-initialisé, à l'aide de la fonction
```language-c
int rc = pthread_mutex_destroy(&lock);
```
## Exercices : verrous
1. Ajouter à votre librairie de wrappers une fonction acquérant un verrou et testant son retour.
2. Ajouter à votre librairie de wrappers une fonction initialisant un verrou et testant son retour.
3. Ajouter à votre librairie de wrappers une fonction détruisant un verrou et testant son retour.
4. Réécrire votre code incrémentant une variable `n`{.language-c} 10'000 fois dans 10 threads en utilisant cette fois
un verrou pour protéger la section critique.
5. Réfléchissez à une expérience vous permettant de mesurer le coût de calcul lié à au verrouillage-déverrouillage d'un verrou dans le cas avec un seul thread, dans le cas où on a plusieurs fils d'exécution. Á présent, mettez-la en œuvre!
# Variables de condition
Les *variables de condition* sont utilisées lorsque nous voulons faire en sorte qu'un signal soit envoyé entre
différents fils d'exécution. Il existe deux fonctions principales qui sont utilisées dans ce cas:
```language-c
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
```
La fonction `pthread_cond_wait()`{.language-c} met le thread qui l'appelle dans un état de sommeil: il attend qu'un autre fil d'exécution
lui envoie un signal pour le réveiller. On constate que cette fonction prend un `pthread_mutex_t`{.language-c} en argument.
On doit donc avoir **acquis un verrou** afin de pouvoir l'appeler et on veut **modifier** l'état du verrou (d'où le pointeur):
il est en fait libéré lors de l'appel à `pthread_cond_wait()`{.language-c} afin de pouvoir être acquis par d'autres threads.
De plus lorsque le thread est réveillé, il réacquiert le verrou, et s'assure ainsi
être toujours dans la section où l'exclusion mutuelle est respectée.
La fonction `pthread_cond_signal()`{.language-c} va permettre au thread qui l'appelle de signaler qu'il a effectué
une opération qui est digne d'intérêt et que la sieste est (peut-être) terminée.
Il est en général important d'acquérir le verrou lorsque vous entrez dans la section critique
d'où vous allez effectuer votre signal.
Comme pour le cas du `pthread_mutex_t`{.language-c} la variable de condition doit être initialisée[^1]:
```language-c
p_thread_cond_t cond = PTHREAD_COND_INITIALIZER; // initialisation statique
p_thread_cond_t cond;
pthread_cond_init(&cond, NULL); // initialisation dynamique
// (ici les attributs sont ceux par défaut)
pthread_cond_destroy(&cond); // destruction dynamique
```
L'initialisation dynamique a l'avantage qu'elle effectue la vérification que tout s'est bien passé (ou pas).
L'utilisation de `pthread_cond_wait()`{.language-c} et `pthread_cond_signal()`{.language-c} peut être résumée comme suit:
1. Initialisation de la variable de condition et du verrou.
2. **Thread 1** tentative d'acquisition du verrou:
- Quand l'acquisition est effectuée
- Vérification de la variable de condition:
- Si la variable de condition n'est pas remplie, libération du verrou: mise en sommeil.
- Lorsque variable de condition est remplie, tentative d'acquisition du verrou: réveil.
- Libération du verrou.
3. **Thread 2** tentative d'acquisition du verrou:
- Quand l'acquisition est effectuée.
- Signalisation de la variable de condition.
- Libération du verrou.
## Exercice: variables de conditions
1. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_cond_wait()`{.language-c} et testant son retour.
2. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_cond_init()`{.language-c} et testant son retour.
3. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_cond_destroy()`{.language-c} et testant son retour.
4. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_cond_signal()`{.language-c} et testant son retour.
5. Créer un petit programme effectuant les tâches suivantes.
- **Thread 1**: tant qu'une variable `ready`{.language-c} est fausse, attendre un signal, puis continuer.
- **Thread 2**: attendre une seconde (utiliser `sleep(1)`{.language-c} de `unistd.h`) mettre la variable `ready`{.language-c} à une valeur vraie et signaler le changement.
6. Réécrire le programme ci-dessus, mais en créant 10 threads qui attendent un signal.
<!-- 4. Que pourrait-il se passer si le code que vous avez écrit ci-dessus (verrou et variable de condition)
était remplacé par une simple variable booléenne? -->
# Les barrières de synchronisation
Nous avons déjà vu que la fonction `pthread_join()`{.language-c}
permettait d'attendre la fin de l'exécution d'un thread
et donc de synchroniser l'exécution d'un code multi-threadé.
Il se peut qu'on ne veuille synchroniser qu'un certain nombre des threads sans
qu'ils soient pour autant terminés. Pour ce faire, on utilise les **barrières de
synchronisation**. Une barrière de synchronisation fonctionne de la manière suivante:
1. Le nombre, $n$, de threads à synchroniser est spécifié lors de la création de la barrière.
2. Chaque thread notifie son arrivée à la barrière.
3. Tant que $n$ threads n’ont pas notifié la barrière, ceux déjà arrivés sont bloqués.
4. Une fois que tous les threads ont notifié la barrière, celle-ci débloque tous les threads en attente (un par un, dans un ordre indéterminé).
5. La barrière est ensuite **réinitialisée** à la valeur spécifiée lors de sa création: la barrière est réutilisable.
La syntaxe des barrières est la suivante:
```language-c
int pthread_barrier_init(pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr, unsigned count); // initialisation
int pthread_barrier_wait(pthread_barrier_t *barrier); // mise en attente
int pthread_barrier_destroy(pthread_barrier_t *barrier); // destruction des ressources
```
La fonction `pthread_barrier_init()`{.language-c} initialise une barrière `barrier`{.language-c} pour
`count`{.language-c} threads. Le deuxième argument, `attr`{.language-c}, est un ensemble d'attributs, nous pouvons le laisser à `NULL`{.language-c}. La notification de l'arrivée de chaque thread se fait à l'arrivée de la fonction `pthread_barrier_wait()`{.language-c}. Finalement, les ressources de la barrière sont libérées
lors de l'appel à la fonction `pthread_barrier_destroy()`{.language-c}.
Cette fonction est bloquante, il faut donc s'assurer qu'aucun thread
n'est bloqué à son appel, car le programme pourrait fort bien se retrouver bloqué jusqu'à la fin des temps.
Comme d'habitude ces fonctions renvoient `0` en cas de succès.
Il est donc nécessaire de bien vérifier que tout se passe bien
lors de l'appel à ces fonctions.
## Exercice: barrière de synchronisation
1. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_barrier_init()`{.language-c} et testant son retour.
2. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_barrier_wait()`{.language-c} et testant son retour.
3. Ajoutez à votre librairie de wrappers, un wrapper de la fonction `pthread_barrier_destroy()`{.language-c} et testant son retour.
4. Écrire un petit programme prenant en argument un nombre de threads.
Chaque thread fera les actions suivantes:
* tirera un nombre aléatoire entier, $t\leq 10$ et affichera son numéro, ainsi que $t$;
* attendra $t$ secondes puis affichera un message avec son identifiant;
* se synchronisera avec les autres threads;
5. A l'aide d'un mutex et d'une variable de condition
écrivez votre propre barrière rien qu'à vous.
[^1]: Puis détruite dans le cas de l'initialisation dynamique.
[^2]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
[^3]: Ce code se trouve dans le fichier <https://githepia.hesge.ch/orestis.malaspin/cours_prog_conc/blob/master/exemples/intro_api/pthread_create.c> .
\ No newline at end of file
---
author:
- Orestis Malaspinas, Steven Liatti
title: Introduction vague aux systèmes d'exploitation
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
mathjax: on
date: 2020-01-01
---
# Avant-propos
Ce chapitre est fortement inspiré du cours en ligne *Introduction to Operating Systems* se trouvant sur [https://classroom.udacity.com/courses/ud923](https://classroom.udacity.com/courses/ud923).
# Généralités sur les systèmes d'exploitation
Dans ce chapitre, nous allons brièvement introduire ce qu'est un système d'exploitation (ou Operating System, abrégé OS, en anglais)
et comment il fait fonctionner un ordinateur. Mais avant de donner une
vague définition de ce qu'est un système d'exploitation,
nous allons d'abord rappeler en quoi consiste un ordinateur.
Un ordinateur est constitué de plusieurs composants: un ou plusieurs processeurs (ou Central Processing Units, abrégé CPU, en anglais) qui
peut contenir plusieurs cœurs, de la mémoire, une carte réseau ou wifi,
une carte graphique, un disque dur, des périphériques USB, ...
Tous ces composants seront utilisés par une ou plusieurs applications
(le butineur, le lecteur multimédia, l'éditeur de texte,
Dota 2, ...).
## Qu'est-ce qu'un système d'exploitation?
Le système d'exploitation est une couche de logiciel qui fait le lien entre les applications et les différents composants matériel
d'un ordinateur. Il n'existe pas de définition unique de ce qu'est un système
d'exploitation. Nous allons plutôt le définir ici par les tâches qu'il est capable
d'effectuer:
0. A un accès privilégié direct au matériel.
1. Il cache la complexité du matériel à l'utilisateur et aux développeurs d'applications.
2. Il gère les ressources matérielles. Quelle application peut utiliser quelle ressource et à quel moment elle peut le faire (leur ordonnancement).
3. Il isole et protège les ressources. Il empêche que plusieurs applications utilisent les mêmes parties des ressources au même moment. En particulier, différentes applications n'ont pas accès à la même partie de la mémoire en même temps.
---
Exemple #
Lorsqu'on lit ou on écrit de la musique sur une mémoire USB et le disque dur d'un ordinateur, à aucun moment il y a besoin de réfléchir où exactement sur le disque, nous allons écrire/lire les bits des fichiers.
Il empêche qu'un fichier soit écrasé sans que l'utilisateur le veuille.
De plus, il n'y a pas de différence notable entre les deux pour un utilisateur
alors que le chemin pour accéder à l'un ou à l'autre est très différent.
De plus, à aucun moment il n'y a besoin d'ordonnancer les différentes applications sur le processeur. Toutes ces opérations complexes sont effectuées pour nous par le système d'exploitation.
---
Quels systèmes d'exploitation connaissez-vous?
1. Windows.
2. Mac-OS (BSD), Linux: basé sur Unix.
3. Android.
4. iOS.
5. ....
Dans cette introduction, on va plutôt discuter les systèmes Linux.
---
Quiz #
Choisissez parmi les possibilités suivantes quels sont les différents composants d'un système d'exploitation:
1. Un éditeur de fichier.
2. Un système de fichier.
3. Un driver de carte graphique.
4. La mémoire cache.
5. Un compilateur.
6. Un ordonnanceur.
---
## De quoi est constitué un système d'exploitation
Il y a trois concepts de base qui constituent un système d'exploitation.
1. Les abstractions: comme les processus, les fils d'exécution (ou threads), les fichiers, les socket, les pages mémoire, ...
2. Les mécanismes: la création, l'ordonnancement des processus/threads, lire/écrire dans dans un fichier, allouer/désallouer de la mémoire, ...
3. Les politiques: qui déterminent comment les mécanismes vont utilisent les abstractions du matériel sous jacent.
---
Exemple #
Afin de gérer de la mémoire, le système d'exploitation utilise **l'abstraction** de la page mémoire. Une page mémoire correspond à
une adresse de taille fixe (4kb par exemple). Le système d'exploitation
va également avoir des **mécanismes** pour interagir avec cette page:
allouer la page dans la mémoire, ou la maper dans l'espace d'adressage d'un processus pour qu'il puisse accéder à ce qu'il y a dans cette page mémoire. Cette page peut être déplacée dans la mémoire ou même sur le disque dur s'il y a besoin de faire de la place dans la mémoire (swapping).
Ces dernières actions sont basées sur la **politique** du système d'exploitation. Une façon de décider si une page doit être copiée sur le disque dur peut être de prendre la page qui a été utilisée le moins récemment.
---
## Mécanismes de protection d'un système d'exploitation
Le système d'exploitation doit contrôler et gérer les ressources matérielles d'un ordinateur.
Pour ce faire il doit avoir des **privilèges** particuliers pour accéder aux différents composants.
Il y a deux modes principaux d'accès au matériel:
1. Le mode utilisateur (sans privilèges).
2. Le mode "noyau" (avec privilèges).
Le système d'exploitation opère évidemment en mode noyau, alors que les
applications (processus) eux opèrent en mode utilisateur. Il existe une
barrière ne permettant pas aux applications d'effectuer
des opérations privilégiées sur le matériel, seul le système d'exploitation
peut les effectuer. Cette barrière est directement supportée par les matériel. En fait, si un processus tente d'effectuer directement
une opération réservée au mode noyau, la tentative sera capturée et
la main sera passée au système d'exploitation pour gérer ce qu'il convient de faire. Afin qu'une application puisse effectuer
une opération privilégiée elle doit passer au travers du système d'exploitation, via un **appel système**. Un appel système est une interface qu'expose le système d'exploitation afin de permettre aux
processus d'effectuer de demander au système d'exploitation d'effectuer
une opération privilégiée pour eux. Par exemple, `open(file)`{.language-c} pour ouvrir un fichier,
`mmap(memory)`{.language-c} pour allouer de la mémoire, créer un fil d'exécution, ...
# Résumé
Dans ce chapitre, nous avons très brièvement discuté ce qu'est un système d'exploitation.
Il permet de cacher la complexité du matériel aux applications en mettant en place des abstractions
et des mécanismes qui permettent de contrôler le matériel et de protéger
les applications entre elles, et de les isoler. De plus il sert à gérer les différentes
applications qui sont exécutées sur un ordinateur.
Dans le chapitre suivant, nous allons voir ce qu'est plus en détail une application (ou processus)
et comment différents processus sont gérés par le système d'exploitation.
\ No newline at end of file
---
author:
- Orestis Malaspinas, Steven Liatti
title: Introduction aux processus et leur gestion
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Avant-propos
Ce chapitre est fortement inspiré du cours en ligne *Introduction to Operating Systems* se trouvant sur
<https://classroom.udacity.com/courses/ud923>.
# Les processus
Dans le chapitre précédent, nous avons discuté des concepts clés des
systèmes d'exploitation et comment ils gèrent le matériel pour les applications à l'aide
d'abstractions et représentent les opérations effectuées sur le matériel à l'aide de différents mécanismes. Dans ce chapitre, nous allons discuter
une abstraction particulière: le processus.
## Qu'est-ce qu'un processus?
Une application est un programme qui se trouve sur un support physique (le disque dur, une mémoire flash, ...): elle est "statique" ou en d'autres termes elle n'est pas en train de s'exécuter. Un processus (ou tâche)
est une instance d'un programme en **cours d'exécution**: le programme est chargé en mémoire et commence à s'exécuter (c'est une entité active).
---
Exemple #
Une instance d'un éditeur de texte dans lequel vous êtes en train d'éditer un code pour le cours de concurrence est un processus. Un autre
processus est le lecteur de musique sur lequel vous écoutez le dernier
morceau de Ludwig von 88. Cela peut également être votre browser que vous avez oublié de fermer et qui tourne arrière plan. Vous pouvez voir tous les processus sur votre machine en effectuant la commande `top`.
---
## De quoi est composé un processus?
Un processus englobe tout ce dont a besoin notre programme pour s'exécuter:
* Son code;
* Les données statiques;
* Les variables que l'application a besoin d'allouer;
* ...
Chaque élément doit être repéré par une adresse unique en mémoire.
En résumé, une abstraction pour représenter l'état d'un processus
est un espace mémoire qui est défini par une séquence d'adresses.
Chaque état d'un processus apparaît dans une région différente en mémoire.
Ils sont séparés en trois parties principales:
1. Une partie statique contenant toutes les données disponibles pour démarrer le processus (le code,
les chaînes de caractères statiques, ...).
2. *Le tas*: durant l'exécution, un processus peut allouer dynamiquement de la mémoire, cela est fait sur le tas.
Le tas est un espace mémoire non contigu, qui peut contenir des "trous" (des régions que le processus ne peut
pas accéder).
3. *La pile*: Contrairement au tas qui n'a aucune structure particulière, la pile est une structure LIFO qui va changer de taille dynamiquement. Typiquement les appels de fonctions (ainsi que leur données) sont mises sur la pile.
Les adresses mémoire qui décrivent l'état d'un processus sont dites virtuelles:
elles ne doivent pas forcément correspondre aux adresses physiques de la mémoire.
Le "mapping" entre les adresses virtuelles et la mémoire physique est géré par
le système d'exploitation. Cette façon de faire a l'avantage de découpler la gestion
de la mémoire physique (et de la simplifier) de l'utilisation qu'aimerait en faire
les différents processus. En fait, à chaque fois qu'un processus alloue une variable,
le système d'exploitation lui alloue une adresse virtuelle et
un mapping sera fait avec une adresse physique. Ce mapping sera inséré dans
une table et pourra être utilisé à chaque fois que l'adresse virtuelle sera accédée
par le processus qui l'aura alloué.
Comme nous l'avons dit précédemment, toutes les adresses mémoires ne sont pas
forcément allouées par un processus: l'espace utilisé peut contenir des trous.
Par ailleurs,
on peut même se retrouver dans un état où il n'existe pas assez d'espace
physique pour allouer toute la mémoire nécessaire à un processus.
Par exemple, si on a un espace d'adressage virtuel qui a une longueur de 32 bits,
et qu'on veut stocker en mémoire un fichier de plus de 4 Gb, on va
dépasser la mémoire disponible. De même, on peut assez aisément dépasser la mémoire
disponible sur un serveur (même très puissant) si on fait tourner plusieurs processus
très gourmands en mémoire. La virtualisation de la mémoire
permet au système d'exploitation de gérer ces cas en décidant quelle partie de la mémoire virtuelle
est mappée sur la mémoire physique et laquelle ne l'est pas.
Lorsque la mémoire physique est épuisée, une partie de l'état d'un processus peut être copiée
sur le disque dur pour faire de la place: on dit qu'elle est "swapée".
La partie de la mémoire mise sur le disque peut ainsi être récupérée
à n'importe quel moment. Sans entrer dans des considérations
sur la façon exacte dont la gestion est faite, il faut se souvenir à présent
que le système d'exploitation doit garder
une trace de ce qui se passe à tout moment avec la mémoire de chaque processus,
non seulement pour pouvoir retrouver la mémoire liée à un processus mais également pour
pouvoir décider si un accès est illégal ou non.
---
Question #
Si on a deux processus, $P_1$ et $P_2$, tournant en même temps sur un système, lequel de ces trois espaces
d'adressage sera utilisé en général ?
* $P_1: 0--32'000$, $P_2: 32'001--64'000$.
* $P_2: 0--32'000$, $P_1: 32'001--64'000$.
* $P_1: 0--64'000$, $P_2: 0--64'000$.
---
## Quelles informations le système d'exploitation possède sur les processus?
Pour pouvoir gérer un processus, un système d'exploitation doit savoir
ce qu'il est en train de faire. En fait, il doit être capable d'interrompre un processus
et de le faire recommencer depuis l'état exact où il se trouvait au moment où il a été arrêté.
Comment le système d'exploitation connaît l'état exact d'un processus ?
Lorsqu'on écrit un programme, on écrit du texte. Puis pour l'exécuter, il faut le compiler
et produire un fichier binaire. Un fichier binaire est une suite d'instructions
qui ne sont pas forcément exécutées de façon séquentielle (il peut y avoir des boucles,
des branchements, ...). A n'importe quel moment, le processeur doit savoir où le programme se trouve
dans la séquence d'instructions. Cette information est stockée dans un registre sur le processeur et est appelée le
compteur du programme (Program Counter, PC). Il existe un grand nombre de registres actifs lors de l'exécution d'un processus
(ils peuvent contenir des adresses mémoire, ou des informations qui vont influencer l'ordre des instructions par exemple).
Une autre partie importante dans un processus est sa pile et en particulier ce qu'il y a à son sommet.
Cette information est contenue dans le pointeur de pile (stack pointer) qui lui aussi doit
être connu à tout moment. Il existe encore d'autres structures nécessaires. Tout cela est
contenu dans le bloc de contrôle du processus (Process Control Block, PCB).
Le PCB est une structure du système d'exploitation qui est maintenue pour chaque
processus qu'il gère. Il contient :
* un compteur de programme (représentant l'endroit où on se trouve dans la suite d'instructions du processus);
* le pointeur de pile (représentant le sommet de la pile du processus);
* des registres et ce qu'ils contiennent;
* on peut y trouver d'autres choses, comme la liste des fichiers ouverts ou des informations relatives à l'utilisation du CPU pour l'ordonnancement;
Le PCB est créé et initialisé lors de la création du processus (par exemple, le compteur de programme
sera mis sur la première instruction du programme).
Il est mis à jour lorsque l'état du processus change. Par exemple,
lorsque le processus demande plus de mémoire,
le système va allouer cette mémoire, créer de nouvelles adresses virtuelles
valides pour le processus et mettre à jour
les informations concernant les adresses
du processus (leur limite et leur validité).
---
Exemple #
Faisons un exemple de ce que cela veut dire.
Imaginons que deux processus $P_1$ et $P_2$ sont gérés
par le système d'exploitation. Ils sont déjà créés et donc
leur PCB sont déjà quelque part en mémoire.
Supposons que seul $P_1$ est en cours d'exécution et que $P_2$
est en attente. A un moment donné, $P_1$ est mis en attente (pour une raison ou une autre)
le système d'exploitation doit donc sauvegarder toutes les informations relatives à $P_1$
dans le PCB de $P_1$. Ensuite, il va exécuter $P_2$, donc il doit charger
toutes les informations contenues dans le PCB de $P_2$ et mettre à
jour les registres du processeur avec. Ensuite, si $P_2$ demande plus de mémoire,
de nouvelles adresses virtuelles seront créées, et le PCB sera mis à jour avec ces nouvelles informations. Ainsi, si $P_2$ se termine,
ou est mis en attente, les informations relatives à $P_2$ seront mises dans son PCB
et le PCB de $P_1$ sera chargé et les registres mis à jour pour continuer l'exécution de $P_1$.
---
### Changement de contexte
Chaque fois qu'un échange est effectué entre deux processus tel que vu dans l'exemple ci-dessus,
on appelle cela un **changement de contexte**.
Plus formellement, un changement de contexte est le mécanisme utilisé par le système
d'exploitation pour changer de contexte d'exécution d'un processus à un autre.
Dans l'exemple ci-dessus, cela se passe quand on passe de l'exécution de $P_1$
à $P_2$ et de $P_2$ à $P_1$.
Un changement de contexte a un coût de calcul certain. En premier lieu,
il y a des coûts directs :
1. Le nombre de cycles CPU nécessaires à la sauvegarde de l'état de $P_1$ dans son PCB;
2. Le nombre de cycles nécessaires au chargement du PCB de $P_2$ en mémoire.
En second lieu, il y a également des coûts indirects : lorsqu'on exécute un processus,
une partie de ses données vont se trouver dans la mémoire cache du CPU (qui est très rapide à accéder
par rapport à l'accès à la mémoire de l'ordinateur, on parle de un à deux ordres de grandeur plus rapide).
Lors d'un changement de contexte entre $P_1$ et $P_2$, les données de $P_1$ ne seront plus d'aucune
utilité dans le cache du CPU et les données de $P_2$ devront y être chargées depuis la mémoire pour remplacer
les données de $P_1$.
## Les états d'un processus
On a vu qu'un processus peut être dans un état **en attente** ou **en exécution**.
Un processus en exécution peut être interrompu et un changement de contexte peut se produire.
A ce moment là, le processus sera mis en attente. Il existe deux autres états pour un processus :
**éligible** ou **stoppé**. Étudions le cycle de la vie d'un processus pour voir à quoi correspondent
ces deux autres cas. Lorsqu'un processus est exécuté, le système fait un certain nombre de vérifications
puis crée et initialise son PCB et lui alloue des ressources initiales. Puis, le processus
est mis dans l'état *éligible* (il n'a pas encore démarré sur le CPU). Il va attendre
que l'ordonnanceur l'autorise à démarrer et à ce moment, il rentre dans l'état en exécution.
Depuis cet état, il peut être interrompu et mis en attente (par exemple s'il doit effectuer
une opération I/O importante), puis retournera dans l'état éligible.
Une autre solution est qu'il soit interrompu, par l'utilisateur par exemple (ctrl+z) ou à cause d'une erreur.
Il est possible que cet arrêt soit définitif ou non (ctrl+z, bg fera en sorte que le processus reprenne en
arrière plan). Dans ce cas il est dans un état *stoppé*. Si cela est définitif le processus est terminé.
## Interactions entre processus
Par défaut, il n'existe pas de communications d'un processus à un autre.
En particulier, la mémoire entre deux processus est isolée pour éviter
des problèmes tragiques. Néanmoins,
le système d'exploitation donne la possibilité aux processus de communiquer entre eux.
Par exemple, on peut imaginer un serveur web consistant de deux processus. Le premier
est un front-end qui accepte les requêtes des clients et le second est une base de données
stockant les profils des utilisateurs et des informations diverses.
Pour faire communiquer les processus, il faut des mécanismes supplémentaires : **les mécanismes de communication inter-processus**.
Ils aident à transférer des informations d'un espace d'adressage à un autre,
tout en maintenant l'isolation entre les processus. En très résumé, le système d'exploitation
met à disposition une mémoire tampon dans laquelle les processus peuvent lire et écrire
ce qui permet la mise en place d'un canal de communication potentiellement bi-directionnel.
L'inconvénient principal de cette technique est que le coût de calcul est
relativement élevé, car cela demande de copier
des quantités parfois très grandes de données et de les relire. Une autre façon de faire
communiquer les processus est d'avoir des régions de la mémoire qui sont mappées par les deux processus
et ainsi ils peuvent **partager** une partie de leur mémoire respective et n'ont pas besoin de faire des copies. Le problème principal de cette approche est qu'il est très simple de faire des tas d'erreurs
et de corrompre la mémoire d'un autre processus.
# Les threads
Nous venons de discuter des processus et en partie de comment ils sont gérés.
Les processus tels qu'ils sont décrits ici ne peuvent être exécutés que sur
un seul CPU. Si nous voulons qu'un processus soit exécutable sur plus
d'un CPU afin de tirer profit des processeurs modernes,
ce processus doit pouvoir posséder plusieurs contextes d'exécution.
Ces contextes d'exécution dans un seul processus sont appelés **fils d'exécution** ou **threads** en anglais.
Dans ce chapitre, nous allons décrire ce que sont les threads, en quoi ils sont différents
des processus et quels sont leurs avantages et inconvénients.
## Les différences entre un fil d'exécution et un processus
Voyons à présent les différences entre un fil d'exécution et un processus.
Un processus est caractérisé par son espace d'adressage mémoire qui contient
les adresses virtuelles et leur cartographie vers leurs contreparties physiques,
son code, ses données, etc. Il est également caractérisé par son contexte d'exécution (ses
registres, son pointeur de pile, ...) qui est représenté par son PCB.
Les fils eux, représentent des contextes d'exécution indépendants.
Ils font partie du même espace d'adressage mémoire et vont partager
les mappings des adresses virtuelles vers les adresses physiques.
Ils vont également partager le code, les fichiers et les données.
Néanmoins, ils vont exécuter des instructions **différentes**,
accéder peut-être à des adresses différentes en mémoire, ...
Cela signifie que chaque thread aura un compteur de programme différent,
un pointeur de pile différent, une pile différente, et des
registres différents. Ces informations seront donc stockées dans
des structures propres à chaque thread.
La représentation du PCB pour le système d'exploitation
sera bien plus complexe que ce que nous venons de discuter pour les processus
n'ayant qu'un seul fil d'exécution. Il contiendra toutes
les informations partagées par chacun des threads, mais également toutes les informations
qui seront propres à chaque fil d'exécution du processus.
## Les avantages du multi-threading
Discutons d'abord pourquoi le multi-threading est avantageux.
Imaginons le cas de l'équation de la chaleur que nous avons vue en exercices.
Nous pouvons assez aisément imaginer découper la matrice de températures
en plusieurs blocs et répartir le travail sur plusieurs threads,
chacun exécutant le même code mais s'occupant d'une partie différente des données.
Cela ne veut pas dire qu'ils exécutent exactement la même opération à un point
donné dans le temps, ils devront donc chacun avoir leurs registres,
leur pile, ... Mais on voit assez aisément que si plus d'un processeur (ou plus d'un cœur)
est disponible, le calcul sera grandement accéléré grâce à la parallélisation du calcul.
Une autre façon d'optimiser l'exécution d'un programme sur un processeur multi-coeurs
est de diviser ce programme en différentes sous-tâches. Par exemple,
on peut avoir un thread gérant l'I/O, un autre l'affichage, un autre les entrées
au clavier, et encore un autre qui gère la souris.
On peut également imaginer avoir une gestion plus efficace des threads dans une application
en différenciant leurs priorités. On peut imaginer mettre plus de fils d'exécution
au service de parties plus importantes de l'application.
Il est également possible de spécialiser les threads comme dans une chaîne de montage.
Chaque thread est responsable d'une partie bien déterminée de l'application,
et effectuera toujours les mêmes opérations sur les mêmes données. De cette façon, sur des
systèmes multi-processeurs, on pourra avoir (éventuellement) les mêmes données présentes dans le cache
de chaque coeur et ainsi gagner en performance.
Mais vous allez me demander : "Pourquoi ne pas simplement faire tout cela avec
un système multi-processus?" Et vous aurez raison.
En fait l'avantage des fils d'exécution est leur relative "légèreté". Si on
utilise un système multi-processus, il faut exécuter chacun des processus
sur un processeur différent. Si cela est le cas, cela signifie que les
processus ne partageront pas le même espace d'adressage et auront un contexte d'exécution totalement
différent. Cela signifie par conséquent qu'il faudra allouer plusieurs espace d'adressage,
rendant le cas multi-processus beaucoup plus gourmand en ressources.
De plus, il est nécessaire d'utiliser les mécanismes de communication inter-processus qui
sont beaucoup plus coûteux.
## Le multi-threading sur un seul CPU
La plupart des exemples que nous avons donnés jusque là concernent des applications qui tournent sur plusieurs CPUs.
Le multi-threading existe depuis bien avant l'apparition des processeurs possédant plusieurs cœurs[^1].
Il doit bien y avoir une raison à cela. En fait, on peut imaginer le scénario suivant.
Imaginons qu'un thread, $T_1$, effectue une requête
vers le disque dur. A ce moment-là, le disque a besoin d'un certain temps
pour répondre à la requête. Pendant ce temps, $T_1$ ne fait qu'attendre.
Le CPU devrait donc simplement attendre sans rien faire. Si ce temps est plus long
que le temps qu'il faudrait pour faire deux changements de contexte, il peut être avantageux
d'utiliser un autre thread, $T_2$, pour effectuer une autre opération en attendant
que le disque ait répondu à la requête de $T_1$.
Néanmoins, cela est également vrai pour les processus. Souvenez-vous
que le changement de contexte d'un processus est beaucoup plus long
(il faut créer un nouvel espace d'adressage, passer par l'ordonnanceur du
système d'exploitation, etc). Le temps de changement de contexte pour
les fils d'exécution est lui beaucoup plus court et il sera beaucoup plus
rapide de faire ce changement de contexte et donc il sera bien plus aisé
d'obtenir des gains de performance.
---
Question #
Est-ce que les assertions suivantes s'appliquent aux fils d'exécutions, aux processus ou aux deux ?
* Peuvent partager le même espace d'adressage.
* Prennent plus de temps pour effectuer un changement de contexte.
* Ont un contexte d'exécution.
* Doit posséder des mécanismes de communication.
---
## Les différents modèles de multi-threading
Il existe un grand nombre de modèles pour décomposer le travail à effectuer par un processus
par plusieurs threads.
Nous allons brièvement en discuter trois ici :
* Le modèle maître-esclave.
* Le modèle pipeline.
* Le modèle pair.
<!-- TODO: Ajouter exemple -->
<!-- Nous allons considérer un exemple de tâche à effectuer, afin d'illustrer les différents modèles. -->
### Le modèle maître-esclave
Le modèle **maître-esclave** (boss/worker model) se caractérise par la présence d'un thread **maître** et d'un certain nombre
de threads **esclaves**. Le maître est chargé de donner du travail aux esclaves et les esclaves
sont responsables d'effectuer une tâche entière qui leur est affectée. Les esclaves se synchronisent
avec le maître lorsqu'ils ont fini leur tâche. Ce modèle est limité par la performance du maître. Si
celui-ci est trop lent à répartir le travail, les esclaves passeront leur temps à l'attendre,
son travail doit donc être le plus simple possible et limiter un maximum les interactions
entre le maître et les esclaves. Par ailleurs, on essaie de limiter au maximum les dépendances
entre esclaves (ce qui ruinerait la performance).
Un exemple de la vie de tous les jours pourrait être le suivant. Considérons une entreprise
fabriquant des marteaux. Ces marteaux sont fabriqués à la demande et sur mesure. Une fois la commande passée il faut réaliser les
tâches suivantes :
1. Accepter la commande;
2. Lire la commande;
3. Découper le manche dans un manche plus gros;
4. Fondre la tête;
5. Peindre le manche et le vernir;
6. Assembler le manche et la tête;
7. Envoyer le marteau.
La répartition du travail entre le maître et les esclaves pourrait être la suivante.
Le maître se contente d'accepter la commande et de la passer à un esclave libre.
Il effectue ainsi un minimum de travail et est immédiatement disponible pour traiter une autre commande. Ensuite l'esclave s'occupe du reste: c'est-à-dire des parties 2 à 7. La tâche principale du maître est de trouver un esclave libre pour
lui donner son travail (mais comme tout bon esclave, il n'aime pas trop travailler alors
il se cache). Comment fait donc le maître ? Une façon de débusquer l'esclave est
pour le maître de garder une liste des esclaves libres. Il doit également
attendre que l'esclave ait accepté sa tâche. Cette façon de faire
n'est donc pas idéale. En revanche, chaque esclave est totalement découplé des autres esclaves. Une autre façon de faire, serait d'avoir une file
entre le maître et les esclaves qui viennent se servir en travail tout seuls. L'avantage de cette approche est que le maître n'a jamais besoin de savoir quel
esclave est libre ou non : il ne fait que mettre un travail dans la file. Le désavantage est que les esclaves doivent se synchroniser entre eux pour éviter de faire le travail à double. Cette méthode est en général préférable, car
la limitation principale de performance est dûe au maître plutôt qu'aux esclaves.
### Le modèle pair
Dans le modèle **pair** (peer model), tous les threads effectuent leur part du travail
en même temps et n'ont pas de chef (ni Dieu, ni Maître).
Dans ce modèle, le thread principal crée tous les autres threads
puis il devient l'équivalent des autres fils d'exécution.
Chaque thread doit gérer à sa manière ses données (il n'y
a pas d'autre thread responsable pour lui donner du travail,
donc il doit les récupérer seul) et
doit avoir son propre mécanisme pour se synchroniser si nécessaire.
Le modèle pair est très indiqué pour les problèmes dont les entrées
sont bien déterminées. Elles permettent de s'affranchir du maître
dans le modèle maître/esclave, car il n'y a aucune gestion à faire
pour le partage du travail. Nous avons vu un tel exemple (l'équation de la chaleur) et en verrons d'autres. D'autres exemples typiques sont
les multiplications de matrices, la recherche en parallèle
dans une base de données, ... Dans le cas de la fabrication de marteau, si le marteau est suffisamment grand, on pourrait imaginer que
plusieurs travailleurs libres tailleraient le manche, le peindraient
et verniraient différentes parties en même temps afin d'accélérer le processus.
### Le modèle pipeline
Le modèle **pipeline** est très similaire à la chaîne de montage.
Le travail global est subdivisé en sous-tâches et chaque thread se spécialise
dans une sous-tâche. Ainsi, on espère que les threads spécialisés seront
plus efficaces que des threads généralistes. Un peu comme dans une chaîne de montage humaine
où chaque travailleur fait de façon très efficace (mais très répétitive) qu'une seul partie de la chaîne.
Dans notre exemple du marteau (cela pourrait aussi être une faucille),
cela voudrait dire qu'on a sept threads qui effectuent chacun une tâche :
un qui accepte la commande, un qui la lit, et ainsi de suite.
La performance de cette approche est clairement limitée
par la tâche la plus lente à effectuer, il est donc primordial
que chacune soit bien équilibrée. A première vue, cette façon de faire ne semble
pas être très efficace, car chaque thread doit attendre sur le thread précédent pour
effectuer sa tâche. En fait cela n'est pas un problème. Lorsque le premier thread reçoit la première commande, il l'accepte et la passe au thread suivant. Il est donc libre de recevoir une autre commande pendant que le deuxième thread lit la commande. Quand le deuxième thread a lu la commande et la passe au troisième thread, il peut recevoir la commande suivante du premier, et ainsi de suite. Dans ce modèle, il est nécessaire d'avoir un mécanisme pour que les threads se passent le travail. On peut imaginer une approche où chaque thread du niveau $n$
passe le travail à un thread libre de niveau $n+1$ mais cela nécessiterait un surcoût dans la mesure où il faudrait que le thread
$n$ attende la réponse du thread $n+1$ pour savoir s'il a accepté le travail. A nouveau, la solution de la file d'attente est la plus adaptée.
Les threads du niveau $n+1$ viennent récupérer dans la file
leur travail et se synchronisent pour éviter que plusieurs
ne prennent la même tâche. L'inconvénient principal de cette
méthode est que chaque niveau doit avoir une charge de travail équivalente et qu'il est assez complexe de maintenir
une charge équivalente au cours du temps : si une tâche commence à prendre plus de temps, il devient nécessaire de revoir l'équilibrage entier du pipeline. On peut, par exemple, imaginer utiliser le modèle pair
pour améliorer les performances d'un étage de la chaîne
en rajoutant des threads.
[^1]: On peut se poser la question de façon plus générale. Peut-on quand même tirer partie d'un code
multi-threadé si on a plus de threads que de cœurs?
---
author:
- Orestis Malaspinas, Steven Liatti
title: Les sémaphores
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Les sémaphores
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^7].
Le sémaphore est une autre primitive de synchronisation inventée
par E. Dijkstra. En fait, les sémaphores peuvent être utilisés
pour remplacer toutes les autres primitives de synchronisation que
nous avons vues.
Ici, nous allons voir comment utiliser les sémaphores pour synchroniser
des threads à l'intérieur d'un même processus, mais ces objets
sont beaucoup plus génériques et peuvent aussi servir
pour synchroniser *différents processus*.
## Définition
Un sémaphore est une structure contenant un nombre entier
que nous pouvons manipuler avec deux fonctions. Dans l'API POSIX
elles s'appellent `sem_wait()`{.language-c} et `sem_post()`{.language-c}[^1].
Comme un sémaphore contient un entier, il faut l'initialiser
avec la fonction `sem_init()`{.language-c}. L'initialisation
prend trois paramètres, une pointeur vers le sémaphore, de type `sem_t`{.language-c},
un entier, `pshared`{.language-c} spécifiant s'il est partagé entre les threads d'un
même processus (dans ce cas il vaut 0) ou entre processus (il vaut une valeur différente de zéro), et la valeur
de l'entier non-signé contenu dans le sémaphore, `value`{.language-c}.
Comme toujours cette fonction retourne 0 en cas de succès.
```language-c
int sem_init(sem_t *sem, int pshared, unsigned int value);
```
Un sémaphore "à un" sera ainsi initialisé comme
```language-c
#include <semaphore.h>
sem_t s;
int sem_init(&s, 0, 1);
// 0 est pour un sémaphore entre les threads d'un même processus
// 1 pour initialiser le sémaphore à un.
```
Avant de nous intéresser à leur implémentation[^2], intéressons-nous aux fonctions
`sem_wait()`{.language-c} et `sem_post()`{.language-c} qui servent à
interagir avec un sémaphore initialisé. En particulier, voyons
comment les utiliser.
Comme décrit dans le pseudo-c ci-dessous,
les fonction `sem_wait()`{.language-c} va décrémenter
la valeur du sémaphore de un et retourner immédiatement si la valeur du sémaphore
est plus grande ou égale à zéro, sinon se mettre en sommeil.
```language-c
int sem_wait(sem_t *s) {
// décrémente de un la valeur du sémaphore
// si la valeur du sémaphore est < 0 s'endormir
}
```
Si `sem_wait()`{.language-c} est appelé plusieurs fois à la suite
les threads mis en sommeil sont insérés dans une file, en attente de réveil.
On constate donc que si le sémaphore est négatif, sa valeur correspond au
nombre de fils qui sont en attente. Bien que ce ne soit pas forcément utile en pratique, c'est une bonne chose à se rappeler pour comprendre
comment fonctionnent les sémaphores.
A l'inverse, `sem_post()`{.language-c} incrémentera la valeur du sémaphore
de un, et s'il y a un thread ou plus en sommeil, en réveiller un.
```language-c
int sem_post(sem_t *s) {
// incrémente de un la valeur du sémaphore
// réveiller un thread endormi, si au moins un dort
}
```
## Les sémaphores comme verrous
Notre but ici est de construire un verrou à l'aide d'un
sémaphore.
---
Question #
Pouvez-vous imaginer comment faire?
---
En fait, il suffit d'initialiser le sémaphore à un, et d'avoir
un appel à `sem_wait()`{.language-c} et à `sem_post()`{.language-c},
avant et après la section critique que nous souhaitons protéger
avec le verrou, comme on peut le voir dans le code ci-dessous.
```language-c
sem_t s;
sem_init(&s, 0, 1);
sem_wait(&s);
// Section critique
sem_post(&s);
```
Pour être sûr que j'ai bien compris ce qui se passe ici, je
vais expliquer plus en détails ce qui se passe.
Le sémaphore est initialisé à un. Puis, le premier thread, $T_1$, qui
appelle `sem_wait()`{.language-c} va décrémenter sa valeur à `0`{.language-c}
et retourner (comme la valeur du sémaphore est $\geq$`0`{.language-c}).
Si aucun thread n'appelle `sem_wait()`{.language-c}, avant que
$T_1$ appelle `sem_post()`{.language-c}, la valeur du sémaphore
sera remise à un et un autre thread pourra appeler
`sem_wait()`{.language-c} et retourner immédiatement.
Si en revanche, pendant que $T_1$ est dans la section critique,
un autre thread, $T_2$, appelle `sem_wait()`{.language-c},
la valeur du verrou sera décrémentée à $-1$ et $T_2$ sera mis
en sommeil. Lorsque $T_1$ sera à nouveau ordonnancé,
il appellera `sem_post()`{.language-c}, incrémentera
le sémaphore de un (il aura la valeur `0`{.language-c}) et réveillera
$T_2$ qui pourra entrer dans la section critique.
Lorsque $T_2$ sort de la section critique et appelle
`sem_post()`{.language-c} à son tour, il fait à nouveau
passer le sémaphore à un et n'a pas de thread à réveiller.
---
Question #
Que se passe-t-il avec trois threads?
---
Si maintenant, nous avons $T_1$, $T_2$ et $T_3$, trois threads qui
tentent d'acquérir le verrou. $T_1$ arrive en premier `sem_wait()`{.language-c}, il décrémente le sémaphore à 0 et entre dans la
section critique. Si $T_2$ appelle `sem_wait()`{.language-c}
à son tour avant que $T_1$ soit sorti de la section critique,
le sémaphore passe à `-1`{.language-c} et le thread se retrouve en sommeil.
Finalement, si $T_3$ arrive également à appeler `sem_wait()`{.language-c}
avant que $T_1$ soit sorti de la section critique, alors
le sémaphore passe à `-2`{.language-c} et $T_3$ est mis en sommeil. Finalement,
lorsque $T_1$ arrive enfin à sortir de la section critique,
appelle `sem_post()`{.language-c},
ce qui fait passer le sémaphore à `-1`{.language-c} et réveille un thread (disons $T_2$).
Celui-ci à son tour appelle `sem_post()`{.language-c}, le sémaphore passe
à `0`{.language-c} et $T_3$ est réveillé. Il appelle également `sem_post()`{.language-c}
ce qui incrémente le sémaphore à `1`{.language-c} à nouveau et ne réveille personne
car personne ne peut être réveillé. Le sémaphore étant à
un de nouveau le "verrou-sémaphore" peut être acquis par un autre thread éventuellement.
## Les sémaphores pour ordonner une séquence
Les sémaphores peuvent aussi être utiles pour ordonner des événements
dans un programme concurrent. On peut par exemple souhaiter
attendre qu'une liste se remplisse pour récupérer des éléments à l'intérieur.
Nous avons déjà utilisé les variables de condition de cette façon
lorsque nous avions des threads qui attendaient qu'on leur
signale que l'état de l'application avait changé.
On peut utiliser les sémaphores de façon similaire. Imaginons
le cas très simple d'un thread thread $T_1$ créant un thread $T_2$,
et que $T_1$ attende que $T_2$ se termine. Un exemple de ce genre
de programme pourrait être le suivant.
```language-c
sem_t s;
void *t2(void *arg) {
printf("Eh bien, tu vas attendre.\n");
sem_post(&s); // signal here
return NULL;
}
int main() {
sem_init(&s, 0, 0);
printf("J'aimerais bien attendre...\n");
pthread_t tid;
pthread_create(&tid, NULL, t2, NULL);
sem_wait(&s);
printf("Merci de m'avoir fait attendre.\n");
return EXIT_SUCCESS;
}
```
Comme on le voit ici le sémaphore doit être initialisé à zéro.
Si `main()`{.language-c} atteint `sem_wait()`{.language-c}
avant que `t2()`{.language-c} atteigne `sem_post()`{.language-c}, il
décrémente la valeur du sémaphore à `-1`{.language-c} et se met en sommeil.
Lorsque `t2()`{.language-c} est ordonnancé et atteint `sem_post()`{.language-c}
il remet le sémaphore à `0`{.language-c} et réveille `main()`{.language-c}.
Les deux threads continuent ensuite leur vie[^3].
L'exécution de ce code afficherait
```txt
J'aimerais bien attendre...
Eh bien, tu vas attendre.
Merci de m'avoir fait attendre.
```
## Le problème producteurs/consommateurs revisité
Comme nous venons de le voir, on peut exprimer
la signalisation avec les sémaphores
et pas seulement avec les variables de conditions.
Bien que l'effet soit le même le raisonnement est différent.
Dans cette section, nous allons revisiter le problème
producteurs/consommateurs avec les sémaphores.
---
Rappel #
On a des threads qui écrivent dans un buffer
d'une taille certaine, à l'aide d'une fonction
`put()`{.language-c} et d'autres qui lisent
depuis le buffer à l'aide d'une fonction `get()`{.language-c}.
Il faut bien synchroniser les threads afin
que le buffer ne soit pas vide quand on essaie de lire
et à l'inverse qu'on essaie pas d'écrire dans un buffer plein.
Pour simplifier, le buffer est un tableau d'entiers
et on essaie de lire et d'écrire un entier à la fois.
Les fonctions `put()`{.language-c} et `get()`{.language-c} sont comme ci-dessous
```language-c
#define MAX 1
int buffer[MAX]; // MAX est déclaré avant
int fill = 0;
int use = 0;
void put(int val) {
buffer[fill] = val;
fill = (fill + 1) % MAX;
}
int get() {
int tmp = buffer[use];
use = (use + 1) % MAX;
return tmp;
}
```
---
### Tentative ratée
En nous inspirant de ce que nous avons fait avec
les variables de conditions, nous allons utiliser
deux sémaphores: `empty`{.language-c} et `full`{.language-c}. Le code pourrait ressembler au code ci-dessous.
```language-c
sem_t empty, full;
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
sem_wait(&empty);
put(i);
sem_post(&full);
}
return NULL;
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) { // condition d'arrêt
sem_wait(&full);
tmp = get();
sem_post(&empty);
printf("%d\n", tmp);
}
}
int main() {
sem_init(&empty, 0, MAX); // MAX buffers sont vides
sem_init(&full, 0, 0); // 0 buffers sont pleins
}
```
Imaginons que `MAX=1`{.language-c} et qu'il y a deux threads
$T_1$ et $T_2$ sur un seul CPU. Dans notre scénario $T_1$ est
le producteur et $T_2$ le consommateur. Si $T_1$ est ordonnancé
en premier, il va appeler `sem_wait(&full)`{.language-c}
en premier. Comme `full`{.language-c} est initialisé à `0`{.language-c}, il sera décrémenté à `-1`{.language-c} et
sera mis dans l'état *bloqué*. A présent $T_2$ est ordonnancé
comme il est seul, et va entrer dans sa boucle,
décrémenter le sémaphore `empty`{.language-c} à `0`{.language-c}
(on l'a initialisé à `MAX=1`{.language-c} rappelez-vous),
appeler `put(i)`{.language-c} , puis appeler
`sem_post(&full)`{.language-c} qui va incrémenter la valeur
du sémaphore `full`{.language-c} à `0`{.language-c}
et réveiller $T_1$ (le mettre
dans l'état *prêt*). Ensuite il peut se passer deux choses:
1. $T_2$ continue son exécution et appeler `sem_wait(&empty)`{.language-c}. Il verra alors que la valeur de `empty`{.language-c}
est nulle et sera bloqué.
2. Si $T_2$ est interrompu et que $T_1$ est ordonnancé,
il consommera la valeur dans le buffer, incrémentera
`empty`{.language-c} à `1`{.language-c}.
Dans les deux cas, le fonctionnement est bien celui qu'on souhaite.
En fait même avec plus de deux threads cela fonctionne (même
si c'est un peu compliqué à voir...).
---
Question #
Que se passe-t-il à présent si `MAX>1`{.language-c}?
---
Dans le cas où `MAX>1`{.language-c}, nous avons plusieurs
producteurs et plusieurs consommateurs. Disons qu'il y a
deux producteurs $P_1$ et $P_2$ qui arrivent plus ou moins
en même temps à l'appel de `put()`{.language-c}. Si $P_1$
passe en premier dans la fonction `put()`{.language-c}
et remplit `buffer[0]`{.language-c} avec la première valeur,
si ensuite avant d'incrémenter `fill`{.language-c}
$P_1$ est interrompu et $P_2$ remplit également
`buffer[0]`{.language-c}. Nous avons un **accès concurrent**!!!
On perd des données des producteurs et notre modèle
ne fonctionne pas.
### Tentative ratée bis
Dans cette implémentation on a pas d'exclusion mutuelle:
quand on écrit/lit dans le buffer, rien ne nous garantit qu'on
ne est le·la seul·e à le faire. On va utiliser
le sémaphore binaire (le "mutex-sémaphore")
de la section précédente
autour de `get()`{.language-c} et `put()`{.language-c} et
voir comment on s'en sort.
```language-c
sem_t empty, full, mutex;
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
sem_wait(&mutex);
sem_wait(&empty);
put(i);
sem_post(&full);
sem_post(&mutex);
}
return NULL;
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) { // condition d'arrêt
sem_wait(&mutex);
sem_wait(&full);
tmp = get();
sem_post(&empty);
sem_post(&mutex);
printf("%d\n", tmp);
}
}
int main() {
sem_init(&empty, 0, MAX); // MAX buffers sont vides
sem_init(&full, 0, 0); // 0 buffers sont pleins
sem_init(&mutex, 0, 1); // It's a lock!
}
```
---
Question #
Est-ce que ça marche?
---
Ici, on pourrait se croire soti·e·s d'affaire. En fait non...
Il y a de très grandes chances qu'on ait un interblocage.
Reprenons nos deux threads $T_1$ (producteur) et $T_2$ (consommateur). $T_2$ est ordonnancé en premier,
il acquière le `mutex`{.language-c}, appelle `sem_wait(&full)`{.language-c}
mais comme il n'y a rien à lire dans le buffer il se met
en état bloqué et attend que $T_1$ le réveille. Sauf que
$T_1$ ne pourra jamais le réveiller, car il ne pourra jamais
acquérir le verrou: **deadlock**! En quelques mots:
$T_1$ détient le verrou et attend un signal de $T_2$ qui pourrait
envoyer le signal mais il attend la libération du verrou.
### Tentative réussie
Après tant de déceptions, il est temps de faire le code qui marche.
---
Question #
La solution est pourtant simple. Saurez-vous la trouver?
---
En fait pour résoudre le problème il suffit d'inverser
l'ordre signal-verrou. Le code suivant fonctionne.
```language-c
sem_t empty, full, mutex;
void *producer(void *arg) {
for (int i = 0; i < loops; ++i) {
sem_wait(&empty);
sem_wait(&mutex);
put(i);
sem_post(&mutex);
sem_post(&full);
}
return NULL;
}
void *consumer(void *arg) {
int tmp = 0;
while (tmp != -1) { // condition d'arrêt
sem_wait(&full);
sem_wait(&mutex);
tmp = get();
sem_post(&mutex);
sem_post(&empty);
printf("%d\n", tmp);
}
}
int main() {
sem_init(&empty, 0, MAX); // MAX buffers sont vides
sem_init(&full, 0, 0); // 0 buffers sont pleins
sem_init(&mutex, 0, 1); // It's a lock!
}
```
## Les verrous lecteurs/rédacteurs
Pour certaines classes de problèmes,
nous avons besoin de verrous qui soient plus flexibles. En particulier,
différentes classes de structures de données peuvent nécessiter
différentes sortes de verrous. Par exemple, imaginons un certain
nombre d'opérations à effectuer sur des listes: l'insertion et
la recherche. Alors que l'insertion change l'état de la liste,
la recherche ne fait que lire dans la structure. Ainsi, aussi longtemps
que nous pouvons garantir qu'aucune insertion ne se fait,
on peut autoriser autant de recherches concurrentes qu'on le souhaite.
Nous allons donc écrire ici un verrou **lecteurs/rédacteur**
basé sur les sémaphores.
Nous voulons donc créer un verrou, de type `rwlock_t`{.language-c},
qui permettra à autant de lecteurs
que nous le voulons de lire des données, mais pas pendant qu'un rédacteur
est en train d'écrire. Nous devrons donc verrouiller/déverrouiller
de deux façons différentes. Une fois nous verrouillerons en écriture:
seul le thread rédacteur peut entrer dans la section critique. L'autre
fois, nous verrouillerons en écriture: autant de threads
lecteurs qu'on le souhaite pourront
entrer dans la section critique, qui ne sera protégée que du/des threads
rédacteurs. Nous aurons donc besoin de deux classes de
verrouillage/déverrouillage: un `acquire_/release_readlock`{.language-c} et un `acquire_/release_writelock`{.language-c}. Pour le verrou lecteur, il faut l'acquérir
lorsque le premier lecteur entre dans la section critique.
On le symbolise par un sémaphore `writelock`{.language-c}. Il ne
sera libéré qu'au moment où le dernier lecteur aura
fini sa lecture. Il faut donc avoir un moyen de garder
la trace du nombre de lecteurs simultanés: on introduit une variable
entière `num_readers`{.language-c} dans ce but. Finalement, cette variable
doit être également protégée par un second verrou
(nommé `lock`{.language-c}), pour éviter
sa modification par plusieurs lecteurs simultanément.
La partie du verrouillage pour le rédacteur est plus simple:
il suffit de verrouiller/déverrouiller le sémaphore binaire
`writelock`. Le code ci-dessous effectue cette implémentation.
```language-c
typedef struct __rwlock_t {
int num_readers;
sem_t lock;
sem_t writelock;
} rwlock_t;
void rwlock_init(rwlock_t *rw) {
rw->num_readers = 0;
sem_init(&lock, 0, 1);
sem_init(&writelock, 0, 1);
}
void rwlock_acquire_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->num_readers += 1;
if (rw->num_readers == 1) { // le premier lecteur acquière le verrou
sem_wait(&rw->writelock); // le sémaphore passe à 0,
// s'il est libre ou à < 0 s'il l'est pas
}
sem_post(&rw->lock);
}
void rwlock_release_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->num_readers -= 1;
if (rw->num_readers == 0) { // le dernier lecteur libère le verrou
sem_post(&rw->writelock); // le sémaphore passe à 1,
// et réveille un éventuel rédacteur
}
sem_post(&rw->lock);
}
void rwlock_acquire_writelock(rwlock_t *rw) {
sem_wait(&rw->writelock); // verrouillage classique
}
void rwlock_release_writelock(rwlock_t *rw) {
sem_post(&rw->writelock); // déverrouillage classique
}
```
Cette implémentation fonctionne. On voit par contre qu'il peut
y avoir un problème d'équité. En effet, il est assez facile
de voir que les threads rédacteurs peuvent souffrir
de la famine à cause des threads lecteurs. Pour éviter
ce problème il faudrait imaginer un système qui
empêche des nouveaux threads lecteurs d'entrer dans le verrou
quand un thread rédacteur attend.
---
Exercice #
Réaliser un verrou lecteurs/rédacteurs plus équitable.[^6]
---
```language-c
typedef struct __rwlock_t {
int num_readers;
sem_t lock;
sem_t writelock;
sem_t waitforwrite;
} rwlock_t;
void rwlock_init(rwlock_t *rw) {
rw->num_readers = 0;
sem_init(&lock, 0, 1);
sem_init(&writelock, 0, 1);
sem_init(&waitforwrite, 0, 1);
}
void rwlock_acquire_readlock(rwlock_t *rw) {
sem_wait(&rw->waitforwrite); // Un seul lecteur à la fois. Vérouillé
// tant qu'un rédacteur attend
sem_post(&rw->waitforwrite);
sem_wait(&rw->lock);
rw->num_readers += 1;
if (rw->num_readers == 1) { // le premier lecteur acquière le verrou
sem_wait(&rw->writelock); // le sémaphore passe à 0,
// s'il est libre ou à < 0 s'il l'est pas
}
sem_post(&rw->lock);
}
void rwlock_release_readlock(rwlock_t *rw) {
sem_wait(&rw->lock);
rw->num_readers -= 1;
if (rw->num_readers == 0) { // le dernier lecteur libère le verrou
sem_post(&rw->writelock); // le sémaphore passe à 1,
// et réveille un éventuel rédacteur
}
sem_post(&rw->lock);
}
void rwlock_acquire_writelock(rwlock_t *rw) {
sem_wait(&rw->waitforwrite); // bloque les lecteurs si sur le point d'écrire
sem_wait(&rw->writelock); // verrouillage classique
}
void rwlock_release_writelock(rwlock_t *rw) {
sem_post(&rw->writelock); // déverrouillage classique
sem_post(&rw->waitforwrite); // libere les lecteurs apres écriture
}
```
## Le dîner des philosophes
Le problème du *dîner des philosophes* a été proposé et résolu par
E. Dijkstra. C'est un problème classique de concurrence qui
concerne le partage de ressources informatiques: l'ordonnancement et leur allocation.
![Le dîner des philosophes, Inkscape sur fond blanc, O. Malaspinas, Musée du Louvre, 2019. Cinq philosophes, cinq fourchettes.](figs/philosophers.svg){#fig:philosophes width=50%}
Considérons cinq philosophes, qui ont chacun à leur droite une
fourchette (cela fait donc cinq fourchettes au total également,
voir @fig:philosophes).
Ils ont devant eux une assiette avec de la nourriture
(par exemple un magnifique et juteux filet de bœuf parfaitement
cuit, donc bleu[^4]). Les philosophes peuvent faire deux choses: penser et manger. Ils peuvent penser pendant un temps indéfini
et n'ont besoin d'aucun matériel pour le faire.
En revanche, pour manger chaque philosophe a besoin de deux fourchettes,
celle qui se trouve à sa gauche et celle qui se trouve à sa droite,
mais chaque fourchette ne peut être tenue que par un seul philosophe
à la fois. Il faut donc que les philosophes reposent leurs fourchettes
dès qu'ils ont fini de manger. Ils peuvent également
se saisir des fourchettes sans que les deux soient libres,
mais doivent impérativement avoir les deux en main pour manger.
Le problème est de réussir à faire en sorte que les philosophes
ne meurent pas de faim, car chacun va manger ou penser, mais
aucun des philosophes ne sait quand les autres vont faire.
Il est plus difficile que prévu d'écrire un algorithme robuste
à ce problème. Par exemple, l'algorithme suivant ne fonctionnerait
pas. Chaque philosophe (qui représentent en fait des threads)
fera ces actions
* Penser jusqu'à ce que la fourchette de droite soit libre, quand elle l'est, la ramasser.
* Penser jusqu'à ce que la fourchette de gauche soir libre,
quand elle l'est la ramasser.
* Manger un certain temps lorsque les deux fourchettes sont
en ma possession.
* Poser la fourchette de gauche.
* Poser la fourchette de droite.
* Recommencer dès le début.
On voit assez facilement, que cet algorithme peut produire un
interblocage. En effet, si par malheur les cinq philosophes
décident de ramasser la fourchette de droite en même temps,
ils vont se retrouver bloqués, sans moyen
de se débloquer. Un autre problème qui pourrait se produire
est qu'un philosophe meure de faim, car il n'arrive pas à
se saisir des deux fourchettes, pendant qu'un ou plusieurs autres se gavent de nourriture.
Le problème principal ici, c'est que nous n'utilisons aucune
primitive d'exclusion mutuelle permettant de synchroniser l'accès
aux ressources. Voyons à comment en pratique résoudre ce problème.
Chaque philosophe va effectuer en boucle la séquence d'opérations suivantes (code en pseudo-c).
```language-c
while(1) {
think();
get_forks();
eat();
put_forks();
}
```
Alors que `tink()`{.language-c} et `eat()`{.language-c},
sont deux opérations qui peuvent d'effectuer sans synchronisation,
les fonctions `get_forks()`{.language-c} et `put_forks()`{.language-c} doivent être écrites avec plus de soin
pour éviter famine, interblocages, et tenter d'avoir
un maximum de philosophes en train de manger (parce que manger c'est bon).
Avant d'aller plus loin, on introduit deux fonctions qui nous
seront utiles.
```language-c
int get_left(int p) {
return (p + 4) % 5;
}
int get_right(int p) {
return p;
}
```
Alors que la fourchette de droite de chaque philosophe a le même
indice que lui, celle de gauche est obtenue grâce à cette
opération modulo. On a par exemple que $P_0$
a à sa droite $f_0$ et à sa gauche $f_{(0+4)\%5}=f_4$
comme prévu (ouf).
Nous allons également utiliser cinq sémaphores
pour synchroniser l'accès aux fourchettes.
```language-c
sem_t forks[5];
```
### Une solution ratée
Comme vous commencez à en avoir l'habitude, on commence
par écrire une solution qui ne marche pas.
On commence à initialiser toues les sémaphores
à un.
```language-c
void get_forks(void *arg) {
int p = *(int *)arg;
sem_wait(&forks[left(p)]);
sem_wait(&forks[right(p)]);
}
void put_forks(void *arg) {
int p = *(int *)arg;
sem_post(&forks[left(p)]);
sem_post(&forks[right(p)]);
}
```
Ici, on a simplement écrit l'algorithme décrit un peu plus haut.
Chaque philosphe commence par attendre que sa fourchette
de gauche soit libre, puis que celle de droite soit libre
avant de pouvoir manger. Puis, lorsqu'il a fini, il repose la fourchette de
gauche, puis celle de droite et se remet à penser.
Exactement comme on l'a discuté tout à l'heure, cette solution
peut donner lieu à un *deadlock*. Si chaque philosophe
prend en même temps sa fourchette de gauche,
tous les philosophes attendront sur leur fourchette
de droite et seront bloqués.
### Une solution possible
La façon la plus simple de résoudre ce problème est de modifier
l'ordre d'acquisition d'un des philosophes. Si $P_0$
acquière d'abord la fourchette de droite au lieu de celle de gauche
il n'y a pas moyen que tous les philosophes se retrouvent en attente.
```language-c
void get_forks(void *arg) {
int p = *(int *)arg;
if (p == 0) {
sem_wait(&forks[right(p)]);
sem_wait(&forks[left(p)]);
} else {
sem_wait(&forks[left(p)]);
sem_wait(&forks[right(p)]);
}
}
```
### Le mot de la faim
Il existe toute une classe de problèmes similaires pour réfléchir
sur les problèmes de concurrence. En particulier, citons
le problème du **barbier assoupi**, ou des **fumeurs de cigarettes**[^5].
## L'implémentation des sémaphores
Histoire de comprendre un peu mieux le fontionnement des sémaphores,
essayons d'en cronstruire un à partir des primitives de synchronisation
que nous avons à disposition: les verrous et les variables
de condition. Nous allons appeler ces sémaphores maison les *séphamores*.
En plus de la variable de condition et du verrou, il faut
utiliser un entier qui stocke la valeur du séphamore.
```language-c
typedef struct __seph_t {
int value;
phtread_cond_t cond;
pthread_mutex_t mutex;
} seph_t;
```
Ensuite, il faut implémenter trois fonctions: l'intialisation, `init()`{.language-c}, la mise en attente `wait()`{.language-c}, et le réveil
`post()`{.language-c}. Ces trois fonctions peuvent être implémentées
de la façon suivante.
```language-c
// Un seul thread appelle ça.
void seph_init(seph_t &s, int value) {
s->value = value;
pthread_cond_init(&s->cond, NULL);
pthread_mutex_init(&s->mutex, NULL);
}
// Un seul thread appelle ça.
void seph_destroy(seph_t &s, int value) {
s->value = value;
pthread_cond_destroy(&s->cond);
pthread_mutex_destroy(&s->mutex);
}
void seph_wait(seph_t &s) {
pthread_mutex_lock(&s->mutex);
while (s->value <= 0) {
pthread_cond_wait(&s->cond);
}
s->value -= 1;
pthread_mutex_unlock(&s->mutex);
}
void seph_post(seph_t &s) {
pthread_mutex_lock(&s->mutex);
s->value += 1;
pthread_cond_signal(&s->cond);
pthread_mutex_unlock(&s->mutex);
}
```
---
Question #
Cette façon d'implémenter les sémaphores ne correspond pas tout à fait
à ce que nous avons vu au début de ce chapitre. Pouvez-vous dire
quelle est la différence?
---
En fait, on constate que le sémaphore tel que nous l'avons implémenté ici
n'a pas d'équivalence entre le nombre de threads en attente
et sa valeur. En effet, ici la valeur du sémaphore ne sera jamais plus
petite que zéro.
[^1]: Historiquement ces fonctions s'appelaient `P()`{.language-c} et
`V()`{.language-c} pour "prolaag" (contraction voulant dire essayer et
diminuer) et "verhoog" pour augmenter en néerlandais, la langue de E. Dijkstra.
[^2]: Il est clair que comme plusieurs threads vont appeler ces fonctions
elles devront contenir des sections critiques qu'il faudra gérer avec soin.
On suppose pour le moment que toutes ces opérations sont atomiques.
[^3]: Le cas où `t2()` atteint `sem_post()`
en premier est laissé à faire en exercice au lecteur. Le premier
`merge request` gagne $0.1$ points sur l'examen.
[^4]: Comme vous pouvez le remarquer ils ont que des fourchettes
pour manger du filet de bœuf c'est pas super réaliste, mais c'est
pas moi qui ai posé le problème...
[^5]: Fumer nuit gravement à la santé.
[^6]: Cet exercice est laissé à faire en exercice au lecteur. Le premier
`merge request` gagne $0.1$ points sur l'examen.
[^7]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
\ No newline at end of file
---
author:
- Orestis Malaspinas, Steven Liatti
title: Structures de données concurrentes
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Les structures de données basées sur les verrous
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^3].
Dans ce chapitre, nous allons brièvement voir comment utiliser des verrous
dans des structures de données standards de façon à ce qu'elles soient
sûres à l'utilisation dans des applications multi-threadées.
La difficulté principale est de garder une bonne performance tout en garantissant un fonctionnement correct.
## Les compteurs concurrents
La structure de données la plus simple qu'on puisse imaginer est un simple
compteur. Nous avons déjà vu dans les exercices comment incrémenter
de façon fausse un compteur de façon concurrente, puis une façon juste mais peu efficace. Histoire de formaliser tout ceci un peu ici,
écrivons un compteur atomique dans la structure `counter_t`{.language-c}
ainsi que les opérations d'incrémentation et de décrémentation.
```language-c
typedef struct __counter_t {
int value; // la valeur du compteur
pthread_mutex_t lock; // le mutex protégeant le compteur
} counter_t;
void init(counter_t *cnt) {
cnt->value = 0;
pthread_mutex_init(&cnt->lock, NULL);
}
void increment(counter_t *cnt) {
pthread_mutex_lock(&cnt->lock);
cnt->value += 1;
pthread_mutex_unlock(&cnt->lock);
}
void decrement(counter_t *cnt) {
pthread_mutex_lock(&cnt->lock);
cnt->value -= 1;
pthread_mutex_unlock(&cnt->lock);
}
int get_value(counter_t *cnt) {
pthread_mutex_lock(&cnt->lock);
int value = cnt->value;
pthread_mutex_unlock(&cnt->lock);
return value;
}
```
Ici, nous avons rendues atomiques les opérations d'incrémentation, de décrémentation et de retour de la valeur du compteur en protégeant les
sections critiques (d'incrémentation, de décrémentation et du compteur)
à l'aide de verrous. Notre structure de données est donc parfaitement
concurrente et fonctionne correctement. Néanmoins, on peut assez facilement
se rendre compte que cette méthode peut souffrir d'un problème de performances. Néanmoins, on a ici une méthode très très simple
de rendre une structure de données concurrente: ajouter un verrou!
Pour rendre ces structures plus efficaces, il faut utiliser la ruse.
Pour illustrer le problème de performances d'un code n'utilisant
qu'un simple verrou, on peut s'intéresser à la performance du code de l'incrémentation de notre compteur un million de fois. Si on mesure le temps d'exécution du code sur un, deux ou quatre fils d'exécution,
on obtient le tableau ci-dessous :
| | 1 thread | 2 threads | 4 threads |
| --------------|:--------------:|:---------------:|:--------------:|
| Temps $[ms]$ | 0.031 | 0.149 | 0.447 |
On voit un ralentissement assez spectaculaire de l'exécution (plus d'un facteur 10) entre le cas à un seul thread et le cas à 4 threads. Ici,
la quantité de travail à effectuer étant constante par processeur (chaque
CPU devait incrémenter un million de fois la variable) on aimerait que
le temps total reste constant (ce qui serait une **mise à l'échelle parfaite**, ou **perfect scaling**).
Différentes approches pour résoudre ce problème ont été proposées. Ici, nous allons étudier celle du **approximate counter**. L'idée générale
est d'avoir **plusieurs** compteurs logiques locaux (un par CPU) et un compteur **global**. On aura en plus un verrou **local** par CPU,
lié aux compteurs locaux (celui-ci n'est nécessaire qu'en cas d'exécution de plusieurs
threads par CPU), un verrou **global** pour le compteur global.
On va incrémenter "localement" tous les compteurs des CPUs qui se synchroniseront
avec leurs verrous locaux respectifs. Comme les threads dans leurs CPUs respectifs
peuvent incrémenter leurs compteurs sans contention, la mise à l'échelle
sera bonne. Néanmoins, il est nécessaire de synchroniser les
compteurs entre les CPUs afin de pouvoir lire la valeur du compteur
si nécessaire. On devra donc périodiquement transférer les valeurs
des compteurs locaux au compteur global de temps en temps,
en additionnant tous les compteurs locaux au compteur global.
A ce moment là les compteurs locaux seront remis à zéro.
Le moment où les compteurs locaux seront synchronisés dépendra d'une variable, $s$. Plus $s$ sera grand, moins la valeur stockée dans $s$ sera précise (plus il y aura de chances qu'elle ne contienne pas la bonne valeur), mais plus le calcul aura une bonne performance sur plusieurs threads. A l'inverse un petit $s$ aura de moins bonnes performances, mais
donnera une valeur plus correcte du compteur.
Considérons un exemple où on a 4 threads $T_1$-$T_4$, avec quatre compteurs locaux $L_1$ à $L_4$, et où $s=4$.
Un exemple d'exécution est résumé dans la table ci-dessous
| Temps | $L_1$ | $L_2$ | $L_3$ | $L_4$ | $G$ |
| --------------|:--------------:|:--------------:|:--------------:|:--------------:|:--------------:|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 0 | 1 | 1 | 0 |
| 2 | 2 | 0 | 1 | 2 | 0 |
| 3 | 3 | 1 | 1 | 3 | 0 |
| 4 | 4 | 1 | 2 | $4\rightarrow 0$ | 4 (de $L_4$) |
| 5 | $4\rightarrow 0$ | 2 | 3 | 0 | 8 (de $L_1$) |
Les compteurs locaux s'incrémentent chacun à leur vitesse, mais lorsqu'un d'entre eux atteint $s=4$, sa valeur
est ajoutée au compteur global $G$, et il est remis à zéro. On voit donc pourquoi
la valeur est approximée. Au temps 5, le compteur
a été incrémenté 13 fois, hors la valeur que nous pouvons lire
dans le compteur global est de huit uniquement.
On peut à présent écrire le code suivant pour le compteur approximé :
```language-c
typedef struct __approx_counter_t {
int glob_counter; // global counter
pthread_mutex_t glob_mutex; // global mutex
int *local_counter; // local counter array
int threshold; // threshold where we communicate
} approx_counter_t;
void init(approx_counter_t *ac, int threshold) {
ac->threshold = threshold;
ac->glob_counter = 0;
pthread_mutex_init(&ac->glob_mutex, NULL);
ac->local_counter = malloc(num_threads * sizeof(int));
ac->local_mutex = malloc(num_threads * sizeof(pthread_mutex_t));
for (int i = 0; i < num_threads; ++i) {
ac->local_counter[i] = 0;
pthread_mutex_init(&ac->local_mutex[i], NULL);
}
}
void increment_by(approx_counter_t *ac, int tid, int amount) {
ac->local_counter[tid] += amount;
if (ac->local_counter[tid] >= ac->threshold) {
pthread_mutex_lock(&ac->glob_mutex); // verrou global
ac->glob_counter += ac->local_counter[tid];
pthread_mutex_unlock(&ac->glob_mutex); // fin verrou global
ac->local_counter[tid] = 0;
}
}
int get_counter(approx_counter_t *ac) {
pthread_mutex_lock(&ac->glob_mutex);
int value = ac->glob_counter;
pthread_mutex_unlock(&ac->glob_mutex);
return value;
}
```
Il faut noter que dans notre structure `approx_counter_t`{.language-c}
nous n'avons pas utilisé de verrou local. On suppose ici par simplicité qu'il n'y a qu'un thread par cœur.
On peut à présent mesurer la performance de ce compteur pour un $s$ donné si on incrémente un million de fois un compteur sur un à quatre threads.
| | 1 thread | 2 thread | 4 thread |
| --------------|:--------------:|:--------------:|:--------------:|
| Temps, $s=1$, $[ms]$ | 0.053 | 0.36 | 0.69 |
| Temps, $s=10$, $[ms]$ | 0.012 | 0.044 | 0.086 |
| Temps, $s=100$, $[ms]$ | 0.008 | 0.018 | 0.032 |
| Temps, $s=1000$, $[ms]$ | 0.009 | 0.01 | 0.012 |
## La liste chaînée concurrente
Une liste chaînée est une structure de données où un élément de notre
liste contient une valeur, ainsi qu'un pointeur vers le prochain
élément de la liste (vous avez déjà vu ces choses là en première).
Un petit code tout simple implémentant
l'insertion en tête de la liste d'un élément
entier peut s'écrire de la façon suivante, ainsi qu'une
fonction permettant de déterminer si un élément est bien présent
dans votre liste peut se trouver ci-dessous :
```language-c
typedef struct __node_t {
int key;
struct __node_t *next;
} node_t;
typedef struct __list_t {
node_t *head;
} list_t;
void init(list_t *l) {
l->head = NULL;
}
int insert(list_t *l, int key) {
node_t *new = malloc(sizeof(node_t));
if (new == NULL) {
printf("Malloc failed.\n");
return -1;
}
new->key = key;
l->head = new;
return 0;
}
int lookup(list_t *l, int key) {
node_t *current = l->head;
while (current) {
if (current->key) {
return 0;
}
current = current->next;
}
return -1;
}
```
Il est évident qu'avec ce petit bout de code il est impossible
d'insérer et de vérifier la présence ou non d'un élément dans notre
liste de façon concurrente.
On veut à présent réécrire ce code pour qu'il soit utilisable dans une application multi-threadée. La première approche serait de protéger
le moment de l'insertion, ainsi que la lecture par un verrou
qui serait intégré dans notre structure `list_t`. Le code deviendrait
```language-c
typedef struct __node_t {
int key;
struct __node_t *next;
} node_t;
typedef struct __list_t {
node_t *head;
pthread_mutex_t mutex;
} list_t;
void init(list_t *l) {
l->head = NULL;
pthread_mutex_init(&l->mutex, NULL);
}
int insert(list_t *l, int key) { // on retourne vrai si tout s'est bien passé
pthread_mutex_lock(&l->mutex); // début de la section critique
node_t *new = malloc(sizeof(node_t));
if (new == NULL) {
printf("Malloc failed.\n");
pthread_mutex_unlock(&l->mutex); // fin de la section critique
return -1; // error
}
new->key = key;
new->next = l->head;
l->head = new;
pthread_mutex_unlock(&l->mutex); // fin de la section critique
return 0; // success
}
int lookup(list_t *l, int key) {
pthread_mutex_lock(&l->mutex); // début de la section critique
node_t *current = l->head;
while (current) {
if (current->key == key) {
pthread_mutex_unlock(&l->mutex); // autre fin de la section critique
return 0;
}
current = current->next;
}
pthread_mutex_unlock(&l->mutex); // autre fin de la section critique
return -1;
}
```
Regardons d'abord la fonction `lookup()`{.language-c}. On constate
qu'au tout début on `lock()`{.language-c} notre verrou, afin de protéger la
partie où on va vérifier si un élément est présent ou non dans notre
liste. Il faut en effet être certain·e·s qu'aucun nouvel élément n'est
ajouté pendant que nous lisons, car cela pourrait avoir des effets dramatiques[^1]. Le verrou est libéré non seulement lorsqu'on a trouvé
l'élément `key`{.language-c} mais également lorsqu'il est absent:
il y a deux endroits distincts où il faut penser à faire un `unlock()`{.language-c}. En effet, il faut libérer le verrou **avant** de sortir
de la fonction sinon le verrou ne sera jamais déverrouillé...
La fonction `insert()`{.language-c} verrouille le `mutex`{.language-c}, crée l'élément suivant dans la liste et l'insère en tête
avant de déverrouiller le `mutex`{.language-c}. Rien de très fou. Néanmoins, il faut remarquer
que le verrou doit être libéré dans le cas où `malloc()`{.language-c}
retourne une erreur. Bien que cela ne se produise pas souvent,
ce cas peut conduire à des threads qui attendent indéfiniment
après une erreur d'allocation. De façon générale,
il faut être prudent·e lorsqu'on a des branchement conditionnels
qui modifient l'exécution d'un programme. Souvent c'est
dans ce genre de branchement que se trouvent les erreurs
les plus difficiles à détecter.
Cette façon d'écrire une liste chaînée concurrente
est simple, mais nous utilisons trop de branchements conditionnels
ce qui augmente les chances d'introduire des erreurs.
Il est possible de modifier légèrement le code
afin d'avoir un plus petit nombre de chemins
possibles pour le code, réduisant le nombre de libérations
de verrous à effectuer :
```language-c
void init(list_t *l) {
l->head = NULL;
pthread_mutex_init(&l->mutex, NULL);
}
void insert(list_t *l, int key) { // on retourne vrai si tout s'est bien passé
node_t *new = malloc(sizeof(node_t)); // malloc est thread safe
if (new == NULL) {
printf("Malloc failed.\n");
return; // error
}
new->key = key;
pthread_mutex_lock(&l->mutex); // début de la section critique
new->next = l->head;
l->head = new;
pthread_mutex_unlock(&l->mutex); // fin de la section critique
}
int lookup(list_t *l, int key) {
int ret_val = -1;
pthread_mutex_lock(&l->mutex); // début de la section critique
node_t *current = l->head;
while (current) {
if (current->key == key) {
ret_val = 0;
break;
}
current = current->next;
}
pthread_mutex_unlock(&l->mutex); // fin de la section critique
return ret_val;
}
```
Ici, nous constatons que nous avons simplement enlevé
l'acquisition du verrou devant `malloc()`{.language-c}.
Cela est possible, car `malloc()`{.language-c} est *thread-safe*.
Nous n'avons besoin de verrouiller que lorsqu'on écrit ou qu'on
lit dans la liste. Par ailleurs, en ne retournant qu'une seule fois
depuis `lookup()`{.language-c}, on s'affranchit
de déverrouiller une fois supplémentaire.
Ce genre de liste chaînée n'a pas une grande efficacité
lorsqu'on augmente le nombre de threads. Néanmoins, nous n'avons pas réussi
à faire beaucoup mieux. Une technique explorée par les chercheurs
a été le "hand-over-hand locking". L'idée est la suivante. Au lieu
d'avoir un seul verrou pour toute la liste chaînée,
on a un verrou par noeud. Lorsqu'on parcourt la liste,
on acquière d'abord le verrou du nœud suivant et libère le
verrou du nœud courant. Ainsi, la liste peut être parcourue de façon
concurrente. Néanmoins, le coût de verrouillage/déverrouillage
rend cette façon de faire moins efficace en pratique.
## La file concurrente
De façon similaire à ce que nous avons fait jusque là, nous
allons écrire une file concurrente. Faites si vous voulez
la partie simple consistant à écrire une file séquentielle,
puis à la rendre concurrente à l'aide d'un verrou
de la façon la plus triviale possible. Ici, nous allons
étudier un algorithme proposé par Michael et Scott[^2].
Un code reproduisant leur idée se trouve ci-dessous :
```{.language-c}
typedef struct __node_t {
int value;
struct __node_t *next;
} node_t;
typedef struct __queue_t {
node_t *head;
node_t *tail;
pthread_mutex_t head_lock;
pthread_mutex_t tail_lock;
} queue_t;
void init(queue_t *q) {
node_t *tmp = malloc(sizeof(node_t));
tmp->next = NULL;
q->head = q->tail = tmp;
pthread_mutex_init(&q->head_lock, NULL);
pthread_mutex_init(&q->tail_lock, NULL);
}
void enqueue(queue_t *q, int value) {
node_t *tmp = malloc(sizeof(node_t));
assert(tmp != NULL);
tmp->value = value;
tmp->next = NULL;
pthread_mutex_lock(&q->tail_lock);
q->tail->next = tmp;
q->tail = tmp;
pthread_mutex_unlock(&q->tail_lock);
}
int dequeue(queue_t *q, int *value) {
pthread_mutex_lock(&q->head_lock);
node_t *tmp = q->head;
node_t *newHead = tmp->next;
if (newHead == NULL) {
pthread_mutex_unlock(&q->head_lock); // attention branchement
return -1; // queue was empty
}
*value = newHead->value;
q->head = newHead;
pthread_mutex_unlock(&q->head_lock); // attention branchement
free(tmp);
return 0;
}
```
Dans ce code, on constate qu'il y a deux verrous: un pour la queue et un
pour la tête. On peut ainsi avoir une approche suffisamment fine pour
enfiler ou défiler de façon concurrente (les deux opérations
ne sont pas mutuellement exclusives). Cela est rendu possible
par la création d'un nœud fictif lors de la création de la file.
Sans lui les fonction `enqueue()`{.language-c} et `dequeue()`{.language-c}
ne pourraient avoir lieu de façon concurrente.
## La table de hachage concurrente
A l'aide de la liste concurrente que nous avons implémenté tout à l'heure,
il est très simple de créer une table de hachage concurrente (très simple)
concurrente: elle sera statique.
```language-c
#define BUCKETS (101)
typedef struct __hash_t {
list_t lists[BUCKETS];
} hash_t;
void init(hash_t *h) {
for (int i = 0; i < BUCKETS; i++) {
list_init(&h->lists[i]);
}
}
int insert(hash_t *h, int key) {
int bucket = key % BUCKETS;
return list_insert(&h->lists[bucket], key);
}
int lookup(hash_t *h, int key) {
int bucket = key % BUCKETS;
return list_lookup(&H->lists[bucket], key);
}
```
On constate que cette table de hachage ne nécessite aucun nouveau verrou.
Toutes les sections critiques sont cachées dans les fonctions
de la liste chaînée. En effet, la table de hachage n'est
rien d'autre qu'un tableau de listes chaînées dans le cas
simple où le nombre d'alvéoles est statique.
---
Exercice #
Implémenter la table de hachage dynamique.
---
[^1]: Plusieurs bébés chats sont morts à la suite de lectures non protégées de listes concurrentes.
[^2]: M. Michael, M. Scott, *Nonblocking Algorithms and Preemption-safe Locking on by Multiprogrammed Shared-memory Multiprocessors* *Journal of Parallel and Distributed Computing*, **51**, No. 1, (1998).
[^3]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
\ No newline at end of file
---
author:
- Orestis Malaspinas, Steven Liatti
title: Introduction aux verrous
autoSectionLabels: false
autoEqnLabels: true
eqnPrefix:
- "éq."
- "éqs."
chapters: true
numberSections: false
chaptersDepth: 1
sectionsDepth: 3
lang: fr
documentclass: article
papersize: A4
cref: false
urlcolor: blue
toc: false
date: 2020-01-01
mathjax: on
include-before: <script src="css/prism.js"></script>
---
# Les verrous
Le contenu de ce chapitre est basé sur l'excellent livre de R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau[^3].
Dans ce chapitre nous allons discuter plus en détails le concept de verrous.
Nous allons d'abord voir comment nous pourrions tenter de construire
un verrou dans un programme et se rendre compte que cela est
quelque chose de compliqué pour que le verrou soit effectivement
un mécanisme d'exclusion mutuelle (qu'il remplisse bel et bien son rôle) et qu'il
soit efficace.
## Les verrous: généralités
Le but d'un verrou est de s'assurer qu'uniquement un fil d'exécution à la fois peut accéder
à une **section critique** d'un code. Une section critique contient des variables (contenant des données)
partagées par plusieurs threads qui y sont modifiées (si on ne fait que lire des données
il n'y a pas de problème en principe). Un exemple, de protection peut se voir avec l'exemple du compteur
des exercices de la section sur l'api des threads POSIX
```language-c
pthread_mutex_t verrou = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
// du code
pthread_mutex_lock(&verrou);
counter += 1;
pthread_mutex_unlock(&verrou);
// encore du code
```
Un verrou est une variable, qui peut être dans deux états:
1. Libre (déverrouillé, disponible): aucun thread ne possède le verrou.
2. Occupée (acquis, verrouillé): exactement un thread possède le verrou et se trouve dans une section critique du programme.
On constate en premier lieu que dans l'interface POSIX un verrou est un `mutex`{.language-c} pour **mutual exclusion**.
La syntaxe pour le verrouillage/déverrouillage du verrou est très simple.
Lorsque que `pthread_mutex_lock()`{.language-c} est appelé dans un thread, le fil d'exécution tente
d'acquérir le verrou. S'il y parvient (cela veut dire qu'aucun autre thread ne l'a verrouillé)
il devient le **propriétaire** du verrou, celui-ci entre dans l'état occupé (c'est pour cela que nous passons
**la référence** au verrou à `pthread_mutex_lock()`{.language-c}),
et le thread entre dans une section critique.
S'il n'y parvient pas, il va rester bloqué dans cette fonction, jusqu'à ce
qu'il puisse acquérir le verrou.
Quand le propriétaire d'un verrou (il doit l'avoir verrouillé avant),
appelle la fonction `pthread_mutex_unlock()`{.language-c},
à la fin d'une section critique. Un autre thread peut donc en devenir le propriétaire
et entrer dans sa section critique. Il n'y a aucune garantie quant à l'ordre
de l'acquisition du verrou par plusieurs threads, si plusieurs sont en attente
à leur fonction `pthread_mutex_lock()`{.language-c}. En revanche,
si aucun fil d'exécution n'attend pour devenir propriétaire du verrou,
il passe dans l'état libre (c'est pour cela que nous passons
**la référence** au verrou à `pthread_mutex_lock()`{.language-c}).
On peut également noter qu'un verrou n'est rien d'autre qu'une variable.
On peut donc en définir différents tout au long de l'exécution d'un programme.
Chacun peut protéger des structures de données différentes dépendant
de la **granularité**, **fine** ou **grossière** que chacun veut donner
aux section critiques à protéger. Utiliser une approche plus fine
permet "d'augmenter la concurrence" mais cela vient aussi avec
une plus grande chance d'introduire des bugs.
---
Remarque #
Il est important de noter que l'api des threads ne permet aucun contrôle sur l'ordre
de leur exécution (tout est géré par l'OS). Les verrous permettent de récupérer
un peu de contrôle en garantissant qu'un seul thread à la fois entre une section critique.
---
## Construisons notre verrou
Comme nous allons le voir dans cette section, construire un verrou n'est pas simple du tout.
En particulier, il serait tentant d'utiliser une simple variable booléenne pour
garantir l'exclusion mutuelle. En fait, on se rendra compte que cela n'est pas possible
sans l'aide du matériel et de l'OS.
---
Notations #
Pour plus de généralité (et de simplicité dans les notations), nous noterons
l'acquisition du verrou par la fonction `lock()`{.language-c} et
sa libération `unlock()`{.language-c} et n'utiliserons pas l'api
POSIX dans ce qui suit.
---
Avant de construire un verrou à proprement parler, nous devons d'abord
définir quelles propriétés il doit avoir:
1. Il doit garantir **l'exclusion mutuelle**. Cela est la tâche primordiale d'un verrou.
S'il ne garantit pas qu'un seul thread à la fois peut accéder à la partie
du code protégée par le verrou, alors le verrou est inutile.
2. Il doit être aussi **équitable** que possible. Tous les fils d'exécution doivent
avoir la possibilité d'acquérir le verrou, sinon ils risquent une terrible
**famine** et n'auront jamais accès à la section critique.
3. La **performance** doit être bonne. Il y a deux cas de figure possibles:
* le cas où il n'y a qu'un fil qui effectue le verrouillage/déverrouillage.
* le cas où plusieurs threads sur un seul processeur combattent pour
acquérir le verrou.
* le cas où plusieurs threads, sur plusieurs CPUs sont en concurrence
pour acquérir le verrou.
Quel est le coût de calcul de cette opération dans ces cas?
Quelle sera la performance d'un verrou dans chacun de ces cas?
Dans ce qui suit, nous allons considérer plusieurs technique et voir
comment elles se comparent entre-elles pour chacun des critères.
### Les interruptions
Une des premières façon de protéger une section critique a été
de désactiver les **interruptions**[^1]. Ainsi, en empêchant
notre programme de s'interrompre pendant qu'il entre dans une section critique, nous
nous assurions qu'aucun autre fil ne peut interrompre le code se trouvant dans une section critique.
Puis lorsque la section critique est terminée, le processeur peut à nouveau interrompre
le thread s'il le souhaite.
Une façon de se représenter ce comportement en pseudo-c, serait d'avoir les fonctions
`lock()`{.language-c} et `unlock()`{.language-c}.
```language-c
void lock() {
disable_interrupts();
}
void unlock() {
enable_interrupts();
}
```
Cette façon de procéder a l'avantage d'être d'une extrême simplicité.
Il y a en revanche plusieurs problèmes fondamentaux également.
---
Question #
Lesquels voyez-vous? Réfléchissez fort! Plus fort! C'est toujours pas assez fort!
Voilà c'est mieux.
---
Le premier problème est que la désactivation/réactivation des interruptions est une action *privilégiée* qu'on ne peut pas laisser
tout le monde effectuer et certainement pas des threads contrôlés par un utilisateur: on ne peut avoir aucune
confiance que ce genre d'opération privilégiée sera utilisée de façon raisonnable. Imaginons
que notre confiance soit mal placée en un programme appelant `lock()`{.language-c} au début de son
code et rendant le processeur complètement inaccessible jusqu'à ce qu'il se termine? Imaginons
qu'en plus il ne se termine jamais. La seule solution serait donc de relancer l'ordinateur. Comme
on le voit ici, il faut un degré de confiance dans les applications qui est beaucoup trop élevé
si on utilise les interruptions comme outil de synchronisation.
Un deuxième problème est que ce système ne peut pas fonctionner sur des systèmes
comprenant plusieurs processeurs. Si les threads tournent sur des processeurs différents
désactiver les interruptions, n'empêchera pas les threads se trouvant sur des CPUs
différents d'entrer dans des sections critiques. Même en supposant qu'on a assez
confiance dans nos applications, cette solution ne marchera simplement pas
sur des systèmes multi-processeurs.
Troisièmement, désactiver les interruptions peut entraîner la "perte" de certaines
interruptions primordiales (émises par d'autres processus). Cela peut arriver lorsqu'il faut réveiller
un thread après la fin d'une opération de lecture sur le disque, ou l'arrivée d'un paquet réseau.
Finalement, cette approche est inefficace. C'est probablement la raison la moins importante,
mais sur le matériel actuel, effectuer ce genre d'opérations est très lent.
La désactivation des interruptions est par conséquent plus utilisées dans
les ordinateurs, mais est encore très présente dans les systèmes embarqués.
Néanmoins, il y a quelques cas extrêmes où le système d'exploitation
lui-même utilise ce mécanisme pour garantir l'atomicité de certains accès
à des structures internes. Le système opérationnel se faisant confiance à lui-même,
le problème de devoir faire confiance à un processus disparaît.
### Un verrou raté
Essayons pour jouer de construire un verrou uniquement grâce à des commandes
logicielles. Pour ce faire, nous allons définir notre verrou comme une simple variable
booléenne, `bool locked`{.language-c}.
Le code ci-dessous implémente le verrou à l'aide d'une variable booléenne.
```language-c
typedef struct {
bool locked;
} lock_t;
void init(lock_t *mutex) {
mutex->locked = false;
}
void lock(lock_t *mutex) {
while (mutex->locked == true) {
// do nothing;
}
mutex->locked = true;
}
void unlock(lock_t *mutex) {
mutex->locked = false;
}
```
Il fonctionne de la façon suivante:
* La fonction `init()`{.language-c} initialise le verrou à `false`{.language-c}.
* La fonction `lock()`{.language-c} vérifie dans une boucle `while`{.language-c}. Si le verrou
est libre il est mis à `true`{.language-c}.
* La fonction `unlock()`{.language-c} met simplement la valeur du verrou à `false`{.language-c}.
Le fonctionnement de ce verrou est très simple. Le premier thread appelant `lock()`
changera `locked` à `true` et pourra entrer dans sa section critique. N'importe quel autre thread
essayant de devenir propriétaire du verrou entrera dans la boucle `while` et sera
en **attente active** (**spin-wait** en anglais) jusqu'à ce que le verrou soit
libéré par le premier thread et qu'il assigne `false` à la variable `locked`.
A ce moment là, le verrou peut être acquis par un autre thread et ainsi entrer dans la section
critique.
"Pour tout problème complexe, il existe une solution simple et élégante... et fausse"[^2]. Ici nous sommes dans ce cas. Bien que simple et élégante cette solution est également fausse.
---
Question #
Pourquoi cette solution est-elle fausse?
Réfléchissez avant de lire la suite. Faites même un dessin si cela peut vous aider.
---
On peut assez voir sur le listing ci-dessous qu'il n'y a pas d'exclusion mutuelle.
| Thread 1 | Thread 2 |
| ----------------------|---------------|
| `lock()`{.language-c} | |
| `while (locked == true)`{.language-c} | |
| **interruption** on passe au thread 2 | |
| | `lock()`{.language-c} |
| | `while (locked == true)`{.language-c} |
| | `locked = true`{.language-c} |
| | **interruption** on passe au thread 1 | |
| `locked = true // A-R-G-L!`{.language-c} | |
Mais que se passe-t-il donc? Le thread 1 tente d'acquérir le verrou. Alors qu'il teste
la valeur de `locked`{.language-c} et sort du `while`{.language-c}.
A ce moment précis, le thread un est préempté, un changement de contexte a lieu et l'exécution est passée au thread 2.
Celui-ci appelle également la fonction `lock()`{.language-c}. La valeur de `locked`{.language-c}
étant toujours `false`{.language-c} il sort de la boucle `while`{.language-c} et assigne
la valeur `true`{.language-c} à la variable `locked`{.language-c}. A ce point
le verrou ne devrait plus pouvoir être acquis par un autre thread.
Hors si une seconde interruption se produit et la main est repassée au thread un,
il continue exactement où il s'est arrêté, c'est-à-dire après
être sorti de la boucle `while`{.language-c}. Il va donc également assigner la valeur
`true`{.language-c} à `locked`{.language-c} et continuer son exécution.
On voit donc ici que deux threads distincts peuvent acquérir le verrou et entrer dans leurs
sections critiques respectives.
Ce verrou **ne fonctionne pas**.
Par ailleurs, mais cela est secondaire étant donné que ce verrou n'en est pas un,
la technique consistant à avoir une attente active avec le `while`{.language-c}
gaspille beaucoup de ressources sans raison. Cela est même absolument terrible dans
le cas d'un système avec un seul CPU.
---
Question #
A votre avis pourquoi?
Je veux voir vos méninges se tordre!
---
### Un verrou qui marche: attente active et **test-and-set**
Comme il est en pratique impossible de se baser sur le mécanisme des interruptions
pour construire des mécanismes d'exclusion mutuelle, les ingénieurs systèmes
ont dû créer du matériel qui supporte des mécanismes de verrou. Le plus simple de tous
à comprendre est l'instruction **test-and-set** (ou **échange atomique**).
Cette instruction est **atomique**, c'est-à-dire qu'il est garanti qu'elle ne sera **jamais**
interrompue. Cette garantie est apportée par le matériel directement. Une telle instruction
aurait une syntaxe en C qui serait la suivante
```language-c
bool test_and_set(bool *old_ptr, bool new) {
bool old = *old_ptr; // on stocke la valeur pointée par d'old_ptr
*old_ptr = new; // on assigne `new` à `*old_ptr`
return old; // on retourne l'ancienne valeur d'`*old_ptr`
}
```
Souvenez-vous que cette instruction n'est jamais implémentée comme cela dans un code.
Cette commande si vous l'implémentez comme cela ne sera pas atomique
(e système d'exploitation fera tout son possible pour ruiner vos plans), il vous faut le
support du matériel pour y arriver.
---
Remarque #
Dans ces pseudo-codes, on utilise des booléens pour des questions de clarté.
En pratique, ce sont des entiers qui sont utilisés (voire des bits uniquement).
---
Avec l'instruction `test_and_set()`{.language-c}, on peut construire un verrou avec
attente active de la façon suivante
```language-c
typedef struct {
bool locked;
} lock_t;
void init(lock_t *mutex) {
mutex->locked = false;
}
void lock(lock_t *mutex) {
while (test_and_set(&mutex->locked, true) == true) {
// do nothing;
}
}
void unlock(lock_t *mutex) {
mutex->locked = false;
}
```
Examinons ce qui se passe à présent lorsqu'un thread appelle
`lock()`{.language-c} et que le verrou est libre (`locked == false`{.language-c}). Quand ce thread appelle la fonction
`test_and_set()`{.language-c}, il recevra en retour l'ancienne
valeur de `locked`{.language-c}, donc `false`{.language-c}, et
sortira de la boucle `while`{.language-c} immédiatement.
De plus, le thread assignera également **atomiquement**
la valeur `true`{.language-c} à `locked`{.language-c}.
Le verrou sera donc acquis et aucun autre thread ne pourra y accéder
jusqu'à ce que ce même thread appelle la fonction `unlock()`{.language-c}.
L'autre possibilité est quand le verrou est déjà possédé par un thread.
Un autre thread appelant la fonction `lock()`{.language-c}.
Il appellera la fonction `test_and_set(locked, true)`{.language-c}.
Cette fonction retournera la valeur stockée dans `locked`{.language-c}
qui se trouve être `true`{.language-c} et lui assignera `true`{.language-c} à nouveau. Le thread entrera donc
dans la boucle `while`{.language-c} et répétera l'opération
jusqu'à ce que le verrou soit libéré.
---
Question #
Quelle est la grande différence entre cette version du verrou,
et notre version avec la simple variable booléenne?
---
La différence est que dans la version `test_and_set()`{.language-c}
le test et l'assignation sont **une seule opération atomique**
qui ne peut pas être interrompue (c'est une garantie du matériel).
De cette façon nous garantissons qu'un seul thread peut acquérir le verrou
à la fois.
Ce verrou est dit à **attente active** (ou **spin-lock**). En résumé il gaspille des cycles CPU jusqu'à ce que le verrou soit libéré. Ce système ne fait pas de
sens sur les systèmes CPU s'il n'y a pas un ordonnanceur préemptif (un ordonnanceur qui va préempter un processus après un certain temps quoi qu'il arrive). En effet, si le processus se retrouve dans la boucle `while`{.language-c} en attendant que le verrou soit libéré, il n'en
sortira jamais. Il faut que ce thread soit préempté et que le thread
possédant le verrou sorte de sa section critique (et ainsi libère le verrou) pour que le programme puisse continuer à s'exécuter, sinon...
### Évaluation de l'efficacité des spin-lock
Maintenant que nous avons construit un verrou qui verrouille et garantit l'exclusion mutuelle (c'était notre première règle
pour avoir un "bon" verrou) nous pouvons nous intéresser
aux autre critères que nous avons énumérés précédemment.
Le deuxième critère est l'équité.
---
Question #
Est-ce que le spin-lock est équitable? Les threads ont-ils tous une chance d'acquérir le verrou?
---
La réponse à cette question est **non** de ce que nous avons brièvement
discuté précédemment. Il n'y a aucune garantie qu'un thread particulier puisse acquérir un spin-lock à un moment ou un autre. Il n'est pas du tout
garanti non plus que les threads ne subissent pas la famine. Et comme
tout ce qui n'est pas garanti il faut penser que cela va se passer, et
en général cela sera au pire moment possible!
Le troisième critère est la performance.
---
Question #
Quelle est la performance d'un spin-lock? Que se passe-t-il
pour plusieurs fils d'exécution sur un CPU unique? Et qu'en est-il pour plusieurs CPUs?
---
Le problème de la performance des spin-locks se manifeste surtout lorsqu'on se trouve sur un système avec un seul CPU (ou lorsqu'il y a plus de threads que de CPUs). Dans ce cas,
si le thread se trouve préempté alors qu'il est dans la section
critique, les autres threads devront attendre que le
thread ayant acquis le verrou soit à nouveau exécuté
pour enfin pouvoir accéder au verrou.
Dans le cas de systèmes multi-CPUs où le nombre de thread
est plus petit ou égal au nombre de CPUs, le problème se pose moins. Les autres processeurs se contenteront de tourner dans
leur boucle `while`{.language-c} tant que le verrou
ne sera pas libéré. Mais cela n'a pas un coût notable,
on utilise juste des cycles CPUs à ne rien faire.
### L'atomicité dans la vraie vie: `compare-and-exchange`
La primitive hardware utilisée dans les systèmes `x86`
est le `compare-and-exchange`. Le pseudo-code C de cette
instruction ressemblerait à
```language-c
bool compare_and_exchange(bool *ptr, bool expected, bool new) {
bool actual = *ptr;
if (actual == expected) {
*ptr = new;
}
return actual;
}
```
Dans cette instruction, on teste d'abord si la valeur
contenue à l'adresse `ptr`{.language-c} est égale
à une valeur attendue, `expected`. Si cela est le cas,
on met à jour cette valeur à une nouvelle valeur `new`.
---
Exercice #
Comment construirait-on un verrou avec attente active avec cette instruction?
---
<!-- ```language-c
void lock(lock_t *lock) {
while (compare_and_exchange(&lock->flag, false, true) == true){
// do nothing
}
}
``` -->
Mais pourquoi introduire une seconde instruction alors que
le `test_and_set()`{.language-c} faisait l'affaire?
En fait, le `compare_and_exchange()`{.language-c} est une instruction est un peu plus puissante que le `test_and_set()`
mais pas pour construire un verrou avec attente active.
Si par miracle nous avons le temps, nous en dirons plus
si nous parlons de synchronisation sans verrou.
### Un verrou équitable
Pour ce verrou, nous avon besoin d'un autre type d'instruction atomique. Une d'un type permettant d'incrémenter une valeur, tout en retournant l'ancienne.
Ce type d'instruction s'appelle `fetch-and-add`.
En pseudo-code C, cela ressemblerait à
```language-c
int fetch_and_add(int *ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}
```
Avec cette instruction, nous pouvons construire le verrou par ticket (ticket lock). Le verrou à ticket est un peu plus complexe
qu'un simple `flag`. Nous allons utiliser un numéro de tour, ainsi qu'un ticket pour créer le verrou. Le code pour
ce type de verrou serait du genre
```language-c
typedef struct {
int ticket;
int turn;
} lock_t;
void lock_init(lock_t *lock) {
lock->ticket = 0;
lock->turn = 0;
}
void lock(lock_t *lock) {
int myturn = fetch_and_add(&lock->ticket);
while (lock->turn != myturn) {
// do nothing
}
}
void unlock(lock_t *lock) {
lock->turn += 1;
}
```
Avant de tenter d'acquérir le verrou, celui-ci
incrémente la valeur du ticket et retourne son ancienne valeur. Si le ticket avait inscrit comme valeur, la
valeur du tour du thread en question, il peut sortir de la boucle `while`. Sinon, il doit attendre. Lorsque le thread
qui a acquis le verrou sort de sa section critique et qu'il déverrouille le verrou, il incrémente la valeur du tour
se trouvant dans le verrou qui est partagé par tous les
threads. Le threads dont c'est le tour en prochain (s'il y en a un) peut acquérir le verrou.
### L'inversion de priorité
Une autre raison de ne pas utiliser les verrous à attente active
est que sur certains systèmes ils ne fonctionnent pas... Ce problème
est connu sous le nom de **l'inversion de priorité**.
Imaginons un système avec deux fils d'exécution, $T_1$ et $T_2$.
De plus $T_2$ a une priorité d'ordonnancement plus élevée
que $T_1$, et donc l'ordonnanceur préemptera toujours $T_1$
pour exécuter $T_2$ lorsque les deux threads sont exécutables.
$T_1$ ne pourra s'exécuter que lorsque $T_2$ est bloqué (en train d'effectuer
une opération I/O).
Imaginons à présent que $T_2$ est bloqué (en train d'effectuer une opération
I/O par exemple). $T_1$ s'exécute et acquière le verrou et
pénètre dans sa section critique. Si à ce moment là, $T_2$
repasse en attente d'exécution, $T_1$ sera préempté, car
$T_2$ a une priorité plus élevée. $T_2$ essaiera d'acquérir le verrou,
et sera donc en attente dans la boucle `while`{.language-c}. Mais comme $T_1$
ne sera jamais ordonnancé (à cause de sa plus faible priorité)
le système sera bloqué et rien ne va se passer.
En fait, le problème d'inversion de priorité peut arriver même avec
des verrous sans attente active. En effet, imaginons à présent trois threads
$T_1$, $T_2$, et $T_3$ avec $T_3$ ayant une priorité plus haute que $T_1$ et $T_2$,
qui lui a une priorité plus élevée que $T_1$. Si $T_1$ acquière le verrou
et que $T_3$ entre dans l'état d'attente d'exécution, il sera immédiatement ordonnancé
et $T_1$ préempté. Mais comme $T_1$ est propriétaire du verrou, $T_3$ est bloqué.
Si à présent $T_2$ commence à s'exécuter, comme il a une priorité plus élevée
il passera toujours avant $T_1$, et donc il y a une chance pour que $T_1$
ne soit jamais ordonnancé, bloquant $T_3$ étant donné qu'il attend que $T_1$
libère le verrou.
Il existe différentes façons de se prémunir contre les inversions de priorité.
La plus simple étant d'avoir tous les threads avec les mêmes priorités.
Une autre façon serait d'avoir un mécanisme où les threads avec des priorités plus
élevées attendant sur des threads avec une priorité plus basse puisse augmenter
la priorité des threads avec une priorité plus basse.
## Aller plus loin que le verrou à attente active
Comme nous l'avons discuté plus haut, les verrous à attente
active marchent et sont très simples. En revanche, ils sont inéquitables et inefficaces. En particulier, si un changement de contexte
intervient lorsqu'un thread est dans une section critique, nous
avons un problème.
Pour pouvoir aller plus loin, il nous faut
le soutient du système d'exploitation.
### L'approche simple: céder
Afin d'aller plus loin, comme on vient de le dire, on va avoir besoin de
l'aide du système d'exploitation. Pour ce faire la stratégie
sera très simple dans un premier temps.
Plutôt que de tourner dans le vide,
on va céder (yield) le contrôle du CPU à un autre thread.
A titre d'illustration, En pseudo-c un verrou ressemblerait à quelque chose de ce genre
```language-c
void init(lock_t *mutex) {
mutex->locked = false;
}
void lock(lock_t *mutex) {
while (compare_and_exchange(&mutex->locked, false, true) == true) {
yield(); // on passe le CPU
}
}
void unlock(lock_t *mutex) {
mutex->locked = false;
}
```
`yield()`{.language-c} est une primitive du système d'exploitation
ayant pour effet de faire passer le fil d'exécution qui l'appelle
de l'état **en cours d'exécution** à l'état **prêt**. Ainsi,
l'ordonnanceur du système d'exploitation peut promouvoir
un autre thread à l'état **en cours d'exécution**.
---
Exemple (favorable) #
Considérons un processus tournant sur un CPU avec deux threads, $T_1$ et $T_2$. Supposons que $T_1$ a acquis le verrou et se trouve dans sa section critique. Si un changement de contexte intervient à ce moment-là,
et que $T_2$ essaie d'acquérir le verrou, il le trouvera verrouillé
et entrera dans la boucle `while`{.language-c} et sera mis dans l'état
**en attente**, donnant une chance au système d'exploitation
d'ordonnancer $T_1$ à nouveau. Celui-ci pourra terminer sa section critique.
---
En fait, cette méthode de céder le CPU lorsque le verrou est déjà acquis par un autre thread, fait reposer toute la responsabilité
sur le système d'exploitation. On vient de voir que c'est une approche raisonnable pour un cas avec peu de threads. Lorsque leur nombre devient trop nombreux cela est beaucoup moins le cas.
---
Exemple (moins favorable) #
Imaginons à présent cent threads donnant un combat à mort pour l'acquisition du verrou, $T_{1-100}$. Supposons que $T_{48}$
a acquis le verrou et est entré dans sa section critique. C'est ce moment
précis que choisit le système d'exploitation (détestant $T_{48}$) pour le préempter. Un autre thread sera ordonnancé et trouvera le verrou
acquis, il appellera donc `yield()`{.language-c} et sera mis **en attente**. Si nous supposons que l'ordonnanceur a une stratégie qui consiste à passer la main à chaque thread à tour de rôle,
nous aurons 99 changements de contexte avant que $T_{48}$ puisse
reprendre la main et en finir avec sa section critique.
Cela reste beaucoup mieux que le spin-lock qui aurait dû
être préempté 99 fois pour revenir à $T_{48}$ mais ce n'est pas parfait,
car cela nécessite quand même un grand nombre de changements de contexte.
---
Bien que bien plus efficace que les spin-locks, ce type de verrou n'est pas parfait.
Comme nous venons de le voir avec notre exemple à 100 threads,
il est nécessaire d'effectuer des changements de contexte et passer la main pour acquérir le verrou. Cela peut être une opération coûteuse
et n'est pas très satisfaisante. Par ailleurs, il n'est pas exclu qu'un
fil d'exécution se retrouve coincé dans une boucle où il cède
le CPU indéfiniment et va se retrouver dans un état de **famine**: il n'entrera jamais dans sa section critique. Ce problème
est encore plus sérieux que celui de la performance, donc il faut trouver une meilleure solution.
### L'utilisation des files
Afin de remédier aux problèmes que nous avons énoncé plus tôt (le gaspillage de ressources, mais plus important encore la famine),
il est nécessaire d'utiliser une autre approche. En fait,
le problème est dû à la trop grande confiance que nous devons
avoir en l'ordonnanceur du système d'exploitation.
Une solution est d'exercer un plus grand contrôle sur quel
thread doit être le prochain à pouvoir acquérir un verrou
quand il a été libéré par son précédent propriétaire.
Pour ce faire, nous allons utiliser un système de file et le support
du système d'exploitation. Pour simplifier, nous allons utiliser
le support du système de `Solaris` pour réveiller et mettre en sommeil
des threads. L'API est la suivante:
* `park()`{.language-c} qui met le thread qui appelle cette fonction en attente;
* `unpark(tid)`{.language-c} qui réveille the thread dont l'identifiant est `tid`{.language-c}.
Avec ces deux appels, il est possible de construire un verrou qui:
1. Mettra en attente un thread qui tentera d'acquérir un verrou déjà verrouillé.
2. Réveillera ce thread lorsque le verrou devient libre.
Le code suivant permet de créer un verrou selon ce modèle.
```language-c
typedef {
bool locked;
bool guard;
queue_t *q; // une file
} lock_t;
void init(lock_t *mutex) {
mutex->locked = false;
mutex->guard = false;
queue_init(mutex->q);
}
void lock(lock_t *mutex) {
while (test_and_set(&lock->gard, true) == true) {
// la valeur de guard est acquise en
// tournant dans la boucle while
}
if (mutex->locked == false) {
mutex->locked = true; // le verrou est maintenant acquis
mutex->guard = false; // la garde est maintenant fausse
} else {
queue_add(mutex->q, get_id()); // on met l'id du thread dans la queue
m->guard = false;
park(); // on met le thread en sommeil
}
}
void unlock(lock_t *mutex) {
while (test_and_set(&lock->gard, true) == true) {
// la valeur de guard est acquise en
// tournant dans la boucle while
}
if (queue_empty(mutex->q)) {
mutex->locked = false; // le verrou est libéré si aucun thread
// est en attente
} else {
unpark(queue_remove(mutex->q)); // si un thread est en attente
// lui garder le thread
}
mutex->guard = false;
}
```
---
Remarque #
On peut constater qu'il y a un verrou à attente active caché dans ce verrou.
En effet, lors de la manipulation de la variable `locked`{.language-c}
et de la file, on utilise l'instruction `compare_and_exchange()`{.language-c} combiné à un `while`{.language-c}. Néanmoins, cela est moins problématique que lorsque le verrou protège des sections critiques
étant donné que le nombre
d'instructions est limité (on met à jour une variable
et ajoute un élément dans une file) en comparaison de la protection
d'une section critique qui peut contenir beaucoup de calculs et
ainsi avoir plus de chances d'être préemptée par le système d'exploitation.
---
Ce verrou fonctionne à l'aide de deux variables booléennes et d'une file d'attente. En premier lieu, nous avons la variable `lcoked`{.language-c}
qui détermine si le verrou est acquis ou non. En second lieu,
vient la variable `guard`{.language-c} qui elle protège `locked`{.language-c}
et la file d'attente contre les accès concurrents.
Finalement, la file d'attente enregistre les identifiants
des threads tentant de prendre possession du verrou
mais ayant été recalés.
Lorsque qu'on appelle la fonction `lock()`{.language-c} et que le verrou
est déjà détenu par un autre thread, on ajoute l'identifiant (`tid`{.language-c})
du thread se battant pour l'acquisition du verrou dans une file d'attente et en mettant `gard`{.language-c} à `false`{.language-c}
avant d'utiliser la fonction `park()`{.language-c}.
---
Question #
Que se passe-t-il si on met `gard`{.language-c} à false après `park()`{.language-c}
plutôt?
---
En fait, on voit bien que si `gard`{.language-c} est mise à jour après `park()`{.language-c} on ne déverrouille jamais le verrou utilisé
pour protéger la file et `locked`. De plus, on ne met jamais `locked` à
`true` après le réveil du thread (après `unpark(tid)`{.language-c}).
Ceci est dû au fait que lors du réveil, le thread continue son
exécution après l'appel à `park()`{.language-c}. Comme à ce moment-là
il n'as pas acquis `guard`{.language-c}, il serait
très dangereux de lui faire modifier `locked`{.language-c}.
Enfin, ce code contient un accès concurrent potentiel.
Si par malheur le système d'exploitation, préemptait
le thread entre le moment où on libère `gard`{.language-c} et l'appel
à `park()`{.language-c}. A ce moment-là, le thread pense que le verrou est
acquis par un autre thread. Si à ce moment-là un autre thread prend la main et parvient à libéré le verrou, lorsque notre premier thread reprendrait la main, il pourrait se retrouver en attente infinie
de libération d'un verrou déjà libéré...
Pour ce prémunir contre ce problème, un autre appel a été créé dans Solaris: `setpark()`{.language-c}. En appelant cette fonction, un thread indique au système qu'on est sur le point d'appeler `park()`{.language-c}.
Ainsi, si `unpark()`{.language-c} est appelé avant que `park()`{.language-c} soit effectivement appelé, `park()`{.language-c} retourne immédiatement plutôt que de se mettre en sommeil. De cette façon
on évite le sommeil à durée indéterminée.
### Et sous Linux?
Comme on l'a vu avec les verrous appels systèmes, il n'existe pas de
façon unique de définir les appels vers le noyaux
pour fabriquer un verrou. Dans les systèmes linux, on a à disposition
le **futex** qui est similaire aux fonctionnalités de Solaris, mais
où chaque futex a un espace mémoire spécifique, ainsi qu'une
file d'attente qui est directement implémentée dans le noyau.
Ci-dessous, vous trouverez l'imprémentation des fonctions
`lock()`{.language-c} et `unlock()`{.language-c} pour
le `mutex` (qui n'est rien d'autre qu'un entier) dans la librairie `libc` (cela se trouve dans `lowlevellock.h`).
```language-c
void mutex_lock (int *mutex) {
int v;
/* Bit 31 was clear, we got the mutex (the fastpath) */
if (atomic_bit_test_set (mutex, 31) == 0)
return;
atomic_increment (mutex);
while (1) {
if (atomic_bit_test_set (mutex, 31) == 0) {
atomic_decrement (mutex);
return;
}
/* We have to waitFirst make sure the futex value
we are monitoring is truly negative (locked). */
v = *mutex;
if (v >= 0)
continue;
futex_wait (mutex, v);
}
}
void mutex_unlock (int *mutex) {
/* Adding 0x80000000 to counter results in 0 if and
only if there are not other interested threads */
if (atomic_add_zero (mutex, 0x80000000))
return;
/* There are other threads waiting for this mutex,
wake one of them up. */
futex_wake (mutex);
}
```
Regardons ce qui se passe ici. En premier lieu,
nous voyons que notre verrou n'est rien d'autre qu'un entier.
Dans le bit de poids fort, on stocke l'information sur le verrou
(est-il acquis ou pas) et dans le reste des bits, on stocke
le nombre de threads qui attendent. Donc si `mutex`
est négatif le verrou est déjà détenu par un thread (si ce bit est mis
à 1, l'entier est négatif).
Pour l'acquisition :
* Si le verrou n'est pas acquis, on incrémente atomiquement
l'entier, puis ont attend activement pour voir si le verrou est toujours (vraiment) acquis. On vérifie par conséquent si la valeur de l'entier est bien négative. Puis, on met le thread en attente via `futex_wait(mutex, v)`{.language-c}. Cet appel système met le thread le thread appelant en sommeil, si la valeur de `mutex` est la même que celle de `v`. Sinon
il retourne immédiatement (pour se prémunir de l'attente indéterminée
qu'on a vue plus haut).
Pour la libération:
* Si le verrou est à zéro (et que la file est vide),
on ajoute `0x80000000`. On a donc que la valeur du verrou est:
`10000000000000000000000000000000`. Sinon, on reveille
le thread suivant avec `futex_wait(mutex)`, où `mutex` est l'adresse du
thread à réveiller.
Il faut noter que cette méthode est très efficace quand il n'y a pas de
contention pour acquérir le verrou. En effet, lorsqu'un seul thread combat
avec lui-même pour acquérir le verrou, il ne fera que deux opération très simples: un `test_and_set` atomique sur un seul bit pour l'acquisition, puis une addition atomique pour la libération.
<!-- DIRE UN MOT SUR LES FUTEX: COMPARE AND EXCHANGE IS ATOMIC -->
[^1]: Sur un système mono-processeur, une interruption est une instruction
matérielle permettant de signaler au processeur qu'il doit sauver l'état dans lequel il se trouve
et s'interrompre pour gérer un événement de plus haute priorité.
[^2]: Citation attribuée à H. L. Mencken.
[^3]: R. H. Arpaci-Dusseau et A. C. Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, Arpaci-Dusseau Books, ed. 0.91, (2015).
\ No newline at end of file
/*!
* Bootstrap v3.0.2 by @fat and @mdo
* Copyright 2013 Twitter, Inc.
* Licensed under http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world by @mdo and @fat.
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#e0e0e0));
background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
background-image: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-primary {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#2d6ca2));
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
background-repeat: repeat-x;
border-color: #2b669a;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2d6ca2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2d6ca2;
border-color: #2b669a;
}
.btn-success {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#419641));
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -moz-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
background-repeat: repeat-x;
border-color: #3e8f3e;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-warning {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#eb9316));
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -moz-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
background-repeat: repeat-x;
border-color: #e38d13;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-danger {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c12e2a));
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -moz-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
background-repeat: repeat-x;
border-color: #b92c28;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.btn-info {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#2aabd2));
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -moz-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
background-repeat: repeat-x;
border-color: #28a4c9;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8));
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #357ebd;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd));
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
}
.navbar-default {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#f8f8f8));
background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
background-image: -moz-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
background-repeat: repeat-x;
border-radius: 4px;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
}
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f3f3f3));
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: -moz-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
}
.navbar-inverse {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#3c3c3c), to(#222222));
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);
background-image: -moz-linear-gradient(top, #3c3c3c 0%, #222222 100%);
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
}
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#222222), to(#282828));
background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%);
background-image: -moz-linear-gradient(top, #222222 0%, #282828 100%);
background-image: linear-gradient(to bottom, #222222 0%, #282828 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.alert-success {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#c8e5bc));
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: -moz-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
background-repeat: repeat-x;
border-color: #b2dba1;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
}
.alert-info {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#b9def0));
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: -moz-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
background-repeat: repeat-x;
border-color: #9acfea;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
}
.alert-warning {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#f8efc0));
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: -moz-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
background-repeat: repeat-x;
border-color: #f5e79e;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
}
.alert-danger {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#e7c3c3));
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: -moz-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
background-repeat: repeat-x;
border-color: #dca7a7;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
}
.progress {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f5f5f5));
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -moz-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
}
.progress-bar {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9));
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
}
.progress-bar-success {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44));
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
}
.progress-bar-info {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5));
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
}
.progress-bar-warning {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f));
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
}
.progress-bar-danger {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c));
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #3071a9;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3278b3));
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #3278b3 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
background-repeat: repeat-x;
border-color: #3278b3;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.panel-default > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8));
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
}
.panel-primary > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd));
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
}
.panel-success > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#d0e9c6));
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: -moz-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
}
.panel-info > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#c4e3f3));
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: -moz-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
}
.panel-warning > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#faf2cc));
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: -moz-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
}
.panel-danger > .panel-heading {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#ebcccc));
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: -moz-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
}
.well {
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#e8e8e8), to(#f5f5f5));
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -moz-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
background-repeat: repeat-x;
border-color: #dcdcdc;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
}
\ No newline at end of file
/*!
* Bootstrap v3.0.2 by @fat and @mdo
* Copyright 2013 Twitter, Inc.
* Licensed under http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world by @mdo and @fat.
*/
.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e0e0e0));background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-moz-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#2d6ca2));background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-moz-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#419641));background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#eb9316));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c12e2a));background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#2aabd2));background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f3f3f3));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#222),to(#282828));background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-moz-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
\ No newline at end of file