Commit af0e4491 authored by gejun's avatar gejun

add several md

parent 6d96e469
bthread([代码](https://svn.baidu.com/public/trunk/bthread))是baidu-rpc使用的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的前身是[DP](http://wiki.babel.baidu.com/twiki/bin/view/Com/Ecom/DistributedProcess)中的[fiber](https://svn.baidu.com/app/ecom/nova/trunk/public/streamfold/fiber/),一个N:1的合作式线程库,等价于event
loop库,但写的是同步代码。
# Goals
- 用户可以延续同步的编程模式,能很快地建立bthread,可以用多种原语同步。
- bthread所有接口可以在pthread中被调用并有合理的行为,使用bthread的代码可以在pthread中正常执行。
- 能充分利用多核。
- 更好的cache locality,更低的延时。
# NonGoals
- 提供pthread的兼容接口,只需链接即可使用。拒绝理由:bthread没有优先级,不适用于所有的场景,链接的方式容易使用户在不知情的情况下误用bthread,造成bug。
- 修改内核让pthread支持同核快速切换。拒绝理由:拥有大量pthread后,每个线程对资源的需求被稀释了,基于thread-local
cache的代码效果都会很差,比如tcmalloc。而独立的bthread不会有这个问题,因为它最终还是被映射到了少量的pthread。
# FAQ
##### Q:bthread是协程(coroutine)吗?
不是。我们常说的协程是N:1线程库,即所有的协程都运行于一个系统线程中,计算能力和各种eventloop等价。由于不跨线程,协程之间的切换不需要系统调用,可以非常快(100ns-200ns),受cache
miss的影响也小。但相应的代价是协程无法高效地利用多核,代码必须非阻塞,否则所有的协程都被卡住,对开发者要求苛刻。协程的这个特点使其特别适合写运行时间确定的IO服务器,典型如http
server,在一些精心调试的场景中,可以达到非常高的吞吐。但百度内大部分在线服务的运行时间可不确定,一个缓慢的函数很容易卡住所有的协程。在这点上eventloop是类似的,一个回调卡住整个loop就卡住了,比如ub**a**server(注意那个a,不是ubserver)是公司内对异步框架的尝试,由多个并行的eventloop组成,真实表现糟糕:回调里打日志慢一些,访问下redis,计算重一点,等待中的其他请求就会大量超时。所以这个框架从未流行起来。
bthread是一个M:N线程库,一个bthread被卡住不会影响其他bthread。关键技术两点:work
stealing调度和butex,前者让bthread更快地被调度到更多的核心上,后者让bthread和pthread可以相互等待和唤醒。这两点协程都不需要。更多线程的知识查看[这里](http://wiki.baidu.com/display/RPC/Threading+Overview)
##### Q: 我应该在程序中多使用bthread吗?
不应该。除非你需要在一次RPC过程中[让一些代码并发运行](http://wiki.baidu.com/pages/viewpage.action?pageId=158717037),你不应该直接调用bthread函数,把这些留给baidu-rpc做更好。
##### Q:bthread和pthread worker如何对应?
pthread worker在任何时间只会运行一个bthread,当前bthread挂起时,pthread
worker先尝试从本地(runqueue)弹出一个待运行的bthread,若没有,则随机偷另一个worker的待运行bthread,仍然没有就睡眠在一个futex上,该futex会在任一worker有新的待运行bthread时被唤醒。
##### Q:bthread中能调用阻塞的pthread或系统函数吗?
可以,只阻塞当前pthread worker。其他pthread worker不受影响。
##### Q:一个bthread阻塞会影响其他bthread吗?
不影响。若bthread因bthread API而阻塞,它会把当前pthread worker让给其他bthread。若bthread因pthread
API或系统函数而阻塞,当前pthread worker上待运行的bthread会被其他空闲的pthread worker偷过去运行。
##### Q:pthread中可以调用bthread API吗?
可以。bthread
API在bthread中被调用时影响的是当前bthread,在pthread中被调用时影响的是当前pthread。使用bthread
API的代码可以直接运行在pthread中。
##### Q:若有大量的bthread调用了阻塞的pthread或系统函数,会影响RPC运行么?
会。比如有8个pthread
worker,当有8个bthread都调用了系统usleep()后,处理网络收发的RPC代码就暂时无法运行了。只要阻塞时间不太长,
这一般没什么影响, 毕竟worker都用完了, 除了排队也没有什么好方法.
在baidu-rpc中用户可以选择调大worker数来缓解问题,
在server端可设置[ServerOptions.num_threads](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-worker线程数)[-bthread_concurrency](http://brpc.baidu.com:8765/flags/bthread_concurrency),
在client端可设置[-bthread_concurrency](http://brpc.baidu.com:8765/flags/bthread_concurrency).
那有没有完全规避的方法呢?
- 一个容易想到的方法是动态增加worker数. 但实际可能很糟糕, 当大量的worker同时被阻塞时,
它们很可能在等待同一个资源(比如同一把锁), 增加worker可能只是增加了更多的等待者.
- 大部分RPC框架采取的方法是区分io线程和worker线程, io线程专门处理收发, worker线程调用用户逻辑,
即使worker线程全部阻塞也不会影响io线程. 但这个方法使得每个请求都要从io线程跳转至worker线程,
增加了一次上下文切换, 在机器繁忙时, 切换都有一定概率无法被及时调度, 会导致更多的延时长尾.
另一个问题是增加一层处理环节(io线程)并不能缓解拥塞, 如果worker线程全部卡住, 程序仍然会卡住,
只是卡的地方从socket缓冲转移到了io线程和worker线程之间的消息队列. 换句话说, 在worker卡住时,
还在运行的io线程做的可能是无用功. 事实上, 这正是上面提到的"没什么影响"真正的含义.
- 一个实际的解决方法是[限制最大并发](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-限制最大并发),
只要同时被处理的请求数低于worker数, 自然可以规避掉"所有worker被阻塞"的情况.
- 另一个解决方法当被阻塞的worker超过阈值时(比如8个中的6个), 就不在原地调用用户代码了,
而是扔到一个独立的线程池中运行. 这样即使用户代码全部阻塞, 也总能保留几个worker处理rpc的收发.
不过目前bthread模式并没有这个机制,
但类似的机制在[打开pthread模式](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-pthread模式)时已经被实现了.
那像上面提到的, 这个机制是不是在用户代码都阻塞时也在做"无用功"呢? 可能是的.
但这个机制更多是为了规避在一些极端情况下的死锁, 比如所有的用户代码都lock在一个pthread mutex上,
并且这个mutex需要在某个RPC回调中unlock, 如果所有的worker都被阻塞, 那么就没有线程来处理RPC回调了,
整个程序就死锁了. 虽然绝大部分的RPC实现都有这个潜在问题, 但实际出现频率似乎很低,
只要养成不在锁内做RPC的好习惯, 这是完全可以规避的.
##### Q:bthread会有[Channel](https://gobyexample.com/channels)吗?
不会。channel代表的是两点间的关系,而很多现实问题是多点的,这个时候使用channel最自然的解决方案就是:有一个角色负责操作某件事情或某个资源,其他线程都通过channel向这个角色发号施令。如果我们在程序中设置N个角色,让它们各司其职,那么程序就能分类有序地运转下去。所以使用channel的潜台词就是把程序划分为不同的角色。channel固然直观,但是有代价:额外的上下文切换。做成任何事情都得等到被调用处被调度,处理,回复,调用处才能继续。这个再怎么优化,再怎么尊重cache
locality,也是有明显开销的。另外一个现实是:用channel的代码也不好写。由于业务一致性的限制,一些资源往往被绑定在一起,所以一个角色很可能身兼数职,但它做一件事情时便无法做另一件事情,而事情又有优先级。各种打断、跳出、继续形成的最终代码异常复杂。
我们需要的往往是buffered
channel,扮演的是队列和有序执行的作用,bthread在r31345之后提供[ExecutionQueue](http://wiki.baidu.com/pages/viewpage.action?pageId=160291992),可以完成这个目的。
baidu-rpc提供了[异步接口](http://wiki.baidu.com/pages/viewpage.action?pageId=213828685#id-创建和访问Client-异步访问),所以一个常见的问题是:我应该用异步接口还是bthread?
短回答:延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用bthread。
# 同步或异步
异步即用回调代替阻塞,有阻塞的地方就有回调。虽然在javascript这种语言中回调工作的很好,接受度也非常高,但只要你用过,就会发现这和我们需要的回调是两码事,这个区别不是[lambda](https://en.wikipedia.org/wiki/Anonymous_function),也不是[future](https://en.wikipedia.org/wiki/Futures_and_promises),而是javascript是单线程的。javascript的回调放到多线程下可能没有一个能跑过,race
condition太多了,单线程的同步方法和多线程的同步方法是两个世界。那是不是服务能搞成类似的形式呢?多个线程,每个都是独立的eventloop。可以,ub**a**server就是(注意带a),但实际效果糟糕,因为阻塞改回调可不简单,当阻塞发生在循环,条件分支,深层子函数中时,改造特别困难,况且很多老代码、第三方代码你根本不可能去改造。结果是代码中会出现不可避免的阻塞,导致那个线程中其他回调都被延迟,流量超时,server性能不符合预期。如果你说,”我想把现在的同步代码改造为大量的回调,除了我其他人都看不太懂,并且性能可能更差了”,我猜大部分人不会同意。别被那些鼓吹异步的人迷惑了,他们写的是从头到尾从下到上全异步且不考虑多线程的代码,和你要写的完全是两码事。
baidu-rpc中的异步和单线程的异步是完全不同的,异步回调会运行在与调用处不同的线程中,你会获得多核扩展性,但代价是你得意识到多线程问题。你可以在回调中阻塞,只要线程够用,对server整体的性能并不会有什么影响。不过异步代码还是很难写的,所以我们提供了[组合访问](http://wiki.baidu.com/pages/viewpage.action?pageId=213828709)来简化问题,通过组合不同的channel,你可以声明式地执行复杂的访问,而不用太关心其中的细节。
当然,延时不长,qps不高时,我们更建议使用同步接口,这也是我们创建bthread的动机:维持同步代码也能提升交互性能。
Icon
判断使用同步或异步:计算qps * latency(in seconds),如果和cpu核数是同一数量级,就用同步,否则用异步。
比如:
- qps = 2000,latency = 10ms,计算结果 = 2000 * 0.01s = 20。和常见的32核在同一个数量级,用同步。
- qps = 100, latency = 5s, 计算结果 = 100 * 5s = 500。和核数不在同一个数量级,用异步。
- qps = 500, latency = 100ms,计算结果 = 500 * 0.1s =
50。基本在同一个数量级,可用同步。如果未来延时继续增长,考虑异步。
这个公式计算的是同时进行的平均请求数(你可以尝试证明一下),和线程数,cpu核数是可比的。当这个值远大于cpu核数时,说明大部分操作并不耗费cpu,而是让大量线程阻塞着,使用异步可以明显节省线程资源(栈占用的内存)。当这个值小于或和cpu核数差不多时,异步能节省的线程资源就很有限了,这时候简单易懂的同步代码更重要。
# 异步或bthread
有了bthread这个工具,用户甚至可以自己实现异步。以“半同步”为例,在baidu-rpc中用户有多种选择:
- 发起多个异步RPC后挨个Join,这个函数会阻塞直到RPC结束。(这儿是为了和bthread对比,实现中我们建议你使用[ParallelChannel](http://wiki.baidu.com/pages/viewpage.action?pageId=213828709#id-组合访问-ParallelChannel),而不是自己Join)
- 启动多个bthread各自执行同步RPC后挨个join bthreads。
哪种效率更高呢?显然是前者。后者不仅要付出创建bthread的代价,在RPC过程中bthread还被阻塞着,不能用于其他用途。
Icon
如果仅仅是为了并发RPC,别用bthread。
不过当你需要并行计算时,问题就不同了。使用bthread可以简单地构建树形的并行计算,充分利用多核资源。比如检索过程中有三个环节可以并行处理,你可以建立两个bthread运行两个环节,在原地运行剩下的环节,最后join那两个bthread。过程大致如下:
这么实现的point:
- 你当然可以建立三个bthread分别执行三个部分,最后join它们,但相比这个方法要多耗费一个线程资源。
- bthread从建立到执行是有延时的(调度延时),在不是很忙的机器上,这个延时的中位数在3微秒左右,90%在10微秒内,99.99%在30微秒内。这说明两点:
- 计算时间超过1ms时收益比较明显。如果计算非常简单,几微秒就结束了,用bthread是没有意义的。
- 尽量让原地运行的部分最慢,那样bthread中的部分即使被延迟了几微秒,最后可能还是会先结束,而消除掉延迟的影响。并且join一个已结束的bthread时会立刻返回,不会有上下文切换开销。
另外当你有类似线程池的需求时,像执行一类job的线程池时,也可以用bthread代替。如果对job的执行顺序有要求,你可以使用基于bthread的[ExecutionQueue](http://wiki.baidu.com/pages/viewpage.action?pageId=160291992)
# 概述
类似于kylin的[ExecMan](http://websvn.work.baidu.com/repos/list#from=/repos/inf_common/show/trunk/kylin/ExecMan.h?revision=HEAD!!handler=loadframe),
[ExecutionQueue](http://websvn.work.baidu.com/repos/list#from=/repos/public/show/trunk/bthread/bthread/execution_queue.h?revision=HEAD!!handler=loadframe)
提供了异步串行执行的功能。
ExecutionQueue的相关技术最早使用在RPC中实现[多线程向同一个fd写数据](http://wiki.baidu.com/display/RPC/IO#IO-%E5%8F%91%E6%B6%88%E6%81%AF).
在r31345之后加入到bthread。 ExecutionQueue 提供了如下基本功能:
- 异步有序执行: 任务在另外一个单独的线程中执行, 并且执行顺序严格和提交顺序一致.
- Multi Producer: 多个线程可以同时向一个ExecutionQueue提交任务
- 支持cancel一个已经提交的任务
- 支持stop
- 支持高优任务插队
和ExecMan的主要区别:
- ExecutionQueue的任务提交接口是[wait-free](https://en.wikipedia.org/wiki/Non-blocking_algorithm#Wait-freedom)的,
ExecMan依赖了lock,
这意味着当机器整体比较繁忙的时候,使用ExecutionQueue不会因为某个进程被系统强制切换导致所有线程都被阻塞。
- ExecutionQueue支持批量处理: 执行线程可以批量处理提交的任务, 获得更好的locality.
ExecMan的某个线程处理完某个AsyncClient的AsyncContext之后下一个任务很可能是属于另外一个AsyncClient的AsyncContex,
这时候cpu cache会在不同AsyncClient依赖的资源间进行不停的切换。
- ExecutionQueue的处理函数不会被绑定到固定的线程中执行, ExecMan中是根据AsyncClient
hash到固定的执行线程,不同的ExecutionQueue之间的任务处理完全独立,当线程数足够多的情况下,所有非空闲的ExecutionQueue都能同时得到调度。
同时也意味着当线程数不足的时候,ExecutionQueue无法保证公平性,
当发生这种情况的时候需要动态增加bthread的worker线程来增加整体的处理能力.
- ExecutionQueue运行线程为bthread, 可以随意的使用一些bthread同步原语而不用担心阻塞pthread的执行.
而在ExecMan里面得尽量避免使用较高概率会导致阻塞的同步原语.
# 背景
在多核并发编程领域, [Message
passing](https://en.wikipedia.org/wiki/Message_passing)作为一种解决竞争的手段得到了比较广泛的应用,它按照业务依赖的资源将逻辑拆分成若干个独立actor,每个actor负责对应资源的维护工作,当一个流程需要修改某个资源的时候,
就转化为一个消息发送给对应actor,这个actor(通常在另外的上下文中)根据命令内容对这个资源进行相应的修改,之后可以选择唤醒调用者(同步)或者提交到下一个actor(异步)的方式进行后续处理。
![img](http://web.mit.edu/6.005/www/fa14/classes/20-queues-locks/figures/producer-consumer.png)
# ExecutionQueue Vs Mutex
ExecutionQueue和mutex都可以用来在多线程场景中消除竞争. 相比较使用mutex,
使用ExecutionQueue有着如下几个优点:
- 角色划分比较清晰, 概念理解上比较简单, 实现中无需考虑锁带来的问题(比如死锁)
- 能保证任务的执行顺序,mutex的唤醒顺序不能得到严格保证.
- 所有线程各司其职,都能在做有用的事情,不存在等待.
- 在繁忙、卡顿的情况下能更好的批量执行,整体上获得较高的吞吐.
但是缺点也同样明显:
- 一个流程的代码往往散落在多个地方,代码理解和维护成本高。
- 为了提高并发度, 一件事情往往会被拆分到多个ExecutionQueue进行流水线处理,
这样会导致在多核之间不停的进行切换,会付出额外的调度以及同步cache的开销,
尤其是竞争的临界区非常小的情况下, 这些开销不能忽略.
- 同时原子的操作多个资源实现会变得复杂, 使用mutex可以同时锁住多个mutex,
用了ExeuctionQueue就需要依赖额外的dispatch queue了。
- 由于所有操作都是单线程的,某个任务运行慢了就会阻塞同一个ExecutionQueue的其他操作。
- 并发控制变得复杂,ExecutionQueue可能会由于缓存的任务过多占用过多的内存。
不考虑性能和复杂度,理论上任何系统都可以只使用mutex或者ExecutionQueue来消除竞争.
但是复杂系统的设计上,建议根据不同的场景灵活决定如何使用这两个工具:
- 如果临界区非常小,竞争又不是很激烈,优先选择使用mutex, 之后可以结合[contention
profiler](http://wiki.baidu.com/display/RPC/contention+profiler)来判断mutex是否成为瓶颈。
- 需要有序执行,或者无法消除的激烈竞争但是可以通过批量执行来提高吞吐, 可以选择使用ExecutionQueue。
总之,
多线程编程没有万能的模型,需要根据具体的场景,结合丰富的profliling工具,最终在复杂度和性能之间找到合适的平衡。
**特别指出一点**,Linux中mutex无竞争的lock/unlock只有需要几条原子指令,在绝大多数场景下的开销都可以忽略不计.
# 使用方式
### 实现执行函数
```
// Iterate over the given tasks
//
// Example:
//
// #include <bthread/execution_queue.h>
//
// int demo_execute(void* meta, TaskIterator<T>& iter) {
// if (iter.is_stopped()) {
// // destroy meta and related resources
// return 0;
// }
// for (; iter; ++iter) {
// // do_something(meta, *iter)
// // or do_something(meta, iter->a_member_of_T)
// }
// return 0;
// }
template <typename T>
class TaskIterator;
```
### 启动一个ExecutionQueue:
```
// Start a ExecutionQueue. If |options| is NULL, the queue will be created with
// default options.
// Returns 0 on success, errno otherwise
// NOTE: type |T| can be non-POD but must be copy-constructible
template <typename T>
int execution_queue_start(
ExecutionQueueId<T>* id,
const ExecutionQueueOptions* options,
int (*execute)(void* meta, TaskIterator<T>& iter),
void* meta);
```
创建的返回值是一个64位的id, ,
相当于ExecutionQueue实例的一个[弱引用](https://en.wikipedia.org/wiki/Weak_reference),
可以wait-free的在O(1)时间内定位一个ExecutionQueue, 你可以到处拷贝这个id, 甚至可以放在RPC中,
作为远端资源的定位工具。
你必须保证meta的生命周期,在对应的ExecutionQueue真正停止前不会释放**.**
### 停止一个ExecutionQueue:
```
// Stop the ExecutionQueue.
// After this function is called:
// - All the following calls to execution_queue_execute would fail immediately.
// - The executor will call |execute| with TaskIterator::is_queue_stopped() being
// true exactly once when all the pending tasks have been executed, and after
// this point it's ok to release the resource referenced by |meta|.
// Returns 0 on success, errno othrwise
template <typename T>
int execution_queue_stop(ExecutionQueueId<T> id);
// Wait until the the stop task (Iterator::is_queue_stopped() returns true) has
// been executed
template <typename T>
int execution_queue_join(ExecutionQueueId<T> id);
```
stop和join都可以多次调用, 都会又合理的行为。stop可以随时调用而不用当心线程安全性问题。
和fd的close类似,如果stop不被调用, 相应的资源会永久泄露
安全释放meta的时机: 可以在execute函数中收到iter.is_queue_stopped()==true的任务的时候释放,
也可以等到join返回之后释放. 注意不要double-free
### 提交任务
```
struct TaskOptions {
TaskOptions();
TaskOptions(bool high_priority, bool in_place_if_possible);
// Executor would execute high-priority tasks in the FIFO order but before
// all pending normal-priority tasks.
// NOTE: We don't guarantee any kind of real-time as there might be tasks still
// in process which are uninterruptible.
//
// Default: false
bool high_priority;
// If |in_place_if_possible| is true, execution_queue_execute would call
// execute immediately instead of starting a bthread if possible
//
// Note: Running callbacks in place might cause the dead lock issue, you
// should be very careful turning this flag on.
//
// Default: false
bool in_place_if_possible;
};
const static TaskOptions TASK_OPTIONS_NORMAL = TaskOptions(false, false);
const static TaskOptions TASK_OPTIONS_URGENT = TaskOptions(true, false);
const static TaskOptions TASK_OPTIONS_INPLACE = TaskOptions(false, true);
// Thread-safe and Wait-free.
// Execute a task with defaut TaskOptions (normal task);
template <typename T>
int execution_queue_execute(ExecutionQueueId<T> id,
typename base::add_const_reference<T>::type task);
// Thread-safe and Wait-free.
// Execute a task with options. e.g
// bthread::execution_queue_execute(queue, task, &bthread::TASK_OPTIONS_URGENT)
// If |options| is NULL, we will use default options (normal task)
// If |handle| is not NULL, we will assign it with the hanlder of this task.
template <typename T>
int execution_queue_execute(ExecutionQueueId<T> id,
typename base::add_const_reference<T>::type task,
const TaskOptions* options);
template <typename T>
int execution_queue_execute(ExecutionQueueId<T> id,
typename base::add_const_reference<T>::type task,
const TaskOptions* options,
TaskHandle* handle);
```
high_priority的task之间的执行顺序也会**严格按照提交顺序**, 这点和ExecMan不同,
ExecMan的QueueExecEmergent的AsyncContex执行顺序是undefined.
但是这也意味着你没有办法将任何任务插队到一个high priority的任务之前执行.
开启inplace_if_possible, 在无竞争的场景中可以省去一次线程调度和cache同步的开销.
但是可能会造成死锁或者递归层数过多(比如不停的ping-pong)的问题,
开启前请确定你的代码中不存在这些问题。
### 取消一个已提交任务
```
/// [Thread safe and ABA free] Cancel the corresponding task.
// Returns:
// -1: The task was executed or h is an invalid handle
// 0: Success
// 1: The task is executing
int execution_queue_cancel(const TaskHandle& h);
```
返回非0仅仅意味着ExecutionQueue已经将对应的task递给过execute,
真实的逻辑中可能将这个task缓存在另外的容器中,所以这并不意味着逻辑上的task已经结束,你需要在自己的业务上保证这一点.
本页说明bthread下使用pthread-local可能会导致的问题。bthread-local的使用方法见[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-bthread-local)
# thread-local问题
调用阻塞的bthread函数后,所在的pthread很可能改变,这使[pthread_getspecific](http://linux.die.net/man/3/pthread_getspecific)[gcc
__thread](https://gcc.gnu.org/onlinedocs/gcc-4.2.4/gcc/Thread_002dLocal.html)和c++11
thread_local变量,pthread_self()等的值变化了,如下代码的行为是不可预计的:
bthread_usleep之后,该bthread很可能身处不同的pthread,这时p指向了之前pthread的thread_local变量,继续访问p的结果无法预计。这种使用模式往往发生在用户使用线程级变量传递业务变量的情况。为了防止这种情况,应该谨记:
- 不使用线程级变量传递业务数据。这是一种槽糕的设计模式,依赖线程级数据的函数也难以单测。判断是否滥用:如果不使用线程级变量,业务逻辑是否还能正常运行?线程级变量应只用作优化手段,使用过程中不应直接或间接调用任何可能阻塞的bthread函数。比如使用线程级变量的tcmalloc就不会和bthread有任何冲突。
- 如果一定要(在业务中)使用线程级变量,使用bthread_key_create和bthread_getspecific。
# gcc4下的errno问题
gcc4会优化[标记为__attribute__((__const__))](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#index-g_t_0040code_007bconst_007d-function-attribute-2958)的函数,这个标记大致指只要参数不变,输出就不会变。所以当一个函数中以相同参数出现多次时,gcc4会合并为一次。比如在我们的系统中errno是内容为*__errno_location()的宏,这个函数的签名是:
由于此函数被标记为__const__,且没有参数,当你在一个函数中调用多次errno时,可能只有第一次才调用__errno_location(),而之后只是访问其返回的int*。在pthread中这没有问题,因为返回的int*是thread-local的,一个给定的pthread中是不会变化的。但是在bthread中,这是不成立的,因为一个bthread很可能在调用一些函数后跑到另一个pthread去,如果gcc4做了类似的优化,即一个函数内所有的errno都替换为第一次调用返回的int*,这中间bthread又切换了pthread,那么可能会访问之前pthread的errno,从而造成未定义行为。
比如下文是一种errno的使用场景:
我们期望看到的行为:
使用gcc4时的实际行为:
严格地说这个问题不是gcc4导致的,而是glibc给__errno_location的签名不够准确,一个返回thread-local指针的函数依赖于段寄存器(TLS的一般实现方式),这怎么能算const呢?由于我们还未找到覆盖__errno_location的方法,所以这个问题目前实际的解决方法是:
**务必在直接或间接使用bthread的项目的gcc编译选项中添加'-D__const__=',即把__const__定义为空,避免gcc4做相关优化。**
把__const__定义为空对程序其他部分的影响几乎为0。另外如果你没有**直接**使用errno(即你的项目中没有出现errno),或使用的是gcc
3.4,即使没有定义-D__const__=,程序的正确性也不会受影响,但为了防止未来可能的问题,我们强烈建议加上。
需要说明的是,和errno类似,pthread_self也有类似的问题,不过一般pthread_self除了打日志没有其他用途,影响面较小,在-D__const__后pthread_self也会正常。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment