Initial commit
This commit is contained in:
commit
189b073c1e
3
.obsidian/app.json
vendored
Normal file
3
.obsidian/app.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"vimMode": true
|
||||||
|
}
|
||||||
1
.obsidian/appearance.json
vendored
Normal file
1
.obsidian/appearance.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
3
.obsidian/community-plugins.json
vendored
Normal file
3
.obsidian/community-plugins.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"obsidian-git"
|
||||||
|
]
|
||||||
33
.obsidian/core-plugins.json
vendored
Normal file
33
.obsidian/core-plugins.json
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"properties": false,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": true,
|
||||||
|
"webviewer": false,
|
||||||
|
"footnotes": false,
|
||||||
|
"bases": true
|
||||||
|
}
|
||||||
426
.obsidian/plugins/obsidian-git/main.js
vendored
Normal file
426
.obsidian/plugins/obsidian-git/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
.obsidian/plugins/obsidian-git/manifest.json
vendored
Normal file
10
.obsidian/plugins/obsidian-git/manifest.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"author": "Vinzent",
|
||||||
|
"authorUrl": "https://github.com/Vinzent03",
|
||||||
|
"id": "obsidian-git",
|
||||||
|
"name": "Git",
|
||||||
|
"description": "Integrate Git version control with automatic backup and other advanced features.",
|
||||||
|
"isDesktopOnly": false,
|
||||||
|
"fundingUrl": "https://ko-fi.com/vinzent",
|
||||||
|
"version": "2.35.2"
|
||||||
|
}
|
||||||
23
.obsidian/plugins/obsidian-git/obsidian_askpass.sh
vendored
Executable file
23
.obsidian/plugins/obsidian-git/obsidian_askpass.sh
vendored
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
PROMPT="$1"
|
||||||
|
TEMP_FILE="$OBSIDIAN_GIT_CREDENTIALS_INPUT"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$TEMP_FILE" "$TEMP_FILE.response"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "$PROMPT" > "$TEMP_FILE"
|
||||||
|
|
||||||
|
while [ ! -e "$TEMP_FILE.response" ]; do
|
||||||
|
if [ ! -e "$TEMP_FILE" ]; then
|
||||||
|
echo "Trigger file got removed: Abort" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
RESPONSE=$(cat "$TEMP_FILE.response")
|
||||||
|
|
||||||
|
echo "$RESPONSE"
|
||||||
629
.obsidian/plugins/obsidian-git/styles.css
vendored
Normal file
629
.obsidian/plugins/obsidian-git/styles.css
vendored
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="git-view"] .button-border {
|
||||||
|
border: 2px solid var(--interactive-accent);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="git-view"] .view-content {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="git-history-view"] .view-content {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading > svg {
|
||||||
|
animation: 2s linear infinite loading;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obsidian-git-center {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obsidian-git-textarea {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obsidian-git-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.obsidian-git-center-button {
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.mod-left {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.mod-right {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limits the scrollbar to the view body */
|
||||||
|
.git-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-tools {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.git-tools .type {
|
||||||
|
padding-left: var(--size-2-1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-tools .type[data-type="M"] {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
.git-tools .type[data-type="D"] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.git-tools .buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.git-tools .buttons > * {
|
||||||
|
padding: 0 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="git-view"] .tree-item-self,
|
||||||
|
.workspace-leaf-content[data-type="git-history-view"] .tree-item-self {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="git-view"]
|
||||||
|
.tree-item-self:hover
|
||||||
|
.clickable-icon,
|
||||||
|
.workspace-leaf-content[data-type="git-history-view"]
|
||||||
|
.tree-item-self:hover
|
||||||
|
.clickable-icon {
|
||||||
|
color: var(--icon-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight an item as active if it's diff is currently opened */
|
||||||
|
.is-active .git-tools .buttons > * {
|
||||||
|
color: var(--nav-item-color-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-author {
|
||||||
|
color: var(--text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-date {
|
||||||
|
color: var(--text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-ref {
|
||||||
|
color: var(--text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-d-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-wrapper {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-header {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border-bottom: 1px solid var(--interactive-accent);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
height: 35px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-header,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-stats {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-stats {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-lines-added {
|
||||||
|
border: 1px solid #b4e2b4;
|
||||||
|
border-radius: 5px 0 0 5px;
|
||||||
|
color: #399839;
|
||||||
|
padding: 2px;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-lines-deleted {
|
||||||
|
border: 1px solid #e9aeae;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
color: #c33;
|
||||||
|
margin-left: 1px;
|
||||||
|
padding: 2px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-name-wrapper {
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
font-size: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-name {
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-wrapper {
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-collapse {
|
||||||
|
-webkit-box-pack: end;
|
||||||
|
-ms-flex-pack: end;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
font-size: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-collapse.d2h-selected {
|
||||||
|
background-color: #c8e1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-collapse-input {
|
||||||
|
margin: 0 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-diff-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-files-diff {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-diff {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-side-diff {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
margin-right: -4px;
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line {
|
||||||
|
padding: 0 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-line {
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-line {
|
||||||
|
padding: 0 4.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line-ctn {
|
||||||
|
word-wrap: normal;
|
||||||
|
background: none;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
-ms-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: pre;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .workspace-leaf-content[data-type="diff-view"] .d2h-code-line del,
|
||||||
|
.theme-light
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-code-side-line
|
||||||
|
del {
|
||||||
|
background-color: #ffb6ba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .workspace-leaf-content[data-type="diff-view"] .d2h-code-line del,
|
||||||
|
.theme-dark
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-code-side-line
|
||||||
|
del {
|
||||||
|
background-color: #8d232881;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line del,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line ins,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-line del,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-line ins {
|
||||||
|
border-radius: 0.2em;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: -1px;
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .workspace-leaf-content[data-type="diff-view"] .d2h-code-line ins,
|
||||||
|
.theme-light
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-code-side-line
|
||||||
|
ins {
|
||||||
|
background-color: #97f295;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .workspace-leaf-content[data-type="diff-view"] .d2h-code-line ins,
|
||||||
|
.theme-dark
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-code-side-line
|
||||||
|
ins {
|
||||||
|
background-color: #1d921996;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line-prefix {
|
||||||
|
word-wrap: normal;
|
||||||
|
background: none;
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .line-num1 {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .line-num1,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .line-num2 {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 3.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .line-num2 {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-linenumber {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border: solid var(--background-modifier-border);
|
||||||
|
border-width: 0 1px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
text-align: right;
|
||||||
|
width: 7.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-linenumber:after {
|
||||||
|
content: "\200b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-linenumber {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border: solid var(--background-modifier-border);
|
||||||
|
border-width: 0 1px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
position: absolute;
|
||||||
|
text-align: right;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-diff-tbody tr {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-linenumber:after {
|
||||||
|
content: "\200b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-emptyplaceholder,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-emptyplaceholder {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border-color: var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-line-prefix,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-linenumber,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-linenumber,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-emptyplaceholder {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-linenumber,
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-code-side-linenumber {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .workspace-leaf-content[data-type="diff-view"] .d2h-del {
|
||||||
|
background-color: #fee8e9;
|
||||||
|
border-color: #e9aeae;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .workspace-leaf-content[data-type="diff-view"] .d2h-ins {
|
||||||
|
background-color: #dfd;
|
||||||
|
border-color: #b4e2b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .workspace-leaf-content[data-type="diff-view"] .d2h-del {
|
||||||
|
background-color: #521b1d83;
|
||||||
|
border-color: #691d1d73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .workspace-leaf-content[data-type="diff-view"] .d2h-ins {
|
||||||
|
background-color: rgba(30, 71, 30, 0.5);
|
||||||
|
border-color: #13501381;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-info {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border-color: var(--background-modifier-border);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-file-diff
|
||||||
|
.d2h-del.d2h-change {
|
||||||
|
background-color: #fdf2d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-file-diff
|
||||||
|
.d2h-del.d2h-change {
|
||||||
|
background-color: #55492480;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-file-diff
|
||||||
|
.d2h-ins.d2h-change {
|
||||||
|
background-color: #ded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-file-diff
|
||||||
|
.d2h-ins.d2h-change {
|
||||||
|
background-color: rgba(37, 78, 37, 0.418);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list-wrapper {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list-wrapper a {
|
||||||
|
color: #3572b0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"]
|
||||||
|
.d2h-file-list-wrapper
|
||||||
|
a:visited {
|
||||||
|
color: #3572b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list-header {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list-title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list-line {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list {
|
||||||
|
display: block;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list > li {
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-list > li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-file-switch {
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-icon {
|
||||||
|
fill: currentColor;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-deleted {
|
||||||
|
color: #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-added {
|
||||||
|
color: #399839;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-changed {
|
||||||
|
color: #d0b44c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-moved {
|
||||||
|
color: #3572b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-tag {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-deleted-tag {
|
||||||
|
border: 2px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-added-tag {
|
||||||
|
border: 1px solid #399839;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-changed-tag {
|
||||||
|
border: 1px solid #d0b44c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-leaf-content[data-type="diff-view"] .d2h-moved-tag {
|
||||||
|
border: 1px solid #3572b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== Line Authoring Information ====================== */
|
||||||
|
|
||||||
|
.cm-gutterElement.obs-git-blame-gutter {
|
||||||
|
/* Add background color to spacing inbetween and around the gutter for better aesthetics */
|
||||||
|
border-width: 0px 2px 0.2px 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--background-secondary);
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-gutterElement.obs-git-blame-gutter > div,
|
||||||
|
.line-author-settings-preview {
|
||||||
|
/* delegate text color to settings */
|
||||||
|
color: var(--obs-git-gutter-text);
|
||||||
|
font-family: monospace;
|
||||||
|
height: 100%; /* ensure, that age-based background color occupies entire parent */
|
||||||
|
text-align: right;
|
||||||
|
padding: 0px 6px 0px 6px;
|
||||||
|
white-space: pre; /* Keep spaces and do not collapse them. */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
/* hide git blame gutter not to superpose text */
|
||||||
|
.cm-gutterElement.obs-git-blame-gutter {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-unified-diff-view,
|
||||||
|
.git-split-diff-view .cm-deletedLine .cm-changedText {
|
||||||
|
background-color: #ee443330;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-unified-diff-view,
|
||||||
|
.git-split-diff-view .cm-insertedLine .cm-changedText {
|
||||||
|
background-color: #22bb2230;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-obscure-prompt[git-is-obscured="true"] #git-show-password:after {
|
||||||
|
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path><circle cx="12" cy="12" r="3"></circle></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-obscure-prompt[git-is-obscured="false"] #git-show-password:after {
|
||||||
|
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"></path><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"></path><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"></path><path d="m2 2 20 20"></path></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override styling of Codemirror merge view "collapsed lines" indicator */
|
||||||
|
.git-split-diff-view .ͼ2 .cm-collapsedLines {
|
||||||
|
background: var(--interactive-normal);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
color: var(--text-accent);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
padding: var(--size-4-1) var(--size-4-1);
|
||||||
|
}
|
||||||
|
.git-split-diff-view .ͼ2 .cm-collapsedLines:hover {
|
||||||
|
background: var(--interactive-hover);
|
||||||
|
color: var(--text-accent-hover);
|
||||||
|
}
|
||||||
204
.obsidian/workspace.json
vendored
Normal file
204
.obsidian/workspace.json
vendored
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "ec994ed12db82d87",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "e0578e41bd228cdb",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "4391c77430fca8f8",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "4.2/1.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "de165f0f9ededdcb",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "4.2/2.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "ffe8c4b94831212e",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "17ee7f46383d07b4",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "c7d6d5d21d68df06",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"autoReveal": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a7dd44f06319ff4d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "133762096bfd3b00",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "1aca95e9c21abecb",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "7ec0b972b1b93586",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "693d66113586cc4e",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "3_1/1.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks for 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6de13a0184cb879d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "3_1/1.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links from 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "757cb1589f578da0",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "83c6df4ad8ca5437",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "3_1/1.md",
|
||||||
|
"followCursor": false,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline of 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"bases:Create new base": false,
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false,
|
||||||
|
"obsidian-git:Open Git source control": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "4391c77430fca8f8",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"4.2/2.md",
|
||||||
|
"4.2/1.md",
|
||||||
|
"3_1/2.md",
|
||||||
|
"3_1/1-0.2.0.md",
|
||||||
|
"3_1/3practice.md",
|
||||||
|
"4.2",
|
||||||
|
"3_1/1v3correction.md",
|
||||||
|
"3_1/3v0.md",
|
||||||
|
"3_1/1v2.md",
|
||||||
|
"Untitled.base",
|
||||||
|
"Untitled 1.canvas",
|
||||||
|
"Untitled.canvas",
|
||||||
|
"3_1/1-0.1.0.md",
|
||||||
|
"Untitled 1.base"
|
||||||
|
]
|
||||||
|
}
|
||||||
163
3_1/1-0.1.0.md
Normal file
163
3_1/1-0.1.0.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
Кооперативная
|
||||||
|
Вытесняющая - пример: процессор
|
||||||
|
преимуще
|
||||||
|
|
||||||
|
для синтаксиса - объяснить, что вызов функции не исполняет ее сразу, как в js, а возвращает (пока что) какое-то значение, которое исполнит функцию по вызову .await на ней
|
||||||
|
|
||||||
|
макрос - привести пример #[tokio::main] и #[tokio::main(...)]
|
||||||
|
|
||||||
|
## Start:
|
||||||
|
|
||||||
|
Допустим, перед вами стоит несколько задач, которые надо выполнить параллельно, но фокусироваться в один момент вы можете только одной задаче. По какой стратегии вы будете переключаться между этими задачами?
|
||||||
|
|
||||||
|
## Кооперативная и вытесняющая многозадачность
|
||||||
|
Одним из вариантов, который может прийти в голову, это сделать небольшой прогресс в одной задаче, потом переключиться на вторую, сделать в ней небольшой прогресс, и так далее. Такая многозадачность, когда исполнитель решает момент, когда нужно переключаться между задачами, называется **вытесняющей** (так как одна задача как бы вытесняет другую). По такому принципу распределяют задачи на процессор все современные десктопные ОС.
|
||||||
|
А теперь представьте, что момент переключения на другую задачу решаете не вы, а сама задача. Такая многозадачность называется **кооперативной** (так как задачам приходится кооперировать друг с другом за процессорное время).
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
В чем отличие кооперативной многозадачности от вытесняющей?
|
||||||
|
1. Исполнитель решает, когда переключаться между задачами
|
||||||
|
Нет, в кооперативной многозадачности момент переключения решают задачи, а не исполнитель
|
||||||
|
2. Задачи переключаются по истечению времени
|
||||||
|
Нет, задачи могут выполняться сколько угодно
|
||||||
|
3. Задачи решают, когда можно переключиться с них
|
||||||
|
Правильно, только задачи решают, когда переключаться и могут выполняться сколько угодно
|
||||||
|
4. Все задачи выполняются только в один поток
|
||||||
|
Нет, такого требования к кооперативности нет
|
||||||
|
|
||||||
|
Квиз-Множественный выбор
|
||||||
|
У каких из этих задач кооперативная многозадачность?
|
||||||
|
1. Потоки процессов на Linux
|
||||||
|
2. У модема по adsl для звонков и интернета (пользоваться интернетом, пока идет звонок, нельзя)
|
||||||
|
3. Пер
|
||||||
|
4. Очередь на печать на принтере
|
||||||
|
5. Обработка
|
||||||
|
## Многозадачность в асинхронном rust
|
||||||
|
В rust для асинхронного кода выбрана кооперативная многозадачность. Такой выбор обоснован тем, что переключение между задачами в кооперативной многозадачности существенно быстрее, и дешевле по производительности обходится безопасность по памяти. Если бы многозадачность была вытесняющей, то при переключении задач пришлось бы выполнять дополнительные действия для сохранения состояния задачи, в то время как в кооперативной задачи сами отвечают за свое состояние. При этом у кооперативной многозадачности есть и недостатки: если задача зависает, то другие задачи не могут выполняться. Поэтому во всех современных десктопных ОС выбрана именно вытесняющая многозадачность, ведь иначе какой-нибудь процесс случайно или злонамеренно мог бы повесить всю систему. И поэтому при написании асинхронных функций нужно следить за тем, как долго они выполняются, и если возникает хоть немного занимающая время задача, ее нужно выносить в отдельный поток (в рантаймах есть встроенные инструменты для этого)
|
||||||
|
|
||||||
|
Квиз-Множественный выбор
|
||||||
|
Выберите преимущества кооперативной многозадачности перед вытесняющей.
|
||||||
|
1. Задачи выполняются в порядке создания
|
||||||
|
Нет, задачи могут выполняться в любом порядке
|
||||||
|
2. Быстрее переключение между задачами
|
||||||
|
Да, реализация переключения при кооперативной многозадачности проще и быстрее, чем при вытесняющей
|
||||||
|
3. Идеальная предсказуемость времени выполнения задачи
|
||||||
|
Нет, кооперативная многозадачность не гарантирует идеальную предсказуемость выполнения. Более того, если одна из других задач будет долго исполняться, выполнение задачи тоже затянется
|
||||||
|
4. Из-за одной неправильной задачи случайно не остановятся другие
|
||||||
|
Нет, это преимущество вытесняющей многозадачности
|
||||||
|
5. Быстрее безопасное обращение к памяти
|
||||||
|
Да, благодаря предсказуемости момента переключения, задача успеет закончить свое обращение к памят
|
||||||
|
|
||||||
|
## Синтаксис async/await
|
||||||
|
Для кооперативной многозадачности в самих задачах нужна логика для передачи управления. Для этого в rust существует синтаксис async/await, он выглядит так:
|
||||||
|
```rust
|
||||||
|
// создание асинхронной функции
|
||||||
|
async fn task1() {
|
||||||
|
do_something().await; // вызов асинхроной функции и ожидание результата от нее
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Сам по себе вызов такой функции (к примеру, просто `do_something()`) ничего не сделает. Для вызова асинхронной функции нужно использовать .await, если вызывать из асинхронной функции, либо вызывать из рантайма.
|
||||||
|
Для асинхронных замыканий тоже добавляется ключевое слово async:
|
||||||
|
```rust
|
||||||
|
let closure = async || {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Или, если нет аргументов, можно опустить `||`:
|
||||||
|
```rust
|
||||||
|
let closure = async {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
Что выведет данный код:
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
async {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
1. Не скомпилируется
|
||||||
|
2. чтото
|
||||||
|
3. Ничего
|
||||||
|
4. другое что-то
|
||||||
|
|
||||||
|
В rust нет встроенного рантайма, они все реализованы в виде библиотек. Самыми популярными являются tokio и smol (раньше еще был async-std, но он теперь discontinued). В данном уроке остановимся на tokio, так как он самый популярный.
|
||||||
|
Чтобы запустить асинхронный код с помошью tokio, можно использовать макрос tokio::main
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
По стандартным настройкам этом макрос cоздает многопоточный рантайм, поэтому он эквивалентен такому коду:
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
do_something().await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Данный код создает многопоточный рантайм tokio, запускает и ожидает завершения функции, переданной в Builder::block_on
|
||||||
|
`.enable_all()` отвечает за возможность использования всего IO tokio и tokio::time
|
||||||
|
Многопоточность в макросе указать явно можно так:
|
||||||
|
```rust
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Так же в макросе можно указать количество потоков, на которых будут запускаться асинхронные функции (по умолчанию их столько же, сколько потоков в процессоре в системе):
|
||||||
|
```rust
|
||||||
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
То же самое, но без макроса:
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(8)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Чтобы рантайм был однопоточным, можно в макросе указать параметр flavor:
|
||||||
|
```rust
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
То же самое, но без макроса:
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
На скольких потоках будут запускаться асинхронные функции, если просто использовать макрос `#[tokio::main]`:
|
||||||
|
1. 1
|
||||||
|
2. ошибка, нужно указать явно
|
||||||
|
3. 8
|
||||||
|
4. сколько потоков в процессоре на данной системе
|
||||||
|
|
||||||
|
|
||||||
221
3_1/1-0.2.0.md
Normal file
221
3_1/1-0.2.0.md
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
ОР — Знает разницу между кооперативной и вытесняющей многозадачностью
|
||||||
|
Допустим, перед вами стоит несколько задач, которые надо выполнить параллельно, но фокусироваться в один момент вы можете только одной задаче. По какой стратегии вы будете переключаться между этими задачами?
|
||||||
|
|
||||||
|
## Кооперативная и вытесняющая многозадачность
|
||||||
|
|
||||||
|
Одним из вариантов, который может прийти в голову, это сделать небольшой прогресс в одной задаче, потом переключиться на вторую, сделать в ней небольшой прогресс, и так далее. Такая многозадачность, когда исполнитель решает момент, когда нужно переключаться между задачами, называется **вытесняющей** (так как одна задача как бы вытесняет другую). По такому принципу распределяют задачи на процессор все современные десктопные ОС. А теперь представьте, что момент переключения на другую задачу решаете не вы, а сама задача. Такая многозадачность называется **кооперативной** (так как задачам приходится кооперировать друг с другом за процессорное время).
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
В чем отличие кооперативной многозадачности от вытесняющей?
|
||||||
|
|
||||||
|
Исполнитель решает, когда переключаться между задачами
|
||||||
|
|
||||||
|
Нет, в кооперативной многозадачности момент переключения решают задачи, а не исполнитель
|
||||||
|
|
||||||
|
Задачи переключаются по истечению времени
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Задачи решают, когда можно переключиться с них
|
||||||
|
|
||||||
|
Правильно, только задачи решают, когда переключаться и могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Все задачи выполняются только в один поток
|
||||||
|
|
||||||
|
Нет, такого требования к кооперативности нет
|
||||||
|
|
||||||
|
## Многозадачность в асинхронном rust
|
||||||
|
|
||||||
|
ОР — Знает преимущества кооперативной многозадачности
|
||||||
|
В rust для асинхронного кода выбрана кооперативная многозадачность. Такой выбор обоснован тем, что переключение между задачами в кооперативной многозадачности существенно быстрее, и дешевле по производительности обходится безопасность по памяти. Если бы многозадачность была вытесняющей, то при переключении задач пришлось бы выполнять дополнительные действия для сохранения состояния задачи, в то время как в кооперативной задачи сами отвечают за свое состояние. При этом у кооперативной многозадачности есть и недостатки: если задача зависает, то другие задачи не могут выполняться. Поэтому во всех современных десктопных ОС выбрана именно вытесняющая многозадачность, ведь иначе какой-нибудь процесс случайно или злонамеренно мог бы повесить всю систему. И поэтому при написании асинхронных функций нужно следить за тем, как долго они выполняются, и если возникает хоть немного занимающая время задача, ее нужно выносить в отдельный поток (в рантаймах есть встроенные инструменты для этого)
|
||||||
|
|
||||||
|
Квиз-Множественный выбор
|
||||||
|
|
||||||
|
Выберите преимущества кооперативной многозадачности перед вытесняющей.
|
||||||
|
|
||||||
|
Задачи выполняются в порядке создания
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться в любом порядке
|
||||||
|
|
||||||
|
Быстрее переключение между задачами
|
||||||
|
|
||||||
|
Да, реализация переключения при кооперативной многозадачности проще и быстрее, чем при вытесняющей
|
||||||
|
|
||||||
|
Идеальная предсказуемость времени выполнения задачи
|
||||||
|
|
||||||
|
Нет, кооперативная многозадачность не гарантирует идеальную предсказуемость выполнения. Более того, если одна из других задач будет долго исполняться, выполнение задачи тоже затянется
|
||||||
|
|
||||||
|
Из-за одной неправильной задачи случайно не остановятся другие
|
||||||
|
|
||||||
|
Нет, это преимущество вытесняющей многозадачности
|
||||||
|
|
||||||
|
Быстрее безопасное обращение к памяти
|
||||||
|
|
||||||
|
Да, благодаря предсказуемости момента переключения, задача успеет закончить свое обращение к памяти
|
||||||
|
|
||||||
|
## Синтаксис async/await
|
||||||
|
|
||||||
|
ОР — Умеет использовать async/await синтаксис
|
||||||
|
|
||||||
|
Для кооперативной многозадачности в самих задачах нужна логика для передачи управления. Для этого в rust существует синтаксис async/await, он выглядит так:
|
||||||
|
|
||||||
|
```
|
||||||
|
// создание асинхронной функции
|
||||||
|
async fn task1() {
|
||||||
|
do_something().await; // вызов асинхроной функции и ожидание результата от нее
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Сам по себе вызов такой функции (к примеру, просто `do_something()`) ничего не сделает. Для вызова асинхронной функции нужно использовать .await, если вызывать из асинхронной функции, либо вызывать из рантайма. Для асинхронных замыканий тоже добавляется ключевое слово async:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async || {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Или, если нет аргументов, можно опустить `||`:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
Что выведет данный код:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
async {
|
||||||
|
println!("42");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Не скомпилируется из-за синтаксической ошибки
|
||||||
|
|
||||||
|
Нет, данный код полностью валидный и скомпилируется
|
||||||
|
|
||||||
|
Не скомпилируется, так как асинхронный код нельзя вызывать из синхронной функции
|
||||||
|
|
||||||
|
Нет, в данном случае просто создается асинхронное замыкание и ничего не вызывается через .await, поэтому код ничего не выведет
|
||||||
|
|
||||||
|
Ничего
|
||||||
|
|
||||||
|
Правильно, данный код просто создаст асинхронное замыкание, без его вызова
|
||||||
|
|
||||||
|
Выведет в консоль 42
|
||||||
|
|
||||||
|
Нет, данный лишь создаст асинхронное замыкание, но не вызовет его, поэтому на экран не будет ничего выведено
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime через макрос
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime вручную
|
||||||
|
|
||||||
|
В rust нет встроенного рантайма, они все реализованы в виде библиотек. Самыми популярными являются tokio и smol (раньше еще был async-std, но он теперь discontinued). В данном уроке остановимся на tokio, так как он самый популярный. Чтобы запустить асинхронный код с помошью tokio, можно использовать макрос tokio::main
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
По стандартным настройкам этом макрос cоздает многопоточный рантайм, поэтому он эквивалентен такому коду:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
do_something().await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Данный код создает многопоточный рантайм tokio, запускает и ожидает завершения функции, переданной в Builder::block_on `.enable_all()` отвечает за возможность использования всего IO tokio и tokio::time Многопоточность в макросе указать явно можно так:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Так же в макросе можно указать количество потоков, на которых будут запускаться асинхронные функции (по умолчанию их столько же, сколько потоков в процессоре в системе):
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(8)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы рантайм был однопоточным, можно в макросе указать параметр flavor:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
На скольких потоках будут запускаться асинхронные функции, если просто использовать макрос `#[tokio::main]`:
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Ошибка, нужно указать явно
|
||||||
|
|
||||||
|
Нет, указывать явно не нужно, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
8
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Столько, сколько потоков в процессоре на данной системе
|
||||||
|
|
||||||
|
Правильно, без явного указания потоков будет столько же, сколько их в процессоре в системе
|
||||||
223
3_1/1v2.md
Normal file
223
3_1/1v2.md
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
==Умеет порождать конкурентные задачи через spawn/join/select.==
|
||||||
|
|
||||||
|
ОР — Знает разницу между кооперативной и вытесняющей многозадачностью
|
||||||
|
Допустим, перед вами стоит несколько задач, которые надо выполнить параллельно, но фокусироваться в один момент вы можете только одной задаче. По какой стратегии вы будете переключаться между этими задачами?
|
||||||
|
|
||||||
|
## Кооперативная и вытесняющая многозадачность
|
||||||
|
|
||||||
|
Одним из вариантов, который может прийти в голову, это сделать небольшой прогресс в одной задаче, потом переключиться на вторую, сделать в ней небольшой прогресс, и так далее. Такая многозадачность, когда исполнитель решает момент, когда нужно переключаться между задачами, называется **вытесняющей** (так как одна задача как бы вытесняет другую). По такому принципу распределяют задачи на процессор все современные десктопные ОС. А теперь представьте, что момент переключения на другую задачу решаете не вы, а сама задача. Такая многозадачность называется **кооперативной** (так как задачам приходится кооперировать друг с другом за процессорное время).
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
В чем отличие кооперативной многозадачности от вытесняющей?
|
||||||
|
|
||||||
|
Исполнитель решает, когда переключаться между задачами
|
||||||
|
|
||||||
|
Нет, в кооперативной многозадачности момент переключения решают задачи, а не исполнитель
|
||||||
|
|
||||||
|
Задачи переключаются по истечению времени
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Задачи решают, когда можно переключиться с них
|
||||||
|
|
||||||
|
Правильно, только задачи решают, когда переключаться и могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Все задачи выполняются только в один поток
|
||||||
|
|
||||||
|
Нет, такого требования к кооперативности нет
|
||||||
|
|
||||||
|
## Многозадачность в асинхронном rust
|
||||||
|
|
||||||
|
ОР — Знает преимущества кооперативной многозадачности
|
||||||
|
В rust для асинхронного кода выбрана кооперативная многозадачность. Такой выбор обоснован тем, что переключение между задачами в кооперативной многозадачности существенно быстрее, и дешевле по производительности обходится безопасность по памяти. Если бы многозадачность была вытесняющей, то при переключении задач пришлось бы выполнять дополнительные действия для сохранения состояния задачи, в то время как в кооперативной задачи сами отвечают за свое состояние. При этом у кооперативной многозадачности есть и недостатки: если задача зависает, то другие задачи не могут выполняться. Поэтому во всех современных десктопных ОС выбрана именно вытесняющая многозадачность, ведь иначе какой-нибудь процесс случайно или злонамеренно мог бы повесить всю систему. И поэтому при написании асинхронных функций нужно следить за тем, как долго они выполняются, и если возникает хоть немного занимающая время задача, ее нужно выносить в отдельный поток (в рантаймах есть встроенные инструменты для этого)
|
||||||
|
|
||||||
|
Квиз-Множественный выбор
|
||||||
|
|
||||||
|
Выберите преимущества кооперативной многозадачности перед вытесняющей.
|
||||||
|
|
||||||
|
Задачи выполняются в порядке создания
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться в любом порядке
|
||||||
|
|
||||||
|
Быстрее переключение между задачами
|
||||||
|
|
||||||
|
Да, реализация переключения при кооперативной многозадачности проще и быстрее, чем при вытесняющей
|
||||||
|
|
||||||
|
Идеальная предсказуемость времени выполнения задачи
|
||||||
|
|
||||||
|
Нет, кооперативная многозадачность не гарантирует идеальную предсказуемость выполнения. Более того, если одна из других задач будет долго исполняться, выполнение задачи тоже затянется
|
||||||
|
|
||||||
|
Из-за одной неправильной задачи случайно не остановятся другие
|
||||||
|
|
||||||
|
Нет, это преимущество вытесняющей многозадачности
|
||||||
|
|
||||||
|
Быстрее безопасное обращение к памяти
|
||||||
|
|
||||||
|
Да, благодаря предсказуемости момента переключения, задача успеет закончить свое обращение к памяти
|
||||||
|
|
||||||
|
## Синтаксис async/await
|
||||||
|
|
||||||
|
ОР — Умеет использовать async/await синтаксис
|
||||||
|
|
||||||
|
Для кооперативной многозадачности в самих задачах нужна логика для передачи управления. Для этого в rust существует синтаксис async/await, он выглядит так:
|
||||||
|
|
||||||
|
```
|
||||||
|
// создание асинхронной функции
|
||||||
|
async fn task1() {
|
||||||
|
do_something().await; // вызов асинхроной функции и ожидание результата от нее
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Сам по себе вызов такой функции (к примеру, просто `do_something()`) ничего не сделает. Для вызова асинхронной функции нужно использовать .await, если вызывать из асинхронной функции, либо вызывать из рантайма. Для асинхронных замыканий тоже добавляется ключевое слово async:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async || {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Или, если нет аргументов, можно опустить `||`:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
Что выведет данный код:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
async {
|
||||||
|
println!("42");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Не скомпилируется из-за синтаксической ошибки
|
||||||
|
|
||||||
|
Нет, данный код полностью валидный и скомпилируется
|
||||||
|
|
||||||
|
Не скомпилируется, так как асинхронный код нельзя вызывать из синхронной функции
|
||||||
|
|
||||||
|
Нет, в данном случае просто создается асинхронное замыкание и ничего не вызывается через .await, поэтому код ничего не выведет
|
||||||
|
|
||||||
|
Ничего
|
||||||
|
|
||||||
|
Правильно, данный код просто создаст асинхронное замыкание, без его вызова
|
||||||
|
|
||||||
|
Выведет в консоль 42
|
||||||
|
|
||||||
|
Нет, данный лишь создаст асинхронное замыкание, но не вызовет его, поэтому на экран не будет ничего выведено
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime через макрос
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime вручную
|
||||||
|
|
||||||
|
В rust нет встроенного рантайма, они все реализованы в виде библиотек. Самыми популярными являются tokio и smol (раньше еще был async-std, но он теперь discontinued). В данном уроке остановимся на tokio, так как он самый популярный. Чтобы запустить асинхронный код с помошью tokio, можно использовать макрос tokio::main
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
По стандартным настройкам этом макрос cоздает многопоточный рантайм, поэтому он эквивалентен такому коду:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
do_something().await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Данный код создает многопоточный рантайм tokio, запускает и ожидает завершения функции, переданной в Builder::block_on `.enable_all()` отвечает за возможность использования всего IO tokio и tokio::time Многопоточность в макросе указать явно можно так:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Так же в макросе можно указать количество потоков, на которых будут запускаться асинхронные функции (по умолчанию их столько же, сколько потоков в процессоре в системе):
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(8)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы рантайм был однопоточным, можно в макросе указать параметр flavor:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
На скольких потоках будут запускаться асинхронные функции, если просто использовать макрос `#[tokio::main]`:
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Ошибка, нужно указать явно
|
||||||
|
|
||||||
|
Нет, указывать явно не нужно, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
8
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Столько, сколько потоков в процессоре на данной системе
|
||||||
|
|
||||||
|
Правильно, без явного указания потоков будет столько же, сколько их в процессоре в системе
|
||||||
420
3_1/1v3correction.md
Normal file
420
3_1/1v3correction.md
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
==Умеет порождать конкурентные задачи через spawn/join/select.
|
||||||
|
|
||||||
|
Асинхронность позволяет эффективно управлять многозадачностью, как и многопоточная программа. Но отличие от многопоточного синхронного кода, асинхронный код позволяет выполнять несколько операций одновременно в одном потоке, что позволяет приложениям, работающих с сетью или файловыми системами, использовать вычислительные ресурсы эффективнее.
|
||||||
|
В этой теме мы рассмотрим два типа многозадачности: кооперативную и вытесняющую. Также мы научимся использовать синтаксис async/await, который используется для написания асинхронного кода в Rust. Вы узнаете, как компилятор обрабатывает асинхронный код и что происходит под капотом.
|
||||||
|
Научимся запускать асинхронные задачи используя tokio, работать с неблокирующими примитивами для сети и файлов, а также порождать конкурентные задачи через spawn/join/select. Всё это поможет создавать быстрые и эффективные асинхронные приложения.
|
||||||
|
|
||||||
|
ОР — Знает разницу между кооперативной и вытесняющей многозадачностью
|
||||||
|
Допустим, перед вами стоит несколько задач, которые надо выполнить параллельно, но фокусироваться в один момент вы можете только одной задаче. По какой стратегии вы будете переключаться между этими задачами?
|
||||||
|
|
||||||
|
## Кооперативная и вытесняющая многозадачность
|
||||||
|
|
||||||
|
Одним из вариантов, который может прийти в голову, это сделать небольшой прогресс в одной задаче, потом переключиться на вторую, сделать в ней небольшой прогресс, и так далее. Такая многозадачность, когда исполнитель решает момент, когда нужно переключаться между задачами, называется **вытесняющей** (так как одна задача как бы вытесняет другую). По такому принципу распределяют задачи на процессор все современные десктопные ОС. А теперь представьте, что момент переключения на другую задачу решаете не вы, а сама задача. Такая многозадачность называется **кооперативной** (так как задачам приходится кооперировать друг с другом за процессорное время).
|
||||||
|
|
||||||
|
И у кооперативной многозадачности есть несколько преимуществ. Во первых, реализация такой многозадачности проще, что позволяет быстрее переключаться между задачами. Второе, это упрощение синхронизации по памяти: задачи сами управляют моментом переключения, так что они успевают завершить запись в память, а при вытесняющей переключение может произойти посреди записей и память будет невалидной для других задач.
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
В чем отличие кооперативной многозадачности от вытесняющей?
|
||||||
|
|
||||||
|
Исполнитель решает, когда переключаться между задачами
|
||||||
|
|
||||||
|
Нет, в кооперативной многозадачности момент переключения решают задачи, а не исполнитель
|
||||||
|
|
||||||
|
Задачи переключаются по истечению времени
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Задачи решают, когда можно переключиться с них
|
||||||
|
|
||||||
|
Правильно, только задачи решают, когда переключаться и могут выполняться сколько угодно
|
||||||
|
|
||||||
|
Все задачи выполняются только в один поток
|
||||||
|
|
||||||
|
Нет, такого требования к кооперативности нет
|
||||||
|
|
||||||
|
## Многозадачность в асинхронном rust
|
||||||
|
|
||||||
|
ОР — Знает преимущества кооперативной многозадачности
|
||||||
|
В rust для асинхронного кода выбрана кооперативная многозадачность. Такой выбор обоснован тем, что переключение между задачами в кооперативной многозадачности существенно быстрее, и дешевле по производительности обходится безопасность по памяти. Если бы многозадачность была вытесняющей, то при переключении задач пришлось бы выполнять дополнительные действия для сохранения состояния задачи, в то время как в кооперативной задачи сами отвечают за свое состояние. При этом у кооперативной многозадачности есть и недостатки: если задача зависает, то другие задачи не могут выполняться. Поэтому во всех современных десктопных ОС выбрана именно вытесняющая многозадачность, ведь иначе какой-нибудь процесс случайно или злонамеренно мог бы повесить всю систему. И поэтому при написании асинхронных функций нужно следить за тем, как долго они выполняются, и если возникает хоть немного занимающая время задача, ее нужно выносить в отдельный поток (в рантаймах есть встроенные инструменты для этого)
|
||||||
|
|
||||||
|
Квиз-Множественный выбор
|
||||||
|
|
||||||
|
Выберите преимущества кооперативной многозадачности перед вытесняющей.
|
||||||
|
|
||||||
|
Задачи выполняются в порядке создания
|
||||||
|
|
||||||
|
Нет, задачи могут выполняться в любом порядке
|
||||||
|
|
||||||
|
Быстрее переключение между задачами
|
||||||
|
|
||||||
|
Да, реализация переключения при кооперативной многозадачности проще и быстрее, чем при вытесняющей
|
||||||
|
|
||||||
|
Идеальная предсказуемость времени выполнения задачи
|
||||||
|
|
||||||
|
Нет, кооперативная многозадачность не гарантирует идеальную предсказуемость выполнения. Более того, если одна из других задач будет долго исполняться, выполнение задачи тоже затянется
|
||||||
|
|
||||||
|
Из-за одной неправильной задачи случайно не остановятся другие
|
||||||
|
|
||||||
|
Нет, это преимущество вытесняющей многозадачности
|
||||||
|
|
||||||
|
Быстрее безопасное обращение к памяти
|
||||||
|
|
||||||
|
Да, благодаря предсказуемости момента переключения, задача успеет закончить свое обращение к памяти
|
||||||
|
|
||||||
|
## Синтаксис async/await
|
||||||
|
|
||||||
|
ОР — Умеет использовать async/await синтаксис
|
||||||
|
|
||||||
|
Для кооперативной многозадачности в самих задачах нужна логика для передачи управления. Для этого в rust существует синтаксис async/await, он выглядит так:
|
||||||
|
|
||||||
|
```
|
||||||
|
// создание асинхронной функции
|
||||||
|
async fn task1() {
|
||||||
|
do_something().await; // вызов асинхроной функции и ожидание результата от нее
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Сам по себе вызов такой функции (к примеру, просто `do_something()`) ничего не сделает. Для вызова асинхронной функции нужно использовать .await, если вызывать из асинхронной функции, либо вызывать из рантайма. Для асинхронных замыканий тоже добавляется ключевое слово async:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async || {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Или, если нет аргументов, можно опустить `||`:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
let closure = async {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Кстати, так же, как и для синхронных сокрытий существуют трейты FnOnce, FnMut и Fn, для асинхронных реализованы AsyncFnOnce, AsyncFnMut, AsyncFn аналогично
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
Что выведет данный код:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
async {
|
||||||
|
println!("42");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Не скомпилируется из-за синтаксической ошибки
|
||||||
|
|
||||||
|
Нет, данный код полностью валидный и скомпилируется
|
||||||
|
|
||||||
|
Не скомпилируется, так как асинхронный код нельзя вызывать из синхронной функции
|
||||||
|
|
||||||
|
Нет, в данном случае просто создается асинхронное замыкание и ничего не вызывается через .await, поэтому код ничего не выведет
|
||||||
|
|
||||||
|
Ничего
|
||||||
|
|
||||||
|
Правильно, данный код просто создаст асинхронное замыкание, без его вызова
|
||||||
|
|
||||||
|
Выведет в консоль 42
|
||||||
|
|
||||||
|
Нет, данный лишь создаст асинхронное замыкание, но не вызовет его, поэтому на экран не будет ничего выведено
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime через макрос
|
||||||
|
|
||||||
|
ОР — Умеет настраивать и запускать tokio runtime вручную
|
||||||
|
|
||||||
|
В rust нет встроенного рантайма, они все реализованы в виде библиотек. Самыми популярными являются tokio и smol (раньше еще был async-std, но он теперь discontinued). В данном уроке остановимся на tokio, так как он самый популярный. Чтобы запустить асинхронный код с помошью tokio, можно использовать макрос tokio::main
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
По стандартным настройкам этом макрос cоздает многопоточный рантайм, поэтому он эквивалентен такому коду:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
do_something().await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Данный код создает многопоточный рантайм tokio, запускает и ожидает завершения функции, переданной в Builder::block_on `.enable_all()` отвечает за возможность использования всего IO tokio и tokio::time Многопоточность в макросе указать явно можно так:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() {
|
||||||
|
do_something().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Так же в макросе можно указать количество потоков, на которых будут запускаться асинхронные функции (по умолчанию их столько же, сколько потоков в процессоре в системе):
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(8)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы рантайм был однопоточным, можно в макросе указать параметр flavor:
|
||||||
|
|
||||||
|
```
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
То же самое, но без макроса:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
|
||||||
|
На скольких потоках будут запускаться асинхронные функции, если просто использовать макрос `#[tokio::main]`:
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Ошибка, нужно указать явно
|
||||||
|
|
||||||
|
Нет, указывать явно не нужно, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
8
|
||||||
|
|
||||||
|
Нет, по умолчанию tokio будет запускать задачи на стольких потоках, сколько потоков у процессора в системе
|
||||||
|
|
||||||
|
Столько, сколько потоков в процессоре на данной системе
|
||||||
|
|
||||||
|
Правильно, без явного указания потоков будет столько же, сколько их в процессоре в системе
|
||||||
|
|
||||||
|
## Конкурентность
|
||||||
|
А теперь, попробуем понаписать асинхронный код. Пускай у нас будет две такие функции:
|
||||||
|
```rust
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
async fn task1() {
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
print("task1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn task2() {
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
print("task2");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Попробуем их вызвать?
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let res1 = task1().await;
|
||||||
|
let res2 = task2().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Попробуйте запустить. Выполнение происходит последовательно. А как нам вызвать их так, чтобы задачи выполнялись параллельно? На помощь вам придут spawn, {{join и select}}[join и select не зависят от рантайма, поэтому представлены в tokio, futures, smol, а join еще и в стандартной библиотеке (пока эксперементально)]. Каждый из них выполняет разные задачи, но одинаковую роль: параллельное исполнение задач, давайте разберем каждого из них!
|
||||||
|
### join!
|
||||||
|
Макрос join ожидает окончания выполнения всех фьючерсов, переданных ему, а потом возвращает результат их всех
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let (res1, res2) = tokio::join!(
|
||||||
|
task1(),
|
||||||
|
task2(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
А еще, в библиотеке futures, есть функция join_all, которая делает то же самое, но на вход она принимает итератор по фьючерсам:
|
||||||
|
```rust
|
||||||
|
use futures::future::join_all;
|
||||||
|
|
||||||
|
async fn foo(i: u32) -> u32 { i }
|
||||||
|
|
||||||
|
let futures = vec![foo(1), foo(2), foo(3)];
|
||||||
|
|
||||||
|
assert_eq!(join_all(futures).await, [1, 2, 3]);
|
||||||
|
```
|
||||||
|
### select!
|
||||||
|
Макрос select ожидает окончания выполнения одного из фьючерсов, и возвращает его значение, которое можно обработать, как в синтаксисе match:
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let res = tokio::select! {
|
||||||
|
res1 = task1() => {
|
||||||
|
// возможность выполнения логики
|
||||||
|
res1
|
||||||
|
},
|
||||||
|
res2 = task2() => res2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
При этом, выполнение второго фьючерса ==отменится==. (Кстати, про то, какие фьючерсы не стоит отменять будет рассказано в следующем уроке). К примеру, с помощью select можно отменять ожидание UDP пакета с таймаутом:
|
||||||
|
```rust
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
let sock = UdpSocket::bind("0.0.0.0:8080").await?;
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
data = sock.recv_from(&mut buf) => {
|
||||||
|
let (len, addr) = data?;
|
||||||
|
println!("{:?} bytes received from {:?}", len, addr);
|
||||||
|
}
|
||||||
|
_ = sleep(Duration::from_secs(10)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
### spawn
|
||||||
|
Функция spawn добавит задачу в очередь задач tokio, и при свободном исполнителе, запустит задачу. А если текущий рантайм tokio многопоточный, то это позволит использовать многопоточность, так как задачи, созданные через spawn, tokio может распределять между потоками. Так же возвращает JoinHandle, аналогичные по функционалу JoinHandle из стандартной библиотеки. Но, функцию невозможно выполнить из других рантаймов.
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let handle = tokio::spawn(async {
|
||||||
|
println!("spawned");
|
||||||
|
});
|
||||||
|
// main может напечататься и до spawned, и после
|
||||||
|
// то есть примерно такое же поведение, как и у многопоточности
|
||||||
|
println!("main");
|
||||||
|
// Дождаться завершения можно вызвав .await,
|
||||||
|
// (как .join() в std::thread::JoinHandle)
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
Что произойдет с остальными фьючерсами, переданными в select, кроме того, из которого получили значение?
|
||||||
|
1. Будет ожидание их завершения
|
||||||
|
Нет, их исполнение отменится
|
||||||
|
2. Отменятся
|
||||||
|
Правильно, их исполнение отмениться (вызовется)
|
||||||
|
3. Продолжат исполняться параллельно
|
||||||
|
Нет, их выполнение отменится
|
||||||
|
4. Останутся в том состоянии, котором были
|
||||||
|
Нет, их выполнение отменится
|
||||||
|
Квиз-Одиночный выбор
|
||||||
|
Что делает макрос tokio::join?
|
||||||
|
5. join возвращает итератор значений, возвращаемых из фьючерсов
|
||||||
|
Нет, он вернет все значения после окончания исполнения фьючерсов
|
||||||
|
6. Будет ожидание завершения всех фьючерсов, потом вернутся все значения
|
||||||
|
Правильно, он вернет все значения
|
||||||
|
7. Ждет выполнения одного фьючерса, потом возвращает его значение, а остальные отменяет
|
||||||
|
Нет, таково поведение макроса select, join же ожидает выполнения всех фьючерсов
|
||||||
|
8. Добавит фьючерсы в очередь задач tokio
|
||||||
|
Нет, такова роль функции tokio::spawn. join будет просто ожидать выполнения всех фьючерсов одновременно
|
||||||
|
## Практика: мини-бэкенд
|
||||||
|
Попробуйте написать функцию handle_connections, которая будет выполнять переданные ей фьючерсы с помощью tokio::spawn:
|
||||||
|
```rust
|
||||||
|
use futures::future::join_all;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
// напишите здесь асинхронную функцию handle_connections
|
||||||
|
|
||||||
|
// Тесты
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
let connections = {
|
||||||
|
let mut connections = Vec::new();
|
||||||
|
for i in 0..10 {
|
||||||
|
let connection = async move {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
println!("Hello from connection {i}");
|
||||||
|
};
|
||||||
|
connections.push(connection);
|
||||||
|
}
|
||||||
|
connections
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
handle_connections(connections).await;
|
||||||
|
let end = start.elapsed();
|
||||||
|
|
||||||
|
assert!(end < Duration::from_millis(500))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Подсказки
|
||||||
|
- tokio::spawn запускает фьючерс без вызова .await
|
||||||
|
- При завершении программы все потоки завершатся без ожидания задач в них
|
||||||
|
- .await на JoinHandle, полученом от tokio::spawn, будет ожидать завершения потока
|
||||||
|
- join_all позволяет ожидать выполнения всех фьючерсов в переданном итераторе
|
||||||
|
Проверьте свой код по чек-листу:
|
||||||
|
Механика квиз-множественный выбор (все ответы верные, без фидб==э==ков)
|
||||||
|
- [ ] Используется tokio::spawn
|
||||||
|
- [ ] Задачи выполняются параллельно
|
||||||
|
- [ ] 10 раз печатается `Hello from connection {i}`
|
||||||
|
- [ ] Тест проходит
|
||||||
|
### Решение
|
||||||
|
```rust
|
||||||
|
async fn handle_connections<I, F>(connections: I)
|
||||||
|
where
|
||||||
|
<F as Future>::Output: Send + 'static,
|
||||||
|
I: IntoIterator<Item = F>,
|
||||||
|
F: Future + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for connection in connections.into_iter() {
|
||||||
|
let handle = tokio::spawn(connection);
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
join_all(handles).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
tokio::spawn запускает фьючерс без вызова .await
|
||||||
|
При завершении программы все потоки завершатся без ожидания задач в них
|
||||||
|
.await на JoinHandle, полученом от tokio::spawn, будет ожидать завершения потока
|
||||||
|
join_all позволяет ожидать выполнения всех фьючерсов в переданном итераторе
|
||||||
|
Проверьте свой код по чек-листу:
|
||||||
|
Механика квиз-множественный выбор (все ответы верные, без фидб==э==ков)
|
||||||
|
Используется tokio::spawn
|
||||||
|
Задачи выполняются параллельно
|
||||||
|
10 раз печатается `Hello from connection {i}`
|
||||||
|
Тест проходит
|
||||||
540
3_1/2.md
Normal file
540
3_1/2.md
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
ОРы:
|
||||||
|
Знает как работает трейт Future.
|
||||||
|
Знает, как компилятор преобразует асинхронный код.
|
||||||
|
Понимает, зачем нужен Pinning.
|
||||||
|
Умеет использовать трейт-объекты Box\<dyn Future>.
|
||||||
|
Понимает, как асинхронный рантайм выполняет задачи.
|
||||||
|
Может написать свою реализацию Future.
|
||||||
|
|
||||||
|
|
||||||
|
Пример идеи фьючерсов и полинга на примере общения/пинга менеджера
|
||||||
|
## Асинхронность под капотом
|
||||||
|
|
||||||
|
io - epoll + timeout
|
||||||
|
time / nothing - futex + timeout
|
||||||
|
|
||||||
|
## Трейт Future
|
||||||
|
|
||||||
|
## Start
|
||||||
|
В прошлом уроке мы узнали, как писать асинхронный код в rust. Но как же он работает под капотом?
|
||||||
|
Выполнение асинхронного кода происходит по запросу, выглядит примерно так:
|
||||||
|
Происходит запрос к Future, может ли он сейчас выполнить работу и выдать результат. Также Future получает референс на контекст, который
|
||||||
|
|
||||||
|
## Концепция асинхронности в Rust
|
||||||
|
В rust асинхронные функции выполняются лениво. При вызове асинхронной функции они выполняют различные вычисления, до того, пока не произойдет io-bound задачи, на которой они передадут выполнение исполнителю, а выполнение функции продолжится когда она вызовет waker
|
||||||
|
Если функция не может вернуть окончательное значение,
|
||||||
|
При этом вызов waker означает, что значение может быть готово
|
||||||
|
|
||||||
|
В основе асинхронности в rust лежит трейт Future.
|
||||||
|
Future — это значение, которое ещё может быть не вычислено до конца.
|
||||||
|
## Ignore this:
|
||||||
|
## ---
|
||||||
|
## Интерфейс асинхронной функции
|
||||||
|
Каждая асинхронная функция в rust превращается в обычную функцию, которая выдает некоторую машину состояний (создается приватный тип, который известен только компилятору, примерно как с сокрытиями), для которой реализован трейт Future.
|
||||||
|
## ---
|
||||||
|
|
||||||
|
## Start 2
|
||||||
|
В прошлом уроке мы узнали про то, что асинхронность в rust реализована через кооперативную многозадачность, а значит задачи должны cами передавать управление потоком. Но как же это происходит в асинхронной функции? Ведь в ней мы делаем возврат из функции только с готовыми значениями. На самом деле async/await это лишь синтаксический сахар, и компилятор сильно преобразует то, что написано в асинхронной функции.
|
||||||
|
## Трейт Future
|
||||||
|
ОР - Знает как работает трейт Future.
|
||||||
|
Для начала мы разберем трейт Future, который позволяет реализовать кооперативность. Его интерфейс весьма прост и состоит лишь из одного метода poll. Вот определение трейта:
|
||||||
|
```rust
|
||||||
|
pub trait Future {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
// Required method
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
poll означает запрос, может ли future выдать прямо сейчас готовое значение. Если да, то возвращается значение Poll::Ready(val), если нет, то Poll::Pending (подобно на Option). Возврат Poll::Pending и является моментом передачи управления потоком от задачи исполнителю и означает, что нужный для исполнения ресурс пока не доступен и нужно подождать (к примеру, попытка подключения к сокету и ожидание, пока соединение установится). Но сколько же исполнителю нужно ждать? Для этого в метод poll передается специальная структура `Waker`, через которую исполнителя можно уведомить, что возможно выполнить прогресс по задаче, вызвав `Waker.wake()` или `Waker.wake_by_ref()`. Получить `Waker` можно через `Context`, переданный в метод poll, вызвав `Context.waker()` (пока `Context` используется только для получения `Waker`). Этот `Waker` уже обычно передается куда-нибудь, что сможет вызывать `Waker` по возможной готовности ресурса (потому что сам future уже вернул управление и никак не может выполнить никакой код).
|
||||||
|
Трейт Future реализован для всех асинхронных функций. На самом деле, когда мы вызываем метод tokio::runtime::Runtime::block_on, мы просто вызываем poll до тех пор, пока он не вернет Poll::Ready(val).
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
ОР - Знает как работает трейт Future.
|
||||||
|
Какой метод вызывается, если исполнитель знает, что можно сделать прогресс у задачи?
|
||||||
|
1. Future::poll
|
||||||
|
Верно, это метод, который обозначает запрос и позволяет узнать, выполнена ли до конца задача
|
||||||
|
2. Waker::wake_by_ref
|
||||||
|
Нет, хоть эта функция и создана для уведомления о возможности прогресса по задаче, сделана она концептуально для вызова со стороны задачи для уведомления исполнителя о готовности
|
||||||
|
3. Poll::Pending
|
||||||
|
Нет, это возвращаемое значение, которое означает, что задача еще до конца не выполнена
|
||||||
|
## Практика
|
||||||
|
ОР - Понимает, как асинхронный рантайм выполняет задачи.
|
||||||
|
Попробуем написать свой мини-tokio. Для этого создадим новый проект через cargo, добавив в зависимости waker-fn и smol. smol - это рантайм, очень похожий на tokio, но в отличии от него, позволяет исполняться своим функциям не в своих рантаймах. Нам в данном примере от него нужны только асинхронные функции для I/O.
|
||||||
|
Для примера, попробуем выполнить простую асинхронную функцию:
|
||||||
|
```rust
|
||||||
|
async fn read_toml() -> String {
|
||||||
|
smol::fs::read_to_string("./Cargo.toml").await.unwrap()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Пока эту функцию можно воспринимать аналогично этой:
|
||||||
|
```rust
|
||||||
|
fn read_toml() -> impl Future<Output = String> {
|
||||||
|
async {
|
||||||
|
smol::fs::read_to_string("./Cargo.toml").await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
То есть, при вызове `read_toml()`, мы получим что-то, что реализует трейт Future (это будет приватный тип, известный только компилятору). На полученном значении мы и будем вызывать poll:
|
||||||
|
```rust
|
||||||
|
use std::task::{Context, Waker};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Создание контекста с Waker, который при вызове ничего не делает
|
||||||
|
let waker = Waker::noop();
|
||||||
|
let mut cx = Context::from_waker(waker);
|
||||||
|
|
||||||
|
let future = read_toml();
|
||||||
|
// Про Pin будет ниже, пока это расматривайте как требование для поллинга фьючерсов
|
||||||
|
let mut future = std::pin::pin!(future);
|
||||||
|
loop {
|
||||||
|
match future.as_mut().poll(&mut cx) {
|
||||||
|
std::task::Poll::Pending => {
|
||||||
|
println!("Pending future");
|
||||||
|
}
|
||||||
|
std::task::Poll::Ready(value) => {
|
||||||
|
println!("Ready! value:\n{value}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
Попробуем запустить код. Он успешно вывел файл! Но у нашего рантайма есть проблема - он никогда не ожидает готовности ресурсов, что приводит к бесмысленной трате процессорного времени. Попробуем это оптимизировать: будем отправлять поток в сон, пока не вызовется Waker:
|
||||||
|
```rust
|
||||||
|
use std::task::{Context, Waker};
|
||||||
|
use waker_fn::waker_fn;
|
||||||
|
|
||||||
|
async fn read_toml() -> String {
|
||||||
|
smol::fs::read_to_string("./Cargo.toml").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let (waker, wait) = make_waker();
|
||||||
|
let mut cx = Context::from_waker(&waker);
|
||||||
|
|
||||||
|
let future = read_toml();
|
||||||
|
let mut future = std::pin::pin!(future);
|
||||||
|
loop {
|
||||||
|
match future.as_mut().poll(&mut cx) {
|
||||||
|
std::task::Poll::Pending => {
|
||||||
|
println!("Pending future");
|
||||||
|
}
|
||||||
|
std::task::Poll::Ready(value) => {
|
||||||
|
println!("Ready! value:\n{value}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns (waker, wait)
|
||||||
|
fn make_waker() -> (Waker, impl Fn()) {
|
||||||
|
let t = std::thread::current();
|
||||||
|
let waker = waker_fn(move || {
|
||||||
|
t.unpark();
|
||||||
|
});
|
||||||
|
let wait = move || {
|
||||||
|
// Go to sleep and wait
|
||||||
|
std::thread::park();
|
||||||
|
};
|
||||||
|
(waker, wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
(Здесь для упрощения примера используется крейт waker-fn, но Waker можно собирать и без него)
|
||||||
|
При исполнении результат будет таким же, но poll не будет вызываться бессмысленно кучу раз. Если сравнить вывод, то теперь `Pending future` вывелся только один раз, хотя в прошлый раз он вывелся очень много раз. Заметьте, это не означает, что poll всегда возвращает Pending только один раз, это значение может возвращаться сколь угодно раз, пока не получится вернуть значение Ready.
|
||||||
|
Данный пример сильно упрощен. На деле рантаймы не просто засыпают, а делают определенный syscall, который вернется при доступности одного из запрошенных ресурсов
|
||||||
|
Квиз
|
||||||
|
ОР - Понимает, как асинхронный рантайм выполняет задачи.
|
||||||
|
Мульти: Когда рантайм вызовет Future::poll?
|
||||||
|
1. В первый раз по возможности
|
||||||
|
Правильно, для первого вызова poll ничего делать не надо
|
||||||
|
2. Сразу после предыдущего вызова poll
|
||||||
|
Нет, рантайм будет ждать, пока не вызовется waker
|
||||||
|
3. Когда не будет других задач для выполнения
|
||||||
|
Нет, рантайм без задач рантайм отправится в ожидание
|
||||||
|
4. После вызова Waker когда исполнитель освободится
|
||||||
|
Правильно, после этого вызова исполнитель будет знать, что в исполнении задачи может произойти прогресс, и вызовет тогда, когда освободится от других задач
|
||||||
|
5. Сразу после вызова Waker
|
||||||
|
Нет, poll вызовется только тогда, когда исполнитель освободится от других задач
|
||||||
|
## Трейт Future
|
||||||
|
ОР - Знает как работает трейт Future.
|
||||||
|
ОР - Может написать свою реализацию Future.
|
||||||
|
А теперь подойдем к реализации асинхронности со стороны задачи. Попробуем написать свое асинхронное ожидание промежутка времени используя трейт Future. Создадим функцию
|
||||||
|
```rust
|
||||||
|
fn wait_for(duration: Duration) -> impl Future<Output = ()> {
|
||||||
|
WaitFor { duration }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WaitFor {
|
||||||
|
duration: Duration,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Теперь попробуем реализовать Future. Пускай для начала просто возвращает Pending:
|
||||||
|
```rust
|
||||||
|
impl Future for WaitFor {
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<Self::Output> {
|
||||||
|
std::task::Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Заметьте, как функция `wait_for` по определению похожа на функцию `read_toml`, что мы определяли ранее. Из-за того, что мы возвращаем фьючерс, это функция является асинхронной. Поэтому мы можем вызывать на ней .await! (который, по сути, является просто синтаксическим сахаром для поллинга)
|
||||||
|
Для этого напишем такой main:
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
println!("Before wait");
|
||||||
|
wait_for(std::time::Duration::from_secs(2)).await;
|
||||||
|
println!("After wait");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Задание: попробуйте написать то же самое по функциональности, но используя рантайм, который мы только что написали сами
|
||||||
|
Попробуем запустить, программа такой результат:
|
||||||
|
```
|
||||||
|
Before wait
|
||||||
|
```
|
||||||
|
|
||||||
|
`After wait` не вывелся, так как Future продолжит исполняться только после вызова Waker (не не обязательно сразу после вызова, так как исполнитель может быть занят другими задачами, и задача не начнет исполняться, пока исполнитель не освободится, так как у нас кооперативная многозадачность). Попробуем вызвать Waker по истечению нужного промежутка времени. Для простоты примера, будет вызываться отдельный поток, но обычно в рантаймах Waker передается в собственный I/O event loop, который вызовет Waker при выполнении нужных условий.
|
||||||
|
```rust
|
||||||
|
fn poll(
|
||||||
|
self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Self::Output> {
|
||||||
|
let waker = cx.waker().clone();
|
||||||
|
let duration = self.duration;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(duration);
|
||||||
|
waker.wake();
|
||||||
|
});
|
||||||
|
std::task::Poll::Pending
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Попробуем вызвать, но в консоли все еще выводится только `Before wait`. Почему так происходит? На самом деле наш poll вызывается, в данном случае, раз в две секунды (потому что создаем поток, который через 2 секунды вызывает Waker), Но мы никогда не возвращаем Ready. Попробуем это исправить:
|
||||||
|
```rust
|
||||||
|
struct WaitFor {
|
||||||
|
duration: Duration,
|
||||||
|
waited: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for(duration: Duration) -> WaitFor {
|
||||||
|
WaitFor { duration, waited: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Future for WaitFor {
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Self::Output> {
|
||||||
|
if self.waited {
|
||||||
|
return std::task::Poll::Ready(());
|
||||||
|
}
|
||||||
|
self.waited = true;
|
||||||
|
let waker = cx.waker().clone();
|
||||||
|
let duration = self.duration;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(duration);
|
||||||
|
waker.wake();
|
||||||
|
});
|
||||||
|
std::task::Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Попробуем запустить:
|
||||||
|
```
|
||||||
|
Before wait
|
||||||
|
After wait
|
||||||
|
```
|
||||||
|
Ура! 🎉 Теперь мы получили ожидаемый результат.
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
ОР - Может написать свою реализацию Future.
|
||||||
|
Какой вызов Future всегда является последним?
|
||||||
|
1. Когда возвращает Self::Output
|
||||||
|
Нет, фьючерс никогда не может просто вернуть Self::Output. Вместо этого по готовности возвращается Poll::Ready(Self::Output)
|
||||||
|
2. Когда возвращает Poll::Ready(Self::Output)
|
||||||
|
Правильно, после возврата Poll::Ready(Self::Output) фьючерс больше не должен вызываться
|
||||||
|
3. Когда возвращает Poll::Pending
|
||||||
|
Нет, Poll::Pending возвращается во все вызовы, кроме последнего, в котором возвращается Poll::Ready(Self::Output)
|
||||||
|
4. Следующий за вызовом Waker
|
||||||
|
Нет, после вызова Waker фьючерс может вызваться сколь угодно раз
|
||||||
|
## Преобразование синтаксиса async/await в Future
|
||||||
|
ОР - Знает, как компилятор преобразует асинхронный код.
|
||||||
|
Теперь, зная что async/await просто превращается во фьючерс, посмотрим, как это происходит на примере такой функции:
|
||||||
|
```rust
|
||||||
|
async fn wait() -> usize {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
67
|
||||||
|
}
|
||||||
|
```
|
||||||
|
На самом деле, при компиляции она превратится в некую машину состояний: (это псевдокод, так как написать в точности, как будет скомпилировано, невозможно)
|
||||||
|
```rust
|
||||||
|
fn wait() -> Wait {
|
||||||
|
Wait::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Wait {
|
||||||
|
state: u8,
|
||||||
|
yield_now: typeof<tokio::task::yield_now()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wait {
|
||||||
|
fn new() -> Self {
|
||||||
|
unsafe { std::mem::zeroed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Future for Wait {
|
||||||
|
type Output = usize;
|
||||||
|
|
||||||
|
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<Self::Output> {
|
||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.state {
|
||||||
|
0 => {
|
||||||
|
self.yield_now = tokio::task::yield_now();
|
||||||
|
self.state += 1;
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
match self.yield_now.poll() {
|
||||||
|
Poll::Pending => return Poll::Pending
|
||||||
|
Poll::Ready(_) => {
|
||||||
|
self.state += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
self.state += 1;
|
||||||
|
return Poll::Ready(67)
|
||||||
|
}
|
||||||
|
_ => panic!("wait called after completion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
Квиз
|
||||||
|
ОР - Знает, как компилятор преобразует асинхронный код.
|
||||||
|
Допустим, у нас есть такая функция:
|
||||||
|
```rust
|
||||||
|
async fn number() -> usize {
|
||||||
|
42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Что вернет первый вызов Future::poll на ней?
|
||||||
|
1. 42
|
||||||
|
Нет, poll всегда возвращает значение типа Poll, в данном случае вернется Poll::Ready(42)
|
||||||
|
2. Poll::Pending
|
||||||
|
Нет, в данном случае значение сразу готово, так как нет .await поинтов, и при первом же вызове вернется Poll::Ready(42)
|
||||||
|
3. Poll::Ready(42)
|
||||||
|
Верно, первый вызов сразу же вернет это значение, так как значение будет сразу готово
|
||||||
|
## Pin
|
||||||
|
ОР - Понимает, зачем нужен Pinning.
|
||||||
|
Заметили, что poll везде принимает загадочный тип Pin? Давайте разберем данную функцию:
|
||||||
|
```rust
|
||||||
|
async fn selfreferential() {
|
||||||
|
let value = 1;
|
||||||
|
let refvalue = &value;
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
println!("value by ref: {}", refvalue);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Как для нее будет выглядеть тип, как из примера выше?
|
||||||
|
```rust
|
||||||
|
struct Selfreferential {
|
||||||
|
value: u32,<----| // refvalue ссылается на value
|
||||||
|
refvalue: &u32,-|
|
||||||
|
yield_now: typeof<tokio::task::yield_now()>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Что произойдет, если для нее выполнить самое обычное перемещение в памяти? референс refvalue станет невалидным! Ведь референс уже будет указывать на невалидную область памяти. А для раста это недопустимо. Поэтому для вызова асинхронной функции требуется Pin: он принимает указатель на объект и дает гарантии, что объект под этим указателем не будет перемещен в памяти
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
ОР - Понимает, зачем нужен Pinning.
|
||||||
|
Если бы для вызова асинхронной функции не требовался тип Pin, какая проблема возникала бы?
|
||||||
|
1. Было бы не безопасно использовать указатели
|
||||||
|
Правильно
|
||||||
|
2. Асинхронные функции не могли бы вызывать друг друга
|
||||||
|
Нет,вызов не связан с пиннингом. Pin нужен для управления размещением в памяти, а не для вызовов функций.
|
||||||
|
3. Выполнение асинхронного кода стало бы значительно медленнее
|
||||||
|
Нет, Pin — это механизм безопасности, а не оптимизации. Его наличие или отсутствие не влияет напрямую на скорость выполнения, только на корректность и безопасность доступа к данным
|
||||||
|
4. Future не смог бы хранить ссылки на другие Future
|
||||||
|
Нет, можно хранить ссылки между Future, если соблюдены правила заимствования. Pin лишь гарантирует, что объект не будет перемещён, но не ограничивает владение
|
||||||
|
## Асинхронная рекурсия
|
||||||
|
ОР - Умеет использовать трейт-объекты Box\<dyn Future>.
|
||||||
|
Допустим, вы решили написать асинхронную рекурсивную функцию. Пускай она выглядит так:
|
||||||
|
```rust
|
||||||
|
async fn recursion() {
|
||||||
|
recursion().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Как будет выглядеть тип для нее (по примеру с Wait из примера выше)? Кажется, вот так:
|
||||||
|
```rust
|
||||||
|
struct Recursion {
|
||||||
|
state: u8,
|
||||||
|
recursion: Recursion,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Но вложенные структуры невозможны, так как тогда они будут бесконечного размера. Тут на помощь приходит Box:
|
||||||
|
```rust
|
||||||
|
struct Recursion {
|
||||||
|
state: u8,
|
||||||
|
recursion: Box<Recursion>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Не забываем, что для вызова нам потом нужен будет Pin, поэтому определение будет такое:
|
||||||
|
```rust
|
||||||
|
struct Recursion {
|
||||||
|
state: u8,
|
||||||
|
recursion: Pin<Box<Recursion>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
И фьючерс стал конечного размера. В самой функции это будет выглядеть так:
|
||||||
|
```rust
|
||||||
|
async fn recursion() {
|
||||||
|
Box::pin(recursion()).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Квиз
|
||||||
|
ОР - Умеет использовать трейт-объекты Box\<dyn Future>.
|
||||||
|
Без какого типа можно создать простую асинхронную функцию, но не создать синхронную?
|
||||||
|
1. Future
|
||||||
|
Нет, Future нужен для всех асинхронных функций, а еще Future - это трейт
|
||||||
|
2. Pin
|
||||||
|
Нет, Pin нужен для вызова poll на всех фьючерсах
|
||||||
|
3. Box
|
||||||
|
Правильно, благодаря нему фьючерс не будет бесконечно большим
|
||||||
|
|
||||||
|
## Практика
|
||||||
|
Порой, выполнение функции по времени нужно ограничить. Попробуйте написать такой таймаут!
|
||||||
|
```rust
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::time::Sleep;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum TimeoutFuture<O> {
|
||||||
|
Result(O),
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timeouted_read<F: IntoFuture>(
|
||||||
|
timeout: Duration,
|
||||||
|
future: F,
|
||||||
|
) -> TimeoutedFuture<<F as IntoFuture>::IntoFuture> {
|
||||||
|
let future = future.into_future();
|
||||||
|
let sleep = tokio::time::sleep(timeout);
|
||||||
|
TimeoutedFuture { future, sleep }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TimeoutedFuture<F: Future> {
|
||||||
|
future: F,
|
||||||
|
sleep: Sleep,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Future> TimeoutedFuture<F> {
|
||||||
|
fn future(self: Pin<&mut Self>) -> Pin<&mut F> {
|
||||||
|
unsafe { self.map_unchecked_mut(|s| &mut s.future) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sleep(self: Pin<&mut Self>) -> Pin<&mut Sleep> {
|
||||||
|
unsafe { self.map_unchecked_mut(|s| &mut s.sleep) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Future> Future for TimeoutedFuture<F> {
|
||||||
|
type Output = TimeoutFuture<F::Output>;
|
||||||
|
|
||||||
|
fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
// Релеазуйте метод poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тесты
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let instant = async { 0 };
|
||||||
|
let result = timeouted_read(Duration::from_millis(123), instant).await;
|
||||||
|
println!("Result: {result:?}"); // Result(0)
|
||||||
|
|
||||||
|
let wait100 = async {
|
||||||
|
let delay = 100;
|
||||||
|
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||||
|
delay
|
||||||
|
};
|
||||||
|
let result = timeouted_read(Duration::from_millis(123), wait100).await;
|
||||||
|
println!("Result: {result:?}"); // Result(100)
|
||||||
|
|
||||||
|
let wait150 = async {
|
||||||
|
let delay = 150;
|
||||||
|
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||||
|
delay
|
||||||
|
};
|
||||||
|
let result = timeouted_read(Duration::from_millis(123), wait150).await;
|
||||||
|
println!("Result: {result:?}"); // Timeout
|
||||||
|
|
||||||
|
let never = std::future::pending::<usize>();
|
||||||
|
let result = timeouted_read(Duration::from_millis(123), never).await;
|
||||||
|
println!("Result: {result:?}"); // Timeout
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Подсказки
|
||||||
|
- Для получение фьчерсов, на которых можно вызвать poll, реализованы помогающие методы TimeoutedFuture::future() и TimeoutedFuture::sleep().
|
||||||
|
- Чтобы два раза вызвать методы, забирающие владение Pin, можно использовать метод Pin::as_mut
|
||||||
|
- Чтобы проверить, не завершился ли фьючерс, можно вызвать метод Future::Poll
|
||||||
|
- По окончанию выполнения Future::poll возвращает Poll::Ready
|
||||||
|
- Poll::Pending означает, что результат еще не готов
|
||||||
|
Проверьте свой код по чек-листу:
|
||||||
|
Механика квиз-множественный выбор (все ответы верные, без фидб==э==ков)
|
||||||
|
- [ ] Вызывается poll на future
|
||||||
|
- [ ] Вызывается poll на sleep
|
||||||
|
- [ ] При завершении future возвращается значение из нее внутри TimeoutFuture::Result
|
||||||
|
- [ ] При таймауте возвращается TimeoutFuture:Timeout
|
||||||
|
- [ ] Все тесты проходят
|
||||||
|
### Решение
|
||||||
|
```rust
|
||||||
|
impl<F: Future> Future for TimeoutedFuture<F> {
|
||||||
|
type Output = TimeoutFuture<F::Output>;
|
||||||
|
|
||||||
|
fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let future = self.as_mut().future();
|
||||||
|
|
||||||
|
match future.poll(cx) {
|
||||||
|
Poll::Ready(output) => return Poll::Ready(TimeoutFuture::Result(output)),
|
||||||
|
Poll::Pending => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sleep = self.as_mut().sleep();
|
||||||
|
|
||||||
|
match sleep.poll(cx) {
|
||||||
|
Poll::Ready(()) => Poll::Ready(TimeoutFuture::Timeout),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
P.S.: то, что вы написали, альтернативно функции tokio::time::timeout
|
||||||
|
|
||||||
|
Чтобы проверить, не завершился ли фьючерс, можно вызвать метод Future::Poll
|
||||||
|
По окончанию выполнения Future::poll возвращает Poll::Ready
|
||||||
|
Poll::Pending означает, что результат еще не готов
|
||||||
|
Проверьте свой код по чек-листу:
|
||||||
|
Механика квиз-множественный выбор (все ответы верные, без фидб==э==ков)
|
||||||
|
Вызывается poll на future
|
||||||
|
Вызывается poll на sleep
|
||||||
|
При завершении future возвращается значение из нее внутри TimeoutFuture::Result
|
||||||
|
При таймауте возвращается TimeoutFuture:Timeout
|
||||||
|
Все тесты проходят
|
||||||
72
3_1/3practice.md
Normal file
72
3_1/3practice.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
### **Практическое задание: задачи с таймаутом**
|
||||||
|
|
||||||
|
Создайте метод TaskPool::create, которая будет отправлять задачу в канал, но если не получается отправить задачу в канал в течение 100 миллисекунд, возвращается None
|
||||||
|
```rust
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{Receiver, Sender, channel, error::SendError},
|
||||||
|
time::timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TaskPool<T> {
|
||||||
|
tasks: Receiver<T>,
|
||||||
|
sender: Sender<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TaskPool<T> {
|
||||||
|
fn new(queue_size: usize) -> Self {
|
||||||
|
let (sender, tasks) = channel::<T>(queue_size);
|
||||||
|
Self { tasks, sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, task: T) -> Option<Result<(), SendError<T>>> {
|
||||||
|
// Напишите свое решение здесь
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pull_task(&mut self) -> Option<T> {
|
||||||
|
self.tasks.recv().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let mut tasks = TaskPool::new(2);
|
||||||
|
|
||||||
|
assert_eq!(tasks.create(()).await, Some(Ok(())));
|
||||||
|
assert_eq!(tasks.create(()).await, Some(Ok(())));
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
assert_eq!(tasks.create(()).await, None);
|
||||||
|
let end = start.elapsed();
|
||||||
|
|
||||||
|
assert!(end > Duration::from_millis(90));
|
||||||
|
assert!(end < Duration::from_millis(110));
|
||||||
|
|
||||||
|
assert_eq!(tasks.pull_task().await, Some(()));
|
||||||
|
assert_eq!(tasks.create(()).await, Some(Ok(())));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Подсказки
|
||||||
|
|
||||||
|
используйте tokio::time::timeout для таймаута
|
||||||
|
tokio::time::timeout принимает фьючерс для которого нужен таймаут
|
||||||
|
используйте .send() для отправки значение
|
||||||
|
обратите внимание, что .send() возвращает фьючерс
|
||||||
|
|
||||||
|
Проверьте свой код по чек-листу:
|
||||||
|
используется tokio::time::timeout
|
||||||
|
используются асинхронные каналы
|
||||||
|
все тесты проходят
|
||||||
|
### **Решение**
|
||||||
|
```rust
|
||||||
|
async fn create(&self, task: T) -> Option<Result<(), SendError<T>>> {
|
||||||
|
let sender = self.sender.send(task);
|
||||||
|
timeout(Duration::from_millis(100), sender).await.ok()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Вы знаете про существование таких синхронизационных примитивов в стандартной библиотеке, как Mutex, RwLock, каналы. И в тоже происходит ожидание. Но они блокируют поток, поэтому в tokio есть альтернативы с идентичным интерфейсом, но асинхронным:
|
||||||
|
|
||||||
|
У асинхронных мьютекса и RwLock есть преимущество: если фьючерс ожидает лока асинхронного мьютекса, токио будет обратно переключаться на другие фьючерсы. В то время, как синхронные версии будут держать поток и не давать другим фьючерсам исполняться. Для синхронных версий это создает проблему: если MutexGuard пересек границу .await, где, к примеру, ожидается ответ в течение 30 секунд, то другой фьючерс, ожидающий лока мьютекса, будет держать целый поток токио в течение всего этого времени, что приведет к потере производительности. А если рантайм токио будет однопоточным, то программа зависнет навсегда.
|
||||||
|
после вызова await залочен, что приводит к ожиданию анлока от других
|
||||||
190
3_1/3v0.md
Normal file
190
3_1/3v0.md
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
В прошлом уроке мы разобрали, как устроена асинхронность в rust под капотом и как рантайм вызывает код. В этом уроке мы окунемся поглубже в то, как в tokio происходит ожидание готовностей ресурсов, то, как работать с асинхронным вводом/выводом, как делать асинхронные сетевые запросы, работать с файловой системой, и как использовать неблокирующие примитивы синхронизации
|
||||||
|
|
||||||
|
## Асинхронные чтение/запись
|
||||||
|
ОР - Умеет работать с асинхронным абстрактным вводом/выводом.
|
||||||
|
В rust для чтения/записи всего (к примеру, файлов, или сокетов) есть трейты Read и Write, но свои задачи они выполняют синхронно. Для асихронного мира существуют трейты AsyncRead и AsyncWrite. Их определение выглядит так:
|
||||||
|
```rust
|
||||||
|
pub trait AsyncRead {
|
||||||
|
// Required method
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<Result<()>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```rust
|
||||||
|
pub trait AsyncWrite {
|
||||||
|
// Required methods
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<Result<usize, Error>>;
|
||||||
|
fn poll_flush(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<(), Error>>;
|
||||||
|
fn poll_shutdown(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<(), Error>>;
|
||||||
|
|
||||||
|
// Provided methods
|
||||||
|
fn poll_write_vectored(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
bufs: &[IoSlice<'_>],
|
||||||
|
) -> Poll<Result<usize, Error>> { ... }
|
||||||
|
fn is_write_vectored(&self) -> bool { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Можно по определению методов заметить, что поведение у них идентично Future::poll - если могут выполнить чтение/запись прямо сейчас, то возвращают Poll::Ready, а если нет, то Poll::Pending. Но, с этими трейтами пользователи библиотек не взаимодействуют, для всех типов, для которых реализованы AsyncRead/AsyncWrite реализованы и трейты AsyncReadExt/AsyncWriteExt соответственно. У них интерфейс уже почти идентичен std::io::{Read,Write}. К примеру у AsyncReadExt:
|
||||||
|
```rust
|
||||||
|
pub trait AsyncReadExt: AsyncRead {
|
||||||
|
// Provided methods
|
||||||
|
fn chain<R>(self, next: R) -> Chain<Self, R>
|
||||||
|
where Self: Sized,
|
||||||
|
R: AsyncRead { ... }
|
||||||
|
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Read<'a, Self>
|
||||||
|
where Self: Unpin { ... }
|
||||||
|
fn read_buf<'a, B>(&'a mut self, buf: &'a mut B) -> ReadBuf<'a, Self, B>
|
||||||
|
where Self: Unpin,
|
||||||
|
B: BufMut + ?Sized { ... }
|
||||||
|
fn read_exact<'a>(&'a mut self, buf: &'a mut [u8]) -> ReadExact<'a, Self>
|
||||||
|
where Self: Unpin { ... }
|
||||||
|
fn read_u8(&mut self) -> ReadU8<&mut Self>
|
||||||
|
where Self: Unpin { ... }
|
||||||
|
// и другие методы
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Эти трейты позволяют писать асинхронные чтение/запись почти идентично синхронным версиям, с одним только отличием - добавлением await
|
||||||
|
## Взаимодействие с интернетом и фс
|
||||||
|
ОР - Может использовать неблокирующее сетевое взаимодействие (TCP, UDP) и работу с файловой системой.
|
||||||
|
Чтобы делать сетевые запросы в tokio реализованы TcpStream, UdpSocket и TcpSocket (асинхронные аналоги таких же синхронных версий из стандартной библиотеки)
|
||||||
|
Давайте попробуем написать небольшой http клиент:
|
||||||
|
```rust
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// Подключение к http://google.com
|
||||||
|
let mut stream = TcpStream::connect(("google.com", 80)).await?;
|
||||||
|
|
||||||
|
// Отправка GET запроса
|
||||||
|
stream.write_all(b"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n").await?;
|
||||||
|
|
||||||
|
// Упрощено для примера, но обычно так не делают
|
||||||
|
// Работает для этого примера, так как
|
||||||
|
// гугл отсылает ответ одним tcp пакетом
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let len = stream.read(&mut buf).await?;
|
||||||
|
let buf = buf[..len].to_vec();
|
||||||
|
let output = String::from_utf8(buf).unwrap();
|
||||||
|
|
||||||
|
println!("Returned:\n{output}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Как видите, интерфейс абсолютно такой же, как и у синхронных трейтов в стандартной библиотеке, только добавляется .await после вызова.
|
||||||
|
Для работы с файловой системой все аналогично:
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let cargo = tokio::fs::read_to_string("./Cargo.toml").await?;
|
||||||
|
println!("Cargo.toml content:\n{cargo}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ОР - Умеет использовать неблокирующие примитивы синхронизации и каналы разных видов.
|
||||||
|
Так же есть похожие на стандартные каналы:
|
||||||
|
`tokio::sync::mpsc::unbounded_channel()` - альтернатива `std::sync::mpsc::channel()` из стандартной библиотеки, но при ожидании результата не блокирует поток, а передает управление
|
||||||
|
`tokio::sync::mpsc::channel(buffer: usize)` - альтернатива `std::sync::mpsc::sync_channel()` из стандартной библиотеки, но при заполнении буфера не блокирует поток, а передает управление
|
||||||
|
И примитивы синхронизации:
|
||||||
|
`tokio::sync::Mutex` - альтернатива `std::sync::Mutex`, но не блокирует поток при вызове lock, а еще не будет происходить дедлока при пересечении .await. Если один фьючерс залочил значение в стандартном мьютексе, потом вызвал асинхронную функцию, и другому фьючерсу понадобилось залочить этот мьютекс, то возникнет дедлок. Поэтому, если MutexGuard проходит через .await, нужно использовать именно `tokio::sync::Mutex`
|
||||||
|
`tokio::sync::RwLock` - те же преимущества, что и у мьютекса
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
ОР - Умеет использовать неблокирующие примитивы синхронизации и каналы разных видов.
|
||||||
|
Чем отличается tokio::sync::mpsc::unbounded_channel() от стандартного std::sync::mpsc::channel()?
|
||||||
|
Он поддерживает передачу только Send + Sync типов
|
||||||
|
Нет, это является требованием для обоих каналов
|
||||||
|
Он использует общий буфер для всех задач
|
||||||
|
Нет, у каждого канала свой буфер
|
||||||
|
Он не блокирует поток при ожидании, а передаёт управление исполнителю
|
||||||
|
Правильно, тем самым исполнитель не простаивает на ожидании
|
||||||
|
Он автоматически очищает буфер после каждого await
|
||||||
|
Нет, очистка буфера не зависит от вызова await
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
Что произойдет, если использовать std::sync::Mutex внутри async-функции и удерживать его guard через .await?
|
||||||
|
Мьютекс автоматически разблокируется при .await
|
||||||
|
Нет, мьютекс не знает ничего про асинхронность
|
||||||
|
Может возникнуть дедлок
|
||||||
|
Правильно, после переключения к мьютексу может обратиться другой фьючерс и возникнет дедлок
|
||||||
|
Компилятор выдаст ошибку
|
||||||
|
Нет, компилятор не выдаст ошибку. Но при этом есть линтеры, которые укажут о прохождение мьютекса из стандартной библиотеки через await
|
||||||
|
|
||||||
|
Квиз
|
||||||
|
Что произойдет при переполнении буфера у `tokio::sync::mpsc::channel()`?
|
||||||
|
Поток заблокируется до освобождения места
|
||||||
|
Нет, если буфер заполнен, текущая задача приостанавливается (`.await`), но поток может продолжить выполнение других задач. Это ключевое отличие от `std::sync::mpsc::sync_channel`
|
||||||
|
Новые сообщения будут теряться
|
||||||
|
Нет, каналы не теряют сообщения
|
||||||
|
Задача временно уступит управление (await), пока не появится место
|
||||||
|
Правильно! При переполнении буфера отправка сообщения асинхронно приостанавливает текущую задачу (`.await`), позволяя планировщику выполнять другие задачи до освобождения места в канале
|
||||||
|
Произойдет паника
|
||||||
|
Нет, каналы при переполнении не паникуют
|
||||||
|
|
||||||
|
## Таймаут по вызову Future
|
||||||
|
ОР - Понимает, как использовать таймауты, неблокирующее ожидание и cancelation safety.
|
||||||
|
Для ожидания фьючерса с таймаутом в токио есть удобная функция tokio::time::timeout
|
||||||
|
```rust
|
||||||
|
if let Err(_) = tokio::time::timeout(Duration::from_secs(1), std::future::pending()).await {
|
||||||
|
println!("не выполнилось в течении 1 секунды");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
По истечении указанного времени исполнение Future отменяется
|
||||||
|
## Вызов cpu-bound кода из асинхронного
|
||||||
|
ОР - Понимает, как использовать таймауты, неблокирующее ожидание и cancelation safety.
|
||||||
|
Так как асинхронность в rust кооперативная, возвращать управление потоком нужно как можно быстрее. Но что если у нас вместо io-bound задачи возникает cpu-bound задача? Тогда задачу нужно вымещать в отдельный thread pool. В токио есть встроенный (который упоминается как worker threads), его удобство заключается в том, что ожидать результата синхронноой функции можно через await, не блокируя исполнения, как с ожиданием у обычного thread pool
|
||||||
|
```rust
|
||||||
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
|
// cpu-bound задачи. К примеру, перемножение матриц
|
||||||
|
multipy_matrixes()
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
Самое интересное, что большинство функций в модуле tokio::fs это такие обертки spawn_blocking над синхронными версиями. К примеру, tokio::fs::read_to_string, что мы использовали ранее в самом токио выглядит так:
|
||||||
|
```rust
|
||||||
|
pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
|
||||||
|
let path = path.as_ref().to_owned();
|
||||||
|
// asyncify - вызывает внутри spawn_blocking,
|
||||||
|
// просто с дополнительной небольшой проверкой
|
||||||
|
asyncify(move || std::fs::read_to_string(path)).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Это обусловленно тем, у некоторых операционных систем блокирующие чтения файлов. Сетевые же запросы, к примеру, сделаны асинхронно
|
||||||
|
|
||||||
|
## Cancelation safety
|
||||||
|
ОР - Понимает, как использовать таймауты, неблокирующее ожидание и cancelation safety.
|
||||||
|
Cancellation safety — это свойство future, которое гарантирует, что если её отменить (вызов drop), это не приведёт к потере данных или нарушению логики программы. К примеру, разберем такой псевдокод:
|
||||||
|
```rust
|
||||||
|
let tcpstream = TcpStream::connect("localhost:8000").await.unwrap();
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
v = read_some_data(&mut tcpstream) => {
|
||||||
|
// do something with data
|
||||||
|
},
|
||||||
|
_ => tokio::time::sleep(Duration::from_millis(10)) => {}
|
||||||
|
}
|
||||||
|
println!("Cyclel timeout");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Выглядит хорошо: ожидаем чтение, а по таймауту перезапускаем цикл. Но что если read_some_data уже успела прочитать какие-то данные до таймаута, но не успела вернуть их? Тогда эти данные будут утеряны навсегда. Такие функции, отмена выполнения которых приводит к потере данных не считаются cancelation safe.
|
||||||
|
Примерами таких функций из токио являются:
|
||||||
|
`tokio::io::AsyncReadExt::read_exact`
|
||||||
|
`tokio::io::AsyncReadExt::read_to_end`
|
||||||
|
`tokio::io::AsyncReadExt::read_to_string`
|
||||||
|
`tokio::io::AsyncWriteExt::write_all`
|
||||||
454
4.2/1.md
Normal file
454
4.2/1.md
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
ОР - Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
|
||||||
|
ОР - Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
|
||||||
|
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
|
||||||
|
ОР - Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
|
||||||
|
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
|
||||||
|
|
||||||
|
- [x] Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
|
||||||
|
- [x] Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
|
||||||
|
- [x] Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
|
||||||
|
- [x] Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
|
||||||
|
- [x] Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
|
||||||
|
- [x] Умеет импользовать bindgen и cc для генерации Rust API из Си header файлов
|
||||||
|
- [x] Практика: сборка Си библиотеки (https://github.com/DaveGamble/cJSON).
|
||||||
|
|
||||||
|
- [ ] Прописать ОРы для глав
|
||||||
|
- [ ] Придумать квизы
|
||||||
|
|
||||||
|
старт
|
||||||
|
- [x] Сначала практически расписать бест практис с с аби,
|
||||||
|
- [x] потом рассказать про биндген
|
||||||
|
- [x] потом сс
|
||||||
|
- [x] потом линковка
|
||||||
|
- [x] ??а если мы захотим уже раст использовать как бблиотеку в другом языке?
|
||||||
|
- [x] рассказать низкоуровнево как прописывать экспорт раст кода
|
||||||
|
- [x] (упомянуть про как посмотреть экспорт)
|
||||||
|
- [x] в том числе и про разные способы линковки
|
||||||
|
- [x] потом показать на примере линковки к питону
|
||||||
|
- [ ] потом рассказать про бест практис (возможно решив какую-нибудь проблему в предыдущей главе)
|
||||||
|
## C ABI Best practice
|
||||||
|
- [x] Рассказать немного про бест практис, протом перейти к bindgen
|
||||||
|
- [x] best practice:
|
||||||
|
- [x] use std::ffi types
|
||||||
|
- [x] use \*-sys
|
||||||
|
- [x] Добавление поля links в Cargo.toml крейта \*-sys
|
||||||
|
- [x] use bindgen
|
||||||
|
- [x] генерация с build.rs (build dependencies)
|
||||||
|
- [ ] Option nonnull for c abi (null optimization)
|
||||||
|
?cstr
|
||||||
|
## Линковка
|
||||||
|
Рассказать про то, как низкоуровнево линкуется при помощи rustc, потом рассказать про то, как линковать с build.rs и потом cc
|
||||||
|
# Start
|
||||||
|
|
||||||
|
В прошлом уроке вы узнали, как описывается C ABI в Rust. Давайте теперь посмотрим, как используется C ABI в production коде. Для этого изучим best practice использования C ABI.
|
||||||
|
|
||||||
|
## Правильное использование C ABI
|
||||||
|
ОР - Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
|
||||||
|
### Совместимость начинается со стандартов
|
||||||
|
Допустим, у нас есть такая волшебная функция на С:
|
||||||
|
```c
|
||||||
|
int square(int x) {
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Как описать эту функцию в Rust? Возможно так?
|
||||||
|
```rust
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn square(x: i32) -> i32;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Но тут возникает проблема: в rust явно указана размерность в 32 бита, в то время как в C, int может быть не только размером 32 бита (к примеру у avr, той самой, которая используется в Arduino, размер int 16 бит). Для таких исключений есть модуль core::ffi (который реэкспортирован как std::ffi, так что его можно встретить с импортом по этому пути, но рекомендуется использовать именно по пути core от для поддержки {{no_std}}[https://docs.rust-embedded.org/book/intro/no-std.html] среды). И для описания функций рекомендуется использовать именно эти типы, так как в rust может добавиться поддержка новой архитектуры, со своими типами из C, и программа может незаметно стать памяти-небезопасной.
|
||||||
|
Перепишем функцию:
|
||||||
|
```rust
|
||||||
|
use core::ffi::c_int;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn square(x: c_int) -> c_int;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Заметка (extern без unsafe):
|
||||||
|
(может быть под тогл/скрываемый блок?)
|
||||||
|
Если читать код библиотек, можно встретить, что extern пишется без unsafe (взаимодействие и смысл этого ключевого слова вы изучите в следующем уроке). Так можно было делать раньше, но в rust это стало обязательным с версии 1.85. Идея заключается в том, что пишущий код человек может задекларировать функцию неправильно, и это обязанность программиста, а не компилятора соблюсти здесь безопасность по памяти (пример такой ситуации вы только что разобрали).
|
||||||
|
### Optional pointer (null optimization) - нужно ли?
|
||||||
|
### Отделение взаимодействия с ffi в отдельный крейт
|
||||||
|
Если вы пишете библиотеку, взаимодействующую с C ABI, отделяйте это взаимодействие в отдельный крейт (который обычно называют \*-sys). На это есть несколько веских {{причин}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages]:
|
||||||
|
- Несколько разных библиотек могут переиспользовать уже написанный код для взаимодействия с библиотекой
|
||||||
|
- Так, одна и та же библиотека не будет собираться несколько раз и не будет слинкована несколько раз (то есть не будет существовать несколько разных или одинаковых версий библиотеки в бинаре)
|
||||||
|
- Легкость изменения библиотеки, от которой зависит бинарь (версии, поиска ее пути и всей остальной конфигурации)
|
||||||
|
|
||||||
|
Квиз - множественный выбор
|
||||||
|
Зачем отделять FFI-взаимодействие в отдельный \*-sys крейт?
|
||||||
|
|
||||||
|
Чтобы ускорить выполнение сборки программы с нуля.
|
||||||
|
Нет. Это не ускорит сборку конченой программы. Но, будет полезно, если разрабатываете библиотеку, ведь при каждом изменении не будет выполнятся build скрипт с линковкой.
|
||||||
|
|
||||||
|
Чтобы ускорить выполнение программы во время рантайма.
|
||||||
|
Нет, отделение логики не ускорит программу.
|
||||||
|
|
||||||
|
Чтобы переиспользовать взаимодействие с библиотекой.
|
||||||
|
Правильно, при отделении логики в -sys крейт, ее смогут переиспользовать другие крейты. Так же, как и при отделении логики в фукнцию, которую можно использовать в другом месте кода
|
||||||
|
|
||||||
|
Чтобы не собирать статическую библиотеку несколько раз.
|
||||||
|
Правильно, при переиспользовании -sys крейта статические библиотеки, которые собираются из исходного кода, будут собраны и включены в бинарь только один раз, вместо сборки в каждом использующем библиотеку крейте
|
||||||
|
|
||||||
|
Чтобы было проще обновлять или перенастраивать зависимую C-библиотеку (версия, путь, параметры сборки).
|
||||||
|
Правильно, программист сможет изменить используемую библиотеку через аттрибут {{target.\<triple>.\<links>}}[https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinks] в файле настроек cargo (./cargo/config.toml)
|
||||||
|
|
||||||
|
Чтобы избежать необходимости писать unsafe-код в основном проекте.
|
||||||
|
Нет. Хоть и часть unsafe логики отделяется в этот крейт (а именно декларация функций), взаимодействие с этими функциями все еще будет не безопасным
|
||||||
|
### Добавление поля links в Cargo.toml крейта \*-sys
|
||||||
|
При написании крейта, который линкуется с одной библиотекой, используйте {{links}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key] в манифесте Cargo.toml, для указания, с какой библиотекой происходит линковка:
|
||||||
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
[package]
|
||||||
|
links = "mylib"
|
||||||
|
```
|
||||||
|
Благодаря этому, программисты, в коде которых будет использоваться такой крейт, смогут {{перезаписать использование build скрипта}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#overriding-build-scripts], благодаря чему мы и получим третий плюс из предыдущего пункта
|
||||||
|
### Автоматическая генерация extern внешних функций
|
||||||
|
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
|
||||||
|
С обновлением версий библиотек, меняется и их интерфейс. К примеру, могут добавиться новые функции. Но поддерживать актуальные декларации внешних функций (так называемые {{байндинги}}[на английском термин binding, исходит от to bind, который переводится как связывать. говорит компилятору языка, что у нас существует такая функция, а линкеру объясняет, с какой функцией надо связывать]) вручную весьма трудозатратная задача (как и в принципе изначальное написание таких байндингов вручную). На помощь приходит автоматическая генерация при помощи крейта {{bindgen}}[https://github.com/rust-lang/rust-bindgen].
|
||||||
|
Одним из вариантов использования bindgen является его cli. Установите его:
|
||||||
|
```bash
|
||||||
|
cargo install bindgen-cli
|
||||||
|
```
|
||||||
|
Создайте новый проект, запишите следующее в файлы:
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
include!("./bindgen.rs");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Squared eleven: {}", unsafe { square(11) });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```c
|
||||||
|
// src/mylib.c
|
||||||
|
int square(int x) {
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```c
|
||||||
|
// src/mylib.h
|
||||||
|
int square(int x);
|
||||||
|
```
|
||||||
|
теперь используем bindgen для генерации байндингов:
|
||||||
|
```bash
|
||||||
|
bindgen src/mylib.h -o src/bindgen.rs
|
||||||
|
```
|
||||||
|
можете посмотреть в файл `src/bindgen.rs`, и увидеть там аналогичную декларацию функции, что вы писали, но сгенерированную автоматически :)
|
||||||
|
Теперь соберем C библиотеку:
|
||||||
|
```bash
|
||||||
|
clang -c src/mylib.c -o mylib.o
|
||||||
|
```
|
||||||
|
*Вместо clang можно использовать и gcc (и, теоретически, любой другой C компилятор), но так как rust зависит от инфраструктуры llvm, все примеры будут именно для clang*
|
||||||
|
И соберем и запустим программу:
|
||||||
|
```bash
|
||||||
|
RUSTFLAGS="-L. -l./mylib.o" cargo run
|
||||||
|
```
|
||||||
|
*Переменная среды RUSTFLAGS позволяет добавить флаги, которые будут переданы rustc. В данном случае переданы флаги, которые говорят включить в сборку вашу библиотеку. То, как обычно это делается (через скрипт сборки и через cc) будет разобрано чуть позже в этом уроке.*
|
||||||
|
|
||||||
|
Ваша программа успешно запустилась и выдала ожидаемый результат:
|
||||||
|
```
|
||||||
|
Squared eleven: 121
|
||||||
|
```
|
||||||
|
## Автоматизация сборки
|
||||||
|
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
|
||||||
|
Для того, чтобы не делать все это вручную, в cargo есть возможность написать свой скрипт сборщика, который выполнит нужные нам действия перед компиляцией и передаст cargo инструкции для компиляции кода. Обычно такой код расположен в файле build.rs (но путь к нему можно изменить через атрибут {{build}}[https://doc.rust-lang.org/cargo/reference/manifest.html#the-build-field] в Cargo.toml).
|
||||||
|
Давайте сделаем то же самое, но используя build.rs. Для начала, подчистим выхлоп предыдущего эксперимента:
|
||||||
|
```bash
|
||||||
|
rm src/bindgen.rs # но пока оставим mylib.o
|
||||||
|
```
|
||||||
|
Теперь напишем build.rs (в корне проекта):
|
||||||
|
```rust
|
||||||
|
// build.rs
|
||||||
|
use bindgen;
|
||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let bindings = bindgen::builder()
|
||||||
|
// Файл, для которого создаются байндинги
|
||||||
|
.header("src/mylib.h")
|
||||||
|
// Перезапуск сборки при изменении переданных файлов
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
// Сгенерировать байндинги
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings");
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
bindings
|
||||||
|
// Записать получившиеся байндинги в файл OUT_DIR/bindgen.rs
|
||||||
|
.write_to_file(out_path.join("bindgen.rs"))
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
// Передать линкеру, что библиотеки нужно искать в нынешней папке
|
||||||
|
// Аналогичен флагу -L.
|
||||||
|
// Информация в cargo передается через stdout
|
||||||
|
println!("cargo::rustc-link-search=.");
|
||||||
|
// Передать линкеру, что нужно слинковать с библиотекой ./mylib.o
|
||||||
|
// Аналогичен флагу -l./mylib.o
|
||||||
|
println!("cargo::rustc-link-lib=./mylib.o");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Как вы тут заметили, вместо вызова cli тут используется вызов крейта bindgen. Это рекомендуемый способ использования bindgen, и cli на практике используется редко. Чтобы иметь возможность использовать его в сборочном скрипте, добавьте его в зависимости для сборки:
|
||||||
|
```bash
|
||||||
|
cargo add bindgen --build
|
||||||
|
```
|
||||||
|
Так же, вместо выхлопа bindgen.rs в папку src/ выходной файл будет находиться внутри папки {{OUT_DIR}}[https://doc.rust-lang.org/cargo/reference/environment-variables.html#:~:text=.exe.-,OUT_DIR,-%E2%80%94%20If%20the%20package]. Это именно та папка, куда build.rs должен ложить все свои выходные файлы. Ни в коем случае не надо ложить файлы в src! Так как выходные файлы не являются сами по себе частью проекта, и являются промежуточным результатом компиляции, то и находится они должны в одной из подпапок target, а именно в OUT_DIR.
|
||||||
|
Так как путь к bindgen.rs изменился, замените предыдущий include! в src/main.rs на такой:
|
||||||
|
```rust
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
|
||||||
|
```
|
||||||
|
Попробуем запустить все это:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
Байндинги автоматически были созданы и линковка тоже произошла, осталось добавить сборку C кода в библиотеку.
|
||||||
|
## Сброка C кода в Rust
|
||||||
|
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
|
||||||
|
Так же, как и для генерации байндингов существует bindgen, для компиляции библиотек существует крейт`cc`. Фактически, он под капотом вызывает компилятор C со всеми задаными флагами. Добавим его в зависимости для сборки:
|
||||||
|
```bash
|
||||||
|
cargo add cc --build
|
||||||
|
```
|
||||||
|
И обновим build.rs:
|
||||||
|
```rust
|
||||||
|
// build.rs
|
||||||
|
use bindgen;
|
||||||
|
use cc;
|
||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let bindings = bindgen::builder()
|
||||||
|
// Файл, для которого создаются байндинги
|
||||||
|
.header("src/mylib.h")
|
||||||
|
// Перезапуск сборки при изменении переданных файлов
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
// Сгенерировать байндинги
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings");
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
bindings
|
||||||
|
// Записать получившиеся байндинги в файл OUT_DIR/bindgen.rs
|
||||||
|
.write_to_file(out_path.join("bindgen.rs"))
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
cc::Build::new()
|
||||||
|
// добавить src/mylib.c в выходную библиотеку
|
||||||
|
.file("src/mylib.c")
|
||||||
|
// скомпилировать C код как библиотеку libmylib.a в папке OUT_DIR
|
||||||
|
.compile("mylib");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Заметьте, что пропало явное обозначение, что нужно слинковать с библиотекой. cc сам передаст cargo название библиотеки, с которой нужно слинковать.
|
||||||
|
Перед запуском, для чистоты эксперимента, удалите старый файл:
|
||||||
|
```bash
|
||||||
|
rm mylib.o
|
||||||
|
```
|
||||||
|
Теперь сборку и запуск можно делать просто через `cargo run`, не выполняя лишних команд.
|
||||||
|
## Линковка с динамической библиотекой
|
||||||
|
До этого мы собирали только со статической библиотекой. Теперь, давайте слинкуем с динамической библиотекой. Для этого, сначала соберем ее:
|
||||||
|
```bash
|
||||||
|
clang src/mylib.c -shared -o libmylib.so
|
||||||
|
```
|
||||||
|
А теперь, в build.rs уберите использование cc и добавьте в конце функции main:
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
// Генерация байндингов
|
||||||
|
// ...
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search=.");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=mylib");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
// update
|
||||||
|
В cargo:rustc-link-lib передано значение dylib=mylib. Оно позволяет явно указать, что нужно подгрузить динамическую библиотеку (dylib) с название mylib (у такой библиотеки будет название libmylib.so на примере linux). Значение \[тип=] опционально и может быть одним из:
|
||||||
|
- dylib - динамическая библиотека
|
||||||
|
- static - статическая библиотека
|
||||||
|
- framework - специфичный формат для MacOS, содержащий динамичекую библиотеку с дополнительными ресурсами
|
||||||
|
cc не поддерживает генерацию динамических библиотек, так как она не будет являться частью создаваемого cargo и rustc бинаря. Динамические библиотеки обычно являются частью системы (те же libc, zlib, openssl, присутствие которых бинари будут ожидать по стандартным путям). Установлены они могут быть, к примеру, через apt, pacman, установщики windows, и так далее. Либо создаются отдельно и поставляются с программой.
|
||||||
|
## Линковка в рантайме
|
||||||
|
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
|
||||||
|
Последний способ линковки в рантайме, позволяет не тратить лишний раз оперативную память, загружая код лишь только тогда, когда он нужен. Для независимости от платформы будем использовать dlopen и dlsym.
|
||||||
|
- dlopen - подгружает нужную нам библиотеку
|
||||||
|
- dlsym - ищет в библиотеке нужный символ и выдает его адрес, и это может быть не только функция, но и, к примеру, константа, статическая переменая, и тд.
|
||||||
|
Для начала, удалите build.rs. Добавьте libc как зависимость.
|
||||||
|
Напишите в src/main.rs:
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
use libc::{dlopen, dlsym};
|
||||||
|
use std::ffi::c_int;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mylib = unsafe { dlopen(c"./libmylib.so".as_ptr(), 0) };
|
||||||
|
let square: extern "C" fn(c_int) -> c_int =
|
||||||
|
unsafe { std::mem::transmute(dlsym(mylib, c"square".as_ptr())) };
|
||||||
|
println!("Squared eleven: {}", unsafe { square(11) });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Запустив это, получите ровно тот же результат, что и раньше, но теперь библиотеку мы подгружаем сами. Кстати, линкер для динамических библиотек делает то же самое, но подгружает библиотеки еще до вызова main, а адреса функций кладет в заранее заготовленную таблицу функций.
|
||||||
|
|
||||||
|
## Практика
|
||||||
|
Создайте новый проект, с таким содержимым в src/main.rs:
|
||||||
|
```rust
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
|
||||||
|
const TEST_JSON: &CStr = c"{
|
||||||
|
\"meaning_of_life\": 42
|
||||||
|
}";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let json: *mut cJSON = unsafe { cJSON_Parse(TEST_JSON.as_ptr()) };
|
||||||
|
|
||||||
|
let json_str = unsafe { cJSON_PrintUnformatted(json) };
|
||||||
|
let json_str = unsafe { CString::from_raw(json_str) };
|
||||||
|
let json_str = json_str.to_str().unwrap();
|
||||||
|
assert_eq!(json_str, r#"{"meaning_of_life":42}"#);
|
||||||
|
|
||||||
|
let meaning_of_life = unsafe { cJSON_GetObjectItem(json, c"meaning_of_life".as_ptr()) };
|
||||||
|
let meaning_of_life = unsafe { cJSON_GetNumberValue(meaning_of_life) };
|
||||||
|
println!("Meaning of life: {}", meaning_of_life);
|
||||||
|
assert_eq!(meaning_of_life, 42f64);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Создайте такой build.rs, чтобы этот код заработал. Используйте библиотеку [cJSON](https://github.com/DaveGamble/cJSON), ее можно склонировать прямо в корень проекта.
|
||||||
|
Подсказки:
|
||||||
|
Экспорт функций можно сгенерировать от cJSON.h используя bindgen
|
||||||
|
Чтобы была возможность использовать код из cJSON, соберите библиотеку через cc
|
||||||
|
Байндинги записываются в bindgen.rs в папке, указанной в переменной среды OUT_DIR
|
||||||
|
Чек-лист:
|
||||||
|
Линкуется cJSON как статическая библиотека
|
||||||
|
Файл bindgen.rs с байндингами находится в папке, указанной в переменной среды OUT_DIR
|
||||||
|
Тесты проходят
|
||||||
|
Решение:
|
||||||
|
```rust
|
||||||
|
use bindgen;
|
||||||
|
use cc;
|
||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let bindings = bindgen::builder()
|
||||||
|
.header("cJSON/cJSON.h")
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings");
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
bindings
|
||||||
|
.write_to_file(out_path.join("bindgen.rs"))
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
cc::Build::new()
|
||||||
|
// добавить src/mylib.c в выходную библиотеку
|
||||||
|
.file("cJSON/cJSON.c")
|
||||||
|
// скомпилировать C код как библиотеку libmylib.a в папке OUT_DIR
|
||||||
|
.compile("cJSON");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust как библиотека
|
||||||
|
ОР - Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
|
||||||
|
ОР - Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
|
||||||
|
В течении всего урока мы использовали C код из Rust кода. А что если нужно будет использовать rust код из других языков, к примеру, того же C, Go, или Python? И это тоже возможно, через тот же C ABI, но на экспорт. Такая функция декларируется так:
|
||||||
|
```rust
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn doublefast(x: u32) -> u32 {
|
||||||
|
x << 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- no_mangle - говорит rust, что название функции не надо преобразовывать
|
||||||
|
- extern "C" - использовать C ABI
|
||||||
|
- pub extern - экспорт функции
|
||||||
|
Создайте новый проект как библиотеку:
|
||||||
|
```bash
|
||||||
|
cargo new --lib mylib
|
||||||
|
```
|
||||||
|
В Cargo.toml добавьте:
|
||||||
|
```toml
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"] # cdylib означает, что нужно собрать динамическую библиотеку
|
||||||
|
```
|
||||||
|
А в src/lib.rs внесите показанный ранее код:
|
||||||
|
```rust
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn doublefast(x: u32) -> u32 {
|
||||||
|
x << 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Соберите библиотеку. Выходным файлом получится target/debug/libmylib.so (В зависимости от вашей ОС расширение .so, используемое в linux, может замениться на .dylib в macos или .dll в windows). Можем проверить наличие символа в библиотеке для функции через nm из пакета binutils:
|
||||||
|
```bash
|
||||||
|
# nm выведет весь список символов
|
||||||
|
# grep покажет только doublefast, если он есть
|
||||||
|
nm target/debug/libmylib.so | grep doublefast
|
||||||
|
```
|
||||||
|
Попробуем использовать нашу функцию, к примеру, такой код в Python:
|
||||||
|
```python
|
||||||
|
from ctypes import cdll
|
||||||
|
mylib = cdll.LoadLibrary("target/debug/libmylib.dylib")
|
||||||
|
print(mylib.doublefast(8))
|
||||||
|
```
|
||||||
|
Успешно выведет 16
|
||||||
|
*Информация о том, какие типы функция принимает и выдает не сохраняется в библиотеке, просто по дефолту python считает, что функция принимает и выдает i32*
|
||||||
|
Такое применение имеет практическую пользу: аналогичный код на rust выполняется в разы быстрее, чем код на python, поэтому много библиотек для python пишутся на C, а со становления rust популяным многие уже пишутся на rust.
|
||||||
|
### Сборка статической библиотеки
|
||||||
|
Что собрать то же самое, но в статическую библиотеку, нужно в `crate-type` изменить `cdylib` на `staticlib` (либо можно оставить и то и то, тогда будут собираться обе версии библиотеки):
|
||||||
|
```toml
|
||||||
|
[lib]
|
||||||
|
crate-type = ["staticlib"]
|
||||||
|
# или можно сделать такой вариант, тогда соберется
|
||||||
|
# два файла с разными расширениями
|
||||||
|
# crate-type = ["cdylib", "staticlib"]
|
||||||
|
```
|
||||||
|
Попробуем использовать статическую библиотеку, но теперь пример для C:
|
||||||
|
```c
|
||||||
|
// main.c
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
uint32_t doublefast(uint32_t);
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Double six: %d\n", doublefast(6));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Соберем C код с использованием нашей библиотеки:
|
||||||
|
```bash
|
||||||
|
clang main.c target/debug/libmylib.a -o ./a.exe
|
||||||
|
```
|
||||||
|
При запуске ./a.exe будет получен ожидаемый результат.
|
||||||
|
// update
|
||||||
|
Для crate-type возможны следующие значения:
|
||||||
|
- staticlib - сборка статической библиотеки
|
||||||
|
- dylib - сборка динамической библиотеки, предназначенной для использования в rust коде. Собираться использующий код и dylib должны одной и той же версией компилятора, так как у dylib нет стабильного интерфейса
|
||||||
|
- cdylib - сборка динамической библиотеки, но предназначеную для использования во всех языках, поэтому отличается от dylib:
|
||||||
|
- интерфейсы фукнций следуют c abi
|
||||||
|
- в библиотеку будет включена стандартная rust библиотека (rust-std)
|
||||||
|
- bin - сборка конечной программ
|
||||||
|
- lib - соберет библиотеку, вид которой будет выбран компилятором (стандартное значение для крейтов-библиотек)
|
||||||
|
- rlib - внутренний тип библиотек rust
|
||||||
|
- proc-macro - крейт, в котором экспортированы proc макросы
|
||||||
|
|
||||||
|
Квиз - одиночный выбор
|
||||||
|
Какое значение нужно указать в crate-type, чтобы использовать ваш rust код как динамическую библитеку в проекте на Go?
|
||||||
|
lib
|
||||||
|
Нет, lib соберет библиотеку на выбор компилятора
|
||||||
|
|
||||||
|
cdylib
|
||||||
|
Правильно, cdylib соберет динамическую библиотеку с C ABI, который подходит для Go
|
||||||
|
|
||||||
|
dylib
|
||||||
|
Нет, хоть dylib и соберет динамическую библиотеку, но полноценно подходить она будет только для rust кода
|
||||||
|
|
||||||
|
staticlib
|
||||||
|
Нет, staticlib соберет статическую библиотеку
|
||||||
|
|
||||||
|
|
||||||
|
// update
|
||||||
|
### cbindgen
|
||||||
|
Так же, как и для генерации rust байндингов из C есть библитека bindgen, для генерации C/C++/cython байндингов из rust кода есть cbindgen:
|
||||||
|
```bash
|
||||||
|
cargo install cbindgen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Итоги
|
||||||
|
В этом уроке мы на практике разобрали, как взаимодействовать с кодом на C в проектах на rust, best practice этого взаимодействия, а так же узнали, как можно взаимодействовать с кодом на rust из других языков. Вы научились автоматически генерировать декларации внешних функций, а так же собирать статические библиотеки при сборке проекта на rust.
|
||||||
|
Далее вы узнаете, что означает ключевое слово unsafe, зачем оно нужно при взаимодействии с внешними библиотеками, а так же изучите best practice при написании unsafe кода.
|
||||||
46
4.2/2.md
Normal file
46
4.2/2.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
- [ ] Умеет писать unsafe код и безопасные обёртки для него.
|
||||||
|
- [ ] Понимает причины UB и знает, как его не допустить. (откуда взялось, что значит и как может нанести вред работе программы)
|
||||||
|
- [ ] для автора - проверка ОРа квизами, включая примеры кода.
|
||||||
|
- [ ] Умеет проектировать безопасный интерфейс для unsafe кода.
|
||||||
|
- [ ] Знает best practice для написания unsafe кода.
|
||||||
|
- [ ] Можно использовать примеры из third party крейтов.
|
||||||
|
|
||||||
|
- [ ] Начать с проблемы, когда компилятор не может гарантировать безопасность по памяти (но без этого невозможно написать программу), возможно из ub
|
||||||
|
- [ ] Рассказать про причины ub
|
||||||
|
- [ ] Рассказать, чем является unsafe, ответственность на программисте, про ub (НЕ является избавлением от borrow checker)
|
||||||
|
- [ ] Рассказать про применение unsafe (взаимодействие с С, оптимизация (вспомнить небезопасную либу для бэкенда: rocket или actix), написание основы/базы языка)
|
||||||
|
- [ ] Определении функции unsafe если соблюдение инвариантов висит на пользователе (при написании такой функции смотреть - является ли сам интерфейс функции safe)
|
||||||
|
- [ ] примеры из third party
|
||||||
|
- [ ] Рассказать про бест практис при написании unsafe:
|
||||||
|
- [ ] Лучший unsafe - отсутствующий unsafe
|
||||||
|
- [ ] Уменьшение зоны unsafe (легче найти баг + проще просчитать ub)
|
||||||
|
- [ ] Safety
|
||||||
|
- [ ] SAFETY
|
||||||
|
- [ ] assert_unsafe_precondition!
|
||||||
|
- [ ] ...
|
||||||
|
- [ ] Практика
|
||||||
|
- [ ] Лайфтаймы как помошники при взаимодействии с c abi
|
||||||
|
- [ ] Drop
|
||||||
|
|
||||||
|
Почему unsafe внутри unsafe fn:
|
||||||
|
фн показывает, что инварианты должен соблюдать пользователь функции
|
||||||
|
ансейф блок же показывает скоуп, где нужно искать баги (где они и будут тк анйсейф операция)
|
||||||
|
подводный камень:
|
||||||
|
при ансейф баг может возникать в любом из блоков анйсефа, даже если он возникает в другом месте
|
||||||
|
|
||||||
|
ub: из-за различий архитектру цп и ос и для оптимизаций
|
||||||
|
|
||||||
|
еслм возникает баг, значит неправильно реализованы инварианты в одном из unsafe блоков с которым взаимодействовали (даже в совершенно другом месте)
|
||||||
|
|
||||||
|
Возможно рассказать еще про это все?
|
||||||
|
- pointers
|
||||||
|
- nonnull
|
||||||
|
- phantomdata (инвариантность/ковариантность и тд)
|
||||||
|
- unsafecell
|
||||||
|
- send/sync + unsafe trait markers (мб практический пример: написание собстенного mutex)
|
||||||
|
- unsafe traits
|
||||||
|
-
|
||||||
|
|
||||||
|
При описании бест практис про Safety, привести пример, почему нужно (а еще потому, что при изменении логики функции самим тоже нужно следить, какие инварианты висят за пользователем)
|
||||||
|
Практика: подумайте над тем, какие инварианты тут должны быть соблюдены и какие из них не соблюдены. или пропробовать найти несоблюденный инвариант и "взломать" программу?)
|
||||||
|
допустим, нам нужно дя создания сокета узнать, можем ли мы использовать tcp/udp/icmp/etc, для этого используем сискол WSAEnumProtocolsA
|
||||||
3
Untitled 1.base
Normal file
3
Untitled 1.base
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
views:
|
||||||
|
- type: table
|
||||||
|
name: Table
|
||||||
1
Untitled 1.canvas
Normal file
1
Untitled 1.canvas
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
3
Untitled.base
Normal file
3
Untitled.base
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
views:
|
||||||
|
- type: table
|
||||||
|
name: Table
|
||||||
1
Untitled.canvas
Normal file
1
Untitled.canvas
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Loading…
x
Reference in New Issue
Block a user