참고 : https://book.async.rs/tutorial/specification.html
채팅은 TCP
를 통한 간단한 텍스트
프로토콜
을 사용합니다. 프로토콜은 \n
으로 구분된 utf-8
메시지로 구성됩니다.
client는 server에 연결하고 login을 첫 번째 라인으로 보냅니다. 이 후 client는 다음 구문을 사용하여 다른 client에 메시지를 보낼 수 있습니다.
login1, login2, ... loginN: message
각 클라이언트는 from login: message
메시지를 수신합니다.
터미널 화면에서 아래와 같이 비슷하게 보일겁니다.
On Alice's computer: | On Bob's computer:
> alice | > bob
> bob: hello < from alice: hello
| > alice, bob: hi!
< from bob: hi!
< from bob: hi! |
채팅 서버가 해야하는 중요한 일들 중 하나는 많은 동시 연결을 추적하는 것
입니다. 채팅 client의 주요 과제는 동시 발신 메시지
, 수신 메시지
및 사용자 입력
을 관리
하는 것입니다.
카고를 이용하여 프로젝트를 생성합니다.
$ cargo new a-chat
$ cd a-chat
Cargo.toml
파일에 아래 라인을 추가합니다.
[dependencies]
futures = "0.3.0"
async-std = "1"
서버 구현을 위한 작업을 하겠습니다.
#![allow(unused)]
use async_std::{
io::{BufReader, BufWriter},
net::{TcpListener, TcpStream, ToSocketAddrs},
prelude::*,
task,
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
async_std
라이브러리에서 비동기 작업을 수행하기 위한 일부 모듈과 네트워킹 관련 모듈을 가져오는 코드
입니다. 이를 통해 러스트에서 비동기 소켓 프로그래밍을 할 수 있습니다.#![allow(unused)]
는 러스트의 컴파일러에게 컴파일러가 사용하지 않은
변수
, 함수
또는 모듈 등
이 있어도 경고를 표시하지 않도록 지시하는 디렉티브
입니다. 이를 사용하면 코드에 특정 부분에 대해 경고를 표시하지 않아도 되기 때문에, 개발자는 불필요한 경고 메시지를 제거하고 더 깨끗한 코드를 유지
할 수 있습니다.
0.1. use 키워드
를 사용하여 다른 crate들의 모듈을 가져올 수 있습니다.
extern
은 Rust에서 다른 crate에 있는 함수나 데이터를 가져와서 사용
하거나, 라이브러리를 링크
하는 데 사용되는 예약어
입니다. extern crate 문은 이전에 사용되었으며, Rust 2018 Edition 이후
로는 더 이상 필요하지 않습니다
. Rust 2018 Edition부터는 crate들이 라이브러리로서 불러오지 않고 모듈로서 불러와져서, extern crate 대신
prelude
: 표준 라이브러리의 prelude는 자주 사용되는 트레잇(trait)들을 쉽게 사용할 수 있게 해주는 모듈
입니다. async_std의 prelude는 주로 비동기 I/O 작업에 필요한 트레잇들을 제공합니다.
1.1. 자주 사용되는 타입
& 트레잇
을 쉽게 사용할 수 있도록 가져오는 모듈.
Rust 표준 라이브러리에는 std::prelude
라는 모듈이 있으며, 이 모듈은 Vec, Option, Result
등과 같은 기본적인 타입
들과, ToString, Into, From
등과 같은트레잇
들을 내부적으로 가져옵니다. 이렇게 prelude 모듈을 사용하면, 매번 긴 경로를 사용하여 타입이나 트레잇을 가져올 필요 없이
, 간단하게 use crate::prelude::*; 구문
을 사용하여 필요한 타입이나 트레잇들을 가져올 수 있습니다. 이는 코드의 가독성
을 높이고, 개발자의 생산성을 향상
시킵니다.
task
: 이 모듈은 비동기 작업을 생성하고 실행하는 데 필요한 함수와 트레잇을 제공
합니다.
net
: 이 모듈은 네트워킹에 필요한 기능들을 제공
합니다. 여기서는 TcpListener
와 ToSocketAddrs
를 가져왔는데, 이들은 각각 TCP 연결을 수신
하고 주소를 소켓 주소로 변환
하는 데 사용됩니다.
BufReader
와 BufWriter
: 입력과 출력을 위한 버퍼링 기능을 제공합니다.
type Result<T> = ...
코드를 통해서 type aliasing이 사용되고 있어요.
5.1. Result: 이것은 러스트에서 에러 처리를 위해 사용되는 enum 타입
입니다. Ok(T)
또는 Err(E)
두 가지 값을 가질 수 있으며, 여기서 T는 연산의 성공 결과 타입, E는 실패했을 때의 에러 타입을 나타냅니다. 이 코드에서는 Result 타입에 대한 별칭을 설정
하고 있으며, 에러 타입
은Box<dyn std::error::Error + Send + Sync>로 설정
되어 있습니다.
5.2. Box<dyn std::error::Error + Send + Sync>
: 이것은 힙
에 할당된 동적 타입의 에러
를 의미합니다. 여기서 Send
와 Sync
는 멀티스레딩 환경에서 이 에러 타입이 안전하게 전송(Send)되거나 공유(Sync)될 수 있음을 보장
합니다.
5.3. 이러한 타입 별칭은 코드의 가독성을 높이고, 반복적으로 긴 타입 선언을 작성하는 것을 줄이는데 도움이 됩니다. 이렇게 설정한 Result 타입은 이후 비동기 네트워킹 코드
에서 에러를 반환할 때 사용
될 것입니다.
이제 서버의 루프를 작성합니다.
async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
let listener: TcpListener = TcpListener::bind(addr).await?;
let mut incoming: async_std::net::Incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream: TcpStream = stream?;
println!("Accepting from: {}", stream.peer_addr()?);
let _handle: task::JoinHandle<()> = spawn_and_log_error(connection_loop(stream));
}
Ok(())
}
메인 루프를 설정
하는 함수
입니다. 이 함수는 주어진 주소에서 들어오는 TCP 연결을 수신
하고, 각 연결
에 대해 처리를 수행
합니다.async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()>:
accept_loop 함수는 비동기 함수(async fn)로 선언되어 있습니다. 이 함수는 소켓 주소로 변환될 수 있는 어떤 타입의 addr
인자를 받습니다(impl ToSocketAddrs). 이 함수는 Result<()> 타입을 반환
하며, 이는 함수가 성공
적으로 수행되면 Ok(())
를 반환하고, 에러
가 발생하면 Err(E)
를 반환함을 의미합니다. 여기서 E
는 이전에 정의한 Box<dyn std::error::Error + Send + Sync> 타입
입니다.
impl ToSocketAddrs
: 이 문법은 함수의 매개변수로 특정 트레잇(Trait)을 구현하는 어떤 타입이든 받을 수 있음을 나타냅니다. ToSocketAddrs는 러스트 표준 라이브러리의 트레잇으로, 소켓 주소로 변환될 수 있는 여러 가지 타입들에 대해 구현되어 있습니다.
즉, accept_loop(addr: impl ToSocketAddrs)
함수는 ToSocketAddrs 트레잇
을 구현하는 어떤 타입의 addr도 인자로 받을 수 있습니다. 이는 String, &str, SocketAddr, (IpAddr, u16), (Ipv4Addr, u16), (Ipv6Addr, u16), (str, u16), (String, u16), [SocketAddr; N], &[SocketAddr]
등과 같은 다양한 타입을 인자로 받을 수 있음
을 의미합니다.
2.1. impl
키워드
2.1.1. 특정 타입에 대해 trait을 구현 할 때
struct MyStruct {
value: i32,
}
impl std::fmt::Display for MyStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MyStruct {{ value: {} }}", self.value)
}
}
2.1.2. 특정 타입에 메소드를 추가할 때
:
impl 키워드는 특정 타입에 메소드를 추가하는 데도 사용됩니다. 예를 들어, 아래 코드는 MyStruct 타입에 new라는 메소드를 추가하고 있습니다
struct MyStruct {
value: i32,
}
impl MyStruct {
fn new(value: i32) -> Self {
Self { value }
}
}
let listener = TcpListener::bind(addr).await?;
: 이 줄은 주어진 주소에 TCP 소켓을 바인딩합니다. bind 함수는 비동기 함수이므로, .await 키워드를 사용하여 함수의 완료를 기다립니다. 만약 bind 함수가 에러를 반환하면 ? 연산자를 통해 에러를 바로 반환합니다.
3.1. Q. 만약 await
을 사용하지 않으면 Future
를 반환함. Future는 Rust에서 비동기 연산을 나타내는 타입입니다. 이 Future는 결과가 준비될 때까지 대기
하고, 준비된 결과
를 가져오는 기능
을 제공합니다.
.await을 사용
하면, Future의 결과가 준비될 때까지 현재의 비동기 작업을 일시 중지
하고 다른 비동기 작업이 실행될 수 있도록
합니다. 그리고 Future의 결과가 준비
되면, .await을 사용한 비동기 작업
이 다시 실행
되어 결과를 가져옵니다.
따라서 .await을 사용하지 않고 Future를 그대로 반환하거나 저장하는 것도 가능합니다. 그러나 이 Future의 결과를 가져오려면 어떤 방식으로든 .await을 사용해야 합니다.
3.2. TcpListener::bind
메서드는 비동기 소켓 서버를 작성할 때 사용됩니다. 이 메서드는 소켓 주소를 매개변수로 받아 TcpListener 인스턴스를 바인딩합니다. 이것은 서버가 클라이언트의 연결 요청을 수신할 수 있도록 합니다.
3.2.1. 매개변수 타입:
addr: impl ToSocketAddrs - ToSocketAddrs
트레잇을 구현하는 타입. 이것은 소켓 주소를 나타내는 값으로, TcpListener를 바인딩할 위치를 지정합니다. 예를 들어, 문자열 "127.0.0.1:8080"
또는 튜플 ("127.0.0.1", 8080) 등
이 될 수 있습니다.
3.2.2. 반환 타입:
Result<TcpListener>
- 이 메서드는 Result를 반환하는데, 이것은 함수가 성공적으로 수행되면 Ok(TcpListener)
를 반환하고, 에러가 발생하면 Err를 반환합니다. TcpListener는 TCP 소켓 서버를 나타내며, 클라이언트의 연결 요청을 수신하는 역할을 합니다.
따라서, 아래와 같이 bind 메서드를 호출하면, 주어진 주소에 TCP 소켓 서버가 바인딩되고, 이 서버를 나타내는 TcpListener 인스턴스가 반환됩니다.
let listener = TcpListener::bind("127.0.0.1:8080").await?;
?
: 연산자는 에러 처리를 간결하게
표현하는 데 사용됩니다. 이 연산자는 Result 타입의 값을 처리
하며, Ok
인 경우에는 내부 값을 언랩하여 반환
하고, Err
인 경우에는 현재 함수에서 바로 에러를 반환
합니다.
4.1. 예시
fn some_function() -> Result<(), SomeError> {
let result = could_fail()?;
// Do something with result
Ok(())
}
여기서 could_fail
함수는 Result<T, SomeError>
를 반환합니다. ?
연산자는 이 Result 값을 처리
하며, could_fail가 성공(Ok)
하면 내부 값을 언랩하여 result
에 저장
하고, 실패(Err)
하면 some_function
에서 바로 에러를 반환
합니다.
이렇게 ? 연산자를 사용하면, match
나 if let
을 사용하여 Result를 수동으로 처리하는 번거로움
없이 에러를 간결하게 처리할 수 있습니다. 이는 Rust에서 에러 처리를 간단하면서도 안전하게 만드는 중요한 기능 중 하나입니다.
let mut incoming:async_std::net::Incoming = listener.incoming();
소켓 서버
에서 들어오는 TCP 연결 요청
을 처리하는 스트림을 생성
합니다.
listener.incoming()
는 들어오는 클라이언트 연결의 무한 스트림을 반환
하는데, 이 스트림의 각 항목은 Result<TcpStream> 타입
입니다. Result는 성공 시에 Ok(TcpStream)을 반환하고, 연결 시도 중에 오류가 발생하면 Err를 반환합니다. TcpStream은 클라이언트와의 TCP 연결을 나타냅니다.
while let Some(stream) = incoming.next().await { // 3 // TODO }
:
6.1. incoming
스트림
에서 새로운 항목
(여기서는 TCP 연결
)을 비동기적으로 가져오는 작업
을 반복적으로 수행
합니다. 이 작업은 incoming.next().await를 통해 수행되며, 이는 incoming 스트림
의 다음 항목
을 비동기적으로 가져옵니다.
while let Some(stream) = incoming.next().await
구문은 incoming.next().await
호출이 Some 값을 반환
하는 한 계속
해서 루프를 수행
합니다. Some 값은 incoming 스트림에서 새로운 TCP 연결을 성공적으로 가져왔음을 의미
합니다. stream 변수에는 이 TCP 연결이 할당됩니다.
만약 incoming.next().await
호출이 None
을 반환
하면, 이는 incoming 스트림에 더 이상 처리할 TCP 연결이 없음
을 의미하고, while 루프는 종료됩니다.
6.2. await 키워드 오른쪽에 코드 블록을 사용하는 경우
, async fn에서 Future의 구현체를 직접 만들 때입니다.
예를 들어, 다음과 같이 async fn
에서 Future
의 구현체를 만들어서 await
키워드로 실행할 수 있습니다.
async fn foo() -> i32 {
let x = async {
// 비동기적으로 실행되는 코드 블록
42
};
x.await
}
#[tokio::main]
async fn main() {
let result = foo().await;
println!("{}", result);
}
여기서 x는 async 블록이며, 이는 Future의 구현체입니다. x.await는 이 Future가 완료될 때까지 현재 태스크를 블록하고, Future의 결과를 반환합니다.
이러한 코드 블록은 async fn에서만 사용할 수 있습니다. async fn을 호출한 코드에서 await 키워드를 사용하는 경우에는 코드 블록 대신 Future의 인스턴스를 반환하는 것이 일반적입니다.
let stream: TcpStream = stream?;
stream
변수에 TcpStream 값을 할당
하되, 만약 값이 Err
일 경우에는 해당 Err을 리턴
한다는 뜻입니다.
println!("Accepting from: {}", stream.peer_addr()?);
stream.peer_addr()
메서드는 해당 TCP 연결의 소켓 주소를 반환
합니다. 이 소켓 주소는 std::net::SocketAddr
형식입니다. println! 매크로에서 {}를 사용하여 해당 소켓 주소를 문자열로 포맷팅하여 출력하고 있습니다. ?
연산자는 이 표현식의 결과를 Result 타입으로 반환
하고, 이 표현식에서 오류가 발생한 경우에는 Ok(())
대신 Err
값을 반환하여 accept_loop 함수에서 예외 처리를 수행
하게 됩니다.
let _handle: task::JoinHandle<()>
는 비동기적으로 실행될 connection_loop
함수를 호출하는 spawn_and_log_error
함수의 반환값입니다.
spawn_and_log_error
함수는 제네릭 타입 F
를 입력으로 받으며, 이는 Future 트레이트
를 구현한 반환값을 가져야 합니다. spawn_and_log_error
함수는 async move 블록으로 구현되어 있으며, 반환값의 타입
은 task::JoinHandle<()>
입니다.
따라서, let _handle: task::JoinHandle<()>
은 spawn_and_log_error(connection_loop(stream))
의 반환값을 할당받는 변수입니다. connection_loop(stream)
은 비동기적으로 실행되는 함수이며, 이를 수행하는 핸들러를 _handle 변수에 할당합니다. _handle 변수는 반환값이 없으며, 핸들러를 정상적으로 수행하기 위해 사용됩니다.
Ok(())
: Rust에서 성공적으로 끝난 함수의 반환값
Rust의 Result 타입은 함수가 성공적으로 완료되거나 (Ok) 또는 오류(Err)로 종료
될 수 있음을 나타냅니다. Ok
와 Err
는 Result 타입의 두 가지 variant
입니다.
Ok(())에서 괄호 안의 ()는 unit type
을 의미하며, 특별한 값이 없음
을 나타냅니다. 이것은 함수가 특정 값을 반환하지 않지만 성공적으로 완료
되었음을 나타내는 일반적인 방법입니다.
따라서, Ok(())는 이 함수가 성공적으로 종료되었고 반환할 특별한 값이 없음
을 나타냅니다.
// main
fn run() -> Result<()> {
let fut = accept_loop("127.0.0.1:8080");
task::block_on(fut)
}
task::block_on
을 사용하여 현재 스레드
에서 future
를 실행
하고 완료될 때까지 차단
합니다.프로토콜의 메시지 수신부를 구현해봅시다.
TcpStream
split
하고 \n
바이트를 utf-8
로 디코딩login: messa
use async_std::{
io::BufReader,
net::TcpStream,
};
fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
where
F: Future<Output = Result<()>> + Send + 'static,
{
task::spawn(async move {
if let Err(e) = fut.await {
eprintln!("{}", e)
}
})
}
async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
let listener: TcpListener = TcpListener::bind(addr).await?;
let mut incoming: async_std::net::Incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream: TcpStream = stream?;
println!("Accepting from: {}", stream.peer_addr()?);
let _handle: task::JoinHandle<()> = spawn_and_log_error(connection_loop(stream));
}
Ok(())
}
async fn connection_loop(mut stream: TcpStream) -> Result<()> {
let reader: BufReader<&TcpStream> = BufReader::new(&stream);
let mut writer: BufWriter<&TcpStream> = BufWriter::new(&stream);
let name: String = match reader.lines().next().await {
None => Err("peer disconnected immediately")?,
Some(line) => line?,
};
println!("name = {}", name);
// write response to client
let response: &str = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
writer.write_all(response.as_bytes()).await?;
writer.flush().await?;
Ok(())
}
spawn_and_log_error
함수는 제네릭 함수
로, F
라는 타입 파라미터를 가지고 있습니다. 이 함수는 F가 Future trait를 구현하고, 결과값이 Result<()> 타입인 것을 요구합니다. 함수는 task::spawn 함수를 호출하여, async move 블록에서 fut을 실행시킵니다. 만약 fut 실행 중 오류가 발생하면 eprintln!("{}", e) 코드를 실행하여 오류 메시지를 출력합니다. task::spawn 함수는 실행된 태스크의 핸들을 반환합니다. 반환된 핸들은 현재 사용하지 않는 것으로 _handle 변수에 할당됩니다.
task::JoinHandle<()>
은 async_std 런타임에서 생성된 future가 실행을 완료한 후 반환하는 결과값이 없는 스레드 핸들을 나타내는 타입
입니다.
좀 더 자세하게 설명하면, task::JoinHandle
은 실행 중인 future
의 실행을 추적
하고 제어
하는 스레드 핸들러
입니다. 이 핸들러는 async_std::task::spawn
함수
에 의해 반환
됩니다.
반환 타입
으로 ()
을 사용하면 해당 future의 결과값이 없음
을 나타내며, 이는 Result 타입을 사용하여 오류 처리를 수행할 필요가 없음
을 의미합니다. 따라서 반환값이 없는 future를 실행하는 경우
에는 task::JoinHandle<()>
을 반환
하는 것이 일반적입니다
where 키워드
는 제네릭 타입 매개변수
에 제약 조건을 지정
하는 데 사용됩니다.
이 코드에서 F
는 Future 트레이트를 구현
해야 하고, 그 출력은 Result<()>
타입이어야 하며, Send
트레이트
를 구현하고, 'static
수명
을 가지도록 제한됩니다.
즉, spawn_and_log_error 함수의 매개변수 F는 Future 트레이트를 구현하며, Result<()> 타입을 출력하는 것이 보장되어야 하며, 다른 스레드로 전송할 수 있도록해야 하며, 수명은 'static이어야 합니다.
3.1.좀 더 알아보기
where
제네릭 타입 매개변수
에 대한 제약 조건
(constraint)을 명시하는 키워드
제네릭 함수
나 제네릭 구조체
에서 타입 매개변수
의 제약 조건
을 명시할 때 사용
where를 사용하여 특정 트레잇을 구현한 타입만 타입 매개변수로 받도록 제약을 걸거나, 타입 매개변수 간의 관계를 명시
3.2. 다른 예시
fn foo<T, U>(x: T, y: U) -> T
where
T: std::fmt::Debug,
U: std::fmt::Debug,
{
println!("x = {:?}", x);
println!("y = {:?}", y);
x
}
fn main() {
let s = String::from("hello");
let n = 42;
let result = foo(s, n);
println!("result = {:?}", result);
}
T
타입은 String
타입으로 추론됩니다. String
은 std::fmt::Debug
trait
을 구현하므로 제약조건을 만족합니다. U
타입은 i32
로 추론됩니다.3.3. where
F: Future<Output = Result<()>> + Send + 'static,
F: Future<Output = Result<()>>
: 이 부분은 제네릭 타입 매개변수 F가 Future 트레이트를 구현해야 한다는 것을 나타냅니다. Future 트레이트는 어떤 비동기 계산을 나타내며, 결국에는 값을 반환합니다. 여기서는 Result<()> 타입을 반환하는 것으로 지정되어 있어요. 이는 성공 시 Ok(())를 반환하거나 에러를 나타내는 Result 타입을 반환함을 의미합니다.+ Send
: 이 부분은 제네릭 타입 F에 대한 추가적인 트레이트 제약을 나타냅니다. Send 트레이트는 해당 타입이 스레드 간에 안전하게 공유될 수 있음을 의미합니다. 즉, 다른 스레드로 이동시킬 수 있고 동시에 접근할 수 있는 타입을 나타냅니다.
'static
: 이 부분은 라이프타임 'static을 의미합니다. 'static
은 프로그램 전체 동안 유지되는 수명
을 나타내는데, 여기서는 F 타입이 'static 수명을 갖는다는 것을 명시합니다. 이는 F 타입이 프로그램 전체에서 유효하고 제한 없이 사용될 수 있음
을 의미합니다.
따라서, F 타입
은 Future 트레이트를 구현
하고 Result<()>
을 반환
하며, Send 트레이트
를 만족
하고 'static 수명을 갖는 타입
이어야 합니다.
질문
: F:Future<Output = Result<()>> + Send
구문에서 Send는 F와 결합하는거야? 'Future<Output = Result<()>> '과 결합하는 거야?
답변
:
F: Future<Output = Result<()>> + Send
구문에서 Send
는 F
와 결합
하는 것입니다. 이는 F
가 Send
트레이트를 구현
해야 한다는 것을 의미합니다. F 타입은 Future<Output = Result<()>>과는 별개로 결합
하는 것이며, 두 가지 조건을 동시에 충족해야 합니다.
{
task::spawn(async move {
if let Err(e) = fut.await {
eprintln!("{}", e)
}
})
}
해당 코드는 task::spawn
함수를 사용하여 비동기 작업
을 실행
하는 부분입니다.
task::spawn
함수는 비동기 클로저
나 비동기 함수
를 인자로 받아서 백그라운드에서 실행
합니다.
async move { ... }
은 비동기 클로저를 정의하는 부분입니다. move 키워드
는 클로저
가 외부
의 변수
를 소유
할 수 있도록 합니다.
if let Err(e) = fut.await { ... }
는 fut.await
표현식의 결과가 Err
일 경우 실행됩니다. 이 부분은 fut
가 완료될 때까지 대기하고, Err 값을 가지면 해당 에러를 출력합니다.
eprintln!("{}", e)
은 에러를 표준 오류 출력에 출력하는 부분입니다.
즉, 해당 코드는 비동기 클로저
를 task::spawn
함수로 실행
하여 비동기 작업을 백그라운드에서 실행
하고, 작업이 완료되면 에러가 발생했을 경우 해당 에러를 출력하는 역할
을 합니다.
러스트 함수
일반함수(일반 함수(Non-Generic Function)
fn add(a: i32, b: i32) -> i32 { a + b }
제네릭 함수(Generic Function)
fn get_first<T>(list: &[T]) -> Option<&T>
{ list.first() }클로저(Closure)
3.1. non-generic closure
let add_one = |x| x + 1;
3.2. generic closure
let map = |arr: &[i32], op: fn(i32) -> i32| -> Vec<i32> {
arr.iter().map(|&x| op(x)).collect()
};
while let Some(line) = lines.next().await {
4.1. while let
은 루프에서 패턴 매칭
을 수행하며, 주어진 패턴에 일치하는 값으로 반복을 진행
하거나 종료
하는 제어 구조
4.1.1. 문법
while let 패턴 = 값 {
// 패턴과 일치하는 경우 실행되는 코드
}
4.2. Some(line) = lines.next().await
는 lines
라는 이터레이터
에서 다음 값
을 가져오고, 해당 값이 Some
이면 패턴 매칭
을 통해 값을 분해
하여 line 변수에 할당
하는 구문입니다.
4.2.1. Some()
: Rust의 Option 열거형의 하나인 Some 변형
let mut reader: BufReader<&TcpStream> = BufReader::new(&stream);
let mut writer: BufWriter<&TcpStream> = BufWriter::new(&stream);
:
5.1. TCP 스트림을 읽고 쓰기 위해 BufReader와 BufWriter를 사용하는 부분입니다.
let mut reader: BufReader<&TcpStream> = BufReader::new(&stream);
5.1.1. BufReader
버퍼링된 읽기를 지원하는 타입
5.1.2. BufReade
r는 주어진 reader(&TcpStream)를 감싸고 버퍼링된 읽기 기능을 제공합니다.
5.1.3. &stream
은 TcpStream
에 대한 불변 참조
를 의미합니다. BufReader
는 TcpStream
을 읽기 위해 참조를 사용합니다.
5.1.4. reader
변수는 BufReader<&TcpStream>
타입으로 선언되어 있습니다. 이는 TcpStream
에서 읽기 작업을 수행하기 위한 버퍼링된 리더 객체를 나타냅니다.
특징
1. 입출력 작업을 효율적으로 처리하는 기술
2. 데이터를 읽거나 쓸 때에는 한 번에 작은 블록 단위로 처리하는 것보다 큰 블록 단위로 처리하는 것이 효율적
3. 사용시 데이터를 작은 블록 단위로 입출력 장치와 직접 통신하는 대신, `메모리에 임시로 저장한 후
더 큰 블록 단위로 입출력 장치와 통신
할 수 있습니다. 이렇게 함으로써 입출력 장치와의 통신 횟수 줄이고, 메모리와 입출력 장치 간의 속도 차이를 완화
할 수 있습니다.
4. 버퍼링은 입출력 작업의 성능을 향상시키고 응용 프로그램의 처리량을 개선하는 데 도움을 줍니다. 또한, 버퍼링은 데이터의 흐름을 관리하여 원활한 입출력 처리를 가능하게 합니다.
let name: String = match reader.lines().next().await { None => Err("peer disconnected immediately")?, Some(line) => line?, };
connection_loop
함수 내부에서 클라이언트로부터 받은 첫 번째 줄을 읽어서 변수 name
에 저장
하는 부분입니다.reader.lines().next().await
는 reader
에서 비동기적으로 한 줄씩 읽어오는 작업
입니다. lines()는 BufReader
의 메서드로, 스트림
을 줄 단위로 읽을 수 있게 해줍니다.next().await
는 비동기적으로 다음 줄을 읽어오는 작업입니다. next()
는 lines()
에서 반환되는 Stream의 다음 아이템을 가져오는 메서드입니다.match
표현식을 사용하여 결과를 처리합니다. reader.lines().next().await
의 결과가 None이면 클라이언트가 즉시 연결을 끊었다는 의미이므로 에러를 반환합니다.Some(line) => line?
은 성공적으로 줄을 읽었을 때 해당 줄을 line
변수에 바인딩하고, line?
을 사용하여 Result 타입의 값
을 얻습니다. 만약 line의 값이 Err일 경우 에러를 반환
합니다.let response: &str = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
7.1. &str
타입을 사용한 이유.
효율성: &str은 불변한 문자열 슬라이스
를 나타내므로 메모리를 효율적
으로 사용할 수 있습니다. 응답 본문과 같은 작은 크기의 문자열 데이터에 적합합니다.
문자열 리터럴과의 호환성
: &str은 Rust에서 문자열 리터럴의 타입입니다. 따라서 문자열 리터럴을 &str으로 사용하면 쉽게 응답 본문과 같은 문자열을 나타낼 수 있습니다.
스트링 슬라이스의 편의성
: &str은 문자열 슬라이스를 나타내므로 다양한 문자열 조작 및 처리 작업에 유용
합니다. 문자열의 일부분에 대한 참조
를 쉽게 가져올 수 있습니다.
자동 변환
: &str은 String과 상호 변환
될 수 있습니다. 필요한 경우 String을 &str으로 변환
하거나, &str을 String으로 변환
할 수 있습니다.
위의 이유들로 인해 &str은 보편적인 문자열 표현 방식으로 많이 사용됩니다.
writer.write_all(response.as_bytes()).await?;
8.1. writer
는 BufWriter
로 생성된 쓰기 인터페이스입니다. BufWriter
는 내부적으로 버퍼링을 수행하여 효율적인 쓰기 작업을 도와줍니다.
8.2. write_all
은 비동기적으로 주어진 바이트 슬라이스
를 전송하는 메서드입니다. response.as_bytes()
는 response 문자열
을 바이트 슬라이스로 변환
하여 전송할 준비를 합니다.
8.3. await는 비동기 작업이 완료될 때까지 기다리는 키워드입니다. write_all 메서드는 전송 작업이 완료
면 Result 타입을 반환
합니다.
8.4. ?
는 Result 타입의 값을 처리
하는 문법으로, 반환된 Result 값을 확인
하고, 에러가 발생한 경우 함수에서 바로 에러를 반환
합니다.
8.5. 즉, writer.write_all(response.as_bytes()).await?;
코드는 비동기적으로 응답
을 클라이언트에게 전송
하는 작업을 수행합니다. 전송이 성공
적으로 완료되면 계속 진행
하고, 전송 중에 에러가 발생
하면 함수에서 에러를 반환
합니다.
writer.flush().await?;
: writer
를 사용하여 버퍼에 남아있는 데이터
를 비동기적으로 클라이언트에게 전송
하는 작업을 수행합니다.
8
, 9
의 차이
writer.write_all(response.as_bytes()).await?;:
이 코드는 response 문자열을 바이트 슬라이스로 변환한 후, 해당 데이터를 비동기적으로 클라이언트에게 전송합니다.
write_all
메서드는 버퍼에 데이터를 쓰는 작업
이므로, 데이터는 버퍼에 임시로 저장
됩니다.
전송 작업이 완료되기 전까지는 실제로 클라이언트에게 전송되지 않습니다.
await는 비동기 작업이 완료될 때까지 기다리는 키워드입니다.
?
는 Result 타입의 값을 처리하는 문법으로, 반환된 Result 값을 확인하고, 에러가 발생한 경우 함수에서 바로 에러를 반환합니다.
writer.flush().await?;:
이 코드는 버퍼에 저장된 데이터
를 목적지로 비우는 작업
을 비동기적으로 수행합니다.
flush 메서드는 버퍼에 남아있는 데이터를 목적지로 전송합니다. 전송 작업이 완료되고 버퍼가 비워질 때까지 기다립니다.
따라서, writer.write_all(response.as_bytes()).await?;
은 버퍼에 데이터를 쓰고 전송 작업
을 시작하는 반면, writer.flush().await?
는 버퍼에 저장된 데이터를 목적지로 비우는 작업
을 수행합니다. 실제 전송
은 flush 메서드를 호출할 때 이루어지며, 버퍼가 비워질 때까지 기다립니다.
Sending
을 구현하는 가장 확실한 방법은 각 클라이언트의 TcpStream 쓰기에 대한 각 connection_loop 액세스 권한을 부여하는 것입니다. 그런 식으로 클라이언트는 수신자에게 직접 .write_all()
로 메시지를 보낼 수 있습니다.
하지만 Alice가 bob: foo
를 보내고 Charley가 bob: bar
를 보내면 Bob은 어쩌면 fobaor
를 받을 수 있습니다. 소켓을 통해 메시지를 보내려면 여러 시스템 호출이 필요하며이는 두 개의 동시 .write_all이 서로 간섭할 수도 있습니다!
일반적으로 단일 작업만 각 TcpStream
에 써야 합니다. 이제 채널을 통해 메시지를 수신하고 소켓에 쓰는 connection_writer_loop
작업을 만들어 보겠습니다. 이 작업은 메시지 직렬화 합니다. Alice와 Charley가 동시에 두 개의 메시지를 Bob에게 보내면 Bob은 메시지가 채널에 도착한 순서대로 메시지를 보게 됩니다.
use futures::channel::mpsc; // 1
use futures::sink::SinkExt;
use std::sync::Arc;
type Sender<T> = mpsc::UnboundedSender<T>; // 2
type Receiver<T> = mpsc::UnboundedReceiver<T>;
async fn connection_writer_loop(
mut messages: Receiver<String>,
stream: Arc<TcpStream>, // 3
) -> Result<()> {
let mut stream = &*stream;
while let Some(msg) = messages.next().await {
stream.write_all(msg.as_bytes()).await?;
}
Ok(())
}
futures::channel::mpsc
: 다중 생산자 단일 소비자(multi-producer, single-consumer) 비동기 채널을 제공합니다.
SinkExt
trait는 Sink
trait에 대한 확장을 제공합니다. Sink trait는 비동기적으로 요소를 소비하는 타입을 정의하는데 사용됩니다. SinkExt는 Sink를 확장하여 추가적인 유용한 메서드들을 제공합니다.
SinkExt trait는 다양한 비동기
적인 메서드
들을 제공하여 요소를 소비하고 처리
하는 데 도움을 줍니다. 예를 들면, SinkExt를 통해 요소를 비동기적으로 전송
하고 버퍼링하는 기능
, 요소를 변환
하고 필터링
하는 기능, 여러 소비자를 조합
하는 기능 등을 사용할 수 있습니다.
이 경우, SinkExt trait를 사용하여 Sink를 확장하고 비동기적인 작업을 수행하는 메서드들을 사용할 수 있게 됩니다. 따라서 futures::sink::SinkExt를 가져오면 비동기적인 소비자와 관련된 다양한 기능을 활용할 수 있게 됩니다.
use std::sync::Arc;
: Arc
는 "Atomic Reference Counting"
의 약자로, 다중 스레드 간에 공유되는 데이터를 안전하게 소유하고 참조하는 데 사용되는 스마트 포인터
입니다. Arc는 여러 스레드에서 안전하게 공유될 수 있는 Rc (Reference Counting)의 상호 배타적인 버전입니다.
Arc
는 Arc<T>
형태로 사용되며, T는 Arc로 공유되는 타입입니다. Arc
는 데이터의 소유권
을 나타내며, 참조자의 수를 추적하여 데이터의 소유권이 필요 없을 때 데이터를 자동으로 정리합니다.
Arc는 주로 다중 스레드 환경에서 데이터를 안전하게 공유해야 하는 경우 사용
됩니다. 다중 스레드 환경에서 Arc로 감싼 데이터를 여러 스레드에서 동시에 접근하고 참조할 수 있으며, 소유권 규칙을 준수하여 안전하게 데이터를 사용할 수 있게 됩니다.
따라서, use std::sync::Arc;를 통해 std::sync 모듈의 Arc 타입을 가져오면 다중 스레드 환경에서 안전하게 데이터를 공유하기 위한 Arc 스마트 포인터를 사용할 수 있습니다.
포인터와 유사한 동작
을 제공하면서도 추가적인 기능
과 보안을 제공
하는 래퍼 타입
소유권 관리
: 스마트 포인터는 자체적으로 데이터의 소유를 관리합니다. 데이터를 생성하거나 소멸할 때 적절한 시점에 소유권을 이전하거나 해제함으로써 메모리 안전성을 유지합니다.빌림 규칙
: 스마트 포인터는 Rust의 빌림 규칙을 적용하여 데이터에 대한 동시 접근을 제한합니다. 빌림 규칙은 컴파일러가 런타임 오류를 방지하기 위해 데이터에 대한 가변 참조자의 수와 라이프타임을 추적합니다.추가적인 기능
: 스마트 포인터는 일반적인 포인터보다 많은 기능을 제공합니다. 예를 들어, 메모리 할당 및 해제, 소유권 전달, 참조 카운팅, 스레드 안전성 등을 처리할 수 있습니다.가장 일반적인 스마트 포인터
로는 Box, Rc, Arc, Cell, RefCell, Mutex, Ref, RefMut 등
이 있습니다. 각각의 스마트 포인터는 특정한 상황에 사용될 수 있으며, 메모리 관리와 동시성을 다루는 다양한 요구 사항에 맞게 선택하여 사용할 수 있습니다.스마트 포인터 | 설명 |
---|---|
Box<T> | 힙(heap)에 데이터를 할당하고 소유하는 가장 간단한 스마트 포인터입니다. |
Rc<T> | 참조 카운팅(reference counting) 스마트 포인터로, 여러 개의 소유자를 허용합니다. |
Arc<T> | 원자적(atomic) 참조 카운팅 스마트 포인터로, 다중 스레드 환경에서 안전하게 공유될 수 있습니다. |
Cell<T> | 내부 가변성(mutable interior)을 제공하여 값을 변경할 수 있는 스마트 포인터입니다. 스레드 간에는 안전하지 않습니다. |
RefCell<T> | 내부 가변성(mutable interior)을 제공하여 값을 변경할 수 있는 스마트 포인터입니다. 여러 개의 소유자를 허용합니다. |
Mutex<T> | 동시 접근을 제어하기 위해 스레드 간에 안전하게 데이터에 상호 배타적인 접근을 제공하는 스마트 포인터입니다. |
Box<T>:
fn main() {
let my_box: Box<i32> = Box::new(42);
println!("Value: {}", *my_box);
}
1.1. let my_box: Box<i32> = Box::new(42);:
Box::new(42)
를 사용하여 힙
에 정수 42
를 할당하고, my_box
라는 변수에 Box<i32> 타입
으로 바인딩
합니다.
Box 스마트 포인터
는 데이터를 힙에 할당
하고, 해당 데이터의 소유권
을 갖습니다.
println!("Value: {}", *my_box);:
*my_box
를 사용하여 my_box
의 소유권을 해제
하고 힙에 할당된 값을 가져옵니다.
println! 매크로를 사용하여 값을 출력합니다.
위 코드는 Box 스마트 포인터를 사용하여 힙에 정수 값을 할당하고 이를 안전하게 소유하며 접근하는 예시입니다. Box 스마트 포인터는 힙에 할당된 데이터의 소유권을 가지고 있기 때문에 메모리 안전성을 보장하면서 힙 데이터에 접근할 수 있습니다.
Rust에서 메모리 관리와 생명주기를 제어하는 개념입니다. Rust는 소유권 규칙을 통해 메모리 안전성을 보장하면서 자원의 생성, 소멸 및 이동을 관리
합니다. 소유권의 라이프사이클은 다음과 같은 단계로 구성됩니다
소유권의 생성:
소유권은 값이 생성되면서 처음으로 소유자에게 부여됩니다.
이때, let 키워드를 사용하여 변수에 값을 할당하거나 스마트 포인터를 생성함으로써 소유권이 생성됩니다.
소유권의 이전 (Transfer):
Rust에서는 한 번에 하나의 소유자만이 값을 소유할 수 있습니다.
소유권을 다른 변수나 함수로 이전하는 과정을 소유권의 이전이라고 합니다.
이때, 이전된 변수는 이후에 사용할 수 없습니다.
소유권의 대여 (Borrowing):
소유권이 이전된 변수나 함수는 대여자(Borrower)로서 소유권을 갖지 않으면서 값을 참조할 수 있습니다.
대여자는 불변 참조자(immutable reference) 또는 가변 참조자(mutable reference)를 통해 값을 읽거나 변경할 수 있습니다.
소유권의 소멸:
소유권이 소멸되는 시점은 소유권을 가진 변수가 스코프를 벗어날 때입니다.
변수의 스코프가 종료되면, 할당된 메모리는 자동으로 해제되고 리소스가 반환됩니다.
이때, 스마트 포인터의 소멸자(destructor)가 호출되어 자원의 정리나 추가적인 작업을 수행할 수 있습니다.
위의 life cycle을 예시 코드로 설명해보겠습니다:
소유권 예시 코드
fn main() {
let my_box: Box<i32> = Box::new(42); // 소유권의 생성
{
let borrowed_value: &i32 = &*my_box; // 소유권의 대여
println!("Borrowed Value: {}", borrowed_value);
} // borrowed_value가 스코프를 벗어나면서 대여 종료
// 다른 작업 수행 가능
} // my_box가 스코프를 벗어나면서 소유권의 소멸 및 메모리 해제
&*my_box
:
1) *
연산자를 사용하여 my_box
가 소유한 힙에 저장된 값을 가져옵니다
. 따라서 *my_box는 힙에 저장된 i32 값에 접근합니다.
2) &*my_box
: &
연산자를 사용하여 *my_box
의 값을 참조하는 불변 참조자
(immutable reference)를 생성
합니다. 이렇게 생성된 참조자는 my_box
의 값에 대한 대여자
가 되어 값을 읽을 수 있습니다. 참조자를 통해
my_box의
값을 변경할 수는 없습니다.`
3) &*my_box
는 my_box가 소유한 힙의 i32 값을 대여하여 참조하는 불변 참조자를 생성
합니다. 이렇게 생성된 참조자를 사용하여 값을 읽을 수 있습니다.
Rc<T>:
use std::rc::Rc;
fn main() {
let shared_value: Rc<i32> = Rc::new(42);
println!("Value: {}", *shared_value);
}
let shared_value: Rc<i32> = Rc::new(42);
:
Rc::new(42)
를 사용하여 정수 42를 포인터로 감싸고
, shared_value
라는 변수에 Rc<i32>
타입으로 바인딩
합니다. 이렇게 하면 shared_valu
e가 42 값을 공유
하는 스마트 포인터가 됩니다.
Rc
스마트 포인터는 참조 카운팅
을 사용하여 여러
개의 소유자를 허용
합니다. 각 소유자는 Rc의 복사본
을 가지고 있으며, 소유자 수에 대한 카운트가 유지
됩니다. 카운트가 0
이 되면 리소스가 자동으로 해제
됩니다.이 코드는 Rc 스마트 포인터를 사용하여 정수값을 공유합니다. Rc는 여러 소유자가 동일한 데이터를 공유하면서 데이터의 수명을 추적하고 메모리 누수를 방지하는 데 사용됩니다.
Arc<T>:
use std::sync::Arc;
use std::thread;
fn main() {
let shared_value: Arc<i32> = Arc::new(42);
let thread1 = thread::spawn({
let shared_value = Arc::clone(&shared_value);
move || {
println!("Thread 1: {}", *shared_value);
}
});
let thread2 = thread::spawn({
let shared_value = Arc::clone(&shared_value);
move || {
println!("Thread 2: {}", *shared_value);
}
});
thread1.join().unwrap();
thread2.join().unwrap();
}
Arc
를 사용하여 정수값 42
를 공유
하는 예시. 두 개의 스레드가 생성
되어 shared_value
를 공유
하고 동시에 접근
하여 값을 출력
합니다.
Arc::new(42)
를 사용하여 정수 42를 Arc 스마트 포인터로 감싸서 생성합니다.
Arc::clone(&shared_value)
을 사용하여 shared_value
의 참조 카운트를 증가
시키고, 새로운 Arc 포인터를 생성
합니다.
Arc::clone()
함수에 &shared_value
와 같이 참조자
(reference)를 전달하는 이유는 Arc 스마트 포인터의 복제(clone) 동작
을 수행
하기 위해서입니다.각 스레드의 클로저에서 Arc를 이동
시키고, 클로저 내부에서 *shared_value
를 통해 값을 참조
하여 출력합니다.
thread::spawn()
을 통해 스레드를 생성
하고, join()
을 사용하여 스레드의 실행이 완료될 때까지 기다립니다.
참조자와 스마트 포인터는 Rust에서 데이터에 대한 참조를 제공하는 두 가지 다른 개념입니다.
참조자 (Reference)
:
하나. 참조자는 값을 소유하지 않고, 다른 값에 대한 참조를 만듭니다.
둘. 참조자는 &
기호를 사용하여 생성되며, 변수 또는 값에 대한 불변 또는 가변 참조를 나타냅니다.
셋. 참조자는 빌림(Borrowing) 개념으로, 데이터의 소유권을 가져가지 않고도 값을 빌려올 수 있습니다.
넷. 참조자는 스코프를 벗어날 때까지 유효하며, 동일한 데이터에 대한 여러 참조자가 존재할 수 있습니다.
스마트 포인터 (Smart Pointer)
:
하나. 스마트 포인터는 값을 소유
하고, 값을 가리키는 포인터 역할
을 합니다.
둘. 스마트 포인터는 일반적으로 특정한 동작 또는 소유권 규칙을 갖춘 데이터 구조입니다.
셋. 스마트 포인터는 주로 메모리 관리, 동시성, 참조 카운팅 등의 작업을 수행하기 위해 사용됩니다.
넷. Rust에서의 스마트 포인터에는 Box, Rc, Arc, Cell, RefCell, Mutex 등이 있습니다.
Cell<T>:
use std::cell::Cell;
fn main() {
let my_cell: Cell<i32> = Cell::new(42);
let value = my_cell.get();
println!("Value: {}", value);
my_cell.set(24);
let new_value = my_cell.get();
println!("New Value: {}", new_value);
}
Cell<T>
은 내부 값을 변경 가능한 셀을 제공하는 스마트 포인터 타입입니다
my_cell
은 Cell<i32>
타입으로 생성되었고, 초기값으로 42가 설정되었습니다. my_cell.get()
을 사용하여 셀의 값을 가져와서 value
변수에 할당하고, 그 값을 출력합니다. 그리고 my_cell.set(24)
를 사용하여 셀의 값을 변경하여 24로 설정한 후, my_cell.get()
을 사용하여 변경된 값을 가져와서 new_value
변수에 할당하고, 그 값을 출력합니다.
RefCell<T>:
use std::cell::RefCell;
fn main() {
let my_ref_cell: RefCell<i32> = RefCell::new(42);
let value = my_ref_cell.borrow();
println!("Value: {}", *value);
*my_ref_cell.borrow_mut() = 24;
let new_value = my_ref_cell.borrow();
println!("New Value: {}", *new_value);
}
이 코드는 RefCell<i32>
를 사용하여 가변성을 런타임에 관리
합니다. RefCell
은 불변 참조(borrow())
와 가변 참조(borrow_mut())
를 제공하므로, 같은 스코프 내에서도 해당 데이터에 대해 가변 참조를 가질 수 있습니다
.
이렇게 RefCell
을 사용하면 컴파일 시점이 아닌 런타임 시점에 데이터의 가변성을 관리
할 수 있습니다. 이는 Rust의 소유권 모델에서 일반적으로 허용되지 않는 동작을 가능
하게 하지만, RefCell은 런타임에 borrow 규칙을 체크하여 안전성을 보장
합니다. 이러한 유연성 때문에 RefCell은 여러 공유 상태를 가진 복잡한 데이터 구조를 다루는 데 유용
합니다.
Mutex<T>:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared_value: Arc<Mutex<i32>> = Arc::new(Mutex::new(42));
let thread1 = thread::spawn({
let shared_value = Arc::clone(&shared_value);
move || {
let mut value = shared_value.lock().unwrap();
*value += 10;
println!("Thread 1: {}", *value);
}
});
let thread2 = thread::spawn({
let shared_value = Arc::clone(&shared_value);
move || {
let mut value = shared_value.lock().unwrap();
*value -= 5;
println!("Thread 2: {}", *value);
}
});
thread1.join().unwrap();
thread2.join().unwrap();
}
두 개
의 스레드를 사용
하여 공유 상태를 동시에 업데이트
하는 예제입니다. Rust의 std::sync::{Arc, Mutex}와 std::thread를 사용하여 공유 상태를 안전하게 처리하고 있습니다.하나. Arc<Mutex<i32>>
타입의 shared_value
를 생성하고 초기값으로 42
를 설정합니다. Arc는 Atomic Reference Counting을 사용하여 여러 스레드 간에 안전하게 공유할 수 있는 참조 카운팅 포인터
입니다. Mutex
는 상호 배제를 위한 동기화 프리미티브로, 한 번에 하나의 스레드만 데이터에 액세스
할 수 있게 합니다.
둘. 첫 번째 스레드(thread1)를 생성
하고 실행
합니다. Arc::clone(&shared_value)
를 통해 shared_value의 참조 카운터를 증가시키고, 이를 새로운 스레드에서 사용할 수 있게 합니다. 스레드는 shared_value의 Mutex를 잠그고(lock()), 성공적으로 잠금을 획득하면 값을 10만큼 증가시키고 출력합니다.
셋. 두 번째 스레드(thread2)
를 생성하고 실행
합니다. thread1과 마찬가지로 Arc::clone(&shared_value)를 사용하여 참조 카운터를 증가시키고, 스레드에서 사용할 수 있게 합니다. 스레드는 shared_value의 Mutex를 잠그고(lock()), 성공적으로 잠금을 획득하면 값을 5만큼 감소시키고 출력합니다.
넷. join().unwrap()
을 사용하여 두 스레드가 모두 완료될 때까지 기다립니다. 이렇게 하면 모든 스레드 작업이 완료되기 전에 메인 스레드가 종료되지 않도록 합니다.
let shared_value: Arc<Mutex<i32>> = Arc::new(Mutex::new(42));
이 코드에서는 두 가지 주요 개념, 즉 Arc
와 Mutex
가 사용됩니다.
Arc (Atomic Reference Counting)
: 이는 참조 카운팅 포인터로, 여러 스레드간에 안전하게 공유
될 수 있습니다. 참조 카운터는 Arc의 복사본이 생성될 때마다 증가
하고, 복사본이 drop될 때마다 감소
합니다. 참조 카운터가 0이 되면, Arc는 자신이 소유하는 메모리를 정리(cleanup)
합니다.
Mutex (Mutual Exclusion)
: 이는 상호 배제를 제공하는 동기화 프리미티브입니다. Mutex는 한 번에 하나의 스레드만
이 데이터에 액세스
하도록 보장합니다. 이는 데이터 레이스(data race)를 방지하는 데 사용되며, 두 개 이상의 스레드가 동시에 동일한 데이터를 변경하려고 할 때 발생할 수 있는 문제를 해결합니다.
그런 다음 두 개의 스레드가 생성됩니다. 각 스레드는 Arc::clone(&shared_value)
를 통해 shared_value에 대한 참조를 복제(clone)
합니다. 이렇게 하면 각 스레드가 shared_value에 독립적으로 액세스
할 수 있습니다.
스레드 내부에서는 lock().unwrap()
메서드를 사용하여 Mutex의 잠금을 획득하려고 시도
합니다. 이 메서드는 두 가지 가능한 결과를 반환합니다:
잠금
을 성공적으로 획득한 경우
: 이는 Mutex가 현재 다른 스레드에 의해 잠겨있지 않음을 의미합니다. 이 경우, 해당 스레드는 Mutex가 보호하는 데이터에 액세스
할 수 있습니다.
잠금
을 획득
하지 못한 경우
: 이는 Mutex가 현재 다른 스레드에 의해 잠겨있음을 의미합니다. 이 경우, lock() 메서드는 현재 스레드를 블록(block)
하여, 잠금을 획득할 수 있을 때까지 대기
하게 합니다.
unwrap() 메서드는 Result 타입을 처리하는 데 사용되며, 이는 lock() 메서드가 실패하면 패닉(즉, 프로그램 종료)을 유발합니다.
join().unwrap()
메서드는 각 스레드가 완료될 때까지 메인 스레드가 대기하도록
합니다. join() 메서드
는 Result 타입을 반환
하는데, 이는 스레드가 패닉 상태
에서 종료된 경우 Err를 반환
합니다. unwrap()는 이 Result를 처리하며, Err인 경우 프로그램을 패닉 상태로 만듭니다.
스레드가 완료되면 Mutex의 잠금이 자동으로 해제되고, 다른 스레드가 잠금을 획득할 수 있게 됩니다. 이렇게 하면 여러 스레드가 동시에 동일한 데이터에 액세스하려고 할 때 발생할 수 있는 데이터 레이스 조건을 방지합니다.
이러한 방식으로, Rust의 Arc와 Mutex는 여러 스레드에서 공유되는 데이터에 대한 동시 액세스를 안전하게 관리합니다. 이는 Rust의 메모리 안전성 보장에 중요한 역할을 합니다. 또한, 이를 통해 스레드 간에 데이터를 안전하게 공유하고 동기화하는 복잡한 작업을 수행할 수 있습니다.
아래 코드 설명중이었음
use futures::channel::mpsc; // 1
use futures::sink::SinkExt;
use std::sync::Arc;
type Sender<T> = mpsc::UnboundedSender<T>; // 2
type Receiver<T> = mpsc::UnboundedReceiver<T>;
async fn connection_writer_loop(
mut messages: Receiver<String>,
stream: Arc<TcpStream>, // 3
) -> Result<()> {
let mut stream = &*stream;
while let Some(msg) = messages.next().await {
stream.write_all(msg.as_bytes()).await?;
}
Ok(())
}
type Sender<T> = mpsc::UnboundedSender<T>; type Receiver<T> = mpsc::UnboundedReceiver<T>;
4.1. mpsc::UnboundedSender<T>
와 mpsc::UnboundedReceiver<T>
는 futures
라이브러리의 mpsc (multi-producer, single-consumer)
채널을 나타냅니다. 이것은 여러 생성자(데이터를 보내는 스레드)와 단일 소비자(데이터를 받는 스레드)가 있는 비동기 메시지 패싱 채널
입니다.
4.2. Sender<T> = mpsc::UnboundedSender<T>;
와 type Receiver<T> = mpsc::UnboundedReceiver<T>;
는 코드를 간결하게 만드는 편의성을 제공하는 타입 별칭
입니다. 이를 통해 mpsc::UnboundedSender<T>
와 mpsc::UnboundedReceiver<T>
대신 간단히 Sender<T>
와 Receiver<T>
를 사용할 수 있습니다.
4.3. UnboundedSender<T>
와 UnboundedReceiver<T>
는 "unbounded"
채널
을 나타냅니다. 이는 채널이 버퍼링된 메시지의 수에 대한 상한선이 없음
을 의미합니다. 이런 종류의 채널은 메시지가 전송되는 속도가 수신되는 속도보다 빠를 때 유용
하지만, 메모리 사용에 주의
해야 합니다. 메시지를 무한히 보낼 수 있기 때문
에, 메시지를 소비하는 속도가 충분히 빠르지 않으면 메모리 부족이 발생
할 수 있습니다.
함수 선언:
5.1. async fn connection_writer_loop(mut messages: Receiver<String>, stream: Arc<TcpStream>) -> Result<()>
5.2. 이 함수는 비동기적으로 실행되며, 두 개의 인자를 받습니다: 메시지를 받는 Receiver와 TcpStream에 대한 Arc(Atomic Reference Counting)
포인터. 이 함수는 Result<()>를 반환합니다.
스트림 레퍼런스 얻기
6.1. let mut stream = &*stream;
6.2. 이 코드는 Arc 포인터
를 디레퍼런스하여 TcpStream에 대한 뮤터블 참조를 얻습니다.
메시지 처리 루프:
7.1. while let Some(msg) = messages.next().await {...}
7.2. 이 코드는 메시지를 가져와서 처리하는 비동기 루프입니다. Receiver<String>
에서 다음 메시지를 가져오는 messages.next().await
가 비동기적으로 실행되며, 메시지가 도착하면 Some(msg)
패턴에 바인딩되어 루프 내부로 들어갑니다.
메시지를 스트림에 쓰기:
8.1. stream.write_all(msg.as_bytes()).await?;
8.2. 이 코드는 msg의 내용을 스트림에 비동기적으로 쓰는 작업
을 수행합니다. write_all
은 주어진 바이트를 모두 쓸 때까지 계속적으로 호출
되며, 작업이 완료
되면 메시지의 모든 바이트
가 네트워크 스트림에 쓰여지게 됩니다
. 이 작업이 실패하면, 함수는 에러를 반환합니다(? 연산자 때문에).
함수의 끝:
9.1. Ok(())
9.2. 모든 메시지가 처리되고 나면, 함수는 Ok(())를 반환하여 성공적으로 완료되었음을 나타냅니다.
이 함수는 비동기
적으로 동작하므로, 네트워크 I/O 작업을 기다리는 동안
다른 작업을 실행
할 수 있습니다. 이는 서버가 여러 클라이언트를 효율적으로 처리할 수 있게 해주는 중요한 특성입니다.