类型擦除
主线位置:两条主线都穿过盒子。就绪主线上
poll_ready经一次虚调用原样转发;请求主线上内层的 future 被Box::pin装箱,Timeout<ConcurrencyLimit<..>>这样的长类型收成BoxService<T, U, E>。
flowchart TB stack["具体的服务栈 S"] -->|"map_future(Box::pin)"| unified["future 统一装箱"] unified -->|"Box::new"| dynsvc["dyn Service + Send"] dynsvc -->|"SyncWrapper::new"| bs["BoxService T U E"] bs -->|"poll_ready"| vcall[虚调用,原样转发] bs -->|"call"| heap[每个请求装箱一次]
上一页说过,tower 的组合子和中间件一个都不装箱,每一层都把内层的类型写进自己的类型里。这让一摞中间件没有额外开销,也让它的类型又长又具体。大多数时候这不是问题,编译器替你推导;但有几种情况必须把类型「擦掉」。
分支返回不同的栈:编译器怎么说
最常见的一种:根据配置决定要不要加几层中间件。
tower = { version = "=0.5.3", features = ["limit", "timeout", "util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread"] }
use std::time::Duration;
use tower::{service_fn, BoxError, Service, ServiceBuilder, ServiceExt};
async fn handle(req: String) -> Result<String, BoxError> {
Ok(req.to_uppercase())
}
fn build(strict: bool) -> impl Service<String, Response = String, Error = BoxError> {
if strict {
ServiceBuilder::new()
.timeout(Duration::from_secs(1))
.concurrency_limit(10)
.service_fn(handle)
} else {
service_fn(handle)
}
}
rustc 1.99.0 的报错(两行指向长类型文件路径的 note 省略):
error[E0308]: `if` and `else` have incompatible types
--> examples/branch.rs:15:5
|
9 | / if strict {
10 | |/ ServiceBuilder::new()
11 | || .timeout(Duration::from_secs(1))
12 | || .concurrency_limit(10)
13 | || .service_fn(handle)
| ||_________________________- expected because of this
14 | | } else {
15 | | service_fn(handle)
| | ^^^^^^^^^^^^^^^^^^ expected `Timeout<ConcurrencyLimit<ServiceFn<_>>>`, found `ServiceFn<fn(String) -> _ {handle}>`
16 | | }
| |____- `if` and `else` have incompatible types
|
= note: expected struct `tower:...le}>>>`
found struct `ServiceFn<fn(String) -> impl std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> {handle}>`
help: you could change the return type to be a boxed trait object
|
8 - fn build(strict: bool) -> impl Service<String, Response = String, Error = BoxError> {
8 + fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError>> {
|
两个分支都是 Service<String, Response = String, Error = BoxError>,但 impl Trait 要求整个函数只返回一个具体类型,而一个是 Timeout<ConcurrencyLimit<ServiceFn<_>>>,一个是 ServiceFn<_>。
照 rustc 的建议把返回类型改成 Box<dyn Service<..>>、两个分支各包一个 Box::new,会得到第二个错误:
error[E0191]: the value of the associated type `Future` in `Service<String>` must be specified
--> examples/branch_dyn.rs:8:35
|
8 | fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: specify the associated type
|
8 | fn build(strict: bool) -> Box<dyn Service<String, Response = String, Error = BoxError, Future = /* Type */>> {
| +++++++++++++++++++++
trait object 必须写出所有关联类型,而两个分支的 Future 也不一样:一个是 timeout::ResponseFuture<concurrency::ResponseFuture<..>>,一个是 handle 返回的匿名 future。光把服务装箱不够,future 也得装箱,统一成一个能写出来的类型。这正是 BoxService 做的事。改成:
use tower::util::BoxService;
fn build(strict: bool) -> BoxService<String, String, BoxError> {
if strict {
ServiceBuilder::new()
.timeout(Duration::from_secs(1))
.concurrency_limit(10)
.service_fn(handle)
.boxed()
} else {
service_fn(handle).boxed()
}
}
#[tokio::main]
async fn main() {
for strict in [true, false] {
let r = build(strict).oneshot("hi".to_string()).await.unwrap();
println!("strict={strict}: {r}");
}
}
strict=true: HI
strict=false: HI
同类的场景还有两个:
- 存进结构体字段或集合。 字段类型要写得出来,而闭包的类型写不出来。
BoxService文档的例子注释就是「Respond to requests using a closure, but closures cannot be named...」。Steer的文档例子把两个不同的服务放进一个Vec,也是先各自BoxService::new,见 Steer 与 MakeService。 - 只有两个分支、又不想装箱。 用
Either,见 Either 与 FutureService。
BoxService 怎么擦
BoxService 的字段、new 和 Service 实现:
pub struct BoxService<T, U, E> {
inner:
SyncWrapper<Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send>>,
}
type BoxFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
impl<T, U, E> BoxService<T, U, E> {
pub fn new<S>(inner: S) -> Self
where
S: Service<T, Response = U, Error = E> + Send + 'static,
S::Future: Send + 'static,
{
// rust can't infer the type
let inner: Box<dyn Service<T, Response = U, Error = E, Future = BoxFuture<U, E>> + Send> =
Box::new(inner.map_future(|f: S::Future| Box::pin(f) as _));
let inner = SyncWrapper::new(inner);
BoxService { inner }
}
// ...
}
impl<T, U, E> Service<T> for BoxService<T, U, E> {
type Response = U;
type Error = E;
type Future = BoxFuture<U, E>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), E>> {
self.inner.get_mut().poll_ready(cx)
}
fn call(&mut self, request: T) -> BoxFuture<U, E> {
self.inner.get_mut().call(request)
}
}
三步:
map_future(|f| Box::pin(f) as _)把S变成一个Future = BoxFuture<U, E>的服务。这样 trait object 的四个类型参数(T、U、E、Future)全都写得出来,E0191 那个问题就解决了。map_future见 service_fn 与 map 一族。Box::new把服务本身放上堆,擦成dyn Service<..> + Send。服务只在这里分配一次。- 包进
SyncWrapper,下一节讲。
S: Send + 'static、S::Future: Send + 'static 是这几步的要求:dyn .. + Send 要求服务 Send,BoxFuture 要求 future Send,trait object 默认 'static。ServiceExt::boxed、ServiceBuilder::boxed、BoxService::layer() 都只是转调 BoxService::new。
Service::Future 变成了 Pin<Box<dyn Future>>,所以每个请求都要分配一次堆内存,下面的动脑筋来数一数。
用一个计数的全局分配器,对同一摞 map_request / map_response / map_err 各调 1000 次(每次 poll_ready、call、把 future poll 到完成),分别不装箱、boxed()、boxed_clone()。服务本身在计数开始前就建好了。
tower = { version = "=0.5.3", features = ["util"] }
use std::alloc::{GlobalAlloc, Layout, System};
use std::convert::Infallible;
use std::future::{ready, Future};
use std::pin::pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use tower::{service_fn, Service, ServiceBuilder, ServiceExt};
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
unsafe { System.dealloc(p, l) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
// poll_ready + call + poll the future to completion, 1000 times
fn run<S: Service<u32, Response = u32>>(name: &str, mut svc: S) {
let waker = std::task::Waker::noop();
let mut cx = Context::from_waker(waker);
let before = ALLOCS.load(Ordering::Relaxed);
for i in 0..1000 {
assert!(matches!(svc.poll_ready(&mut cx), Poll::Ready(Ok(()))));
let fut = pin!(svc.call(i));
assert!(matches!(fut.poll(&mut cx), Poll::Ready(Ok(_))));
}
let n = ALLOCS.load(Ordering::Relaxed) - before;
println!("{name:<28} {n} allocations / 1000 calls");
}
fn main() {
let echo = service_fn(|x: u32| ready(Ok::<u32, Infallible>(x)));
let stack = || {
ServiceBuilder::new()
.map_request(|x: u32| x + 1)
.map_response(|x: u32| x * 2)
.map_err(|e: Infallible| e)
.service(echo)
};
run("stack", stack());
run("stack.boxed()", stack().boxed());
run("stack.boxed_clone()", stack().boxed_clone());
}
Sync 从哪来:SyncWrapper
dyn Service<..> + Send 只有 Send,没有 Sync。可是 BoxService 的类型里没有任何对 Sync 的要求,它却是 Sync 的,文件末尾还有一个测试专门检查这件事(is_sync)。来源是 sync_wrapper crate(tower 0.5.3 用的是 1.0.2)的一行:
unsafe impl<T> Sync for SyncWrapper<T> {}
无条件 Sync,对 T 没有任何要求。它之所以安全,是因为 SyncWrapper 只提供 get_mut(&mut self)、get_pin_mut、into_inner(self),没有任何通过 &self 拿到内部的方法。Sync 的意思是「&Self 可以跨线程共享」,而一个 &SyncWrapper<T> 什么也做不了,共享它不会出事。
Service 的两个方法都是 &mut self,BoxService 正好只需要 get_mut。所以即使内层服务本身不是 Sync,BoxService 也是:装箱之后,服务里哪怕有一个 Cell,BoxService 也能放进要求 Sync 的地方。
下面的程序用一个带 Cell 的服务(Send、Clone,但不是 Sync)分别装进三种盒子,检查结果是不是 Sync:
tower = { version = "=0.5.3", features = ["util"] }
use std::cell::Cell;
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::task::{Context, Poll};
use tower::util::{BoxCloneService, BoxCloneSyncService, BoxService};
use tower::Service;
// Send + Clone, but not Sync: Cell
#[derive(Clone)]
struct Counter {
n: Cell<u32>,
}
impl Service<()> for Counter {
type Response = u32;
type Error = Infallible;
type Future = Ready<Result<u32, Infallible>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _: ()) -> Self::Future {
self.n.set(self.n.get() + 1);
ready(Ok(self.n.get()))
}
}
fn assert_sync<T: Sync>(_: &T) {}
fn main() {
let c = Counter { n: Cell::new(0) };
assert_sync(&BoxService::new(c.clone()));
assert_sync(&BoxCloneService::new(c.clone()));
let _ = BoxCloneSyncService::new(c);
}
rustc 1.99.0 报两个错(注册表和工具链路径缩成 ...,长类型文件的两行 note 省略),第 31 行的 BoxService 没有报错:
error[E0277]: `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send` cannot be shared between threads safely
--> examples/sync.rs:32:15
|
32 | assert_sync(&BoxCloneService::new(c.clone()));
| ----------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send` cannot be shared between threads safely
| |
| required by a bound introduced by this call
|
= help: the trait `Sync` is not implemented for `dyn CloneService<(), Error = _, Future = _, Response = u32> + Send`
= note: required for `Unique<_>` to implement `Sync`
note: required because it appears within the type `Box<_>`
--> .../library/alloc/src/boxed.rs:236:12
|
236 | pub struct Box<
| ^^^
note: required because it appears within the type `BoxCloneService<(), u32, Infallible>`
--> .../tower-0.5.3/src/util/boxed_clone.rs:58:12
|
58 | pub struct BoxCloneService<T, U, E>(
| ^^^^^^^^^^^^^^^
note: required by a bound in `assert_sync`
--> examples/sync.rs:27:19
|
27 | fn assert_sync<T: Sync>(_: &T) {}
| ^^^^ required by this bound in `assert_sync`
error[E0277]: `Cell<u32>` cannot be shared between threads safely
--> examples/sync.rs:33:36
|
33 | let _ = BoxCloneSyncService::new(c);
| ------------------------ ^ `Cell<u32>` cannot be shared between threads safely
| |
| required by a bound introduced by this call
|
= help: within `Counter`, the trait `Sync` is not implemented for `Cell<u32>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU32` instead
note: required because it appears within the type `Counter`
--> examples/sync.rs:10:8
|
10 | struct Counter {
| ^^^^^^^
note: required by a bound in `BoxCloneSyncService::<T, U, E>::new`
--> .../tower-0.5.3/src/util/boxed_clone_sync.rs:29:65
|
27 | pub fn new<S>(inner: S) -> Self
| --- required by a bound in this associated function
28 | where
29 | S: Service<T, Response = U, Error = E> + Clone + Send + Sync + 'static,
| ^^^^ required by this bound in `BoxCloneSyncService::<T, U, E>::new`
三种结果对应三种做法:
BoxService:SyncWrapper让它无条件Sync。BoxCloneService:盒子里是dyn CloneService<..> + Send,没有+ Sync,所以它永远不是Sync,与内层是否Sync无关。它也不能借SyncWrapper的办法:Clone::clone只有&self,而SyncWrapper恰恰不给&self访问。BoxCloneSyncService:把+ Sync写进 trait object,代价是new要求内层S: Sync,带Cell的服务在构造时就被拒绝。
#702 之前,BoxService 的字段就是 Box<dyn Service<..> + Send>,没有 Sync。issue #697 指出这没有必要:「This type doesn't have any methods accessing the inner value with &self, so by SyncWrapper logic it can be unconditionally Sync — however currently, it never implements the trait.」并列出三个选项:加一个 sync_wrapper 依赖、在 tower 里写 unsafe、等 SyncWrapper 进标准库。同一个 issue 里也写了为什么 BoxCloneService 不行:「this doesn't apply to BoxCloneService, since that type needs Clone」。
#702(2022-11 合并)选了第一个:引入 sync_wrapper,把字段换成 SyncWrapper<Box<..>>,poll_ready 和 call 改成 self.inner.get_mut(),就是上面引用的代码。CHANGELOG 记作「BoxService is now Sync」。第一个包含它的版本是 tower 0.5.0。util feature 从此多依赖一个 sync_wrapper(当时是 0.1.1)。
四种盒子
| 类型 | 对内层的要求 | 自身 | 构造入口 |
|---|---|---|---|
UnsyncBoxService | S: 'static,S::Future: 'static | 不 Send、不 Sync、不 Clone;future 不 Send | UnsyncBoxService::new、::layer() |
BoxService | S: Send + 'static,S::Future: Send + 'static | Send + Sync(SyncWrapper),不 Clone | ServiceExt::boxed、ServiceBuilder::boxed、BoxService::new、::layer() |
BoxCloneService | 再加 S: Clone | Send + Clone,不 Sync | ServiceExt::boxed_clone、ServiceBuilder::boxed_clone、::new、::layer() |
BoxCloneSyncService | 再加 S: Sync | Send + Sync + Clone | ServiceBuilder::boxed_clone_sync、::new、::layer();ServiceExt 上没有对应方法 |
后三种的 future 都是 Pin<Box<dyn Future + Send>>(BoxCloneService 一族写成 futures_util 的 BoxFuture<'static, _>,是同一个类型)。
几处细节:
UnsyncBoxService用于 future 不是Send的场合(比如 future 里持有Rc),只能在单线程运行时上用。它没有走map_future,而是包一层私有的UnsyncBoxed,在call里Box::pin。BoxService以前也是这样写的,#598 把它换成了map_future,「which is a bit shorter」,只改了sync.rs。BoxCloneService能Clone,靠的是一个私有 traitCloneService:Clone本身不能做成 trait object(clone返回Self),于是加一个返回Box<dyn CloneService<..> + Send>的clone_box,再给所有Service + Send + Clone + 'static的类型实现它。Clone for BoxCloneService只是Self(self.0.clone_box())。克隆走的是内层自己的Clone,就绪状态带不带过去由内层决定,规则见 Service trait。BoxCloneSyncService是 0.5.2 加的(#777,来自 issue #770:「CurrentlyBoxCloneServicedoes not implementSynceven though the inner Service implementsSync.」)。boxed_clone_sync.rs里的CloneService是同名的另一个私有 trait,只多了+ Sync。ServiceBuilder::boxed_clone_sync对 future 的要求是Send + Sync + 'static,比BoxCloneSyncService::new的Send + 'static多一个Sync。这个方法还会把之前整串 layer 一起擦掉,见 ServiceBuilder。
问: 我 .boxed() 之后想把服务 clone 给每个连接,编译器说 BoxService 没有实现 Clone。为什么不干脆让它能克隆?
答: 克隆一个 trait object 需要内层能克隆,而 BoxService::new 不要求 S: Clone,很多服务(RateLimit、SpawnReady、Reconnect)本来就不能克隆。所以能克隆的版本单独做成了 BoxCloneService,用 .boxed_clone() 得到,前提是内层 Clone。内层不能 Clone 又要多处共享时,用 Buffer 把它放进后台 worker,Buffer 本身可以克隆。
BoxLayer 一族
服务能擦,layer 也能擦。BoxLayer 把任意 layer 包成一个类型固定的 layer,产出的服务是 BoxService:
pub struct BoxLayer<In, T, U, E> {
boxed: Arc<dyn Layer<In, Service = BoxService<T, U, E>> + Send + Sync + 'static>,
}
impl<In, T, U, E> BoxLayer<In, T, U, E> {
pub fn new<L>(inner_layer: L) -> Self
where
L: Layer<In> + Send + Sync + 'static,
L::Service: Service<T, Response = U, Error = E> + Send + 'static,
<L::Service as Service<T>>::Future: Send + 'static,
{
let layer = layer_fn(move |inner: In| {
let out = inner_layer.layer(inner);
BoxService::new(out)
});
Self {
boxed: Arc::new(layer),
}
}
}
- 用
layer_fn把「先套原来的 layer,再BoxService::new」做成一个新 layer,Arc起来擦成dyn Layer。 - 用
Arc而不是Box,Clone就只是加引用计数,不要求原来的 layer 能克隆。 - 类型参数里的
In是内层服务的类型,擦不掉:Layer<In>的输入类型仍然要固定。
它的文档例子就是本页开头的问题换到 layer 上:只有设置了环境变量才加 timeout,两个分支的 layer 类型不同,各自 BoxLayer::new 之后就是同一个类型。另外两个是 BoxCloneServiceLayer(产出 BoxCloneService,要求 L::Service: Clone)和 BoxCloneSyncServiceLayer(产出 BoxCloneSyncService,再要求 Sync)。没有产出 UnsyncBoxService 的 layer 版本,dyn Layer 本身要 Send + Sync。
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
stack 0 allocations / 1000 calls
stack.boxed() 1000 allocations / 1000 calls
stack.boxed_clone() 1000 allocations / 1000 calls
- stack:0 次。三层组合子的 future 是嵌套的结构体,
pin!把它放在栈上,整个请求路径不碰堆。 - boxed():每次
call一次,就是map_future里那个Box::pin(f)。BoxService自己的那次Box::new发生在构造时,不在计数范围内。 - boxed_clone():同样每次一次,
BoxCloneService::new也是map_future(|f| Box::pin(f) as _)。
小结
- 分支返回不同的中间件栈时报 E0308;照 rustc 的建议改成
Box<dyn Service>又报 E0191,因为Future也得写出来。BoxService把 future 装箱、把服务装进 trait object,两个问题一起解决。 BoxService::new=map_future(Box::pin)+Box::new+SyncWrapper::new;要求S和S::Future都Send + 'static。SyncWrapper无条件Sync,因为它不给&self访问;Service的方法都是&mut self,所以BoxService是Sync,内层不必是(#702,0.5.0)。BoxCloneService能Clone但永远不是Sync;要两者都有用BoxCloneSyncService,代价是内层必须Sync。UnsyncBoxService给 future 不Send的场合用。- 代价:每个请求一次堆分配,
poll_ready、call和 poll future 都变成动态分发。BoxLayer一族用Arc<dyn Layer>擦 layer 的类型。