Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/cn/bthread.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[English version](../en/bthread.md)

[bthread](https://github.com/apache/brpc/tree/master/src/bthread)是brpc使用的M:N线程库,目的是在提高程序的并发度的同时,降低编码难度,并在核数日益增多的CPU上提供更好的scalability和cache locality。”M:N“是指M个bthread会映射至N个pthread,一般M远大于N。由于linux当下的pthread实现([NPTL](http://en.wikipedia.org/wiki/Native_POSIX_Thread_Library))是1:1的,M个bthread也相当于映射至N个[LWP](http://en.wikipedia.org/wiki/Light-weight_process)。bthread的前身是Distributed Process(DP)中的fiber,一个N:1的合作式线程库,等价于event-loop库,但写的是同步代码。

# Goals
Expand Down
2 changes: 2 additions & 0 deletions docs/cn/bthread_id.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[English version](../en/bthread_id.md)

bthread_id是一个特殊的同步结构,它可以互斥RPC过程中的不同环节,也可以O(1)时间内找到RPC上下文(即Controller)。注意,这里我们谈论的是bthread_id_t,不是bthread_t(bthread的tid),这个名字起的确实不太好,容易混淆。

具体来说,bthread_id解决的问题有:
Expand Down
2 changes: 2 additions & 0 deletions docs/cn/bthread_or_not.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[English version](../en/bthread_or_not.md)

brpc提供了[异步接口](client.md#异步访问),所以一个常见的问题是:我应该用异步接口还是bthread?

短回答:延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用bthread。
Expand Down
1 change: 1 addition & 0 deletions docs/cn/bthread_tagged_task_group.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[English version](../en/bthread_tagged_task_group.md)

# Bthread tagged task group

Expand Down
2 changes: 2 additions & 0 deletions docs/cn/bthread_tracer.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[English version](../en/bthread_tracer.md)

gdb(ptrace)+ gdb_bthread_stack.py主要的缺点是要慢和阻塞进程,需要一种高效的追踪bthread调用栈的方法。

bRPC框架的协作式用户态协程无法像Golang内建的抢占式协程一样实现高效的STW(Stop the World),框架也无法干预用户逻辑的执行,所以要追踪bthread调用栈是比较困难的。
Expand Down
2 changes: 2 additions & 0 deletions docs/cn/execution_queue.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[English version](../en/execution_queue.md)

# 概述

类似于kylin的ExecMan, [ExecutionQueue](https://github.com/apache/brpc/blob/master/src/bthread/execution_queue.h)提供了异步串行执行的功能。ExecutionQueue的相关技术最早使用在RPC中实现[多线程向同一个fd写数据](io.md#发消息). 在r31345之后加入到bthread。 ExecutionQueue 提供了如下基本功能:
Expand Down
68 changes: 64 additions & 4 deletions docs/en/bthread.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,67 @@
# uthread
[中文版](../cn/bthread.md)

This document has not yet been translated into English.
# bthread

Please refer to the [Chinese version](../cn/bthread.md) for the full content.
[bthread](https://github.com/apache/brpc/tree/master/src/bthread) is the M:N threading library used by brpc. The goal is to raise concurrency, keep coding simple, and get better scalability and cache locality on CPUs with more and more cores. "M:N" means M bthreads are mapped onto N pthreads, and M is usually much larger than N. Because Linux pthread ([NPTL](http://en.wikipedia.org/wiki/Native_POSIX_Thread_Library)) is 1:1, those M bthreads are also mapped onto N [LWPs](http://en.wikipedia.org/wiki/Light-weight_process). bthread grew out of the fiber in Distributed Process (DP), an N:1 cooperative threading library. That model is equivalent to an event-loop library, except that users write synchronous code.

Contributions to translate this document are welcome. See [TRANSLATING](TRANSLATING) for guidelines.
# Goals

- Users keep the synchronous programming style, can create a bthread in a few hundred nanoseconds, and can synchronize with a variety of primitives.
- Every bthread API is callable from a pthread and has reasonable behavior. Code that uses bthread APIs can run correctly inside a pthread.
- Make full use of multiple cores.
- Better cache locality; NUMA support is a plus.

# Non-goals

- Provide a pthread-compatible ABI that works just by linking. **Rejected because**: bthread has no priorities and is not suitable for every workload. Silent replacement by linking would make users pick up bthread without knowing it, and cause bugs.
- Intercept every possibly blocking glibc function and syscall so that they block the bthread instead of the system thread. **Rejected because**:
- Blocking a bthread may switch the underlying system thread, so functions that depend on system TLS have undefined behavior.
- Mixing them with functions that block pthreads can deadlock.
- These hooks are usually slower, because they often need extra syscalls such as epoll. The same coverage is more useful for an N:1 cooperative library (fiber): the hook itself is slower, but without it the whole system thread blocks and every fiber stalls.
- Patch the kernel so that pthread can switch quickly on the same core. **Rejected because**: with a large number of pthreads, per-thread resources are diluted and thread-local caches (for example tcmalloc) work poorly. A separate bthread library does not have this problem, because it still maps onto a small number of pthreads. A large part of bthread's speedup over pthread comes from concentrating thread resources. Portability also matters: bthread prefers pure userland code.

# FAQ

##### Q: Is bthread a coroutine?

No. "Coroutine" here means an N:1 threading library: all coroutines run in one system thread. Compute power is equivalent to an event-loop library. Because they never leave that thread, switches need no syscall (about 100ns–200ns) and cache-coherence cost is small. The cost is that coroutines cannot use multiple cores well, and the code must be non-blocking, otherwise every coroutine stalls. That makes them a good fit for IO servers whose run time is deterministic, such as an HTTP server. Carefully tuned, they can reach very high throughput. Most online services at Baidu do not have deterministic run time, and a search is often built by dozens of people. One slow function stalls every coroutine. Event loops have the same problem: one blocking callback stalls the whole loop. ub**a**server (note the **a**, not ubserver) was Baidu's attempt at an async framework of several parallel event loops. In practice it behaved poorly: a slightly slow log in a callback, a hiccup talking to Redis, or a bit more compute caused waiting requests to time out in bulk. The framework never caught on.

bthread is an M:N threading library. One stalled bthread does not stall the others. The two key techniques are work-stealing scheduling and butex. The former schedules bthreads onto more cores quickly; the latter lets bthreads and pthreads wait for and wake each other. Neither is needed by a coroutine. See [threading overview](threading_overview.md) for more on threading models.

##### Q: Should I create lots of bthreads in my program?

No. Unless you need to [run some code concurrently inside one RPC](bthread_or_not.md), do not call bthread APIs directly. Leave that to brpc.

##### Q: How do bthreads map onto pthread workers?

A pthread worker runs exactly one bthread at a time. When the current bthread suspends, the worker first tries to pop a ready bthread from its local runqueue. If that is empty, it steals a ready bthread from a random other worker. If that also fails, it sleeps and is woken when a new ready bthread appears.

##### Q: Can a bthread call blocking pthread or system functions?

Yes. That only blocks the current pthread worker. Other pthread workers are unaffected.

##### Q: Does one blocked bthread affect other bthreads?

No. If the bthread blocks on a bthread API, it yields the current pthread worker to other bthreads. If it blocks on a pthread API or a system function, ready bthreads on that worker are stolen by idle pthread workers.

##### Q: Can pthread code call bthread APIs?

Yes. A bthread API called from a bthread affects the current bthread; called from a pthread, it affects the current pthread. Code that uses bthread APIs can run directly in a pthread.

##### Q: If many bthreads call blocking pthread or system functions, does that hurt RPC?

Yes. For example, with 8 pthread workers, if 8 bthreads all call `usleep()`, RPC code that handles network IO cannot run for a while. As long as the block is not too long, this usually **does not matter much**: the workers are all busy, and queuing is about the only option left.
In brpc you can raise the worker count to mitigate this: on the server set [ServerOptions.num_threads](server.md#number-of-worker-pthreads) or [-bthread_concurrency](flags.md); on the client set [-bthread_concurrency](flags.md).

Is there a way to avoid this completely?

- Dynamically adding workers is the obvious idea, but it often fails in practice. When many workers block at once, they are often waiting for the same resource (for example the same lock). Adding workers may only add more waiters.
- Split IO threads and worker threads? IO threads would only send and receive, so a fully blocked worker pool would not stall IO. An extra hop does not relieve congestion. If every worker is stuck, the program is still stuck; the stall just moves from the socket buffer to the queue between IO threads and workers. In other words, IO threads that keep running while workers are stuck may be doing useless work. That is what **does not matter much** above really means. Another cost is that every request jumps from an IO thread to a worker, adding a context switch. On a busy machine that switch is sometimes not scheduled promptly, which lengthens the tail latency.
- A practical fix is to [limit max concurrency](server.md#limit-concurrency). If the number of in-flight requests stays below the worker count, "all workers blocked" does not happen.
- Another fix: when blocked workers pass a threshold (for example 6 of 8), stop running user code in place and throw it into a separate thread pool. Even if all user code blocks, a few workers remain to handle RPC IO. bthread mode does not have this mechanism today, but a similar one is implemented when [pthread mode](server.md#pthread-mode) is on. Is that "useless work" while user code is fully blocked, as above? Possibly. The mechanism is more about avoiding a rare deadlock: all user code holds a pthread mutex that must be unlocked in an RPC callback; if every worker is blocked, nothing can run that callback and the process deadlocks. Most RPC implementations have this latent issue, but it is rare in practice. Do not issue RPCs while holding a lock, and you can avoid it.

##### Q: Will bthread have [Channel](https://gobyexample.com/channels)?

No. A channel models a relationship between two points, while many real problems are many-to-many. The natural channel solution is then: one role owns a thing or a resource, and every other thread sends commands to that role over a channel. Give the program N roles, each doing its job, and the program runs in an orderly way. So using channels implies splitting the program into roles. Channels are intuitive, but they cost extra context switches. Nothing finishes until the callee is scheduled, processes the message, and replies. No amount of cache-locality tuning removes that cost. Another reality: channel-heavy code is hard to write. Business consistency often binds resources together, so one role wears several hats, cannot do two things at once, and work has priorities. Interrupts, early exits, and resumes make the final code very complex.

What we usually need is a buffered channel that acts as a queue with ordered execution. bthread provides [ExecutionQueue](execution_queue.md) for that.
41 changes: 37 additions & 4 deletions docs/en/bthread_id.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
# uthread id
[中文版](../cn/bthread_id.md)

This document has not yet been translated into English.
# bthread_id

Please refer to the [Chinese version](../cn/bthread_id.md) for the full content.
`bthread_id` is a special synchronization structure. It can serialize different steps of an RPC, and it can find the RPC context (the Controller) in O(1) time. We are talking about `bthread_id_t` here, not `bthread_t` (the bthread tid). The name is unfortunate and easy to confuse.

Contributions to translate this document are welcome. See [TRANSLATING](TRANSLATING) for guidelines.
Concretely, `bthread_id` solves:

- The response arriving while the request is still being sent, so response handling races with send code.
- A timer firing immediately after it is set, so timeout handling races with send code.
- Several responses from retries arriving at the same time and racing with each other.
- Finding the RPC context from a `correlation_id` in O(1) time, without a global hash map from `correlation_id` to context.
- Cancelling an RPC.

Those bugs show up widely in other RPC frameworks. Here is how brpc uses `bthread_id` to close them.

A `bthread_id` has two parts: a user-visible 64-bit id, and a hidden `bthread::Id` struct. User APIs all operate on the id. Mapping from id to struct is the same as [other structures](memory_management.md) in brpc: 32 bits are the pool offset, 32 bits are the version. The former locates in O(1); the latter avoids ABA.

The `bthread_id` API is not small:

- create
- lock
- unlock
- unlock_and_destroy
- join
- error

The extra APIs exist to cover different flows.

- Send request: `bthread_id_create` → `bthread_id_lock` → … register timer and send RPC … → `bthread_id_unlock`
- Receive response: `bthread_id_lock` → … process response → `bthread_id_unlock_and_destroy`
- Error handling: timeout / socket fail → `bthread_id_error` → run the `on_error` callback (which takes the lock). Then either:
- Retry / backup request: register timer and send RPC again → `bthread_id_unlock`
- Cannot retry, final failure: `bthread_id_unlock_and_destroy`
- Wait synchronously for the RPC to finish: `bthread_id_join`

To cut waiting, `bthread_id` has a few extra mechanisms:

- When an error arrives and the id is already locked, the error is pushed onto a pending queue and `bthread_id_error` returns immediately. On `bthread_id_unlock`, pending work is taken off the queue and run.
- When the RPC finishes and there is a user callback, first call `bthread_id_about_to_destroy` so in-flight `bthread_id_lock` waiters fail immediately, then run the user callback (which may take a long, unpredictable time), and finally `bthread_id_unlock_and_destroy`.
67 changes: 63 additions & 4 deletions docs/en/bthread_or_not.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,66 @@
# uthread or not
[中文版](../cn/bthread_or_not.md)

This document has not yet been translated into English.
# bthread or not

Please refer to the [Chinese version](../cn/bthread_or_not.md) for the full content.
brpc provides an [asynchronous API](client.md#asynchronous-call), so a common question is: should I use the async API or bthread?

Contributions to translate this document are welcome. See [TRANSLATING](TRANSLATING) for guidelines.
Short answer: when latency is not high, start with the simple synchronous API. If that is not enough, use the async API. Use bthread only when you need parallel compute across cores.

# Sync or async

Async means replacing blocking with callbacks: wherever you would block, you get a callback. Callbacks work well in JavaScript and are widely accepted there, but that is a different kind of callback from what server code needs. The difference is not [lambda](https://en.wikipedia.org/wiki/Anonymous_function) or [future](https://en.wikipedia.org/wiki/Futures_and_promises); it is that JavaScript is single-threaded. Drop those callbacks into a multi-threaded program and few of them would even run — too much contention. Single-threaded sync and multi-threaded sync are completely different. Can a service look similar: several threads, each an independent event loop? Yes. ub**a**server (note the **a**) did that, and the result was poor. Turning blocking into callbacks is not simple. When the block sits inside a loop, a branch, or a deep helper, the rewrite is especially hard, and a lot of legacy or third-party code cannot be rewritten at all. Unavoidable blocking then delays every other callback on that thread, traffic times out, and the server misses its performance target. If someone says "I want to turn our sync code into a pile of callbacks that nobody else understands, and it might even be slower", most people will say no. Do not be sold by async evangelism written for programs that are async top to bottom and ignore multi-threading. That is not the code you have to write.

Async in brpc is not single-threaded async. The callback runs on a different thread from the caller, so you get multi-core scalability, but you must deal with multi-threading. You can block inside the callback; as long as there are enough threads, overall server performance is fine. Async code is still hard to write, which is why we provide [combo channels](combo_channel.md): by composing channels you declare complex access patterns without sweating every detail.

When latency is short and QPS is not high, we still recommend the sync API. That is also why bthread exists: keep synchronous code and still improve interactive performance.

**Choosing sync or async**: compute `qps * latency` (latency in seconds). If the result is on the same order as the number of CPU cores, use sync; otherwise use async.

Examples:

- qps = 2000, latency = 10ms, result = 2000 * 0.01s = 20. Same order as a typical 32-core machine → sync.
- qps = 100, latency = 5s, result = 100 * 5s = 500. Not the same order as core count → async.
- qps = 500, latency = 100ms, result = 500 * 0.1s = 50. Roughly the same order → sync is OK. If latency keeps growing, consider async.

The formula is the average number of in-flight requests (try proving it). It is comparable to thread count and CPU cores. When it is much larger than the core count, most operations are not burning CPU; they are parking a lot of threads. Async then saves thread resources (stack memory) in a visible way. When the value is at or below the core count, the thread-resource savings from async are small, and simple sync code matters more.

# Async or bthread

With bthread you can even implement async yourself. Take "semi-sync" as an example. In brpc you have several options:

- Start several async RPCs and Join them one by one. Join blocks until the RPC finishes. (This is only for comparison with bthread. In real code we recommend [ParallelChannel](combo_channel.md#parallelchannel) instead of joining by hand.)
- Start several bthreads, each doing a sync RPC, then join the bthreads.

Which is faster? The first. The second pays for creating bthreads, and those bthreads stay blocked for the whole RPC and cannot be used for anything else.

**If you only need concurrent RPCs, do not use bthread.**

Parallel compute is a different story. bthread makes it easy to build a tree of parallel work and use multiple cores. If a search has three stages that can run in parallel, start two bthreads for two stages, run the third in place, then join the two bthreads:

```c++
bool search() {
...
bthread th1, th2;
if (bthread_start_background(&th1, nullptr, part1, part1_args) != 0) {
LOG(ERROR) << "Fail to create bthread for part1";
return false;
}
if (bthread_start_background(&th2, nullptr, part2, part2_args) != 0) {
LOG(ERROR) << "Fail to create bthread for part2";
return false;
}
part3(part3_args);
bthread_join(th1);
bthread_join(th2);
return true;
}
```

Notes:

- You could start three bthreads and join all of them, but that costs one extra thread resource compared with running one stage in place.
- There is a delay from creating a bthread to running it (scheduling delay). On a machine that is not very busy, the median is about 3 microseconds, 90% finish within 10 microseconds, and 99.99% within 30 microseconds. Two consequences:
- The payoff is clear when the compute takes more than 1ms. If the work finishes in a few microseconds, bthread is not worth it.
- Run the slowest stage in place. Then even if the bthread stages are delayed by a few microseconds, they may still finish first, and the delay disappears. Joining an already-finished bthread returns immediately, with no context-switch cost.

If you need something like a thread pool that runs one class of jobs, you can also replace the pool with bthreads. If job order matters, use bthread's [ExecutionQueue](execution_queue.md).
Loading