<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/modules/content/">
  <channel>
    <title><![CDATA[Infinity Life & Code]]></title>
    <link>https://larrystd.github.io</link>
    <description><![CDATA[记录技术、生活与持续思考。]]></description>
    <language>zh</language>
    <lastBuildDate>Sun, 06 Jul 2025 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://larrystd.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <image>
      <url>https://larrystd.github.io/logo.svg</url>
      <title>Infinity Life &amp; Code</title>
      <link>https://larrystd.github.io</link>
    </image>
    
        <item>
          <title><![CDATA[brpc(1)—bthread和brpc]]></title>
          <link>https://larrystd.github.io/posts/brpc(1)—bthread和brpc</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/brpc(1)—bthread和brpc</guid>
          <pubDate>Sun, 06 Jul 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[brpc 是百度开源的一个网络框架, 它几乎是开源的C++高性能网络框架的唯一选择。它是国内C++最优秀的开源作品之一，我相信国内大厂内部的闭源RPC网络库也参考过它]]></description>
          <content:encoded><![CDATA[<p>brpc 是百度开源的一个网络框架, 它几乎是开源的C++高性能网络框架的唯一选择。它是国内C++最优秀的开源作品之一，我相信国内大厂内部的闭源RPC网络库也参考过它</p>
<p>brpc的重要特性</p>
<ol>
<li>bthread，这是几乎唯一的工业级开源协程库（虽然作者不认为它是协程）,在开源界几乎没有替代品</li>
<li>标准的rpc框架和接口, 四参数service接口
(google::protobuf::RpcController* cntl_base,
const Request* request,
Response* response,
google::protobuf::Closure* done)
终结了rpc 各种各样的自定义调用接口问题，以后开发rpc默认使用这种接口，</li>
<li>完备的可观测性；logging和trace, span; bvar 支持的监控指标</li>
</ol>
<h2>bthread</h2>
<p>bthread 是有栈协程, 使用用户栈来代表协程对象。它使用队列管理协程等待和切换（类似goroutine），例如加协程锁时，第一个bthread进入执行，后续bthread进入等待队列，当第一个bthread执行完后，唤醒等待队列的一个bthread进入临界区。</p>
<p>bthread主要依靠TaskGroup和TaskControl两个类管理，task其实就是指bthread。</p>
<h3>TaskGroup</h3>
<p>taskgroup 是 Thread-local group of tasks. 代表一个pthread 上的bthread协程。taskgroup负责执行一个bthread，切换当前线程的bthread等。主要方法有：</p>
<ol>
<li>start_foreground 立即运行bthread</li>
</ol>
<pre><code class="language-cpp">static int start_foreground(TaskGroup** pg,
                                bthread_t* __restrict tid,
                                const bthread_attr_t* __restrict attr,
                                void * (*fn)(void*),
                                void* __restrict arg);
</code></pre>
<ol start="2">
<li>start_background 将bthread加入到队列_rq，等待调度</li>
</ol>
<pre><code class="language-cpp">    template &#x3C;bool REMOTE>
    int start_background(bthread_t* __restrict tid,
                         const bthread_attr_t* __restrict attr,
                         void * (*fn)(void*),
                         void* __restrict arg);
</code></pre>
<ol start="3">
<li>sched(TaskGroup** pg); 从pg的队列取出一个bthread，执行。如果要切换bthread，也是调用TaskGroup::sched</li>
</ol>
<p>TaskGroup有两个任务队列， _rq和_remote_rq。_rq 是当前线程的bthread执行流加的任务, _remote_rq是其他线程向当前线程提交的任务。_rq相比_remote_rq， 始终是单线程执行，不需要加锁，性能更高。使用两个队列也是bthread对性能的优化</p>
<pre><code class="language-cpp">void TaskGroup::sched(TaskGroup** pg) {
    TaskGroup* g = *pg;
    bthread_t next_tid = 0;
    // Find next task to run, if none, switch to idle thread of the group.
#ifndef BTHREAD_FAIR_WSQ
    const bool popped = g->_rq.pop(&#x26;next_tid);
#else
    const bool popped = g->_rq.steal(&#x26;next_tid);
#endif
    if (!popped &#x26;&#x26; !g->steal_task(&#x26;next_tid)) {
        // Jump to main task if there's no task to run.
        next_tid = g->_main_tid;
    }
    sched_to(pg, next_tid);
}

    bool steal_task(bthread_t* tid) {
        if (_remote_rq.pop(tid)) {
            return true;
        }
#ifndef BTHREAD_DONT_SAVE_PARKING_STATE
        _last_pl_state = _pl->get_state();
#endif
        return _control->steal_task(tid, &#x26;_steal_seed, _steal_offset);
    }
</code></pre>
<ol start="4">
<li>void sched_to(TaskGroup** pg, bthread_t next_tid); 立即执行指定tid的bthread任务, 也就是协程切换</li>
</ol>
<pre><code class="language-cpp">inline void TaskGroup::sched_to(TaskGroup** pg, bthread_t next_tid) {
    TaskMeta* next_meta = address_meta(next_tid);  // 直接通过tid 加地址找到taskmeta
    if (next_meta->stack == NULL) {
        ContextualStack* stk = get_stack(next_meta->stack_type(), task_runner);
        if (stk) {
            next_meta->set_stack(stk);
        } else {
            ...
        }
    }
    // Update now_ns only when wait_task did yield.
    sched_to(pg, next_meta);
}

void TaskGroup::sched_to(TaskGroup** pg, TaskMeta* next_meta) {
    TaskGroup* g = *pg;

    // Save errno so that errno is bthread-specific.
    const int saved_errno = errno;
    void* saved_unique_user_ptr = tls_unique_user_ptr;

    TaskMeta* const cur_meta = g->_cur_meta;
    const int64_t now = butil::cpuwide_time_ns();
    const int64_t elp_ns = now - g->_last_run_ns;
    g->_last_run_ns = now;
    cur_meta->stat.cputime_ns += elp_ns;

    // Switch to the task
    if (__builtin_expect(next_meta != cur_meta, 1)) {
        g->_cur_meta = next_meta;
        // Switch tls_bls
        cur_meta->local_storage = tls_bls;
        tls_bls = next_meta->local_storage;

        // Logging must be done after switching the local storage, since the logging lib
        // use bthread local storage internally, or will cause memory leak.
        if ((cur_meta->attr.flags &#x26; BTHREAD_LOG_CONTEXT_SWITCH) ||
            (next_meta->attr.flags &#x26; BTHREAD_LOG_CONTEXT_SWITCH)) {
            LOG(INFO) &#x3C;&#x3C; "Switch bthread: " &#x3C;&#x3C; cur_meta->tid &#x3C;&#x3C; " -> "
                      &#x3C;&#x3C; next_meta->tid;
        }

        if (cur_meta->stack != NULL) {
            if (next_meta->stack != cur_meta->stack) {
                CheckBthreadScheSafety();
                jump_stack(cur_meta->stack, next_meta->stack);
                // probably went to another group, need to assign g again.
                g = BAIDU_GET_VOLATILE_THREAD_LOCAL(tls_task_group);

    ....
}
</code></pre>
<ol start="5">
<li>int usleep(TaskGroup** pg, uint64_t timeout_us); void yield(TaskGroup** pg);</li>
</ol>
<p>sleep加一个定时任务，然后切换协程</p>
<pre><code class="language-cpp">int TaskGroup::usleep(TaskGroup** pg, uint64_t timeout_us) {
    if (0 == timeout_us) {
        yield(pg);
        return 0;
    }
    TaskGroup* g = *pg;
    // We have to schedule timer after we switched to next bthread otherwise
    // the timer may wake up(jump to) current still-running context.
    SleepArgs e = { timeout_us, g->current_tid(), g->current_task(), g };
    g->set_remained(_add_sleep_event, &#x26;e);
    sched(pg);
    g = *pg;

void TaskGroup::yield(TaskGroup** pg) {
    TaskGroup* g = *pg;
    ReadyToRunArgs args = {  g->_cur_meta, false };
    g->set_remained(ready_to_run_in_worker, &#x26;args);
    sched(pg);
}

</code></pre>
<h3>TaskControl</h3>
<p>TaskControl管理Taskgroup</p>
<ol>
<li>创建TaskGroup, 调用_add_group(group, tag)</li>
</ol>
<p>TaskControl使用<code>std::vector&#x3C;TaggedGroups> _tagged_groups;</code>管理TaskGroup, 增加TaskGroup 使用<code>_tagged_groups[tag][ngroup] = g;</code></p>
<p>taskControl的每个tag对应若干taskgroup</p>
<pre><code class="language-cpp">    // Create a TaskGroup in this control.
    TaskGroup* create_group(bthread_tag_t tag);

TaskGroup* TaskControl::create_group(bthread_tag_t tag) {
    TaskGroup* g = new (std::nothrow) TaskGroup(this);
    if (NULL == g) {
        LOG(FATAL) &#x3C;&#x3C; "Fail to new TaskGroup";
        return NULL;
    }
    if (g->init(FLAGS_task_group_runqueue_capacity) != 0) {
        LOG(ERROR) &#x3C;&#x3C; "Fail to init TaskGroup";
        delete g;
        return NULL;
    }
    if (_add_group(g, tag) != 0) {
        delete g;
        return NULL;
    }
    return g;
}

int TaskControl::_add_group(TaskGroup* g, bthread_tag_t tag) {
    std::unique_lock&#x3C;butil::Mutex> mu(_modify_group_mutex);
    if (_stop) {
        return -1;
    }
    g->set_tag(tag);
    g->set_pl(&#x26;_pl[tag][butil::fmix64(pthread_numeric_id()) % PARKING_LOT_NUM]);
    size_t ngroup = _tagged_ngroup[tag].load(butil::memory_order_relaxed);
    if (ngroup &#x3C; (size_t)BTHREAD_MAX_CONCURRENCY) {
        _tagged_groups[tag][ngroup] = g;
        _tagged_ngroup[tag].store(ngroup + 1, butil::memory_order_release);
    }
    mu.unlock();
    return 0;
}

typedef std::array&#x3C;TaskGroup*, BTHREAD_MAX_CONCURRENCY> TaggedGroups;
std::vector&#x3C;butil::atomic&#x3C;size_t>> _tagged_ngroup;
std::vector&#x3C;TaggedGroups> _tagged_groups;
</code></pre>
<ol start="2">
<li><code>bool steal_task(bthread_t* tid, size_t* seed, size_t offset);</code>  TaskControl从当前线程的taskgroup拿一个bthread执行。</li>
</ol>
<pre><code class="language-cpp">bool TaskControl::steal_task(bthread_t* tid, size_t* seed, size_t offset) {
    // 当前线程的taskgroup, tls_task_group。
    // tls_task_group 定义为每个线程的内部变量
    auto tag = tls_task_group->tag();

    const size_t ngroup = tag_ngroup(tag).load(butil::memory_order_acquire/*1*/);
    if (0 == ngroup) {
        return false;
    }

    // NOTE: Don't return inside `for' iteration since we need to update |seed|
    bool stolen = false;
    size_t s = *seed;
    auto&#x26; groups = tag_group(tag);
    for (size_t i = 0; i &#x3C; ngroup; ++i, s += offset) {
        TaskGroup* g = groups[s % ngroup];
        // g is possibly NULL because of concurrent _destroy_group
        if (g) {
            if (g->_rq.steal(tid)) {
                stolen = true;
                break;
            }
            if (g->_remote_rq.pop(tid)) {
                stolen = true;
                break;
            }
        }
    }
    *seed = s;
    return stolen;
}
</code></pre>
<ol start="3">
<li><code>TaskControl::signal_task(int num_task, bthread_tag_t tag)</code> 唤醒tag对应的taskgroup</li>
</ol>
<p>值得注意的是, signal 借助ParkingLot 类， 通过futex_wait_private和futex_wake_private封装了一种信号量</p>
<pre><code class="language-cpp">void TaskControl::signal_task(int num_task, bthread_tag_t tag) {
    if (num_task &#x3C;= 0) {
        return;
    }
    if (num_task > 2) {
        num_task = 2;
    }
    auto&#x26; pl = tag_pl(tag);
    int start_index = butil::fmix64(pthread_numeric_id()) % PARKING_LOT_NUM;
    num_task -= pl[start_index].signal(1);
    if (num_task > 0) {
        for (int i = 1; i &#x3C; PARKING_LOT_NUM &#x26;&#x26; num_task > 0; ++i) {
            if (++start_index >= PARKING_LOT_NUM) {
                start_index = 0;
            }
            num_task -= pl[start_index].signal(1);
        }
    }
}
</code></pre>
<ol start="4">
<li><code>TaskGroup* choose_one_group(bthread_tag_t tag);</code> 随机选择一个tag对应的TaskGroup</li>
</ol>
<pre><code class="language-cpp">TaskGroup* TaskControl::choose_one_group(bthread_tag_t tag) {
    CHECK(tag >= BTHREAD_TAG_DEFAULT &#x26;&#x26; tag &#x3C; FLAGS_task_group_ntags);
    auto&#x26; groups = tag_group(tag);
    const auto ngroup = tag_ngroup(tag).load(butil::memory_order_acquire);
    if (ngroup != 0) {
        return groups[butil::fast_rand_less_than(ngroup)];
    }
    CHECK(false) &#x3C;&#x3C; "Impossible: ngroup is 0";
    return NULL;
}
</code></pre>
<h3>bthread协程的实现原理</h3>
<p>bthread是有栈协程，在创建协程时，会在线程空间创建协程栈，并装载协程函数；运行协程是运行协程栈的函数，类似线程栈的函数入栈出栈；切换协程是保存协程上下文到协程栈；退出协程则会销毁协程栈。</p>
<p>协程实现包括bthread_make_fcontext 创建协程栈、bthread_jump_fcontext 切换协程栈 函数，在context.cpp文件里。</p>
<p>协程实现原理还可以参考：<a href="https://sf-zhou.github.io/brpc/brpc_01_bthread.html">https://sf-zhou.github.io/brpc/brpc_01_bthread.html</a></p>
<h3>总结</h3>
<p>bthread 提供了工业级的协程实现，让brpc的函数可以在bthread中执行。bthread提供sleep, yield, join等常用调用。</p>
<p>taskcontrol是管理和调度bthread的类, 还提供steal_task方法从某个线程中拿取bthread，这个接口可以用来优化线程load的均衡。</p>
<p>用户可以通过以下步骤管理协程</p>
<ol>
<li>taskcontrol.init(int nconcurrency) 创建指定数量的线程</li>
<li>taskcontrol.create_group(tag) 创建taskgroup, taskgroup会由固定的一个线程负责， 返回taskgroup</li>
<li>taskgroup.start_background, 在taskgroup启动一个bthread任务并执行, 可以实现《在指定线程创建协程》这个灵活操作</li>
</ol>
<h2>brpc</h2>
<p>brpc 是一个网络库，下层接socket、event和网络包，上层接http/proto网络协议和service。同时提供name service根据域名获取下游机器组和lb 负载均衡选择下游机器。</p>
<h3>channel 和客户端</h3>
<p>channel通道，是网络中相当常见的概念。在brpc中，专门指客户端发起的通道。A Channel represents a communication line to one server or multiple servers which can be used to call that Server's services. Servers may be running on another machines. Normally, you should not call a Channel directly, but instead construct a stub Service wrapping it.</p>
<p>channel 主要是两个接口</p>
<h4>channel::Init 创建shannel，发起链接</h4>
<ol>
<li>int Init(const char* server_addr_and_port, const ChannelOptions* options); 初始化channel，也就是创建发起一个连接</li>
</ol>
<pre><code class="language-cpp">int Channel::Init(const char* server_addr, int port,
                  const ChannelOptions* options) {
    GlobalInitializeOrDie();
    butil::EndPoint point;
    const AdaptiveProtocolType&#x26; ptype = (options ? options->protocol : _options.protocol);
    const Protocol* protocol = FindProtocol(ptype);
    if (protocol == NULL || !protocol->support_client()) {
        LOG(ERROR) &#x3C;&#x3C; "Channel does not support the protocol";
        return -1;
    }
    if (protocol->parse_server_address != NULL) {
        if (!protocol->parse_server_address(&#x26;point, server_addr)) {
            LOG(ERROR) &#x3C;&#x3C; "Fail to parse address=`" &#x3C;&#x3C; server_addr &#x3C;&#x3C; '\'';
            return -1;
        }
        point.port = port;
    } else {
        if (str2endpoint(server_addr, port, &#x26;point) != 0 &#x26;&#x26;
            hostname2endpoint(server_addr, port, &#x26;point) != 0) {
            return -1;
        }
    }
    return InitSingle(point, server_addr, options, port);
}
</code></pre>
<p>Channel::InitSingle 函数</p>
<p>a. GlobalInitializeOrDie();
b. InitChannelOptions(options)
c. CreateSocketSSLContext(_options, &#x26;ssl_ctx)
d. SocketMapInsert(SocketMapKey(server_addr_and_port, sig),</p>
<p>显然核心函数在SocketMapInsert, SocketMap::Insert(const SocketMapKey&#x26; key, SocketId* id,</p>
<p>a.  <code>SingleConnection* sc = _map.seek(key);</code> 如果链接已经有了，返回
b. <code>_options.socket_creator->CreateSocket(opt, &#x26;tmp_id)</code> 创建链接
c. <code>_map[key] = new_sc</code>; 链接加入到map</p>
<p>Socket::OnCreated</p>
<p>a. 配置成员变量，如_remote_side，_local_side 等
b. DoConnect(options.connect_abstime, NULL, NULL); 创建和发起socket链接</p>
<pre><code class="language-cpp">int Socket::DoConnect(const timespec* abstime,
                      int (*on_connect)(int, int, void*), void* data) {
    if (_conn) {
        return _conn->Connect(this, abstime, on_connect, data);
    } else {
	    // 会重新创建socket，然后::connect
        return Connect(abstime, on_connect, data);
    }
}
</code></pre>
<h4>stub.method 通过channel 向服务端发送请求</h4>
<p>客户端调用</p>
<pre><code class="language-cpp">    // 初始化channel
    brpc::Channel channel;

    // Initialize the channel, NULL means using default options.
    brpc::ChannelOptions options;
    options.protocol = FLAGS_protocol;
    options.connection_type = FLAGS_connection_type;
    options.timeout_ms = FLAGS_timeout_ms/*milliseconds*/;
    options.max_retry = FLAGS_max_retry;
    if (channel.Init(FLAGS_server.c_str(), FLAGS_load_balancer.c_str(), &#x26;options) != 0) {
        LOG(ERROR) &#x3C;&#x3C; "Fail to initialize channel";
        return -1;
    }

    example::EchoService_Stub stub(&#x26;channel);
	
	example::EchoRequest request;
	example::EchoResponse response;
	brpc::Controller cntl;

	request.set_message(g_request);
	cntl.set_log_id(log_id++);  // set by user

	cntl.request_attachment().append(g_attachment);

	stub.Echo(&#x26;cntl, &#x26;request, &#x26;response, NULL);

// 通过channel->CallMethod 发送数据
void EchoService_Stub::Echo(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
                              const ::example::EchoRequest* request,
                              ::example::EchoResponse* response,
                              ::google::protobuf::Closure* done) {
  channel_->CallMethod(descriptor()->method(0),
                       controller, request, response, done);
}
</code></pre>
<p>Channel::CallMethod</p>
<pre><code class="language-cpp">void Channel::CallMethod(const google::protobuf::MethodDescriptor* method,
                         google::protobuf::RpcController* controller_base,
                         const google::protobuf::Message* request,
                         google::protobuf::Message* response,
                         google::protobuf::Closure* done) {
    const int64_t start_send_real_us = butil::gettimeofday_us();
    Controller* cntl = static_cast&#x3C;Controller*>(controller_base);
    cntl->OnRPCBegin(start_send_real_us);
    ...
    cntl->_response = response;
    cntl->_done = done;
    cntl->_pack_request = _pack_request;
    cntl->_method = method;
    cntl->_auth = _options.auth;

    if (SingleServer()) {
        cntl->_single_server_id = _server_id;
        cntl->_remote_side = _server_address;
    }

    // Share the lb with controller.
    cntl->_lb = _lb;

    _serialize_request(&#x26;cntl->_request_buf, cntl, request);

    cntl->IssueRPC(start_send_real_us);
    if (done == NULL) {
        // done如果是nullptr，同步等待
        Join(correlation_id);
        if (cntl->_span) {
            cntl->SubmitSpan();
        }
        cntl->OnRPCEnd(butil::gettimeofday_us());
    }
}
</code></pre>
<p>cntl->IssueRPC</p>
<pre><code class="language-cpp">void Controller::IssueRPC(int64_t start_realtime_us) {
    _current_call.begin_time_us = start_realtime_us;

    // Pick a target server for sending RPC
    _current_call.need_feedback = false;
    _current_call.enable_circuit_breaker = has_enabled_circuit_breaker();
    SocketUniquePtr tmp_sock;
    if (SingleServer()) {
        const int rc = Socket::Address(_single_server_id, &#x26;tmp_sock);
        
        _current_call.peer_id = _single_server_id;
    } else {
        // 使用lb选择下游服务器
        LoadBalancer::SelectIn sel_in =
            { start_realtime_us, true,
              has_request_code(), _request_code, _accessed };
        LoadBalancer::SelectOut sel_out(&#x26;tmp_sock);
        const int rc = _lb->SelectServer(sel_in, &#x26;sel_out);
        if (rc != 0) {
            std::ostringstream os;
            DescribeOptions opt;
            opt.verbose = false;
            _lb->Describe(os, opt);
            SetFailed(rc, "Fail to select server from %s", os.str().c_str());
            return HandleSendFailed();
        }
        _current_call.need_feedback = sel_out.need_feedback;
        _current_call.peer_id = tmp_sock->id();

        _remote_side = tmp_sock->remote_side();
    }

    // Make request
    butil::IOBuf packet;
    SocketMessage* user_packet = NULL;
    _pack_request(&#x26;packet, &#x26;user_packet, cid.value, _method, this,
                  _request_buf, using_auth);
    // TODO: PackRequest may accept SocketMessagePtr&#x3C;>?
    SocketMessagePtr&#x3C;> user_packet_guard(user_packet);
    Socket::WriteOptions wopt;
    wopt.id_wait = cid;
    wopt.abstime = pabstime;
    wopt.pipelined_count = _pipelined_count;
    wopt.auth_flags = _auth_flags;
    wopt.ignore_eovercrowded = has_flag(FLAGS_IGNORE_EOVERCROWDED);
    wopt.write_in_background = write_to_socket_in_background();
    int rc;
    size_t packet_size = 0;
    // 写数据到socket
    if (user_packet_guard) {
        if (span) {
            packet_size = user_packet_guard->EstimatedByteSize();
        }
        rc = _current_call.sending_sock->Write(user_packet_guard, &#x26;wopt);
    } else {
        packet_size = packet.size();
        rc = _current_call.sending_sock->Write(&#x26;packet, &#x26;wopt);
    }

    if (using_auth) {
        _current_call.sending_sock->SetAuthentication(rc);
    }
}
</code></pre>
<h3>Acceptor 和EventDispatcher</h3>
<p>brpc 的rpc 客户端是通过channel 创建链接和向链接发送请求。类似channel的两个特点, 服务端的基础就是从socket accpet链接和接收数据。完成这个的分别是acceptor和EventDispatcher</p>
<h4>Acceptor 接受链接</h4>
<p>Acceptor 是server的成员变量，在server.Start()里调用Acceptor::StartAccept</p>
<pre><code class="language-cpp">    Acceptor* _am;
    Acceptor* _internal_am;

    // listen socket
    _listen_addr = endpoint;
    for (int port = port_range.min_port; port &#x3C;= port_range.max_port; ++port) {
        _listen_addr.port = port;
        butil::fd_guard sockfd(tcp_listen(_listen_addr));
        if (_listen_addr.port == 0) {
            // port=0 makes kernel dynamically select a port from
            // https://en.wikipedia.org/wiki/Ephemeral_port
            _listen_addr.port = get_port_from_fd(sockfd);    
    ...
    // Pass ownership of `sockfd' to `_am'
    if (_am->StartAccept(sockfd, _options.idle_timeout_sec,
                            _default_ssl_ctx,
                            _options.force_ssl) != 0) {
        LOG(ERROR) &#x3C;&#x3C; "Fail to start acceptor";
        return -1;
    }
</code></pre>
<p>Acceptor::StartAccept 会把 sockfd 注册可读事件给EventDispatcher，回调函数是OnNewConnections，也就是创建新链接。</p>
<p>显然EventDispatcher 是一个epoll模型的程序, 不停的触发网络事件，并发送给对应注册事件的线程。</p>
<pre><code class="language-cpp">void Acceptor::OnNewConnections(Socket* acception) {
    int progress = Socket::PROGRESS_INIT;
    do {
        OnNewConnectionsUntilEAGAIN(acception);
        if (acception->Failed()) {
            return;
        }
    } while (acception->MoreReadEvents(&#x26;progress));
}

void Acceptor::OnNewConnectionsUntilEAGAIN(Socket* acception) {
    while (1) {
        struct sockaddr_storage in_addr;
        bzero(&#x26;in_addr, sizeof(in_addr));
        socklen_t in_len = sizeof(in_addr);
        butil::fd_guard in_fd(accept(acception->fd(), (sockaddr*)&#x26;in_addr, &#x26;in_len));
        if (in_fd &#x3C; 0) {
            continue;
        }

        Acceptor* am = dynamic_cast&#x3C;Acceptor*>(acception->user());
        if (NULL == am) {
            return;
        }
        
        SocketId socket_id;
        SocketOptions options;
        options.keytable_pool = am->_keytable_pool;
        options.fd = in_fd;
        butil::sockaddr2endpoint(&#x26;in_addr, in_len, &#x26;options.remote_side);
        options.user = acception->user();
        options.force_ssl = am->_force_ssl;
        options.initial_ssl_ctx = am->_ssl_ctx;
            options.on_edge_triggered_events = InputMessenger::OnNewMessages;
        }
        options.use_rdma = am->_use_rdma;
        options.bthread_tag = am->_bthread_tag;
        if (Socket::Create(options, &#x26;socket_id) != 0) {
            LOG(ERROR) &#x3C;&#x3C; "Fail to create Socket";
            continue;
        }

        SocketUniquePtr sock;
        if (Socket::AddressFailedAsWell(socket_id, &#x26;sock) >= 0) {
            bool is_running = true;
            {
                BAIDU_SCOPED_LOCK(am->_map_mutex);
                is_running = (am->status() == RUNNING);
                am->_socket_map.insert(socket_id, ConnectStatistics());
            }
}
</code></pre>
<h4>EventDispatcher</h4>
<p>EventDispatcher 可以配置，一般每个tag tasakgroup配置一个dispather，也就是一个线程一个, 以bthread的形式运行</p>
<p>循环执行epoll_wait -> CallInputEventCallback。 epoll_wait不会阻塞整个线程?  epoll_wait时线程阻塞，但有事件到来继续执行时，该线程会创建多个bthread处理。这里需要详细分析brpc 线程切换的行为</p>
<pre><code class="language-cpp">void InitializeGlobalDispatchers() {
    g_edisp = new EventDispatcher[FLAGS_task_group_ntags * FLAGS_event_dispatcher_num];
    for (int i = 0; i &#x3C; FLAGS_task_group_ntags; ++i) {
        for (int j = 0; j &#x3C; FLAGS_event_dispatcher_num; ++j) {
            bthread_attr_t attr =
                FLAGS_usercode_in_pthread ? BTHREAD_ATTR_PTHREAD : BTHREAD_ATTR_NORMAL;
            attr.tag = (BTHREAD_TAG_DEFAULT + i) % FLAGS_task_group_ntags;
            // dispatcher.Start
            CHECK_EQ(0, g_edisp[i * FLAGS_event_dispatcher_num + j].Start(&#x26;attr));
        }
    }

int EventDispatcher::Start(const bthread_attr_t* consumer_thread_attr) {
    if (_event_dispatcher_fd &#x3C; 0) {
        LOG(FATAL) &#x3C;&#x3C; "epoll was not created";
        return -1;
    }
    if (_tid != 0) {
        LOG(FATAL) &#x3C;&#x3C; "Already started this dispatcher(" &#x3C;&#x3C; this 
                   &#x3C;&#x3C; ") in bthread=" &#x3C;&#x3C; _tid;
        return -1;
    }
    // 
    int rc = bthread_start_background(&#x26;_tid, &#x26;epoll_thread_attr, RunThis, this);
    if (rc) {
        LOG(FATAL) &#x3C;&#x3C; "Fail to create epoll thread: " &#x3C;&#x3C; berror(rc);
        return -1;
    }
    return 0;
}

// bthread运行EventDispatcher::Run
void EventDispatcher::Run() {
    while (!_stop) {
        epoll_event e[32];
        const int n = epoll_wait(_event_dispatcher_fd, e, ARRAY_SIZE(e), -1);
        if (_stop) {
            break;
        }
        if (n &#x3C; 0) {
            if (EINTR == errno) {
                // We've checked _stop, no wake-up will be missed.
                continue;
            }
            PLOG(FATAL) &#x3C;&#x3C; "Fail to epoll_wait epfd=" &#x3C;&#x3C; _event_dispatcher_fd;
            break;
        }
        for (int i = 0; i &#x3C; n; ++i) {
            if (e[i].events &#x26; (EPOLLIN | EPOLLERR | EPOLLHUP)
                ) {
                // We don't care about the return value.
                CallInputEventCallback(e[i].data.u64, e[i].events, _thread_attr);
            }
        }
        for (int i = 0; i &#x3C; n; ++i) {
            if (e[i].events &#x26; (EPOLLOUT | EPOLLERR | EPOLLHUP)) {
                // We don't care about the return value.
                CallOutputEventCallback(e[i].data.u64, e[i].events, _thread_attr);
            }
        }
    }
}
</code></pre>
<h3>protocol service</h3>
<p>上层应用层协议</p>
<p>EventDispatcher的CallOutputEventCallback 首先调用InputMessenger::OnNewMessages 从socket读数据，然后调用InputMessenger::CutInputMessage 确认协议类型。</p>
<p>确定协议类型的办法</p>
<ol>
<li>遍历_handlers</li>
<li>执行<code> _handlers[cur_index].parse(&#x26;m->_read_buf, m, read_eof, _handlers[cur_index].arg);</code></li>
<li>如果成功，找到Index；如果失败，继续下一轮尝试
这个一个个的解析尝试的办法，肯定会影响性能把</li>
</ol>
<p>handle类型就是协议类型，确认了协议类型就调用对应的handle解析协议了</p>
<pre><code class="language-cpp">    Protocol http_protocol = { ParseHttpMessage,
                               SerializeHttpRequest, PackHttpRequest,
                               ProcessHttpRequest, ProcessHttpResponse,
                               VerifyHttpRequest, ParseHttpServerAddress,
                               GetHttpMethodName,
                               CONNECTION_TYPE_POOLED_AND_SHORT,
                               "http" };
    if (RegisterProtocol(PROTOCOL_HTTP, http_protocol) != 0) {
        exit(1);
    }

</code></pre>
<p>以protocol rpc service为例，数据从socket读取到msg后， 存放在InputMessageBase</p>
<pre><code class="language-cpp">// 任何网络包都可以解析成header+data两部分
struct BAIDU_CACHELINE_ALIGNMENT MostCommonMessage : public InputMessageBase {
    butil::IOBuf meta;
    butil::IOBuf payload;
    PipelinedInfo pi;

    inline static MostCommonMessage* Get() {
        return butil::get_object&#x3C;MostCommonMessage>();
    }
</code></pre>
<ol>
<li>
<p>ParsePbFromIOBuf(&#x26;meta, msg->meta)</p>
</li>
<li>
<p>将msg->payload解析到messages->Request()msg->payload.cutn(&#x26;req_buf, body_without_attachment_size); ParseFromCompressedData(req_buf, messages->Request(), req_cmp_type)</p>
</li>
<li>
<p><code>std::unique_ptr&#x3C;Controller> cntl(new (std::nothrow) Controller);</code></p>
</li>
<li>
<p>设置google::protobuf::Closure* done = ::brpc::NewCallback&#x3C;
int64_t, Controller*, RpcPBMessages*,
const Server*, MethodStatus*, int64_t>(&#x26;SendRpcResponse,</p>
</li>
<li>
<p>获得service
const Server::MethodProperty* mp =
server_accessor.FindMethodPropertyByFullName(
svc_name, request_meta.method_name());
svc = mp->service;
拿到service对象</p>
</li>
<li>
<p>svc->CallMethod(method, cntl.release(),
messages->Request(),
messages->Response(), done);
CallMethod具体的实现在pb.h中</p>
</li>
<li>
<p>执行完service函数, 最后SendRpcResponse(meta.correlation_id(),
cntl.release(), messages,
server, method_status,
msg->received_us());</p>
</li>
</ol>
<h2>TODO</h2>
<p>brpc的线程模型和切换</p>
<p>brpc的使用和性能资源分析</p>
<p>brpc和butil实现上的优秀、值得借鉴的地方</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[network]]></category><category><![CDATA[brpc]]></category>
        </item>
        <item>
          <title><![CDATA[计算(2)——GPU计算和大模型]]></title>
          <link>https://larrystd.github.io/posts/计算(2)—GPU计算和大模型</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/计算(2)—GPU计算和大模型</guid>
          <pubDate>Sat, 03 May 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[前面提到，CPU运算的瓶颈往往不在于计算，而在于内存、存储和网络。相比于设计算法模型，CPU计算更侧重于工程。]]></description>
          <content:encoded><![CDATA[<p>前面提到，CPU运算的瓶颈往往不在于计算，而在于内存、存储和网络。相比于设计算法模型，CPU计算更侧重于工程。</p>
<p>相比下，GPU运算才是真正的高性能运算，虽然它的瓶颈同样可能来自GPU、显存、存储和网络，但提高计算能力、设计优秀的算法、编写高性能低开销的算子，是GPU运算的核心。GPU运算的典型场景就是大模型。</p>
<h3>GPU 运算</h3>
<p>前面提到，CPU 通过SIMD提供了向量化引擎，适用于OLAP存储和OLAP少量维度的数据分析。但如果是以下场景，就需要GPU了。</p>
<ol>
<li>数据维度高，相比OLAP往往存储二维结构化数据，且列数较少；GPU处理的数据维度高（例如图片数据）、或者是非结构化数据（如自然语言序列）</li>
<li><strong>需要高性能矩阵运算</strong>，如矩阵乘法、矩阵加法。</li>
<li>计算单元<strong>用有向无环图的形式组织</strong>，当前运算单元的输出是下一运算单元的输入，计算层很深，计算量庞大</li>
<li>指令数量和条件分支判断数量少</li>
<li>指令数量少意味着不用经常访问内存，cache数量少，无须多级cache。（GPU访问显存的频率远低于CPU访问cache的频率）</li>
</ol>
<h4>CUDA GPU并行编程模型</h4>
<p>CUDA（Compute Unified Device Architecture）是NVIDIA为GPU设计的并行计算平台和编程模型。nvidia gpu物理上使用​​CUDA核心（FP32/INT32）​​执行浮点和整数运算, 每一个Cuda Core由1个浮点数单元FPU和1个逻辑运算单元ALU组成。除了cuda Core，nivida还用张量核Tensor Core模块用于执行融合乘法加法。</p>
<p>cuda的thread是最小计算单元, 用来处理单个数据。多个thread组成block，可用来执行矩阵处理。多个线程块的集合组成grid，用来表示一个大规模计算任务。cuda 引擎会调度thread/block/grid上的计算到合适的GPU硬件单元上执行。</p>
<p>向量加法的例子，用一个block执行向量加法</p>
<pre><code class="language-cpp">// 向量加法核函数
__global__ void addKernel(float *a, float *b, float *c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i &#x3C; n) {
        c[i] = a[i] + b[i];
    }
}
// 调用：addKernel&#x3C;&#x3C;&#x3C;ceil(n/256), 256>>>(a, b, c, n);
</code></pre>
<h4>pytorch 深度学习框架</h4>
<p>PyTorch 是由 Facebook AI Research (FAIR) 开发的开源深度学习框架，以其 ​​动态计算图​​、​​易用性​​ 和 ​​高效的 GPU 加速​​ 著称，广泛应用于学术研究、工业界模型开发和部署。</p>
<p>python前端接口调用, 文档: <a href="https://pytorch.org/docs/stable/index.html">https://pytorch.org/docs/stable/index.html</a></p>
<pre><code class="language-python">import torch
x = torch.randn(3, 3)  # 创建张量
y = x.cuda()           # 移动到 GPU
z = y + 1              # GPU 加速计算

x = torch.tensor(2.0, requires_grad=True)
y = x**2 + 3*x + 1
y.backward()           # 计算梯度 d(y)/d(x) = 2x + 3
print(x.grad)          # 输出: 7.0
</code></pre>
<p>后端C++引擎libtorch, 可以直接基于libtorch进行C++开发。</p>
<ol>
<li>​ATen 库​​：核心张量计算库，支持 CPU/GPU 统一代码。</li>
<li>​TorchScript​​：将 Python 模型转换为静态计算图（ScriptModule），用于高性能推理。</li>
<li>​​CUDA 集成​​：通过 c10::cuda 实现低延迟 GPU 操作。</li>
</ol>
<p>pytorch 可以很容易调用cuda开发的算子，参考 <a href="https://zhuanlan.zhihu.com/p/595851188">https://zhuanlan.zhihu.com/p/595851188</a></p>
<p>代码结构</p>
<pre><code>├── ops
│   ├── __init__.py
│   ├── ops_py
│   │   ├── __init__.py
│   │   └── sum.py
│   └── src
│       ├── reduce_sum
│       │   ├── sum.cpp
│       │   └── sum_cuda.cu
│       └── sum_two_arrays
│           ├── two_sum.cpp
│           └── two_sum_cuda.cu
├── README.md
├── setup.py
└── test_ops.py
</code></pre>
<p>src/sum_two_arrays/two_sum_cuda.cu</p>
<pre><code class="language-cpp">#include &#x3C;cstdio>

#define THREADS_PER_BLOCK 256
#define WARP_SIZE 32
#define DIVUP(m, n) ((m + n - 1) / n)

__global__ void two_sum_kernel(const float* a, const float* b, float * c, int n){
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx &#x3C; n){
        c[idx] = a[idx] + b[idx];
    }
}

void two_sum_launcher(const float* a, const float* b, float* c, int n){
    dim3 blockSize(DIVUP(n, THREADS_PER_BLOCK));
    dim3 threadSize(THREADS_PER_BLOCK);
    two_sum_kernel&#x3C;&#x3C;&#x3C;blockSize, threadSize>>>(a, b, c, n);
}
</code></pre>
<p>src/sum_two_arrays/two_sum.cpp</p>
<pre><code class="language-cpp">#include &#x3C;torch/extension.h>
#include &#x3C;torch/serialize/tensor.h>

#define CHECK_CUDA(x) \
  TORCH_CHECK(x.type().is_cuda(), #x, " must be a CUDAtensor ")
#define CHECK_CONTIGUOUS(x) \
  TORCH_CHECK(x.is_contiguous(), #x, " must be contiguous ")
#define CHECK_INPUT(x) \
  CHECK_CUDA(x);       \
  CHECK_CONTIGUOUS(x)

void two_sum_launcher(const float* a, const float* b, float* c, int n);

void two_sum_gpu(at::Tensor a_tensor, at::Tensor b_tensor, at::Tensor c_tensor){
    CHECK_INPUT(a_tensor);
    CHECK_INPUT(b_tensor);
    CHECK_INPUT(c_tensor);

    const float* a = a_tensor.data_ptr&#x3C;float>();
    const float* b = b_tensor.data_ptr&#x3C;float>();
    float* c = c_tensor.data_ptr&#x3C;float>();
    int n = a_tensor.size(0);
    two_sum_launcher(a, b, c, n);
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
  m.def("forward", &#x26;two_sum_gpu, "sum two arrays (CUDA)");
}
</code></pre>
<p>PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("forward", &#x26;two_sum_gpu ...)} 表示</p>
<ol>
<li>pybind11 宏，声明一个 Python 模块，并创建模块对象</li>
<li>将 C++ 函数 two_sum_gpu 绑定到 Python 模块，并命名为 forward。</li>
</ol>
<p>使用setup.py编译（也可以使用jit编译）</p>
<pre><code class="language-python">from setuptools import find_packages, setup
from torch.utils.cpp_extension import BuildExtension, CUDAExtension

setup(
    name='CudaDemo',
    packages=find_packages(),
    version='0.1.0',
    author='xxx',
    ext_modules=[
        CUDAExtension(
            'sum_double',
            ['./ops/src/sum_two_arrays/two_sum.cpp',
             './ops/src/sum_two_arrays/two_sum_cuda.cu',]
        ),
    ],
    cmdclass={
        'build_ext': BuildExtension
    }
)
</code></pre>
<p>pytorch调用cuda算子</p>
<pre><code class="language-python"># ops/ops_py/sum.py
import torch
from torch.autograd import Function
import sum_double

class SumDouble(Function):

    @staticmethod
    def forward(ctx, array1, array2):
        """sum_double function forward.
        Args:
            array1 (torch.Tensor): [n,]
            array2 (torch.Tensor): [n,]
        
        Returns:
            ans (torch.Tensor): [n,]
        """
        array1 = array1.float()
        array2 = array2.float()
        ans = array1.new_zeros(array1.shape)
        sum_double.forward(array1.contiguous(), array2.contiguous(), ans)
        return ans

    @staticmethod
    def backward(ctx, g_out):
        # return None, None   # if the function is no need for backpropogation

        g_in1 = g_out.clone()
        g_in2 = g_out.clone()
        return g_in1, g_in2

sum_double_op = SumDouble.apply
</code></pre>
<h4>cudnn和nccl</h4>
<p>cuDNN​​（CUDA Deep Neural Network Library）是英伟达推出的专为深度学习设计的GPU加速库。它针对深度神经网络中的核心操作（如卷积、池化、归一化等）提供高度优化的实现</p>
<pre><code class="language-python"># 启动cudnn加速
torch.backends.cudnn.enabled = True        # 全局启用
torch.backends.cudnn.benchmark = True      # 允许自动选择最优算法（固定输入大小时启用）

# 混和精度训练
with torch.cuda.amp.autocast():
    outputs = model(inputs)
</code></pre>
<p>​​NCCL（NVIDIA Collective Communications Library）​​ 是 NVIDIA 开发的 ​​GPU 专用通信库​​，旨在优化多 GPU 和多节点间的数据传输效率。cuDNN 可与 NCCL 结合实现多卡通信。</p>
<pre><code class="language-python">model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
</code></pre>
<h3>transformer和self-attention</h3>
<p>以上讲了cpu适合通用计算，也就是cpu需要能够各种任务，包括进程任务，cache和内存读写，IO存储，需要具备中断处理能力，就像工作的人一样。CPU还提供了向量化指令增加向量数据处理的能力。GPU适合专门处理矩阵浮点型运算，这种矩阵浮点运算最显著的场景就是深度学习和大模型运算。</p>
<p>目前大模型基本是基于transformer和self-attention架构开发，相关分析 <a href="https://zhuanlan.zhihu.com/p/624740065">https://zhuanlan.zhihu.com/p/624740065</a> 比较全面，我整理下总结</p>
<ol>
<li>每个transformer层的参数量为12h^2,训练时每个参数占20字节，推理时每个参数占2个字节。h是隐藏层维度</li>
<li>每个参数前向计算需要2个浮点数运算（相当于一次乘法一次加法），反向需要4个浮点数运算（反向需要根据误差计算梯度，然后根据梯度更新权重，需要两次运算，计算量是前向计算的2倍）。一般采用激活重计算技术降低中间激活层的内存占用，这样子又需要一次前向计算。每个参数总计需要8个浮点数运算。推理时每个参数只要2个浮点数运算。</li>
<li>显存占用主要由1. 模型参数 2. 前向计算过程产生的中间激活 3. 后向计算得到的梯度 4.优化器状态四个方面组成。其中模型参数、后向计算得到的梯度、优化器状态参数之和只和隐藏层size有关，而前向计算过程产生的中间激活参数和batchsize、序列长度有关。可以通过减少batchsize，额外的激活重计算来降低中间激活参数。</li>
</ol>
<h4>1. 模型参数</h4>
<p><img src="/images/self-attention.png" alt="self-attention"></p>
<ol>
<li>transformer由l层组成，每层分为self-attention（多头注意力）和MLP两部分。self-attention块包含3个QKV权重矩阵和一个输出权重矩阵，每个矩阵维度[h, h], 加上偏置参数量为4h^2+4h, h为隐藏层维度</li>
<li>MLP块由两个线性层组成，第一个线性层维度为[h, 4h], 第二个线性层维度为[4h, h], 加上偏置参数量为8h^2+5h</li>
<li>self-attention块和MLP块之后各有一个layer norm层，包含两个参数，缩容参数alpha和偏置参数beta，2个layer norm参数量合计为4h</li>
</ol>
<p>综上, 每个transformer层参数量为12h^2+13h, 对于l层transformer, 参数了近似为12lh^2</p>
<p><img src="/images/transformer_params.png" alt="transformer_params"></p>
<h4>2. 参数显存分析</h4>
<p>略</p>
<h4>3. kvcache分析</h4>
<p>加入kvcache后，一个典型的大模型生成式推断包含了两个阶段：</p>
<ol>
<li>预填充阶段：输入一个prompt序列，为每个transformer层生成 key cache和value cache（KV cache）。</li>
<li>解码阶段：使用并更新KV cache，一个接一个地生成词，当前生成的词依赖于之前已经生成的词。</li>
</ol>
<p>kvcache 只影响第一步QKV矩阵的生成，将矩阵-矩阵乘法 降低为矩阵-向量乘法，减少参数量加速计算</p>
<p><img src="/images/kvcache.png" alt="kvcache"></p>
<p>KV Cache是Transformer推理性能优化的一项重要工程化技术，各大推理框架都已实现并将其进行了封装。可以看这篇文章 <a href="https://zhuanlan.zhihu.com/p/63083259">https://zhuanlan.zhihu.com/p/63083259</a></p>
<h3>大模型加速</h3>
<p>大模型加速，主要分为模型侧、计算侧和内存IO侧。模型侧主要是压缩和量化，计算侧包括并行计算，cuda算子优化以及MOE训练等，内存IO侧主要在内存分配，共享，高性能存储网络等。</p>
<h4>1. 模型压缩</h4>
<ol>
<li>
<p>剪裁(Pruning), 核心思想是尽可能保证模型精度不受影响下减少网络的参数量，例如减少网络中神经元的数量
剪裁中常用的步骤1. 预训练大模型 2. 修剪网络, 训练小模型 3. 通过微调恢复剪裁对模型的损耗</p>
</li>
<li>
<p>量化Quantization
量化的基本思想是将浮点计算替换成更低比特的计算，从而降低模型体积加快推理速度。量化可以采用定点近似（直接缩小位宽降低精度）和范围近似（通过统计学缩放映射浮点数，需要量化和反量化，精度较高）。</p>
</li>
<li>
<p>知识蒸馏(Knowledge Distillation)
一种教师-学生的训练结构，通常是已训练好的教师模型提供知识，学生模型通过蒸馏训练来获取知识。将教师模型的输出作为软标签与学生模型的软预测计算蒸馏损失，将真实的硬标签与学生模型的硬预测计算学生损失，最终将两种损失结合训练学生模型</p>
</li>
</ol>
<h4>2. 计算侧加速</h4>
<ol>
<li>并行计算
数据并行（数据集拆分），流水线并行（模型拆分成子模型），张量并行（模型按层拆分），专家并行（MOE）</li>
</ol>
<p>其中流水线并行是GPU内存不足的无奈之举，各层之间仍然是顺序执行的，并不能加速模型的运算。</p>
<p>张量并行可以使用nivida的Megatron库，将模型内部改为ColumnParallelLinear, ParallelMLP, ParallelAttention等结构</p>
<p>专家并行特指MOE训练（混和专家模型）</p>
<ol start="2">
<li>
<p>kvcache
加速KQV矩阵的生产运算</p>
</li>
<li>
<p>cuda优化和算子融合
cuda 执行矩阵乘法，激活函数，softmax等，每个操作都对应一次cuda调用。可以自定义cuda Attention优化，以及将多个cuda算子融合到一起，减少cuda调用次数，提高性能。</p>
</li>
</ol>
<h4>内存和IO优化</h4>
<ol>
<li>
<p>FlashAttention
加速注意力计算并减少内存占用。FlashAttention的核心原理是通过<strong>将输入分块</strong>并在每个块上执行注意力操作，从而减少对高带宽内存（HBM）的读写操作。参考文章 <a href="https://zhuanlan.zhihu.com/p/676655352">https://zhuanlan.zhihu.com/p/676655352</a></p>
</li>
<li>
<p>FlashDEcoding
FlashAttention对batch size和query length进行了并行化加速，Flash-Decoding在此基础上增加了一个新的并行化维度：keys/values的序列长度。即使batch size很小，但只要上下文足够长，它就可以充分利用GPU。</p>
</li>
<li>
<p>Continuous Batching
一个批次中，某些请求可能会比其他请求提前“完成”，但这些完成的请求需要等待整个批次完成才释放资源。
Continuous Batching 不会等待批次中的每个序列完成生成，而是实现迭代级调度，一旦批处理中的序列完成生成，就可以在其位置插入新序列，不必等待整个批次完成。 参考文章 <a href="https://github.com/PaddleJitLab/CUDATutorial/blob/develop/docs/13_continuous_batch/README.md">https://github.com/PaddleJitLab/CUDATutorial/blob/develop/docs/13_continuous_batch/README.md</a></p>
</li>
<li>
<p>PagedAttention
现有的推理系统将 KV Cache 存储在连续的显存空间中，导致显存碎片浪费，以及显存无法共享。</p>
</li>
</ol>
<p>PagedAttention 将 KV cache 组织成了固定大小的 KV blocks，类似虚拟内存中的页。管理显存的分配，同时对推理的重复计算内存共享。</p>
<h3>大模型推理框架</h3>
<h4>vLLM</h4>
<p>vLLM 是一个快速、易于使用的 LLM 推理和服务库。可以接收流式的处理请求，并调度GPU和模型执行推理和输出</p>
<ol>
<li>调度器
在每1个推理阶段，决定要把哪些数据送给模型做推理，同时负责给这些模型分配KV Cache物理块。</li>
<li>Worker
CacheEngine：负责管控gpu/cpu上的KV cache物理块（调度器的block manager只负责物理块id的分配）
Worker.model：负责加载模型，并执行推理。</li>
</ol>
<p><a href="https://zhuanlan.zhihu.com/p/691045737">https://zhuanlan.zhihu.com/p/691045737</a></p>
<h4>TensorRT</h4>
<p>​​TensorRT​​ 是 NVIDIA 推出的 ​​高性能深度学习推理优化器，相比vLLM主要从调度层和计算/内存资源分配层做优化，TensorRT 主要在cuda和硬件层进行优化。</p>
<ol>
<li>层融合（Layer Fusion）​​：合并卷积、激活、归一化等连续操作为单一内核，减少内存访问开销。</li>
<li>​​INT8​​：通过量化感知训练或校准集动态量化，速度提升 2-4 倍。</li>
<li>​Dynamic Tensor Memory 在每个tensor的使用期间，TensorRT会为其指定显存，避免显存重复申请，减少内存占用和提高重复使用效率。</li>
<li>Multi stream execution 使用CUDA中的stream技术，最大化实现并行操作。</li>
</ol>
<h3>MOE训练</h3>
<p>混合专家模型（Mixture of Experts, MOE）训练​​ 是一种通过动态路由机制将输入分配给多个子网络（专家）的高效模型架构，在大规模模型中广泛应用</p>
<ol>
<li>​​专家网络​​, 多个独立的子模型（如全连接层、Transformer块），每个专家专注不同特征模式。可以实现并行处理</li>
<li>​​门控网络（Gating）​​	根据输入生成权重，决定各专家的贡献比例（稀疏或软选择）</li>
<li>​​路由策略​​	控制输入如何分配给专家（如 Top-K 选择、负载均衡约束）。</li>
</ol>
<p>优势</p>
<ol>
<li>​模型容量扩展​​，混和专家模型每次只计算局部的参数（激活局部的专家），因此能在GPU有限情况下训练大量参数的模型。增加专家数量可提升模型能力，而计算量仅随激活的专家数增长。</li>
<li>​​稀疏计算​​：仅部分专家参与推理（如 K=2），适合资源受限场景（如 GPU 显存优化）。</li>
<li>​多模态学习​​：不同专家可处理不同类型输入（文本、图像等）。</li>
</ol>
<p>参考文章，<a href="https://huggingface.co/blog/moe">https://huggingface.co/blog/moe</a></p>
<h3>deepseek开源</h3>
<h4>1. FlashMLA</h4>
<p>deepseek 借鉴了FlashAttention项目中的一些理念，针对MLA进行优化的CUDA内核算子，并可集成到pytorch</p>
<p>链接 <a href="https://github.com/deepseek-ai/FlashMLA">https://github.com/deepseek-ai/FlashMLA</a></p>
<p>文档, <a href="https://github.com/deepseek-ai/FlashMLA/blob/main/docs/20250422-new-kernel-deep-dive.md">https://github.com/deepseek-ai/FlashMLA/blob/main/docs/20250422-new-kernel-deep-dive.md</a></p>
<h4>2. DeepEP</h4>
<p>DeepEP 是一个专门为混合专家（MoE）模型和专家并行（EP）设计的通信库。DeepEP is a communication library tailored for Mixture-of-Experts (MoE) and expert parallelism (EP). It provides high-throughput and low-latency all-to-all GPU kernels, which are also as known as MoE dispatch and combine. The library also supports low-precision operations, including FP8.</p>
<p>链接 <a href="https://github.com/deepseek-ai/DeepEP">https://github.com/deepseek-ai/DeepEP</a></p>
<h4>3. DeepGEMM</h4>
<p>通用矩阵乘法算子</p>
<h4>4. DualPipe &#x26; EPLB</h4>
<p>DualPipe 训练时流水线调度，采用了一种独特的调度策略，使得前向传播和反向传播可以在不同的GPU上同时进行
EPLB 实现专家负载均衡</p>
<h4>5. 3FS</h4>
<p>为大模型推理提供数据集和模型读写能力，主要关注小IO和大IO读，以及大IO写。</p>
<h3>总结</h3>
<p>CPU运算的适用于逻辑运算（如数据清洗），简单整数和向量计算（例如OLAP），而GPU运算适用于矩阵运算（如卷积、矩阵乘法等）。一般的计算模型是GPU-CUDA-Pytorch三件套。</p>
<p>大模型基于transformer构建，是基于多层矩阵运算的复杂模型。本文分析了transformer一层的参数量，计算量，显存使用量和kvcache、重计算加速等。注意大模型和传统流式处理(如Flink的区别)是，大模型是高度密集型矩阵运算，而Flink流式处理的运算并不复杂，侧重于数字或向量计算而非矩阵运算。但大模型推理后续替代Flink犹未可知。</p>
<p>对于大模型加速，主要分为模型侧、计算侧和内存IO侧。模型侧主要是压缩和量化，计算侧包括并行计算，cuda算子优化以及MOE训练等，内存IO侧主要在内存分配，共享，高性能存储网络等。</p>
<p>大模型推理框架，vLLM是一套推理工程的解决方案。最后deepseek开源了包括矩阵运算、MLP算子、高性能网络、MOE训练负责均衡以及高性能存储的项目，都很值得学习。</p>
<p>可以把GPU训练看到类似IO存储，以后的业务层</p>
<ol>
<li>接收网络请求，准备执行环境</li>
<li>执行顺序和if-else逻辑，包括鉴权、流控、日志等</li>
<li>执行IO存储逻辑，包括写数据库/写文件/写缓存/写oss等</li>
<li>执行在线大数据处理逻辑，例如搜广推等（CPU计算）</li>
<li>执行大模型GPU推理逻辑，推理生成序列或预测结果</li>
<li>获得3,4,5的结果，返回给客户端</li>
</ol>
<p>业务层本身是无状态的CPU计算，主要关注的</p>
<ol>
<li>接收海量请求，也就是高并发</li>
<li>自身可以水平扩展</li>
<li>明确后台的能力，为数据库层，模型推理层提供缓存，流控，队列等，防止后端压力过大</li>
<li>复杂的业务逻辑解耦</li>
</ol>
<p>IO存储、在线大数据处理、大模型推理层负责提供高并发、高性能的存储和计算推理服务。</p>
<p>by the way, 显然实时性的业务更具有挑战性，需要低延迟、高吞吐和高QOS。例如后端业务层开发（毫无疑问要是实时返回的）、搜索广告推荐（需要实时数据分析）、大模型推理（需要实时推理）、分布式数据库和存储（需要提供实时表和文件读写服务）、量化交易（需要低延迟自动化和手段触发策略）等。如果某业务无实时性处理要求，那技术性将会大打折扣。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[compute]]></category>
        </item>
        <item>
          <title><![CDATA[计算(1)——CPU计算和大数据]]></title>
          <link>https://larrystd.github.io/posts/计算(1)—CPU计算和大数据</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/计算(1)—CPU计算和大数据</guid>
          <pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[由于本人是存储方向，前面的文章大多数是关于存储的。计算机系统=计算+存储，其中计算包括计算单元（CPU、GPU）、cacheline和内存态的处理, 而存储特指IO处理。计算可分为CPU计算和GPU计算两部分，前者的典型场景是操作系统处理、进程执行、互联网业务处理和大数据和OLAP处理等。后者的典型场景是大模型的训练推理。]]></description>
          <content:encoded><![CDATA[<p>由于本人是存储方向，前面的文章大多数是关于存储的。计算机系统=计算+存储，其中计算包括计算单元（CPU、GPU）、cacheline和内存态的处理, 而存储特指IO处理。计算可分为CPU计算和GPU计算两部分，前者的典型场景是操作系统处理、进程执行、互联网业务处理和大数据和OLAP处理等。后者的典型场景是大模型的训练推理。</p>
<p>本文介绍CPU计算。CPU计算主要包括业务处理和大数据处理两部分，<strong>CPU运算的瓶颈往往不在于计算，而在于内存、存储和网络</strong>（可能这是ddia 把大数据计算也放到内的原因）。相比于算法模型，CPU计算更侧重于工程。</p>
<h3>CPU的计算</h3>
<h4>流水线和分支预测、指令重排序</h4>
<p>假设系统只有一个进程运行，进程文件首先被装载到内存。CPU执行的过程大致是：取指（Fetch, 载入指令存入指令寄存器）、解码（Decode，将指令解码）、执行（Execute，执行指令）、访存（Memory Access，执行指令期间可能需要访问内存）和写回（Write Back，将指令执行结果写回内存）。常见的指令有</p>
<ol>
<li>x = 1, 赋值指令，只需要读存、写存</li>
<li>x = x + 1, 加法指令，需要读存、加法、写存</li>
<li>if (x), 条件判断指令，需要读存、比较、更新指令寄存器（记录下一个执行的指令）</li>
</ol>
<p>假设一个进程有4条指令，执行每个指令都需要取值、解码、执行、访存和写回五步。原始的执行方法每个指令都需要5个CPU周期，总共需要4*5=20个周期。其中取值、解码、访存和写回CPU都是空闲的，这时候的CPU利用率只有20%。</p>
<p>CPU可以采用pipeline（流水线）算法执行以上5条指令，因为取值、解码、访存和写回操作是不占CPU周期的，因此CPU可以在一个周期内完成第一条指令的执行、第二条指令的解码、第三条指令的访存，如图所示。这时候CPU完成4条指令只需要8个CPU周期，CPU利用率为50%。
<img src="/images/cpupipeline.png" alt="cpupipeline"></p>
<p>在流水线算法中，每个指令任务由链式的的子任务组成，有的子任务要操作（执行阶段），有的子任务则只需要看一眼（取指、解码、访存、写回操作），不占用CPU执行周期。摆在CPU面前的是若干并行的指令，CPU每个周期只执行一个任务需要操作的子任务，在多个任务并行的情况下，CPU每个周期都有活干，不会有周期空闲。</p>
<p>两个因素可能导致流水线暂停</p>
<ol>
<li>条件判断指令
条件判断指令导致流水线暂停。由于下一条指令取决于条件判断指令的执行结果，CPU需要等待条件判断指令执行完，期间无法正常的流水线任务。（正常的流水线任务旧的指令不断结束，新的指令不断加入流水线，但执行条件判断指令相当于在它执行完之前，无新的指令加入流水线）。</li>
</ol>
<p>处理手段是采用分支预测，CPU或编译期会预测条件指令true/false哪个概率更高，直接将概率高的指令加入流水线（相当于条件执行转为顺序执行），如果预测失败，流水线u回滚，重新执行分支后的指令。C/C++ 可以通过likely/unlikely 宏来提醒编译期概率高的条件。</p>
<ol start="2">
<li>指令间相互依赖
也就是指令1未完全执行完，指令2无法执行。如果有这样的依赖关系，CPU就不能通过子任务流水线方式运行指令1和2，而是必须等指令1执行完最后一步写回，才能执行指令2。</li>
</ol>
<p>处理手段是指令重排序和乱序执行，若指令1，2执行有依赖，如果指令3不依赖1和2，CPU可以转而执行指令3。执行流变为1，3，2，1、3可以流水线并行，3、2也可以流水线并行。</p>
<p>CPU指令重排序可以保证单线程的排序前后的执行结果一致，但无法保证多线程一致（主要是可见性无法保证）。可以通过在程序中加内存屏障和内存序，保证两个指令之间不会重排序，且store-release指令的执行结果load-acquire指令一定可以看到。</p>
<h4>CPU执行的计算</h4>
<p>CPU的工作分为以下几种</p>
<ol>
<li>寄存器赋值，访存等读写操作。为了提高访存效率（CPU获取指令会大量访存，访存会降低CPU使用率）, CPU会引用三级cache。</li>
<li>x >0 等bool 运算, 用于条件分支判断</li>
<li>x+1, x*2等整数算数运算</li>
<li>3.4*5.6 等浮点数运算</li>
<li>MMU 地址转换，MMU有TLB cache和转换单元组成。操作系统把MMU的输出到物理地址的映射记录到内存地址中，地址先通过MMU运算出结果，再根据结果从内存中找到物理地址。</li>
<li>CPU硬中断（如缺页中断），转而去指定地址执行中断处理程序</li>
<li>时钟，记录CPU周期，作为心跳触发CPU循环执行指令</li>
<li>控制器/调度器，负责指令执行更精细化的控制和调度</li>
</ol>
<p><strong>CPU像真实的一个人</strong>，他的工作是循环执行指令，这些指令是串行发来的，需要调度器的思考进行分支预测、指令重排序等优化以便于流水线并行。同时随时会有其他的急事通过中断打断他。CPU更适合执行通用复杂的指令，应付各种场景，如果给他一个计算密集型任务，例如密码解密，CPU会很吃力。</p>
<p>相比之下GPU更像工厂的机器，专门执行特定任务，效率远高于CPU。CPU控制GPU如同现实中的人操纵机器。</p>
<p>**将专用计算从CPU卸载是硬件相当重要的发展方向，**如收发网络包由CPU处理网卡到DMA再到RDMA，浮点运算由CPU 到GPU等。</p>
<h4>CPU的向量化计算</h4>
<p>为了提高CPU的浮点和向量运算能力，英特尔引入了SIMD(Single Instruction, Multiple Data)指令集，SIMD指令集可以实现一条指令计算多个数据，如向量加法、向量乘法、向量比较、向量位运算等。</p>
<p>SIMD有专门的向量处理单元，SIMD寄存器一般很长，整个向量直接存到寄存器中，向量运算指令将向量数据从寄存器中取出，传入向量运算单元运算，将运算结果写入寄存器。</p>
<p>SIMD在IO层往往配合列式存储，执行分析运算时。程序首先从存储层读取少量列到内存，然后依靠SIMD指令对列进行向量化运算。列式存储还可以充分利用寄存器和cache的局部性，需要反复计算的向量不必经常访存。列式存储配合数据压缩，减少列式存储的size，尽可能将列数据放于缓存和内存中，减少IO和网络开销，提升性能。</p>
<p>ClickHouse​ 就是一个采用向量化引擎、列存储和列压缩的高性能分析性数据库。</p>
<h4>增加CPU计算能力和虚拟化</h4>
<p>稳定、性能、成本是分析一个系统的主要切入点，我们从这三个角度分析增加CPU处理能力的手段</p>
<ol>
<li>多核CPU，通过多核并行能力提高CPU计算能力。与之而来的是操作系统的多进程/线程批处理，目的之一是为了提高CPU处理能力。</li>
<li>多线程的锁争抢会影响线程执行，线程的阻塞（如阻塞在锁或IO）也会降低CPU的有效利用率。另外由于分配线程需要资源，操作系统能够分配的线程有限。因此引入协程来增强处理能力，协程下层的线程不会阻塞，一般创建CPU核数的线程，线程之上创建大量协程，是计算侧提高CPU利用率的有效方案。</li>
</ol>
<p>由于CPU的任务很复杂，不想GPU那样只有单一的矩阵浮点运算任务，因此CPU利用率往往比较波动。可能某时刻有大量请求进来，某时刻请求有比较少。这时候我们希望CPU的计算具有扩展能力。例如我们有一台96核12G的物理机，当请求量变多时，机器用来处理一个服务；当请求量年少时，这个机器可以处理其他服务。这样子这台物理机的CPU利用率会比较稳定。</p>
<p>计算侧扩展能力就要引入虚拟化，虚拟化的手段主要有两个，虚拟机和docker容器</p>
<ol>
<li>虚拟机通过 ​​Hypervisor​​（如 VMware、VirtualBox、KVM）在物理硬件上虚拟化完整的操作系统（Guest OS），每个 VM 独立运行，彼此隔离。虚拟机可以借助VT-x和virtio等技术减少虚拟化的开销。虚拟机的资源隔离性强，但耗费资源多、性能有损失、迁移能力较差，主要使用在云计算的云服务器，是计算和内存资源的包装，属于IASS层。</li>
<li>容器是操作系统内核提供的虚拟化技术，通过 cgroups 和 namespaces 实现资源隔离。容器相比虚拟机资源消耗低、性能高、迁移能力强，但物理机上的容器公用一个操作系统，资源可能相互影响。容器一般在物理机上以应用的形式运行，当容器资源不足，可以在线扩容容器的CPU和内存资源；若物理机资源不足，可动态将容器迁移至其他物理机。<strong>为了提高容器的调度能力，容器上的服务一般是无状态的</strong>，容器使用存储接口访问物理机或分布式文件系统的存储服务。</li>
<li>使用容器，降低了服务的计算成本，同时提高了服务的稳定性。一个容器挂了，备用容器会自动顶上来。容器集群中，负责容器调度的是master，而服务所在容器是worker。也就是说，容器集群中，我们的互联网服务，包括收发包、路由、鉴权、流控等，都是分布式计算。而用到的缓存、数据库和文件存储等，是分布式存储。</li>
</ol>
<p>由于CPU的任务很复杂，因此CPU利用率往往比较波动，提升比较困难。需要全方面系统的分析。</p>
<h3>大数据计算</h3>
<p>大数据计算的3V问题</p>
<ol>
<li>Volume（数据量）​​, PB级数据存储与处理（HDFS和S3）</li>
<li>Velocity（速度）​​: 实时/近实时计算（如Flink）。</li>
<li>Variety（多样性）​​: 结构化、半结构化、非结构化数据混合处理</li>
</ol>
<p>数据的生命周期</p>
<ol>
<li>数据获取，离线批处理需要从日志等复杂数据源中获取，而在线处理往往只需要从数据仓库获取关键数据，以提高实时性
<ol>
<li>如日志文件（描述系统的耗时、QPS、trace等），用户请求（请求携带了用户信息、请求参数、用户行为等），数据库和仓库（历史的用户行为，用户画像等）</li>
<li>用户数据是互联网公司的核心资产之一，互联网流量打的公司也具有更多有价值的数据，如阿里的电商数据、字节的视频数据、百度的搜索数据、腾讯的社交和游戏数据等</li>
</ol>
</li>
<li>数据清洗，数据清洗也是一种大数据处理，往往用于批处理。如统一字段格式，删除重复数据，修正异常数据，对数据进行归一化等</li>
<li>数据模型处理
<ol>
<li>如果是离线批处理，数据模型处理往往比较简单，例如从用户请求中提取特征和预测标签，将整理好的数据写入到数据仓库。</li>
<li>如果是在线处理，要拿当前的输入数据和从数据仓库获得价值数据输入模型，结合业务场景（比如推荐、广告、搜索等），拿到TopN的结果（粗排+精排），返回给客户。这个过程需要准确、快速，也需要结合业务影响（例如广告竞价排名，广告推荐等）。</li>
</ol>
</li>
<li>数据输出和存储。对于离线数据处理，数据会输出到数据仓库，对于在线数据处理，数据返回给客户端，或到下一层进一步处理。</li>
</ol>
<p>离线数据处理更像是纯粹的分布式计算，而在线数据数据处理需要考虑多种因素，包括实时性、业务场景、准确性指标等。</p>
<h4>OLAP和数据仓库</h4>
<p>数据仓库存储的数据通常是结构化的，即按照事先定义好的格式和模式进行组织和存储，集成了来自多个不同来源的数据。</p>
<ol>
<li>数据仓库中的数据是按照一定的主题域进行组织。主题是指用户使用数据仓库进行决策时所关心的重点方面。<strong>数据仓库的数据是用来分析的结构化数据</strong>（对应数据库的数据是主要用来处理用户请求，而非用于数据分析）。</li>
</ol>
<p>OLAP（Online Analytical Processing），是一种分析型数据库，<strong>常作为数据仓库的存储引擎</strong>。适合OLAP的场景</p>
<ol>
<li>​​数据以批量/流式​顺序​写入​​，插入和更新操作少。读取时不按照主键读取一行，而是直接读取一列</li>
<li>每个列代表维度，例如数据设计时间、区域、用户群等多维度</li>
</ol>
<p>OLTP（Online Transaction Processing）OLTP通常按行（记录）存储，OLTP支持按主键读取一行，和更新数据等。OLAP表往往直接按照列读取用于分析，行数通常远大于列数（例如实时日志、用户实时特征数据等），往往以列的方式存储。</p>
<p>显然OLAP适合少量维度数据，每个维度大量数据的数据挖掘读取。OLAP是数据仓库的存储引擎，从数据源获取数据，进行数据清洗后，将数据写入到OLAP。OLAP支持数据挖掘模型按列读取数据进行分析处理（例如降维、预测等，会增加或减少维度），将数据处理结果写回到OLAP。</p>
<h4>ETL 离线大数据处理流程</h4>
<p>大数据的ETL（Extract, Transform, Load）​模型​ 是数据从<strong>源系统到目标数据仓库或数据湖</strong>的核心流程</p>
<ol>
<li>​​数据抽取（Extract）​，从数据库、文件、API等拉取数据，支持全量/增量同步</li>
<li>​​数据转换（Transform）​清洗（去重、缺失值处理）、标准化（字段映射）、聚合计算</li>
<li>​​数据加载（Load）​写入目标存储（数据仓库、数据湖），优化存储格式（列存/分区</li>
</ol>
<p>例如，电商用户行为数据ETL​
​1. ​抽取​​：从Nginx日志（JSON）和MySQL订单表增量拉取数据。
​2. ​转换​​：日志解析（提取user_id、event_time）。订单表与日志数据JOIN，生成用户行为宽表。
3. ​加载​​：写入Hive分区表（按dt分区）供BI分析。</p>
<p>可以看到，离线大数据处理真正模型计算的部分比较简单，大致就是select检索，非结构化数据解析和清洗，维度聚合等，且计算模型往往是框架提供的。大多数的工作量在数据抽取、数据清洗上的计算、存储上的优化上。（相对比较无趣）</p>
<p>湖仓一体化，用数据湖（主要是S3）存储原始数据（结构化、半结构化、非结构化），数据仓库存储经过清洗、结构化处理的数据。模型的分析从数据仓库（OLAP）读取数据、处理、写回数据。</p>
<h4>Flink 实时数据处理</h4>
<p>在线数据处理的场景包括搜索、推荐、广告等，需要实时性，结果准确，同时需要结合业务。Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台, 本部门主要看流处理。</p>
<p>Flink的架构</p>
<ol>
<li>Flink支持通过producer-consumer模型接受需要处理的流数据，将流数据源做我datasource</li>
<li>JobManager是Flink系统的协调者，它负责接收Flink Job，调度组成Job的多个子Task的执行。JobManager还负责收集Job的状态信息。</li>
<li>TaskManager也是一个Actor，它是实际负责执行计算的Worker，负责执行Flink Job的Task。</li>
<li>每个task对应flink的一个算子，如map(...).setParallelism(2), 可并行执行</li>
<li>Flink基于Checkpoint机制实现容错，它的原理是不断地生成分布式Streaming数据流Snapshot。在流处理失败时，通过这些Snapshot可以恢复数据流处理。</li>
</ol>
<p><img src="/images/flink.png" alt="flink"></p>
<p>应用场景</p>
<ol>
<li>
<p>日志实时监控
例如队列接受数据源的日志数据，实时监控服务器日志、应用性能指标，触发异常告警（如错误率突增、服务宕机）。</p>
</li>
<li>
<p>实时推荐系统
通过消息队列接受用户实时行为（点击、搜索），读取OLAP的用户画像，调用模型预测，将推荐结果返回给用户</p>
</li>
</ol>
<p><img src="/images/onlineprocess.png" alt="onlineprocess"></p>
<p>基于CPU的大数据计算是低维向量运算，计算量并不大。大数据计算的瓶颈往往不在于计算，而在于内存/存储。相比于算法模型，大数据计算更侧重于工程。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[compute]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言——C++右值和右值引用]]></title>
          <link>https://larrystd.github.io/posts/编程语言—C++右值和右值引用</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言—C++右值和右值引用</guid>
          <pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[C++右值和右值引用是最容易用错的特性之一，相关教程普遍写得不清晰。实际上，只有在所有权转移时才需要使用右值引用，移动语义移动的是所有权，完美转发转发的也是所有权。]]></description>
          <content:encoded><![CDATA[<p>C++右值和右值引用是最容易用错的特性之一，相关教程普遍写得不清晰。实际上，只有在所有权转移时才需要使用右值引用，移动语义移动的是所有权，完美转发转发的也是所有权。</p>
<h3>总结</h3>
<p>先放总结</p>
<ol>
<li>左值引用的语义是绑定、const指针和不转移所有权(rust称为借用)，右值引用的语义是绑定、const指针和转移所有权</li>
<li><strong>是否使用右值引用，取决于是否要转移变量所有权</strong>。右值引用和左值引用在传参时都不会调用构造函数，性能一样高效。</li>
<li><strong>如果是字面量, 如果只是读字面量，直接用const&#x26; 参数接收即可</strong> （最常见）。如果要转移所有权，例如使用容器时，字面值在当前函数创建随即交给容器，字面量的生命周期后续由容器负责。这时候容器函数应该使用&#x26;&#x26;，例如emplace()的接口就是右值引用。const&#x26;和&#x26;&#x26;的开销都是一次字面量的构造函数</li>
<li><strong>如果是左值，如果需要转移所有权，推荐使用&#x26;&#x26;和std::move()。如果不要, 用左值引用或指针即可</strong>。是不是要转移所有权，在于你想要这个左值变量在当前函数析构，还是在函数外析构</li>
<li>新建对象，通过传std::move()左值，可以通过移动构造加快构造速度。如果传字面量，通过可以通过移动构造加快速度。但如果传左值，则需要调用拷贝构造函数。</li>
<li>函数返回值不要考虑右值引用，右值引用只在函数参数上使用</li>
</ol>
<h3>右值和右值引用</h3>
<p>也许只有C++语言才有左值和右值的概念，程序语言中，左值和右值普遍的概念分别是变量和字面量。</p>
<p>变量可以认为是到内存地址的映射，(变量->内存地址)，通过变量我们可以拿到想访问内存的地址，修改变量也就是修改指定地址中的内存。显然变量，或者说左值是可以取地址的。</p>
<p>字面量是程序中写的值，这完全是编译期的概念。编译器编译成运行的指令，将字面量放入内存中。运行期时，只能通过变量来获取值（指针也是一种变量）。因此，变量/左值是编译期和运行期都存在的概念，而字面量/右值只是编译期的概念！</p>
<p>这也导致了左值引用和右值引用的区别，左值引用同时是编译期语义和运行期语义</p>
<ol>
<li>编译期语义，左值引用是一种静态类型，静态类型也是编译期的语义，运行期不存在静态类型的语义。引用类型的语义是绑定，也就是左值引用可以绑定左值，右值引用可以绑定右值。</li>
<li>运行期语义，左值引用在运行期的语义基本等于const 指针, 即可以修改引用的对象，但不能修改指针的指向。</li>
</ol>
<p>右值引用同样1. 编译期语义，右值引用也是静态类型, 可以绑定右值 2. 运行期语言，右值引用记录字面量对象的地址</p>
<p>注意<strong>字面量虽然不能被取地址，但它是有地址的，右值引用就记录字面量对象的地址。因此右值引用可以取地址，修改字面量对象，和左值引用一样。</strong></p>
<h4>引用绑定和构造函数</h4>
<p>引用类型的编译期语义是绑定，类型的构造函数，即拷贝构造、移动构造、拷贝赋值和移动赋值就是<strong>依赖引用类型的绑定来接收左值或右值</strong>。</p>
<p>下列代码可以得到</p>
<ol>
<li>拷贝构造和拷贝赋值传入左值引用，移动构造和移动赋值传入右值引用。这代表编译期拷贝函数的参数需要绑定左值，移动函数的参数需要绑定右值。如果传入左值，则不可调用移动函数，反之不可调用拷贝函数。</li>
<li>在使用变量创建IntVector时，由于拷贝函数绑定左值，因此会调用拷贝函数; 使用字面量(如IntVector{})创建IntVector，会调用移动函数。</li>
<li>移动构造和移动赋值的语义是所有权转移，例如使用字面量创建IntVector{}，如auto x = IntVector(IntVector{});, 代表字面量对象的所有权交给了新建的对象x。</li>
</ol>
<p>移动构造和移动赋值的所有权语言更典型的体现是在容器中，例如vec.emplace_back(1), 表示字面量的所有权交给了vec容器。清理容器时会清理这个字面量，字面量的生命周期由容器控制，不再由函数控制。</p>
<p>std::move可以把左值转为右值，这也是编译期的概念。例如vec.emplace_back(std::move(a)), 表示a的所有权交给了vec容器。清理vec时会清理a，<strong>a的生命周期由vec控制，不再由函数控制。</strong> 变量通过转换成右值，借助移动构造函数将所有权转移，当前函数后续就不能直接操作这个变量，当前函数不具有变量的所有权。</p>
<p>简单讲一下所有权，所有权代表生命周期控制，函数/对象都可以持有所有权。持有所有权代表函数/类负责对象的生命周期，例如类持有成员变量的所有权吗，<strong>当对象的生命周期脱离控制，也就是失去了该对象的所有权。</strong></p>
<pre><code class="language-cpp">#include &#x3C;algorithm> // std::copy
#include &#x3C;utility>   // std::swap

class IntVector {
public:
    IntVector() : data_(nullptr), size_(0) {}

    explicit IntVector(size_t size) 
        : data_(new int[size]), size_(size) {}

    ~IntVector() {
        delete[] data_;
    }

    // 拷贝构造函数（深拷贝）
    IntVector(const IntVector&#x26; other) 
        : data_(new int[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 移动构造函数（转移资源）
    IntVector(IntVector&#x26;&#x26; other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr; // 置空原对象指针
        other.size_ = 0;
    }

    // 拷贝赋值运算符
    IntVector&#x26; operator=(const IntVector&#x26; other) {
        if (this != &#x26;other) { // 避免自我赋值
            delete[] data_;   // 释放现有资源
            data_ = new int[other.size_];
            size_ = other.size_;
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }

    // 移动赋值运算符
    IntVector&#x26; operator=(IntVector&#x26;&#x26; other) noexcept {
        if (this != &#x26;other) {
            delete[] data_;    // 释放现有资源
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr; // 置空原对象
            other.size_ = 0;
        }
        return *this;
    }

    // 获取数组大小
    size_t size() const { return size_; }

    // 访问元素
    int&#x26; operator[](size_t index) { return data_[index]; }
    const int&#x26; operator[](size_t index) const { return data_[index]; }

    // 交换函数（辅助移动赋值）
    friend void swap(IntVector&#x26; a, IntVector&#x26; b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

private:
    int* data_;
    size_t size_;
};
</code></pre>
<p>这部分总结就是</p>
<ol>
<li>左值引用和右值引用的编译期语义分别是绑定左值和右值</li>
<li>拷贝函数参数是左值引用，它绑定一个左值，根据这个左值拷贝得到新对象；</li>
<li>移动函数参数是右值引用，它绑定一个右值，根据这个右值移动得到新对象。移动构造函数实现时可以移动右值的成员到新对象，可以减少拷贝，这本身是所有权转移，于是右值引用又增加了移动所有权的语义。</li>
</ol>
<p>前面讲到左值引用和右值引用都有绑定和指针的语义，这里又增加了右值引用的移动所有权的语义，相反左值引用的语义则是不移动所有权</p>
<ol>
<li>需要注意C++的灵活性，你通过左值引用传左值到新函数，同时在新函数将该左值析构。这代码上没错，但违反了左值引用不移动所有权的语义。正确的使用是传通过右值引用传右值到新函数，新函数可以将该右值移动或析构。在rust中，前一种操作会直接编译报错。</li>
<li><strong>总之编码时只有右值引用才转移所有权（例如将unique_ptr传给新函数，推荐使用右值引用，因为发生所有权转移）。传指针/引用都不应该转移所有权，这种编码风格才是推荐的。通过函数参数就知道对象被谁管理。</strong></li>
</ol>
<p>为什么不拷贝/移动函数不使用值传递，因为值传递过程中就需要拷贝，拷贝有需要拷贝函数。。。拷贝函数/移动函数通过左值引用和右值引用，在传参时都不会有拷贝。</p>
<h4>移动函数和拷贝函数的调用时机</h4>
<p>观察下面程序，可以得到</p>
<ol>
<li><code>MyClass bb = a</code> 调用构造函数，<code>MyClass bb; bb = std::move(a)</code> 调用赋值函数。</li>
<li>执行<code>c = MyClass{}</code>，虽然是调用移动函数, 但会MyClass{}字面量的构造需要调用一次构造函数。<code>MyClass bb = a</code> 只需要一次拷贝构造函数，不用构造函数</li>
<li><code>c = std::move(a);</code> 是真正的只要调用一次移动赋值函数</li>
</ol>
<pre><code class="language-cpp">#include &#x3C;algorithm> // std::copy
#include &#x3C;utility>   // std::swap
#include &#x3C;cstdio>

class MyClass {
public:
    MyClass() {
        printf ("构造函数\n");
    }
    ~MyClass() {
        printf ("析构函数\n");  
    }
    MyClass(const MyClass&#x26; other) {
        printf ("拷贝构造函数\n");
    }
    MyClass(MyClass&#x26;&#x26; other) noexcept {
        printf ("移动构造函数\n");
    }
    MyClass&#x26; operator=(const MyClass&#x26; other) {
        printf ("拷贝赋值函数\n");
        return *this;
    }
    MyClass&#x26; operator=(MyClass&#x26;&#x26; other) noexcept {
        printf ("移动赋值函数\n");
        return *this;
    }
};

int main() {
    MyClass a;
    printf ("MyClass b(a);\n");
    MyClass b(a);
    printf ("MyClass bb = a;\n");
    MyClass bb = a;
    printf ("MyClass c(std::move(a));\n");
    MyClass c(std::move(a));
    printf ("b = a\n");
    b = a;
    printf ("c = std::move(a)\n");
    c = std::move(a);
    printf ("c = MyClass{};\n");
    c = MyClass{};
}

// 输出
构造函数
MyClass b(a);
拷贝构造函数
MyClass bb = a;
拷贝构造函数
MyClass c(std::move(a));
移动构造函数
b = a
拷贝赋值函数
c = std::move(a)
移动赋值函数
c = MyClass{};
构造函数
移动赋值函数
析构函数
析构函数
析构函数
析构函数
析构函数
</code></pre>
<p>字面量使用const&#x26;和&#x26;&#x26;的区别,
观察下面程序，可以看到</p>
<ol>
<li>const&#x26;和&#x26;&#x26;作为引用，传参时都不会调用构造函数</li>
<li>左值优先匹配const&#x26;，右值优先匹配&#x26;&#x26;</li>
<li>std::move()后的左值优先匹配&#x26;&#x26;</li>
<li>const&#x26; 函数对传的变量只能只读, 而&#x26;&#x26;可以修改参数变量。</li>
</ol>
<pre><code class="language-cpp">void func(const MyClass&#x26; object) {
    printf ("const&#x26; 接收\n");
}

void func(MyClass&#x26;&#x26; object) {
    printf ("&#x26;&#x26;接收\n");
}

void func1(const MyClass&#x26; object) {
    printf ("强制const&#x26; 接收\n");
}

int main() {
    MyClass object1;
    printf ("func(object1);\n");
    func(object1);
    printf ("func(std::move(object1));\n");
    func(std::move(object1));
    printf ("func(MyClass());\n");
    func(MyClass());
    printf ("func1(MyClass());\n");
    func1(MyClass());
}

// 输出
构造函数
func(object1);
const&#x26; 接收
func(std::move(object1));
&#x26;&#x26;接收
func(MyClass());
构造函数
&#x26;&#x26;接收
析构函数
func1(MyClass());
构造函数
强制const&#x26; 接收
析构函数
析构函数
</code></pre>
<p>右值引用可以设置值，取地址</p>
<pre><code class="language-cpp">void func(MyClass&#x26;&#x26; object) {
    printf ("&#x26;&#x26;接收\n");
    object.a = 1;
    printf ("object address %p\n", &#x26;object);
}
</code></pre>
<p>左值引用的取地址是绑定变量的地址</p>
<h4>函数返回值</h4>
<p>前面讲了函数参数，我们得到当传一个左值或右值且所有权转移，使用右值引用绑定；否则使用左值引用或指针绑定。</p>
<p>引用传参，无论是左值引用还是右值引用，都不会触发构造语义。如果我们返回一个对象呢，例如</p>
<pre><code class="language-cpp">MyClass create() {
    return MyClass();  // ✅ RVO 触发，直接在调用处构造
}
MyClass x = create();
</code></pre>
<p>事实上返回值一般会执行返回值优化，即返回的对象在调用处直接构造，无须构造临时对象。因此我们没有任何必要调用<code>return std::move(xx)</code>。</p>
<p>实际编码中，我们推荐</p>
<ol>
<li>使用引用或指针函数参数只读或修改对象</li>
<li>用专门的工厂函数创建对象，并返回对象的指针，或者返回shared_from_this()</li>
<li>函数返回错误码，尽量避免返回对象</li>
</ol>
<p>因此右值引用一般只用于函数参数，用来绑定右值并执行移动语义和所有权转移，无须在返回值中考虑右值引用</p>
<h4>什么时候用右值引用</h4>
<p>由上我们总结, 右值引用和左值引用在传参时都不会调用构造函数，性能一样高效。是否使用右值引用，取决于是否要转移变量所有权
1.<strong>如果是字面量, 如果只是读字面量，直接用const&#x26; 参数接收即可</strong> 。如果要转移所有权，例如使用容器时，字面值在当前函数创建随即交给容器，字面量的生命周期后续由容器负责。这时候容器函数应该使用&#x26;&#x26;，例如emplace()的接口就是右值引用。使用左值引用接收右值，需要1构造+1拷贝函数，而右值引用需要1构造+1移动函数
2. <strong>如果是左值，如果需要转移所有权，推荐使用&#x26;&#x26;和std::move()。如果不要, 用左值引用或指针即可</strong>。是不是要转移所有权，在于你想要这个左值变量在当前函数析构，还是在函数外析构
3. 移动构造函数使用右值引用作为参数，可以加快构造速度。但移动构造函数只有在新建对象时才使用。
4. 函数返回值不要考虑右值引用，右值引用只在函数参数上使用</p>
<h4>完美转发</h4>
<p>std::move(x) 将左值转为右值，从而利用右值引用函数参数(右值也是转成右值)</p>
<p><code>std::forward&#x3C;T></code>, 若T是左值引用，返回左值。若 T 是右值引用，返回右值。完美转发主要处理《右值引用也是一种左值》这个问题，使用forward，可以把右值引用再次转成右值，从而继续使用下一个函数的右值引用函数参数。</p>
<p>所有权语义上分析，完美转发就是在A函数将对象所有权转移到B函数，B函数再将对象所有权转给C函数。移动语义移动的是所有权，完美转发转发的也是对象所有权。</p>
<p>如果参数是通用引用，对于需要转移所有权的右值引用，B函数会将参数传递给C函数，对于无须转移所有权的左值引用，B函数不会转移所有权给C函数</p>
<pre><code class="language-cpp">#include &#x3C;utility>
#include &#x3C;iostream>

// 目标函数
void process(int&#x26; x) { std::cout &#x3C;&#x3C; "左值: " &#x3C;&#x3C; x &#x3C;&#x3C; "\n"; }
void process(int&#x26;&#x26; x) { std::cout &#x3C;&#x3C; "右值: " &#x3C;&#x3C; x &#x3C;&#x3C; "\n"; }

template &#x3C;typename T>
void relay(T&#x26;&#x26; arg) {
    process(std::forward&#x3C;T>(arg)); // 完美转发, 二次转发对象。arg如果是左值引用，这里转型为左值，如果是右值引用，这里转型为右值
    // 如果是process(arg), arg将一直认为是左值
}

int main() {
    int a = 10;
    relay(a);       // 传递左值 → 调用 process(int&#x26;)
    relay(20);      // 传递右值 → 调用 process(int&#x26;&#x26;)
    relay(std::move(a)); // 传递右值引用 → 调用 process(int&#x26;&#x26;)
}
</code></pre>
<p>转型、左右值、左右值函数引用，函数参数优先匹配等逻辑，都是编译期的行为。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言——C++协程和高性能编程]]></title>
          <link>https://larrystd.github.io/posts/编程语言—C++协程和高性能编程</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言—C++协程和高性能编程</guid>
          <pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[C++可以写出性能高效的程序，一个原因来自语言本身的因素，例如 1. C++程序编译器可以进行优化，编译直接得到机器码，这让编译后需要执行的指令更少（解释器性能比编译期差的主要原因就是是解释器单行编译执行，而编译器是文件编译执行，获得的信息更多，优化空间更大。明确的类型信息也让编译期获得内存信息，可以在无须创建对象情况下进行优化） 2. 没有虚函数的C...]]></description>
          <content:encoded><![CDATA[<p>C++可以写出性能高效的程序，一个原因来自语言本身的因素，例如</p>
<ol>
<li>C++程序编译器可以进行优化，编译直接得到机器码，这让编译后需要执行的指令更少（解释器性能比编译期差的主要原因就是是解释器单行编译执行，而编译器是文件编译执行，获得的信息更多，优化空间更大。明确的类型信息也让编译期获得内存信息，可以在无须创建对象情况下进行优化）</li>
<li>没有虚函数的C++程序编译后的执行码和C语言一样，没有golang interface{}, java 虚函数等额外的内存开销。C++的class, template等功能抽象不会带来额外的性能开销</li>
<li>C++可以直接管理内存，轻松写出内存零拷贝的程序，无需GC额外的性能开销。</li>
</ol>
<p>除了语言本身的因素，生态因素对于高性能同样重要。用户程序不可能每次都造轮子，如果没有高性能的库，C++不会成为性能高效程序的首选。例如Python语言的性能虽然差，但python有tensorflow, pytorch等高性能神经网络框架，这让python写出的神经网络性能同样高效。</p>
<p>dpdk和spdk工具链让C++开发高性能网络服务器和高性能存储服务变得容易，GPU等新硬件和cuda等生态让C++成为高性能计算的基础。C++20提供了协程支持，deepseek 开源的3FS 就是C++20高性能编程的典型例子。</p>
<h3>C++20协程</h3>
<p>C++20 提供了协程支持，协程可以看做任务，任务执行期间可以在某个位置暂停-继续。</p>
<p>C++ 提供std::coroutine_handle&#x3C;>作为协程句柄, 协程句柄需要传入自定义的Promise类型，用来指定协程的返回值，初始化和退出行为等。
promise_type 必须实现以下成员函数</p>
<ol>
<li><code>std::coroutine_handle&#x3C;promise></code> get_return_object() 返回coroutine_handle对象</li>
<li>initial_suspend()	控制协程启动时是否立即挂起，返回 suspend_always 表示协程创建后挂起, 需要调用一次resume才会执行, 返回 suspend_never表示协程创建后立即执行</li>
<li>final_suspend()	控制协程结束时是否挂起, 同样可以返回 suspend_always 或者 suspend_never</li>
<li>yield_value(value)	接受co_yield 表达式返回值, 可以处理</li>
<li>return_value(value)  接受co_return value返回值，可以处理</li>
<li>return_void() 执行co_return后执行该函数</li>
<li>unhandled_exception()	处理协程中未捕获的异常。</li>
</ol>
<p>通过以上函数，可以实现协程创建后，销毁前，co_return/co_yield之后的行为</p>
<pre><code class="language-cpp">template&#x3C;typename T>
struct promise_type {
    T current_value;
    // 协程挂起时的返回值
    auto yield_value(T value) {
        current_value = value;
        return std::suspend_always{}; // 每次生成后暂停
    }
    // 协程初始化设置
    auto initial_suspend() { return std::suspend_always{}; }
    auto final_suspend() noexcept { return std::suspend_always{}; }
    Generator get_return_object() { return std::coroutine_handle&#x3C;promise_type>::from_promise(*this); }
    void unhandled_exception() { std::terminate(); }
    void return_void() {}
};

std::coroutine_handle&#x3C;promise_type>;
</code></pre>
<p>协程一重要的关键字是co_await，表示挂起当前协程，执行流切换到其他任务。co_await 后面需要加Awaiter对象.
awaiter必须要实现的三个函数</p>
<ol>
<li>bool await_ready() const noexcept; 表示调用co_await后是否立即执行，如果返回true，则直接执行，不会挂起当前协程。</li>
<li>void await_suspend(std::coroutine_handle&#x3C;> handle) noexcept; 参数为当前协程句柄, 可以将执行流切给指定的coroutine_handle</li>
<li>T await_resume() noexcept; 当协程处于co_await状态，调用resume时执行该函数。</li>
</ol>
<pre><code class="language-cpp">struct custom_awaiter {
    // 判断是否直接继续执行（true=不挂起）
    bool await_ready() noexcept;

    // 挂起时执行（参数为当前协程句柄）
    void await_suspend(std::coroutine_handle&#x3C;> h) noexcept;

    // 恢复时执行的逻辑与返回值
    int await_resume() noexcept;
};
</code></pre>
<p>协程执行例子</p>
<pre><code class="language-cpp">#include &#x3C;coroutine>
#include &#x3C;cstdio>
#include &#x3C;thread>

// 1. 定义协程返回类型 Task
struct Task {
    struct promise_type {
        Task get_return_object() { 
            printf ("创建协程对象\n");
            return Task{std::coroutine_handle&#x3C;promise_type>::from_promise(*this)}; 
        }
        std::suspend_always initial_suspend() { // 返回suspend_always 对象, 协程初始化后挂起
            printf ("初始化挂起\n");
            return {}; 
        }
        std::suspend_always final_suspend() noexcept { // 协程结束后挂起
            printf ("最终挂起\n");
            return {}; 
        }
        void return_void() { 
            printf ("协程返回\n"); 
        }
        void unhandled_exception() { 
            std::terminate(); 
        }
    };
    std::coroutine_handle&#x3C;promise_type> handle;
    explicit Task(std::coroutine_handle&#x3C;promise_type> h) : handle(h) {}
    ~Task() {
        if (handle) handle.destroy();
        printf ("销毁协程\n");
    }
    // 恢复当前协程执行
    void resume() {
        if (!handle.done()) {
            printf ("恢复协程执行\n");
            handle.resume();
        }
    }
};

// 2. 定义可等待对象（Awaiter）
struct AsyncOperation {
    bool await_ready() const { 
        printf("检查是否就绪\n");
        return false; // 总是挂起
    }
    void await_suspend(std::coroutine_handle&#x3C;> h) {
        printf ("开始异步操作...\n");
        // 新建额外线程, suspend不阻塞
        std::thread([h] {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            printf ("异步操作完成\n");
            h.resume(); // 完成后恢复协程
        }).detach();
    }
    void await_resume() {
        printf ("处理操作结果\n");
    }
};

// 3. 协程函数定义
Task my_coroutine() {
    printf ("协程开始执行\n");
    co_await AsyncOperation{}; // 等待异步操作
    printf ("继续执行协程体\n");
    co_await std::suspend_always{}; // 主动挂起
    printf ("协程最终阶段\n");
}

// 4. 主函数
int main() {
    Task task = my_coroutine(); // 创建并初始化协程, 协程挂起
    printf ("首次恢复协程:\n");
    task.resume();             // 第一次, 启动协程
    printf ("执行流切回主线程:\n");
    sleep(5);
    printf ("二次恢复协程:\n");
    task.resume();             // 第二次恢复
    
    printf ("\n程序结束\n");
    return 0;
}
// 执行结果
创建协程对象
初始化挂起
首次恢复协程:
恢复协程执行
协程开始执行
检查是否就绪
开始异步操作...
执行流切回主线程:
异步操作完成
处理操作结果
继续执行协程体
二次恢复协程:
恢复协程执行
协程最终阶段
协程返回
最终挂起

程序结束
销毁协程
</code></pre>
<p>执行流分析</p>
<ol>
<li>Task task = my_coroutine() 时，创建协程和初始化对象, 分别执行Task get_return_object() 和std::suspend_always initial_suspend()两个函数</li>
<li>task.resume(); 执行会启动协程，协程执行到co_await，挂起。协程挂起后，执行流交给main函数, 也就是调用resume()的函数</li>
<li>await_suspend 创建了新线程用来执行异步任务，异步任务执行完成后在新线程中调用h.resume()，协程继续在新线程执行，主线程这时候在sleep()</li>
<li>新线程继续执行协程，直到co_await std::suspend_always{}; 直接挂起（显然std::suspend_always{}; 也是一种awaiter表示无条件挂起）。这时候新线程执行完毕退出，但协程对象挂在co_await</li>
<li>最后主线程sleep()完执行task.resume(); 协程又继续执行, 直到协程执行完毕, 调用final_suspend由于final_suspend返回std::suspend_always{};，协程被挂起而不是销毁</li>
<li>主线程执行完毕, 最后调用handle.destroy();销毁协程</li>
</ol>
<p>执行co_await 时将当前协程交给线程池异步执行, 当前协程切回协程resume的位置</p>
<pre><code class="language-cpp">struct ThreadPoolAwaiter {
    ThreadPool&#x26; pool;

    bool await_ready() { return false; }
    
    void await_suspend(std::coroutine_handle&#x3C;> h) {
        pool.enqueue([h] { h.resume(); }); // 提交到线程池
    }
    
    void await_resume() {}
};

co_await ThreadPoolAwaiter{my_thread_pool};
</code></pre>
<h4>协程链式调用</h4>
<p>我们想要像函数调用那样实现协程调用, 也是co_awaiter一个协程, 当子协程执行完毕后, 返回给父协程继续执行。</p>
<ol>
<li>这样的子协程是一个类, 需要实现promise_type管理协程生命周期, 也需要实现Awaiter 接受co_await调用</li>
<li>await_suspend函数会传父协程handle，需要把它记住。当子协程执行完毕, 需要在final_suspend()中把父协程恢复。</li>
</ol>
<p>举例,</p>
<ol>
<li>每个协程函数都返回Task类型。执行co_await Task, 会调用Task的await_suspend(), 将父协程设置成子协程的nextjob, 返回当前协程(Task同时是Awaiter和协程), 表示执行当前协程</li>
<li>对于PromiseBase, final_suspend() 返回一个FinalAwaiter.协程销毁后会调用FinalAwaiter的await_suspend(), 执行协程的nextjob，也就是恢复父协程</li>
</ol>
<pre><code class="language-cpp">  struct FinalAwaiter {
    auto await_ready() const noexcept -> bool { return false; }
    template &#x3C;typename Promise>
    auto await_suspend(std::coroutine_handle&#x3C;Promise> handle) noexcept -> void
    {
      assert(handle.done() &#x26;&#x26; "handle should done here");
      auto&#x26; promise = handle.promise();
      // 这一步同时更新了promise.mNextJob==nullptr, 也就是标识task执行完了
      auto next = promise.mNextJob.exchange(nullptr);
      if (next == nullptr) {
        if (promise.getState() != nullptr) [[unlikely]] {
          promise.getState()->store(JobState::Final, std::memory_order_release);
          promise.getState()->notify_one();
        }
      } else if (next == &#x26;detail::kDetachJob) {
        if (promise.getState() != nullptr) [[unlikely]] {
          promise.getState()->store(JobState::Final, std::memory_order_release);
          promise.getState()->notify_one();
        }
        promise.mThisHandle.destroy();
      } else if (next != &#x26;detail::kEmptyJob) {
        // 把nextjob加入到loop, 由主线程执行(ExeOpt::prefInOne)
        Proactor::get().execute(next, ExeOpt::prefInOne());
      }
    }
    auto await_resume() noexcept -> void {}
  };

struct PromiseBase {

  auto initial_suspend() noexcept -> std::suspend_always { return {}; }
  auto final_suspend() noexcept -> FinalAwaiter { return {}; }
  auto unhandled_exception() noexcept -> void { mExceptionPtr = std::current_exception(); }

  auto setNextJob(WorkerJob* next) noexcept -> void { mNextJob = next; }
  auto getNextJob() noexcept -> std::atomic&#x3C;WorkerJob*>&#x26; { return mNextJob; }

  CoroJob mThisJob{this, &#x26;CoroJob::run};
  std::coroutine_handle&#x3C;> mThisHandle;
  std::atomic&#x3C;WorkerJob*> mNextJob{nullptr};
  std::exception_ptr mExceptionPtr;
};

template &#x3C;typename T>
class Task {
public:
  using promise_type = Promise&#x3C;T>;
  using coroutine_handle_type = std::coroutine_handle&#x3C;promise_type>;
  using value_type = T;

  Task() noexcept = default;
  explicit Task(coroutine_handle_type handle) noexcept : mHandle(handle)
  {
    assert(mHandle != nullptr);
    mHandle.promise().setCoHandle(mHandle);
  }
  struct AwaiterBase {
    auto await_ready() const noexcept -> bool { return false; }
    template &#x3C;typename Promise>
    auto await_suspend(std::coroutine_handle&#x3C;Promise> handle) noexcept -> void
    {
    // 设置为handle.promise.nextjob
      mHandle.promise().setNextJob(handle.promise().getThisJob());
      mHandle.promise().setState(handle.promise().getState());
        // 执行当前协程
      Proactor::get().execute(mHandle.promise().getThisJob(), ExeOpt::prefInOne());
    }
    coroutine_handle_type mHandle;
  };
</code></pre>
<p>folly库实现的协程链式调用</p>
<ol>
<li>保存父协程promise.continuation_ = continuation;</li>
<li>返回当前task的coro_handle, coro_, 执行当前task</li>
</ol>
<pre><code class="language-cpp">    bool await_ready() noexcept { return false; }

    template &#x3C;typename Promise>
    FOLLY_NOINLINE auto await_suspend(
        coroutine_handle&#x3C;Promise> continuation) noexcept {
      DCHECK(coro_);
      auto&#x26; promise = coro_.promise();

      promise.continuation_ = continuation;

      auto&#x26; calleeFrame = promise.getAsyncFrame();
      calleeFrame.setReturnAddress();

      if constexpr (detail::promiseHasAsyncFrame_v&#x3C;Promise>) {
        auto&#x26; callerFrame = continuation.promise().getAsyncFrame();
        folly::pushAsyncStackFrameCallerCallee(callerFrame, calleeFrame);
        return coro_;
      } else {
        folly::resumeCoroutineWithNewAsyncStackRoot(coro_);
        return;
      }
    }

    T await_resume() {
      DCHECK(coro_);
      SCOPE_EXIT {
        std::exchange(coro_, {}).destroy();
      };
      return std::move(coro_.promise().result()).value();
    }
</code></pre>
<h4>协程和异步</h4>
<p>我们可以思考链式协程模式的特点</p>
<ol>
<li>每个协程Task既是一个挂起-恢复的协程任务，也是一个Awaiter。协程的await_suspend会记录父协程, final_suspend恢复父协程。</li>
<li>协程的阻塞任务, 包括申请锁, sleep, IO等都可以封装成一个Awaiter, 这个awaiter可以在suspend的时候挂起，如果需要调用read等阻塞调用，需要创建新线程执行</li>
<li>执行协程的线程会执行一个循环(eventloop)，这个线程用于不会阻塞。这个线程做的事情就是1. 循环遍历，如果有协程挂起并创建新线程执行异步任务，就查看异步任务是否执行完，若执行完恢复该协程的调用。 2. 如果协程全部执行完了, 线程根据链式调用, 恢复该协程父协程的执行。3. 尝试接受新的协程任务</li>
</ol>
<p><strong>协程锁可以通过标志和等待队列实现,</strong></p>
<ol>
<li>尝试申请锁时, 如果锁未被申请, 则持有锁, 协程继续执行</li>
<li>如果锁已被申请, 则将当前协程加入等待队列, 并挂起当前协程</li>
<li>释放锁时，唤醒处于等待队列的一个协程恢复它执行
执行协程的线程会执行一个循环，如果无协程可执行（协程全部在挂起），线程则进入下一个循环</li>
</ol>
<p>协程也可以实现类似golang的channel。channel 由一个ringbuffer和reader, writer两个队列组成。</p>
<ol>
<li>reader 进入时, 如果ringbuffer为空，则挂起reader; ringbuffer不为空，则读取ringbuffer中的数, 同时唤醒等待队列的writer</li>
<li>writer写入时，如果ringbuffer满了，则挂起；否则写入并唤醒所有reader; reader被唤醒时，读取writer写入的数据。
显然协程的一个核心是《唤醒》的实现，只需要coroutine.resume()即可, 这比线程的唤醒要简单很多。</li>
</ol>
<p>协程可以在await_suspend中调用liburing等异步io函数，然后挂起。同时主线程循环检查liburing的cqe(complete queue entry)，对于完成的IO，唤醒对应的协程继续处理。</p>
<h4>有栈协程</h4>
<p>C++20的协程是无栈协程，无栈协程就是一个Task对象，这个task对象可以通过co_await挂起, 并记录当前的状态。等到resume时，可以从当前的状态继续执行。coroutine对象会记录当前协程函数内的局部变量（包括参数）、挂起点等状态。</p>
<p>相比无栈协程，有栈协程更容易理解，在有栈协程中，每个协程函数都相当于给指定线程的任务队列加入一个任务</p>
<ol>
<li>一般来说，每个线程会执行一个loop循环, 该循环从任务队列取出一个协程任务，执行，然后下一个</li>
<li>当协程需要挂起时，会保存当前的上下文，将自己放到等待队列，将挂起的任务提交到异步执行</li>
<li>当异步任务完成后，从等待队列唤醒对应的任务加到执行队列，继续执行。</li>
</ol>
<p>执行有栈协程的线程 相当于操作系统的线程CPU调度，典型的就是golang的协程。golang的GMP 调度模型 就类似操作系统的进程-CPU-调度器。有栈协程不需要固定的对象保存状态，需要挂起协程时就创建栈保存状态，协程继续执行时就清理栈恢复状态。而无栈协程通过coroutine对象来保存状态和管理协程生命周期。无栈协程的状态保存和恢复相比有栈协程性能更高，同时执行流更清晰，有利于编译器的优化。</p>
<p>代码越静态，结果越容易预测，越有利于编译期的优化。</p>
<h3>异步编程模型</h3>
<h4>SQE/CQE 模型</h4>
<p>​​SQE（Submission Queue Entry，提交队列项）和 CQE（Completion Queue Entry，完成队列项）​​ 是高性能异步 I/O 框架（如 Linux io_uring、SPDK、DPDK）中的核心机制，用于实现高效的 ​​生产者-消费者模型​​。</p>
<p>SQE和CQE 一般是两个环形队列，对于SQE，用户程序是生产者，内核/硬件是消费者。对于CQE，内核/硬件是生产者，应用程序是消费者。</p>
<ol>
<li>​SQE（任务提交）​​, 应用程序将 I/O 请求（如读、写）封装为 SQE，提交到​​提交队列（Submission Queue, SQ）​​，通知硬件或内核处理。</li>
<li>​CQE（完成通知）​​：硬件或内核处理完请求后，生成 CQE 并放入​​完成队列（Completion Queue, CQ）​​，应用程序轮询或异步接收结果。</li>
</ol>
<p>C++ 协程可以很容易的和SQE/CQE 模型结合，await_suspend函数将任务加到SQE队列等待执行, 而主线程定期轮询CQE队列，对于CQE中完成的异步任务，主线程会唤醒协程继续执行。</p>
<p>io_uring 的SQE和CQE模型</p>
<pre><code class="language-cpp">#include &#x3C;linux/io_uring.h>

// 提交队列项（SQE）结构体
struct io_uring_sqe {
    __u8    opcode;     // 操作类型（如 IORING_OP_READV）
    __u64   addr;       // 数据地址（如缓冲区指针）
    __u32   len;        // 数据长度
    __u64   user_data;  // 用户自定义数据（用于关联请求上下文）
    // ... 其他字段（文件描述符、标志位等）
};

// 完成队列项（CQE）结构体
struct io_uring_cqe {
    __u64   user_data;  // 对应 SQE 的 user_data
    __s32   res;        // 操作结果（成功时为字节数，失败时为负的错误码）
    __u32   flags;      // 附加标志
};

// 提交请求到 SQ
void io_uring_prep_read(struct io_uring_sqe *sqe, int fd, void *buf, unsigned nbytes, off_t offset);
io_uring_submit(&#x26;ring); // 提交 SQEs 到内核

// 从 CQ 获取结果
struct io_uring_cqe *cqe;
io_uring_peek_cqe(&#x26;ring, &#x26;cqe); // 非阻塞获取 CQE
io_uring_cq_advance(&#x26;ring, 1);  // 标记 CQE 已处理
</code></pre>
<p>SPDK 通过SQE/CQE模型向nvme ssd驱动提交IO请求</p>
<pre><code class="language-cpp">struct spdk_nvme_ns *ns = ...;
struct spdk_nvme_qpair *qpair = ...;
char *buffer = ...;

// 提交读请求（SQE）
spdk_nvme_ns_cmd_read(ns, qpair, buffer, lba, lba_count, 
                      completion_cb, NULL, 0);

// 处理 CQE（回调函数）
void completion_cb(void *ctx, const struct spdk_nvme_cpl *cpl) {
    if (spdk_nvme_cpl_is_error(cpl)) {
        // 错误处理
    } else {
        // 处理数据
    }
}
</code></pre>
<p>DPDK 通过SQE/CQE模型向提交网络包</p>
<pre><code class="language-cpp">struct rte_mbuf *tx_pkts[32];
// 填充发送包（SQE）
for (int i = 0; i &#x3C; 32; i++) {
    tx_pkts[i] = ...; // 构造数据包
}
// 提交发送请求
uint16_t sent = rte_eth_tx_burst(port_id, queue_id, tx_pkts, 32);

// 接收完成的数据包（CQE）
struct rte_mbuf *rx_pkts[32];
uint16_t received = rte_eth_rx_burst(port_id, queue_id, rx_pkts, 32);
</code></pre>
<h4>事件通知模型</h4>
<p>事件通知模型不像sqe/cqe模型有明确的任务提交队列和任务完成队列，而是通过接收事件来确定是否可读可写。典型的就是linux的epoll。对于读操作，sqe/cqe模型只需要提交一个读任务，然后等待完成队列中的读任务完成即可。但对epoll，需要等待读事件触发，才能执行读操作。sqe/cqe是一个主动请求等待返回的模型, 而epoll是一个被动等待触发的模型。</p>
<p>mtcp 实现了一个用户态的epoll，</p>
<ol>
<li>通过 DPDK 的轮询模式驱动（Poll Mode Driver, PMD）直接从网卡收取数据包，解析为 TCP 报文。</li>
<li>当收到TCP报文时，会将对应的连接设置成数据可读（EPOLLIN）、可写（EPOLLOUT）或新连接到达（EPOLLACCEPT）事件，并将该事件加入到就绪队列中</li>
<li>应用层通过 mtcp_epoll_wait() 等接口从就绪队列中获取事件，执行回调或进一步处理。</li>
</ol>
<p>协程和事件通知模型结合时，await_suspend可以将操作加入等待队列，而epoll_wait 返回触发的事件时，可以将数据写到指定的buffer，然后通知等待队列里对应的协程继续执行。</p>
<p>可见相比epoll的事件通知模型，协程liburing等sqe/cqe 模型更自然的结合, 通过轮询cqe的方式对IO完成的协程继续执行。</p>
<p>DPDK（Data Plane Development Kit）和SPDK（Storage Performance Development Kit）是两款由英特尔发起的开源项目，分别专注于提升网络数据平面和存储I/O的性能。DPDK目的是成为用户态网络包处理的标准框架，SPDK则是成为用户态绕过内核操作nvme 协议的块设备IO处理的标准框架。</p>
<p>DPDK​, 包括mTCP, 是一个用户态TCP协议栈。数据包直接从网卡读取，发给用户态组成TCP包。</p>
<ol>
<li>​用户态网络驱动​​。绕过内核协议栈，直接在用户态处理网络包，减少数据拷贝和上下文切换。</li>
<li>​​零拷贝技术​​。通过大页内存（HugePage）和内存池（Memory Pool）减少内存访问开销。</li>
<li>轮询模式​​。使用无锁队列（Ring）和CPU轮询（Poll-mode Driver）避免中断延迟。</li>
<li>​多核扩展​​ 基于线程绑定（CPU affinity）和流水线模型实现高性能多核处理。</li>
</ol>
<p>​​SPDK​，是用户态的NVME设备IO框架。IO数据直接从nvme设备到用户态</p>
<ol>
<li>​用户态NVMe驱动​​ 完全用户态实现NVMe协议，避免内核存储栈的开销。</li>
<li>​异步无锁设计​​ 使用异步I/O和事件驱动模型，减少锁竞争。</li>
<li>​​零拷贝访问​​ 通过内存映射（Memory-mapped I/O）和直接访问SSD的PRP（Physical Region Page）列表提升效率。</li>
<li>​高并发优化​​ 支持多队列（Multi-queue）和并行I/O请求，充分利用NVMe SSD的多核能力。</li>
</ol>
<p>iouring 是linux 内核提供的异步IO接口，相比spdk, 存储层还是会经过内核，走vfs接口, 利用内核的pagecache缓存和块设备抽象。</p>
<p>rdma是远程内存访问，通过支持RDMA的网卡（RNIC）直接读写远程内存，需要专门的RDMA网卡。RDMA可以和dpdk/spdk/iouring结合，rdma 是针对rdma专用网卡的网络协议、数据收发，而后者侧重描述的是从用户传输层到网卡的通用架构。rdma 侧重于高性能计算存储，dpdk侧重于更通用的tcp服务。</p>
<p>dpdk, spdk, rdma, iouring 采用的模型都是cqe/sqe 生产消费模型。C++ 通过以上生态，实现了网络和存储IO的高性能。再加上流行的gpu cuda高性能计算框架，使C++在高性能计算/存储/网络领域不可替代。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[cpp]]></category><category><![CDATA[coroutine]]></category>
        </item>
        <item>
          <title><![CDATA[存储——谈存储文件系统]]></title>
          <link>https://larrystd.github.io/posts/存储—谈存储文件系统</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/存储—谈存储文件系统</guid>
          <pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[文件系统是组织磁盘的形式, 文件系统提供了文件元数据信息，以及文件内容寻址能力（即将文件offset+length映射为磁盘位置）；另一方面适配不同的磁盘硬件，让用户程序无须考虑硬件的区别。对用户程序来说，存储等于文件系统。]]></description>
          <content:encoded><![CDATA[<p>文件系统是组织磁盘的形式, 文件系统提供了文件元数据信息，以及文件内容寻址能力（即将文件offset+length映射为磁盘位置）；另一方面适配不同的磁盘硬件，让用户程序无须考虑硬件的区别。对用户程序来说，存储等于文件系统。</p>
<p>文件系统整体可以由客户端、元数据服务、数据服务三个部分组成, 包括read, write, setattr, getattr, lookup, readdir, create, rename, hardlink, symlink, readlink, remove等12个接口，支持随机读写文件。</p>
<p>为什么要有分布式文件系统? 1. 提供水平可扩展, 海量高性能存储 2. 提供多客户端的并发访问服务</p>
<h3>对象存储系统和minIO</h3>
<p>文件系统大致包括如下12个接口</p>
<ol>
<li>read, write读写文件</li>
<li>setattr, getattr，读写文件属性（元数据）</li>
<li>lookup，目录结构使用，根据父handle+文件名获得子文件handle</li>
<li>readdir, 目录结构使用，获得目录中的文件名，相当于读目录</li>
<li>create，创建文件</li>
<li>rename，目录结构使用，将某文件从当前目录move到其他目录下</li>
<li>hardlink，目录结构用，创建文件的硬链接（硬链接相当于文件具有两个path）</li>
<li>symlink, readlink，目录结构用，创建和读软链接，基本等价于create和read</li>
<li>remove，删除文件或空目录</li>
</ol>
<p>最简单的是不支持目录的系统，也就是对象存储。对象存储的文件系统和文件分别称为bucket和object，object 完全平铺在bucket 根目录。
对象存储只支持read(getobject), write(putobject, appendobject), setattr(putobject), getattr(getobjectmeta)，create(putobject), remove(remove object)</p>
<p>对象存储的写文件只支持append写，不支持随机写，不支持删除数据(truncate)</p>
<p>对象存储几乎是最简单的文件系统，我们可以看下对象存储简化操作后的好处</p>
<ol>
<li>不支持目录，水平扩展简单。目录通过父目录-子目录的递归结构将文件系统组织成树的形式，目录树的水平扩展就需要按照目录树拆分，并且即使按照目录树划分，水平扩展也没有那么丝滑。对象存储水平扩展简单，一个object可以存在任意机器。这让oss的bucket 的object数量理论无上限，具有规模成本优势，可以随意建大规模object，无须担心性能退化。</li>
<li>文件read, write, create, remove等接口的调用也容易水平扩展，这让对象存储的IOPS极高。</li>
<li>只支持append写，容易加EC码降成本，性能不会受随机写和truncate的波动，性能稳定</li>
</ol>
<p>对象存储容量和IOPS可水平扩展，性能高且稳定，十分适合作为数据的基础存储，例如ceph就使用对象存储作为块和文件存储的基础。</p>
<p>minIO是一个开源的对象存储，采用Golang语言实现。集群部署采用去中心化无共享架构，各节点间为对等关系，连接至任一节点均可实现对集群的访问。</p>
<ol>
<li>一般的，客户端使用hash算法确定某个bucket和object name对应的erasure set 位置，object的数据和元数据指定存储在某erasure set。（每个erasure set包含一组硬盘，其数量通常为4至16块；EC的每一笔写会划分成若干段，例如8+3纠删码分成8段，同时生成3个校验块，最后11段数据每段独立写到不同磁盘，随机写不利于纠删码创建和优化）</li>
<li>如果后台增删节点导致object位置发生变化，后端服务器将请求路由到目标服务器，客户端更新位置信息。</li>
<li>object数据和元数据在相同erasure set中存储，object之间shared-nothing（就是完全独立），容易水平扩展。object的数据和元数据都对应server指定磁盘文件系统的文件。</li>
<li>客户端使用https之上的s3协议。</li>
</ol>
<p>minIO 旨在提供s3协议的对象存储服务，用来存储静态数据，并支持EC保证数据安全。但其并非作为存储底座，因此性能一般且没有太多性能优化。此外。listobject性能很差。</p>
<p>对象存储作为存储底座（可横向扩展、高性能、容错的分布式文件系统），可以参考ceph和lustre。</p>
<h3>linux ext4文件系统</h3>
<p>2008年发布</p>
<p>元数据由内存的inode 数据结构记录，inode 索引表，是个kv结构，key是inode号，value是对应的块位置。用来快速根据inode号找到inode结构在磁盘的位置。inode索引表常驻内存。每个inode 在内存中通过address_space（基数树）标志inode 使用的page 树。内存的page 会记录该page对应的磁盘块，从而调动磁盘读写数据。</p>
<p>linux/ext4 系统使用文件名 hash快速找到文件inode结构（通过dentry）</p>
<p>ext4用块为单位组织磁盘，块大小为4KB，块可以分为两类，元数据块用来存inode结构，对应内存的inode结构，数据库则用来存数据，对应内存的page。块使用bitmap标志块有无使用, 作用1. 快速分配新块来写数据，2. 快速定位块的位置</p>
<p>linux 使用 vfs 向实现不同的文件系统向用户程序提供提供一致的接口，vfs只支持posix语义的文件系统，也就是必须支持上述12个文件系统操作，以及支持文件随机读写和truncate操作。vfs支持的posix文件系统包括ext4, nfs, fuse等</p>
<p>ext4 文件系统的问题</p>
<ol>
<li>ext4文件系统不可跨磁盘存在，可扩展性有限</li>
<li>ext4文件系统元数据和数据一起存放，未分离，但运行时文件元数据基本存放在内存中，元数据性能很强</li>
</ol>
<h3>Glusterfs</h3>
<p>GlusterFS 文件系统，</p>
<ol>
<li>存储服务器的Brick 是存储基本单位，Brick通过冗余性保证数据安全。Brick是一个目录。客户端根据 hash 算法定位到 Brick，找到Brick后进一步读写文件数据和元数据</li>
<li>Brick是文件系统的一个目录，Brick本身是可以横向扩展的。Brick中的文件读写创建和内部目录的readdir 性能都比较高</li>
<li>但需要跨Brick的rename, ls性能比较差</li>
<li>客户端通过 FUSE 或 NFS/SMB 协议挂载 GlusterFS 卷，执行读写操作。客户端直接与存储节点通信，无需经过中心元数据服务器。
GlusterFS 相当于简单粗暴的直接按照目录拆分文件系统实现水平扩展，Brick目录内部的操作（包括子目录）性能很好，但ls, rename, link等需要跨Brick的操作性能较差。</li>
</ol>
<p>文档, <a href="https://docs.gluster.org/en/main/Quick-Start-Guide/Architecture/#types-of-translators">https://docs.gluster.org/en/main/Quick-Start-Guide/Architecture/#types-of-translators</a></p>
<h3>Lustre</h3>
<p>Lustre 2003年发布, 底座为对象存储的分布式文件系统。由元数据服务(mds)、对象存储服务(oss)和客户端(client) 三个部分组成</p>
<p>mds，提供目录和元数据服务。元数据和目录均按照固定大小的条带存储，通过handle hash来定位存储位置（类似ext4）。文件系统的目录可能跨条带（对于大目录），跨mds 的rename 操作需要分布式锁保护，性能较低。</p>
<p>oss 对象存储作为数据存储, 每个oss object代表一个文件, object内部也是固定大小的条带化存储。Lustre的object 支持随机读写，object内部的条带像是块存储的条带，支持随机读写。</p>
<p><img src="/images/lustre.png" alt="lustre"></p>
<h3>CephFS</h3>
<p>Ceph 发布于2006年，底层是RADOS（Reliable Autonomic Distributed Object Store）对象存储系统，这个对象同样是可以随机读写的（实际是条带）
RADOS 是具有负载均衡、容错的分布式存储服务，组成</p>
<ol>
<li>​​OSD（Object Storage Daemon）具体存放数据的存储节点</li>
<li>​​PG（Placement Group）​负责维护对象到OSD的映射，以及基于映射的负载均衡</li>
</ol>
<p><img src="/images/ceph.png" alt="ceph"></p>
<p>元数据服务MDS,</p>
<ol>
<li>MDS 将目录树拆分为多个子树，分配给不同 MDS 实例管理。会根据子树的访问热度执行动态调度</li>
<li>MDS 在内存态存放每个子树目录的目录项，文件元数据。持久化时还是以键值对形式存储在目录的 RADOS 对象中。对于MDS的操作会先通过日志持久化到 RADOS</li>
</ol>
<p>官方文档，<a href="https://docs.ceph.com/en/latest/architecture/">https://docs.ceph.com/en/latest/architecture/</a></p>
<h3>GPFS</h3>
<p>IBM 商业文件系统，闭源。</p>
<p>节点角色</p>
<ol>
<li>NSD（Network Shared Disk） Server, 管理物理存储设备（如磁盘阵列、SAN），将本地磁盘抽象为逻辑 NSD（Network Shared Disk）, 负责数据容错，存储节点。文件被分割为固定大小的块（默认 256KB~16MB），分布到多个 NSD 上。</li>
<li>Manager Node​​, 文件系统管理器（File System Manager）​​，负责元数据操作（如目录结构、文件锁）以及颁发token用来协调客户端。同时通过仲裁节点（Quorum Node）实现高可用，通常部署奇数个节点（如 3 个）。Manager Node是无状态的, 元数据（inode、目录项）集中存储于专用 NSD。</li>
<li>​​Client Node。挂载 GPFS 文件系统的计算节点，直接读写数据。客户端通过向Manager Node申请令牌（Token）协调并发访问，避免冲突和减少锁的使用（例如如果只有一个客户端执文件创建，那目录NSD的写入操作不需要分布式锁）</li>
</ol>
<p>令牌是一种lease，客户端需向Manager Node申请令牌，获得权限后方可操作。​​令牌类型包括</p>
<ol>
<li>​元数据锁​​, 控制目录结构修改（如重命名、删除）。</li>
<li>​数据锁​​, 协调文件块的读写冲突（如并发写入同一区域）。</li>
</ol>
<p>客户端节点缓存元数据（如目录项、文件属性），减少对Manager Node的频繁访问, 其他客户端的修改会触发缓存失效。</p>
<p>在高性能分布式系统中，通过lease协调的读写减少分布式锁的使用</p>
<ol>
<li>避免多写只允许一个节点写</li>
<li>每个客户端节点都有缓存，写完的节点通知其他节点更新缓存
这种<strong>客户端/服务端协调的读写架构，性能远比允许客户端随便发读写请求、通过加锁保证一致性的性能高。</strong></li>
</ol>
<p>gpfs文档，<a href="https://www.ibm.com/docs/en/storage-scale/5.2.1?topic=overview-gpfs-architecture">https://www.ibm.com/docs/en/storage-scale/5.2.1?topic=overview-gpfs-architecture</a></p>
<h3>HDFS</h3>
<p>不支持posix 语义, 设计目录是作为大数据分析中的存储系统，是GFS 的一种实现。</p>
<p>结构比较简单</p>
<ol>
<li>使用NameNode 管理元数据，同时维护数据块物理信息，元数据不可水平扩展</li>
<li>DataNode 存储数据，数据块可以水平扩展</li>
<li>Client，元数据操作访问NameNode， 数据操作访问DataNode按需更新NameNode</li>
</ol>
<p>NameNode 是性能和容量瓶颈, 并且由于hdfs是JAVA 开发, 一般用来配合Hbase 等hadoop全家桶用于大数据领域。</p>
<p><img src="/images/hdfs.png" alt="hdfs"></p>
<h3>JuiceFS</h3>
<p>支持posix 语义, 数据存储在对象存储（如 S3、OSS），元数据支持 Redis/MySQL/TiDB等一系列数据库作为元数据存储引擎, 借助TiDB 等分布式数据库提供了元数据的可扩展性。客户端借助fuse提供 操作系统接口。</p>
<ol>
<li>最主要的优点是数据存储直接和S3对接，利用对象存储无限容量和底成本存储优势</li>
<li>本地内存和磁盘缓存加速元数据和数据性能。元数据可能是性能瓶颈，但付费版支持高性能元数据引擎</li>
</ol>
<p>分布式数据库的数据表可以横向扩展（一般以partition为单位），文件系统的元数据就是若干数据表（KV表），因此自然提供了元数据扩展性。但通用的分布式KV引擎的readdir, rename 性能可能不高，需要针对优化。因此得到文件系统元数据对分布式KV存储的要求</p>
<ol>
<li>单点查询性能高</li>
<li>范围查询（对应readdir）性能高</li>
<li>支持事务, 删除旧key+增加新key的性能高（对应rename）</li>
<li>partition 拆分合理, 同一文件系统的若干partition 分布在距离近的机器, 需要有针对优化</li>
</ol>
<p>JuiceFS架构文档, <a href="https://juicefs.com/docs/zh/community/architecture/">https://juicefs.com/docs/zh/community/architecture/</a></p>
<h3>3FS</h3>
<p>3FS (Fire-Flyer File System) 是一款高性能的分布式文件系统, 由 DeepSeek 在 2025 年 2 月开源。</p>
<ol>
<li>3FS 的元数据以表的形式存在foundationDB服务中，操作元数据时，请求先发给元数据服务处理，然后发送到foundationDB。foundationDB以事务的形式写元数据，Read-only transactions used for metadata queries: fstat, lookup, listdir etc. Read-write transactions used for metadata updates: create, link, unlink, rename etc.</li>
<li>storage service用户存储数据，数据被划分成等大的块(chunk， 条带)，每三个块组成一条复制链（链式写入三副本保证容错性）,以chain为单位打散storage server。文件到数据块的layout信息存在元数据服务中。storage server使用rocksdb维护自己的元数据信息（例如可用的块列表，块的分配情况等）。</li>
<li>提供FUSE 客户端和用户态客户端USRBIO（绕过内核和ring零拷贝），客户端会尽可能缓存文件的layout信息（文件chunk的分布情况），read/write请求客户端直接访问storage server。</li>
<li>客户端，meta server，storage server两两之间均使用RDMA通信。</li>
</ol>
<p>3FS文档, <a href="https://github.com/deepseek-ai/3FS/blob/main/docs/design_notes.md">https://github.com/deepseek-ai/3FS/blob/main/docs/design_notes.md</a></p>
<h3>总结</h3>
<p>对象存储，<strong>无目录结构</strong>，数据和元数据水平扩展简单，性能高，适合作为基础存储服务。
块存储，<strong>整个块设备可以认为是一个文件</strong>。支持随机读写和设备大小水平拓展。块设备需要极低延迟，块设备上可以格式化Ext4等单机文件系统。</p>
<p>支持分布式元数据的文件系统GlusterFS、CephFS、GPFS、JuiceFS、3FS; Lustre和HDFS不支持</p>
<p>使用块来存储目录项的文件系统, Ext4、GlusterFS、Lustre、CephFS、GPFS；使用KV 引擎存储目录项的文件系统, JuiceFS和3FS。在kv存储中，readdir 等于key的范围查询, rename等操作需要支持事务, 通过数据表水平拆分实现分布式元数据。</p>
<p>通过lease 来减少分布式锁的文件系统, GPFS。</p>
<ol>
<li>Ext4。单机文件系统，posix语义, 数据元数据按照块组织, 元数据和layout常驻内存。元数据和数据无法扩展</li>
<li>GlusterFS。Brick 是组织数据和元数据基本单位，代表文件系统的一个目录（即按照目录拆分）。客户端的操作首先根据fsname和文件名定位到Brick，跨Brick的操作性能低。数据和元数据操作可能相互影响。</li>
<li>Lustre。条带化对象存储作为数据存储，MDS负责管理元数据，元数据和目录项存到MDS本地，无元数据扩展能力。采用专用硬件（高速网络、低延迟存储）, 适用于高性能存储。</li>
<li>CephFS。底层使用对象存储RADOS提供可扩展性和容错性，MDS 将目录树按子树拆分，MDS是无状态的，目录项和元数据内容同样持久化到RADOS中。为通用存储设计。</li>
<li>GPFS。NSD 负责数据存储，Manager Node管理元数据，元数据同样存储到NSD。客户端通过申请不同粒度的令牌在实现一致性的同时减少锁冲突。</li>
<li>HDFS。非posix语义，NameNode 管理元数据, 元数据不可水平扩展，为大数据分析设计。</li>
<li>JuiceFS可直接用S3 存储数据, KV和OLTP数据库管理元数据。利用本地内存和磁盘缓存加速元数据和数据性能。</li>
<li>3FS适用于高性能存储, 网络通信采用RDMA，元数据使用foundationDB, 数据使用链式块组织。
此外，基于paxos协议实现的zookeeper, etcd也是一种分布式文件系统, 提供高可用。但其只对外提供元数据服务，不适合存储大规模数据。常用的使用方式是1. 通过创建文件和lease 对外提供分布式锁 2. 以KV的形式存储少量重要数据，例如配置文件</li>
</ol>
<p>以上，<strong>分布式文件系统建议基于CephFS和3FS建设</strong>，CephFS是通用文件系统，3FS侧重于高性能计算领域。优先考虑使用分布式KV作为元数据引擎。</p>
<p>by the way
我曾统计过我接触到的阿里云内部基础服务的开源替代性，发现最难以替代的是盘古分布式文件系统（其次是夸父高性能网络）。高性能，高可用，低成本的分布式文件系统毫无疑问是各大公司的核心科技。开源存储系统方便使用且满足高性能需求的很少，且相比于以上简要的架构，分布式存储系统的优化更重要。主要的优化点可能是</p>
<ol>
<li>客户端和服务端联合优化，包括实现缓存减少后端访问，前后端流控，零拷贝，避免一方空转</li>
<li>分布式元数据。paxos协议高可用, 元数据尽可能内存化（通过压缩等手段存内存，性能比从磁盘读好很多），减少分布式锁导致性能下降，增强元数据可扩展性。元数据自动根据load调度</li>
<li>存储层提高IO性能，保证数据安全，降低存储成本，以及坏盘检测等优化</li>
<li>网络通信，通过用户态网络、rdma等技术，降低传输延迟</li>
</ol>
<p>最后推荐三篇阿里云的论文</p>
<ol>
<li>盘古存储 More Than Capacity: Performance-oriented Evolution of Pangu in Alibaba. 链接 <a href="https://www.usenix.org/system/files/fast23-li-qiang_more.pdf">https://www.usenix.org/system/files/fast23-li-qiang_more.pdf</a></li>
<li>夸父网络 From Luna to Solar: The Evolutions of the Compute-to-Storage Networks in Alibaba Cloud. 链接 <a href="https://rmiao.github.io/assets/pdf/solar-sigcomm22.pdf">https://rmiao.github.io/assets/pdf/solar-sigcomm22.pdf</a></li>
<li>分布式KV引擎ArkDB ArkDB A Key-Value Engine for Scalable Cloud Storage Services 链接 <a href="https://dl.acm.org/doi/10.1145/3448016.3457553">https://dl.acm.org/doi/10.1145/3448016.3457553</a> 这个链接无法下载论文，可以看 <a href="https://zhuanlan.zhihu.com/p/414054332">https://zhuanlan.zhihu.com/p/414054332</a></li>
</ol>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[storage]]></category>
        </item>
        <item>
          <title><![CDATA[leveldb(2)—线程模型和并发控制]]></title>
          <link>https://larrystd.github.io/posts/leveldb(2)—线程模型和并发控制</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/leveldb(2)—线程模型和并发控制</guid>
          <pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[leveldb前台可并发读，但只能串行写。]]></description>
          <content:encoded><![CDATA[<p>leveldb前台可并发读，但只能串行写。</p>
<p>leveldb memtable通过无锁skiplist结构 支持读写并发</p>
<p>leveldb的后台线程只有一个，只处理compaction这一个任务, leveldb的minor compaction和major compaction是一起处理的</p>
<h3>线程调度和后台compaction</h3>
<p>线程调度的函数</p>
<p>util/env_posix.cc</p>
<p>PosixEnv::Schedule</p>
<ol>
<li>如果background_thread 未创建，创建之</li>
<li>background_thread  执行BackgroundThreadEntryPoint函数，即从background_work_queue_中拿任务执行；如果没有任务，执行cv.wait()</li>
</ol>
<pre><code class="language-cpp">void PosixEnv::Schedule(
    void (*background_work_function)(void* background_work_arg),
    void* background_work_arg) {
  background_work_mutex_.Lock();

  // Start the background thread, if we haven't done so already.
  if (!started_background_thread_) {
    started_background_thread_ = true;
    std::thread background_thread(PosixEnv::BackgroundThreadEntryPoint, this);
    background_thread.detach();
  }

  // If the queue is empty, the background thread may be waiting for work.
  if (background_work_queue_.empty()) {
    background_work_cv_.Signal();
  }

  background_work_queue_.emplace(background_work_function, background_work_arg);
  background_work_mutex_.Unlock();
}

static void BackgroundThreadEntryPoint(PosixEnv* env) {
env->BackgroundThreadMain();
}

void PosixEnv::BackgroundThreadMain() {
  while (true) {
    background_work_mutex_.Lock();

    // Wait until there is work to be done.
    while (background_work_queue_.empty()) {
      background_work_cv_.Wait();
    }

    assert(!background_work_queue_.empty());
    auto background_work_function = background_work_queue_.front().function;
    void* background_work_arg = background_work_queue_.front().arg;
    background_work_queue_.pop();

    background_work_mutex_.Unlock();
    background_work_function(background_work_arg);
  }
}
</code></pre>
<p>PosixEnv::Schedule在哪里被调用</p>
<ol>
<li>DBImpl::MaybeScheduleCompaction
background_compaction_scheduled_ = true;
env_->Schedule(&#x26;DBImpl::BGWork, this);</li>
</ol>
<p>DBImpl::BGWork minor, major compaction都执行</p>
<p>只有compaction被调用。。</p>
<p>PosixEnv::StartThread
直接开启新线程执行thread_main</p>
<pre><code class="language-cpp">void StartThread(void (*thread_main)(void* thread_main_arg),
			   void* thread_main_arg) override {
std::thread new_thread(thread_main, thread_main_arg);
new_thread.detach();
}
</code></pre>
<p>StartThread 被测试和benchmark调用</p>
<h3>前台线程</h3>
<p>DBImpl的接口
Status Put(const WriteOptions&#x26;, const Slice&#x26; key,
const Slice&#x26; value) override;
Status Delete(const WriteOptions&#x26;, const Slice&#x26; key) override;
Status Write(const WriteOptions&#x26; options, WriteBatch* updates) override;
Status Get(const ReadOptions&#x26; options, const Slice&#x26; key,
std::string* value) override;
可以被多线程执行，即前台多线程。</p>
<p>leveldb前台可并发读，但只能串行写。</p>
<h4>Get 读</h4>
<p>支持多线程读，即以下逻辑执行是可以多线程的</p>
<pre><code class="language-cpp">  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &#x26;s)) {
      // Done
    } else if (imm != nullptr &#x26;&#x26; imm->Get(lkey, value, &#x26;s)) {
      // Done
    } else {
      s = current->Get(options, lkey, value, &#x26;stats);
      have_stat_update = true;
    }
    mutex_.Lock();
  }
</code></pre>
<p>不担心和写冲突吗？</p>
<ol>
<li>如果是读tablefile，读只可能和compaction冲突。但对于compaction时的sst，读操作只可能读旧文件。即对于执行A->B，前台读只可能读到A，因此前台读不会和compaction 写冲突</li>
<li>如果读memtable，采用的是无锁跳表，保证读写原子性。</li>
</ol>
<p>读skiplist key流程，即SkipList&#x3C;Key, Comparator>::Iterator::Seek(const Key&#x26; target)</p>
<p>读和写skiplist核心都是获得FindGreaterOrEqual，读的目的是拿到key节点，写的目的是拿到key节点，然后插入它的next</p>
<pre><code class="language-cpp">template &#x3C;typename Key, class Comparator>
inline void SkipList&#x3C;Key, Comparator>::Iterator::Seek(const Key&#x26; target) {
  node_ = list_->FindGreaterOrEqual(target, nullptr);
}

template &#x3C;typename Key, class Comparator>
typename SkipList&#x3C;Key, Comparator>::Node*
SkipList&#x3C;Key, Comparator>::FindGreaterOrEqual(const Key&#x26; key,
                                              Node** prev) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    Node* next = x->Next(level);
    if (KeyIsAfterNode(key, next)) {
      // Keep searching in this list
      x = next;
    } else {
      if (prev != nullptr) prev[level] = x;
      if (level == 0) {
        return next;
      } else {
        // Switch to next list
        level--;
      }
    }
  }
}

template &#x3C;typename Key, class Comparator>
struct SkipList&#x3C;Key, Comparator>::Node {
  explicit Node(const Key&#x26; k) : key(k) {}

  Key const key;

  // Accessors/mutators for links.  Wrapped in methods so we can
  // add the appropriate barriers as necessary.
  Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].store(x, std::memory_order_release);
  }
 private:
  // Array of length equal to the node height.  next_[0] is lowest level link.
  std::atomic&#x3C;Node*> next_[1];
};
</code></pre>
<p>写skiplist流程</p>
<pre><code class="language-cpp">Status WriteBatchInternal::InsertInto(const WriteBatch* b, MemTable* memtable) {
  MemTableInserter inserter;
  inserter.sequence_ = WriteBatchInternal::Sequence(b);
  inserter.mem_ = memtable;
  return b->Iterate(&#x26;inserter);
}

class MemTableInserter : public WriteBatch::Handler {
 public:
  SequenceNumber sequence_;
  MemTable* mem_;

  void Put(const Slice&#x26; key, const Slice&#x26; value) override {
    mem_->Add(sequence_, kTypeValue, key, value);
    sequence_++;
  }
  void Delete(const Slice&#x26; key) override {
    mem_->Add(sequence_, kTypeDeletion, key, Slice());
    sequence_++;
  }
};

void MemTable::Add(SequenceNumber s, ValueType type, const Slice&#x26; key,
                   const Slice&#x26; value) {
  // Format of an entry is concatenation of:
  //  key_size     : varint32 of internal_key.size()
  //  key bytes    : char[internal_key.size()]
  //  value_size   : varint32 of value.size()
  //  value bytes  : char[value.size()]
  size_t key_size = key.size();
  size_t val_size = value.size();
  size_t internal_key_size = key_size + 8;
  const size_t encoded_len = VarintLength(internal_key_size) +
                             internal_key_size + VarintLength(val_size) +
                             val_size;
  char* buf = arena_.Allocate(encoded_len);
  char* p = EncodeVarint32(buf, internal_key_size);
  std::memcpy(p, key.data(), key_size);
  p += key_size;
  EncodeFixed64(p, (s &#x3C;&#x3C; 8) | type);
  p += 8;
  p = EncodeVarint32(p, val_size);
  std::memcpy(p, value.data(), val_size);
  assert(p + val_size == buf + encoded_len);
  table_.Insert(buf);
}

template &#x3C;typename Key, class Comparator>
void SkipList&#x3C;Key, Comparator>::Insert(const Key&#x26; key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  Node* x = FindGreaterOrEqual(key, prev);

  // Our data structure does not allow duplicate insertion
  assert(x == nullptr || !Equal(key, x->key));

  int height = RandomHeight();
  if (height > GetMaxHeight()) {
    for (int i = GetMaxHeight(); i &#x3C; height; i++) {
      prev[i] = head_;
    }
    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (nullptr), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since nullptr sorts after all
    // keys.  In the latter case the reader will use the new node.
    max_height_.store(height, std::memory_order_relaxed);
  }
  // 插入节点到prev数组元素的next
  x = NewNode(key, height);
  for (int i = 0; i &#x3C; height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}
</code></pre>
<h4>Write写</h4>
<p>DBImpl::Write，为什么释放锁？
log_->AddRecord和 WriteBatchInternal::InsertInto 实际只有一个线程执行</p>
<pre><code class="language-cpp">      mutex_.Unlock();
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
      bool sync_error = false;
      if (status.ok() &#x26;&#x26; options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      if (status.ok()) {
        status = WriteBatchInternal::InsertInto(write_batch, mem_);
      }
      mutex_.Lock();
</code></pre>
<p>原因在于writer
构造使用mutex_构造
3. 构造writer
4. 将writer放入writers_的尾部
5. 线程持续等待，知道当前writer是writers_的队首</p>
<p>这保证只有一个线程把writer处理完，从writers_出队，后面的线程才能处理writer</p>
<pre><code class="language-cpp">Status DBImpl::Write(const WriteOptions&#x26; options, WriteBatch* updates) {
  Writer w(&#x26;mutex_);
  w.batch = updates;
  w.sync = options.sync;
  w.done = false;

  MutexLock l(&#x26;mutex_);
  writers_.push_back(&#x26;w);
  while (!w.done &#x26;&#x26; &#x26;w != writers_.front()) {
    w.cv.Wait();
  }
  if (w.done) {
    return w.status;
  }

// writer构造， mutex用于writer.wait()
struct DBImpl::Writer {
  explicit Writer(port::Mutex* mu)
      : batch(nullptr), sync(false), done(false), cv(mu) {}

  Status status;
  WriteBatch* batch;
  bool sync;
  bool done;
  port::CondVar cv;
};
</code></pre>
<h3>并发benchmark</h3>
<p>写</p>
<pre><code class="language-cpp">  void WriteRandom(ThreadState* thread) { DoWrite(thread, false); }

  void DoWrite(ThreadState* thread, bool seq) {
    if (num_ != FLAGS_num) {
      char msg[100];
      std::snprintf(msg, sizeof(msg), "(%d ops)", num_);
      thread->stats.AddMessage(msg);
    }

    RandomGenerator gen;
    WriteBatch batch;
    Status s;
    int64_t bytes = 0;
    KeyBuffer key;
    for (int i = 0; i &#x3C; num_; i += entries_per_batch_) {
      batch.Clear();
      for (int j = 0; j &#x3C; entries_per_batch_; j++) {
        const int k = seq ? i + j : thread->rand.Uniform(FLAGS_num);
        key.Set(k);
        batch.Put(key.slice(), gen.Generate(value_size_));
        bytes += value_size_ + key.slice().size();
        thread->stats.FinishedSingleOp();
      }
      s = db_->Write(write_options_, &#x26;batch);
      if (!s.ok()) {
        std::fprintf(stderr, "put error: %s\n", s.ToString().c_str());
        std::exit(1);
      }
    }
    thread->stats.AddBytes(bytes);
  }
</code></pre>
<p>读</p>
<pre><code class="language-cpp">  void ReadRandom(ThreadState* thread) {
    ReadOptions options;
    std::string value;
    int found = 0;
    KeyBuffer key;
    for (int i = 0; i &#x3C; reads_; i++) {
      const int k = thread->rand.Uniform(FLAGS_num);
      key.Set(k);
      if (db_->Get(options, key.slice(), &#x26;value).ok()) {
        found++;
      }
      thread->stats.FinishedSingleOp();
    }
    char msg[100];
    std::snprintf(msg, sizeof(msg), "(%d of %d found)", found, num_);
    thread->stats.AddMessage(msg);
  }
</code></pre>
<p>多线程执行
线程数量为n</p>
<pre><code class="language-cpp">  void RunBenchmark(int n, Slice name,
                    void (Benchmark::*method)(ThreadState*)) {
    SharedState shared(n);

    ThreadArg* arg = new ThreadArg[n];
    for (int i = 0; i &#x3C; n; i++) {
      arg[i].bm = this;
      arg[i].method = method;
      arg[i].shared = &#x26;shared;
      ++total_thread_count_;
      // Seed the thread's random state deterministically based upon thread
      // creation across all benchmarks. This ensures that the seeds are unique
      // but reproducible when rerunning the same set of benchmarks.
      arg[i].thread = new ThreadState(i, /*seed=*/1000 + total_thread_count_);
      arg[i].thread->shared = &#x26;shared;
      g_env->StartThread(ThreadBody, &#x26;arg[i]);
    }
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[storage]]></category><category><![CDATA[leveldb]]></category>
        </item>
        <item>
          <title><![CDATA[redis(2)——网络处理和持久化]]></title>
          <link>https://larrystd.github.io/posts/redis(2)—网络处理和持久化</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/redis(2)—网络处理和持久化</guid>
          <pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[redis 通过网络服务接收远程命令，进行处理，将执行结果返回。]]></description>
          <content:encoded><![CDATA[<p>redis 通过网络服务接收远程命令，进行处理，将执行结果返回。</p>
<p>redis 4.0版本监听网络请求，处理请求和命令都由一个主线程完成。</p>
<ol>
<li>好处是redis 主IO路径无须加锁保护。</li>
<li>缺点是redis 不可处理耗时长的命令，这在使用自定义命令和复杂lua脚本里需要注意。此外redis 无法利用多核cpu，这对部署机器的cpu单核能力有要求。</li>
</ol>
<p>redis的rdb和aof 都是经典的数据持久化/备份手段，rdb关注数据库的数据, aof关注数据库的操作。</p>
<h3>网络处理</h3>
<p>redis 的网络处理类似常见的IO多路复用网络库，通过事件通知实现单线程处理大量来自不同客户端的事件。</p>
<h4>事件和eventloop 事件循环</h4>
<p>三种事件aeFileEvent（注册的网络事件），aeTimeEvent（定时事件），firedEvent用来存放epoll返回的就绪事件（即需要处理的事件）</p>
<p>网络事件通过epoll 监听fd可读可写（即网络包可读可写）触发；定时事件通过获得下一个最近事件的执行时间、定时触发；</p>
<pre><code class="language-cpp">aeFileEvent
```cpp
/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *next;
} aeTimeEvent;

/* A fired event */
typedef struct aeFiredEvent {
    int fd;
    int mask;
} aeFiredEvent;
</code></pre>
<p>eventloop的核心是aeProcessEvents函数，执行逻辑如下</p>
<ol>
<li>计算最近的定时事件需要等待的时间t。shortest = aeSearchNearestTimer(eventLoop)</li>
<li>numevents = aeApiPoll(eventLoop, tvp); 执行epoll_wait，超时时间为上述需要等待的时间t</li>
<li>遍历eventLoop->fired[j].fd获得触发的事件，根据事件读写类型执行处理函数；fe->rfileProc(eventLoop,fd,fe->clientData,mask);  fe->wfileProc(eventLoop,fd,fe->clientData,mask);</li>
<li>处理定时事件processTimeEvents</li>
</ol>
<h4>事件处理函数</h4>
<ol>
<li>acceptTcpHandler
用来接受和建立连接，acceptTcpHandler会创建client</li>
</ol>
<p>创建Client时，同时创建对该client的fileevent 可读事件，处理函数是readQueryFromClient。linux的accept() 会从监听 socket 的已完成连接队列中取出一个客户端连接，并为其创建一个 新的 socket。该fd标志了client和filevent可读事件。</p>
<ol start="2">
<li>readQueryFromClient</li>
</ol>
<p><code>void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)</code></p>
<ol>
<li>生成c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);</li>
<li>nread = read(fd, c->querybuf+qblen, readlen);</li>
<li>调用processInputBuffer(c);函数</li>
</ol>
<p>processInputBuffer 内部是processCommand，主要执行两步</p>
<ol>
<li>获取命令<code>c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);</code></li>
<li>处理命令call(c,CMD_CALL_FULL);</li>
</ol>
<h3>线程模型</h3>
<p>redis 4.0版本监听网络请求，处理请求和命令都由一个主线程完成。</p>
<ol>
<li>好处是redis 主IO路径无须加锁保护。</li>
<li>缺点是redis 不可处理耗时长的命令，这在使用自定义命令和复杂lua脚本里需要注意。此外redis 无法利用多核cpu，这对部署机器的cpu单核能力有要求。</li>
</ol>
<p>redis 使用多线程的地方很少，只有adb, aof后台执行，和lazyfree等操作。</p>
<h4>fork 子进程</h4>
<p>fork() 创建子进程，对于父进程：fork() 返回新创建子进程的进程ID（PID）。这是一个正整数。
对于子进程：fork() 返回0。
如果 fork() 失败：则返回-1，并设置 errno 以指示错误原因。</p>
<p>fork使用到的地方</p>
<ol>
<li>后台创建子进程执行rdb备份任务</li>
</ol>
<pre><code class="language-cpp">int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    openChildInfoPipe();

    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;

        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename,rsi);
</code></pre>
<ol start="2">
<li>aof 创建子进程执行rewriteAppendOnlyFile</li>
</ol>
<pre><code class="language-cpp">int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    if (aofCreatePipes() != C_OK) return C_ERR;
    openChildInfoPipe();
    start = ustime();
    if ((childpid = fork()) == 0) {
        char tmpfile[256];

        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-aof-rewrite");
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);
            ....
    } else {
        /* Parent */
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
</code></pre>
<ol start="3">
<li>server如果设置了daemonize，则当前进程退出，创建子进程执行后续任务</li>
</ol>
<pre><code class="language-cpp">void daemonize(void) {
    int fd;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO) close(fd);
    }
}
</code></pre>
<ol start="4">
<li>sentinelRunPendingScripts，子进程执行脚本，父进程返回</li>
</ol>
<pre><code class="language-cpp">/* Run pending scripts if we are not already at max number of running
 * scripts. */
void sentinelRunPendingScripts(void) {
    listNode *ln;
    listIter li;
    mstime_t now = mstime();

    /* Find jobs that are not running and run them, from the top to the
     * tail of the queue, so we run older jobs first. */
    listRewind(sentinel.scripts_queue,&#x26;li);
    while (sentinel.running_scripts &#x3C; SENTINEL_SCRIPT_MAX_RUNNING &#x26;&#x26;
           (ln = listNext(&#x26;li)) != NULL)
    {
        sentinelScriptJob *sj = ln->value;
        pid_t pid;

        pid = fork();

        if (pid == -1) {
            /* Parent (fork error).
             * We report fork errors as signal 99, in order to unify the
             * reporting with other kind of errors. */
            sentinelEvent(LL_WARNING,"-script-error",NULL,
                          "%s %d %d", sj->argv[0], 99, 0);
            sj->flags &#x26;= ~SENTINEL_SCRIPT_RUNNING;
            sj->pid = 0;
        } else if (pid == 0) {
            /* Child */
            execve(sj->argv[0],sj->argv,environ);
            /* If we are here an error occurred. */
            _exit(2); /* Don't retry execution. */
        } else {
            sentinel.running_scripts++;
            sj->pid = pid;
            sentinelEvent(LL_DEBUG,"+script-child",NULL,"%ld",(long)pid);
        }
    }
</code></pre>
<h4>pthread_create 多线程</h4>
<p>pthread_create创建线程被bio封装</p>
<pre><code class="language-cpp">/* Initialize the background system, spawning the thread. */
void bioInit(void) {
    pthread_attr_t attr;
    pthread_t thread;
    size_t stacksize;
    int j;

    /* Initialization of state vars and objects */
    for (j = 0; j &#x3C; BIO_NUM_OPS; j++) {
        pthread_mutex_init(&#x26;bio_mutex[j],NULL);
        pthread_cond_init(&#x26;bio_newjob_cond[j],NULL);
        pthread_cond_init(&#x26;bio_step_cond[j],NULL);
        bio_jobs[j] = listCreate();
        bio_pending[j] = 0;
    }

    /* Set the stack size as by default it may be small in some system */
    pthread_attr_init(&#x26;attr);
    pthread_attr_getstacksize(&#x26;attr,&#x26;stacksize);
    if (!stacksize) stacksize = 1; /* The world is full of Solaris Fixes */
    while (stacksize &#x3C; REDIS_THREAD_STACK_SIZE) stacksize *= 2;
    pthread_attr_setstacksize(&#x26;attr, stacksize);

    /* Ready to spawn our threads. We use the single argument the thread
     * function accepts in order to pass the job ID the thread is
     * responsible of. */
    for (j = 0; j &#x3C; BIO_NUM_OPS; j++) {
        void *arg = (void*)(unsigned long) j;
        if (pthread_create(&#x26;thread,&#x26;attr,bioProcessBackgroundJobs,arg) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}

void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
    struct bio_job *job = zmalloc(sizeof(*job));

    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;
    pthread_mutex_lock(&#x26;bio_mutex[type]);
    listAddNodeTail(bio_jobs[type],job);
    bio_pending[type]++;
    pthread_cond_signal(&#x26;bio_newjob_cond[type]);
    pthread_mutex_unlock(&#x26;bio_mutex[type]);
}
</code></pre>
<p>bioCreateBackgroundJob 使用的地方</p>
<ol>
<li>aof后台fsync</li>
</ol>
<pre><code class="language-cpp">void aof_background_fsync(int fd) {
    bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
}
</code></pre>
<ol start="2">
<li>lazyfree</li>
</ol>
<pre><code class="language-cpp">/* Empty a Redis DB asynchronously. What the function does actually is to
 * create a new empty set of hash tables and scheduling the old ones for
 * lazy freeing. */
void emptyDbAsync(redisDb *db) {
    dict *oldht1 = db->dict, *oldht2 = db->expires;
    db->dict = dictCreate(&#x26;dbDictType,NULL);
    db->expires = dictCreate(&#x26;keyptrDictType,NULL);
    atomicIncr(lazyfree_objects,dictSize(oldht1));
    bioCreateBackgroundJob(BIO_LAZY_FREE,NULL,oldht1,oldht2);
}
</code></pre>
<h3>rdb持久化</h3>
<p>rdb, replicate db</p>
<p>暂只看rdbsave和rdbload</p>
<h4>rdbsave</h4>
<p><code>int rdbSave(char *filename, rdbSaveInfo *rsi)</code></p>
<ol>
<li>snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w");</li>
<li>rioInitWithFile(&#x26;rdb,fp);</li>
<li>rdbSaveRio(&#x26;rdb,&#x26;error,RDB_SAVE_NONE,rsi)</li>
<li>fflush(fp) == EOF fsync(fileno(fp)  fclose(fp) == EOF</li>
<li>rename(tmpfile,filename)</li>
</ol>
<p>fflush是flush文件流到page cache，fsync是刷page cache到文件落盘</p>
<p>其中的rdbSaveRio 函数，rdbSaveRio(&#x26;rdb,&#x26;error,RDB_SAVE_NONE,rsi)</p>
<ol>
<li>写入"REDIS%04d",RDB_VERSION</li>
<li>rdbSaveInfoAuxFields(rdb,flags,rsi)</li>
<li>遍历server.dbnum
<ol>
<li>对每个db，redisDb。每个db内部其实是db->dict，</li>
<li>写入一些元数据，包括rdbSaveType(rdb,RDB_OPCODE_SELECTDB)</li>
<li>rdbSaveLen(rdb,j)</li>
<li>rdbSaveType(rdb,RDB_OPCODE_RESIZEDB)</li>
<li>rdbSaveLen(rdb,db_size)</li>
<li>rdbSaveLen(rdb,expires_size)</li>
<li>遍历db->dict，拿到sds keystr = dictGetKey(de);，robj key, <code>*o = dictGetVal(de);</code>，执行rdbSaveKeyValuePair(rdb,&#x26;key,o,expire,now)</li>
</ol>
</li>
</ol>
<h4>rdbsave 的调用</h4>
<p>rdbSaveBackground执行调用childpid = fork() 启动进程，在子进程里执行rdbSave(filename,rsi);</p>
<p>fork()的子进程初始利用写时复制共享父进程内存。写时复制情况下，父子进程修改共享内存页时，均会复制共享页到新的物理内存页。</p>
<p>写时复制保证子进程的内存始终和父进程调用fork()时的状态一致，就是说，**子进程rdbSave 的dict 内容和父进程fork()时的一致，且不会因后续父进程对dict的写入而改变。。**以上由操作系统保证。太6了</p>
<p>如果父进程大量修改数据（触发大量 COW），可能导致内存和 CPU 压力增大，但这属于资源问题，影响可控</p>
<p>rdbsave的定时和手动调度</p>
<p>后台执行时会设置执行的child_pid，从而判断是否后台正在执行备份
server.rdb_child_pid != -1 || server.aof_child_pid != -1</p>
<p>replicationCron -> startBgsaveForReplication
replicationCron 一秒钟执行一次</p>
<p>syncCommand-> startBgsaveForReplication</p>
<h4>rdbLoadRio</h4>
<p><code>int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi)</code></p>
<p>从文件里读数据</p>
<ol>
<li>首先读到"REDIS\0"</li>
<li>然后是一字节版本，rdbver = atoi(buf+5);。算法
从第一个非空白字符开始，取一个可选的初始加号或者减号，后跟尽可能多的十进制数字，并将它们转换为一个<code>int</code>类型的数值。</li>
<li>循环执行
<ol>
<li>type = rdbLoadType(rdb)</li>
<li>根据type 类型执行以下操作，type的类型可以是RDB_OPCODE_EOF表示结束</li>
<li>解析出key = rdbLoadStringObject(rdb) 和val = rdbLoadObject(type,rdb)</li>
<li>将kv写入到hash table dbAdd(db,key,val);</li>
</ol>
</li>
</ol>
<p>显然rdb会将某时刻hashtable的所有kv写入到rdb文件中。</p>
<h3>aof持久化</h3>
<p>aof, append only file</p>
<p>rdb关心的是复制db的dict 的key, value，通过写kv数据恢复数据库状态。aof关心的是复制db的增删改操作，通过replay操作恢复数据库状态。</p>
<h4>aof rewrite</h4>
<p>aof 可以实现全量复制和增量复制。</p>
<p>aof 的rewrite 是将当前状态的dict的key value，全部保存为<code>set key value</code>操作，从而实现全量复制。</p>
<p>aof 增量复制通过父进程将写操作发给子进程实现。进程通信的方式是使用管道。创建管道会返回两个fd，分别给父子进程。进程利用fd和fileevent事件可以监听管道，子进程监听可写事件，父进程监听可读事件。</p>
<p>当执行aof时，父进程接收到写请求，在处理请求之外，会把写操作写入到管道。当执行rdb或aof时，redis会把执行的子进程id记录，通过判断子进程id存在可以得到是否正在执行rdb/aof。</p>
<p>Redis 4.0 引入了 混合持久化（通过配置 aof-use-rdb-preamble yes 启用）：重写后的 AOF 文件会以 RDB 格式开头，后续追加增量 AOF 命令。
此时，AOF 重写过程会先生成 RDB 数据，再追加新写入的 AOF 命令。</p>
<p>子进程执行逻辑</p>
<ol>
<li>如果设置了server.aof_use_rdb_preamble，处理rdb；否则，执行rewriteAppendOnlyFileRio，遍历dict，将当前dict存放的key, value转换成SET key value 写入到aof文件</li>
<li>循环执行aofReadDiffFromParent(); 读rewrite期间父进程的写入，到server.aof_child_diff</li>
<li>执行rioWrite(&#x26;aof,server.aof_child_diff,sdslen(server.aof_child_diff)，写入到rio</li>
</ol>
<p>aofReadDiffFromParent 从管道读数据</p>
<pre><code class="language-cpp">/* This function is called by the child rewriting the AOF file to read
 * the difference accumulated from the parent into a buffer, that is
 * concatenated at the end of the rewrite. */
ssize_t aofReadDiffFromParent(void) {
    char buf[65536]; /* Default pipe buffer size on most Linux systems. */
    ssize_t nread, total = 0;

    while ((nread =
            read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
        server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
        total += nread;
    }
    return total;
}
</code></pre>
<p>rio对应一个文件</p>
<pre><code class="language-cpp">void rioInitWithFile(rio *r, FILE *fp) {
    *r = rioFileIO;
    r->io.file.fp = fp;
    r->io.file.buffered = 0;
    r->io.file.autosync = 0;
}

static const rio rioFileIO = {
    rioFileRead,
    rioFileWrite,
    rioFileTell,
    rioFileFlush,
    NULL,           /* update_checksum */
    0,              /* current checksum */
    0,              /* bytes read or written */
    0,              /* read/write chunk size */
    { { NULL, 0 } } /* union for io-specific vars */
};
</code></pre>
<p>主进程执行逻辑</p>
<ol>
<li>格式化数据到Buf，<code>server.expireCommand,argv[1], exarg</code></li>
<li>如果开启了aof，执行server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));</li>
<li>如果server.aof_child_pid != -1，表示有rewrite进程，执行aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));</li>
</ol>
<h3>pub-sub</h3>
<p>pub-sub 是常用的网络服务</p>
<p>订阅-发布和关注某个up主，收到他们更新的消息，是类似的</p>
<h4>SubscribeChannel</h4>
<p>Subscribe a client to a channel. Returns 1 if the operation succeeded</p>
<p>c->pubsub_channels 是一个dict。注册的结果，key是channel，value是一个client list</p>
<h4>pubsubPublishMessage</h4>
<ol>
<li>利用dictFind(server.pubsub_channels,channel) 找到channel(key)，返回client list（value）</li>
<li>遍历list，对每个client，执行<code>addReply(c,shared.mbulkhdr[3]);</code>，<code>addReply(c,shared.messagebulk);</code> addReplyBulk(c,channel); addReplyBulk(c,message);</li>
</ol>
<p>addReply</p>
<ol>
<li>传入<code>robj *obj</code></li>
<li>addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)</li>
</ol>
<p>subscribeCommand</p>
<pre><code class="language-cpp">void subscribeCommand(client *c) {
    int j;

    for (j = 1; j &#x3C; c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
    c->flags |= CLIENT_PUBSUB;
}
</code></pre>
<p>publish command</p>
<pre><code class="language-cpp">void publishCommand(client *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[redis]]></category><category><![CDATA[storage]]></category>
        </item>
        <item>
          <title><![CDATA[redis(1)——数据结构]]></title>
          <link>https://larrystd.github.io/posts/redis(1)—数据结构</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/redis(1)—数据结构</guid>
          <pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[redis 是最值得学习的开源项目， 1. 它广泛应用 2. 它实现了单机缓存数据库，同时支持网络访问、复制、集群、订阅等高级特性 3. redis依赖很少，数据结构、日志等基础库也是自行实现，有利于学习 4. redis代码精悍、质量高，甚至很难找到优化点。]]></description>
          <content:encoded><![CDATA[<p>redis 是最值得学习的开源项目，</p>
<ol>
<li>它广泛应用</li>
<li>它实现了单机缓存数据库，同时支持网络访问、复制、集群、订阅等高级特性</li>
<li>redis依赖很少，数据结构、日志等基础库也是自行实现，有利于学习</li>
<li>redis代码精悍、质量高，甚至很难找到优化点。</li>
</ol>
<p>本文阐释redis数据结构，redis 数据结构设计的一大特点是节省内存。原因是redis作为内存数据库需要节省内存使用(连续内存的数组能减少内存碎片)。这对架构设计同样有借鉴意义，通过节省元数据内存使用来把更多元数据/索引放在内存上，可以大大提高处理的吞吐和延迟。</p>
<h3>数据结构</h3>
<p>在redis的数据结构中</p>
<ol>
<li>sds 最常用做dict的key</li>
<li>list 作为redis 内部结构使用，例如事件列表，客户端列表</li>
<li>dict 作为数据库使用</li>
<li>redisObject 作为数据库的value使用，object可以划分成string, list, hashtable, set, sorted set 五种基本类型
<ol>
<li>string 通过sds实现</li>
<li>list 通过quicklist实现；quicklist又是list和ziplist的组合</li>
<li>hashtable，少量数据通过ziplist实现（减小内存碎片），大量数据通过dict实现</li>
<li>set，整数且少量数据通过intset实现，大量数据通过dict实现</li>
<li>sorted set，少量数据通过ziplist实现，大量数据通过dict+skiplist实现（skiplist用来保序）</li>
</ol>
</li>
</ol>
<h4>sds, simple dynamic string</h4>
<p>sds 就是动态字符串，在C char* 之上的封装，增加了常用的变量和成员函数。<strong>sds 在redis使用广泛，最常用的是作为dict的key。</strong></p>
<p>sds有sdshdr8，sdshdr16，sdshdr32等。后面的8, 16, 32 表示sds的最长长度为2^8, 2^16, 2^32(128byte, 64K, 4M)。一般来说，sdshdr8和sdshdr16就够用了</p>
<p>一般来说</p>
<pre><code class="language-cpp">/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
</code></pre>
<ol>
<li>buf 实际存放数据，等同于C语言的<code>char *</code></li>
<li>获得长度不需要遍历buf，只需要将(s)-(sizeof(struct sdshdr##T))强制转型为struct sdshdr##T，然后调用sdshdr##T的方法即可</li>
</ol>
<p>SDS_HDR(T,s) 表示就s 转型为struct sdshdr##T</p>
<pre><code class="language-cpp">typedef char *sds;

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&#x26;SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        ...
</code></pre>
<p>创建sds，传入C风格的char *，</p>
<ol>
<li>通过strlen获得C风格字符串长度</li>
<li>根据长度确定type</li>
<li>构造sdsHeader</li>
<li>memcpy(s, init, initlen); 将C风格字符串 copy到sds 的buf成员</li>
</ol>
<pre><code class="language-cpp">/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 &#x26;&#x26; initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen &#x3C;&#x3C; SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        ...
    }
    if (initlen &#x26;&#x26; init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
</code></pre>
<p>sds还实现了trim, cmp等常用函数, 是s字符串设计的良好参考</p>
<h4>list</h4>
<p>双向链表, 增加了len成员变量记录链表的长度</p>
<p>redis 双向链表主要应用在系统结构，例如事件列表fileEvents，定时器事件列表，client列表，slave列表等</p>
<pre><code class="language-cpp">typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;
</code></pre>
<h4>dict</h4>
<p>dict 是kv哈希表，是redis 的核心存储单元。redis 接收到的kv 就是存放在dict结构里。</p>
<p>每个dict对象有两个dictht，每个dictht是一个hash表。使用两个hash表是后续用来rehash。rehash是哈希表扩容，哈希表扩容期间，由于长度变大，会导致key重新排布。rehash的期望是在key重新排布期间，不影响前台IO对哈希表的读写操作。</p>
<ol>
<li>redis rehash线程和前台操作使用相同线程，可以避免对dict加锁</li>
<li>使用两张表，可以渐进式rehash，每次迁移一个桶。rehash期间，表1的长度是旧长度，表2是新长度。读操作需要先读2，后读1；写操作只需要写表2，删除更新操作也需要同时处理两个表。</li>
<li>rehash时，只需要复制key和value的指针，不需要拷贝数据，因此对内存的消耗有限</li>
</ol>
<pre><code class="language-cpp">typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;
</code></pre>
<h4>object</h4>
<p>object最常见的使用是作为dict的value存在,</p>
<p>redis key 只有sds一种类型，value有五种类型。</p>
<ol>
<li>String字符串，支持SET, GET命令，底层使用sds实现</li>
<li>List	有序可重复的列表，支持LPUSH, RPOP, LRANGE命令，底层使用quicklist实现</li>
<li>Hash	键值对集合，支持HSET, HGET, HGETALL，底层使用ziplist, hashtable实现</li>
<li>Set	无序唯一的集合，支持SADD, SMEMBER，底层使用intset, hashtable实现</li>
<li>Sorted Set有序唯一的集合（带权重），支持ZADD, ZRANGE, ZRANK命令，底层使用ziplist, skiplist+hashtable组合结构实现</li>
</ol>
<p>intset 只用来实现set，不用来实现sorted set，原因是sorted set的entry需要有参数作为排序的权重, intset不能支持。</p>
<p>其中redis的hashtable就是dict</p>
<pre><code class="language-cpp">typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;
</code></pre>
<p>redis 的常用操作命令</p>
<pre><code># 设置key value, value 
SET key value

# value 是列表
LPUSH key value1 value2  # 左侧插入
RPUSH key value1 value2  # 右侧插入
LPOP key                 # 左侧弹出元素
RPOP key                 # 右侧弹出元素

# value是哈希表
HSET key field1 val1 field2 val2   # 批量设置&#x3C;field val>
HMGET key field1 field2  # 批量获取

# value是集合
SADD key member1 member2  # 添加成员
SMEMBERS key              # 获取所有成员

# value是有序集合
ZADD key score1 member1 score2 member2  # 添加带分值的成员
ZRANGE key start stop [WITHSCORES]      # 按分值升序获取成员
</code></pre>
<p>object核心的成员，type，encoding，ptr</p>
<pre><code class="language-cpp">typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
</code></pre>
<p>encoding有10种, robj可以看成不同实现数据结构的统一接口</p>
<pre><code class="language-cpp">/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */

robj *createQuicklistObject(void) {
    quicklist *l = quicklistCreate();
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

robj *createSetObject(void) {
    dict *d = dictCreate(&#x26;setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

robj *createIntsetObject(void) {
    intset *is = intsetNew();
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}
</code></pre>
<p>object 可以序列化到rdb，也可以被load</p>
<pre><code class="language-cpp">/* Load a Redis object of the specified type from the specified file.
 * On success a newly allocated object is returned, otherwise NULL. */
robj *rdbLoadObject(int rdbtype, rio *rdb) {
    robj *o = NULL, *ele, *dec;
    uint64_t len;
    unsigned int i;

    if (rdbtype == RDB_TYPE_STRING) {
        /* Read string value */
        if ((o = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
        o = tryObjectEncoding(o);
...
</code></pre>
<h4>quicklist</h4>
<p>quicklist 用来实现列表类型value。</p>
<p>quicklist 是一个由多个 ziplist（压缩列表）节点组成的双向链表。quicklist的节点就是 ziplist。</p>
<p>quicklist支持对 ziplist 节点进行 LZF 压缩, 从而节省内存。
（redis是内存数据库，节省内存的使用是它必须考虑的设计）</p>
<pre><code class="language-cpp">/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually &#x3C; 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    // ziplist
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

typedef struct quicklistIter {
    const quicklist *quicklist;
    quicklistNode *current;
    unsigned char *zi;
    long offset; /* offset in current ziplist */
    int direction;
} quicklistIter;

typedef struct quicklistEntry {
    const quicklist *quicklist;
    quicklistNode *node;
    unsigned char *zi;
    unsigned char *value;
    long long longval;
    unsigned int sz;
    int offset;
} quicklistEntry;
</code></pre>
<h4>ziplist</h4>
<p>quicklist的元素是ziplist, 除了quicklist, ziplist还作为元素数量少的 hashtable和sorted set的存储结构。</p>
<p>ziplist是一种紧凑的、连续内存存储的线性数据结构，</p>
<pre><code>+---------+--------+--------+--------+--------+---------+
| zlbytes | zltail | zllen  | entry1 | entry2 | ... | zlend |
+---------+--------+--------+--------+--------+---------+

zltail（4字节）：最后一个节点的偏移量（用于快速定位尾部）。

+------------------+----------------+----------------+
| prev_entry_len   | encoding       | content        |
+------------------+----------------+----------------+

prev_entry_len 前驱节点长度，用于反向遍历
encoding 编码类型
</code></pre>
<p>每个entry主要由三部分组成， prev_entry_len，encoding，content。content就是一段序列，char *p。</p>
<p>当ziplist存放hashtable时，content是dictEntry。当ziplist存放sorted set是，content就是sds。</p>
<pre><code class="language-cpp">
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previos entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;
</code></pre>
<h4>intset</h4>
<p>intset 整数集合用来实现元素数量较小的set，需要满足</p>
<ol>
<li>元素均为整数</li>
<li>元素个数小于阈值set-max-intset-entries 512</li>
</ol>
<p>intset 采用连续内存存储，内部元素有序排布，支持二分查找</p>
<p>intset 支持三种整数类型，按需动态升级：</p>
<ol>
<li>INTSET_ENC_INT16：每个元素占 2 字节（范围 -32,768 ~ 32,767）。</li>
<li>INTSET_ENC_INT32：每个元素占 4 字节（范围 -2^31 ~ 2^31-1）。</li>
<li>INTSET_ENC_INT64：每个元素占 8 字节（范围 -2^63 ~ 2^63-1）。</li>
</ol>
<pre><code class="language-cpp">// intset.h
typedef struct intset {
    uint32_t encoding;  // 编码方式（INTSET_ENC_INT16/32/64）
    uint32_t length;    // 元素数量
    int8_t contents[];  // 动态数组，按 encoding 存储整数
} intset;
</code></pre>
<p>intset add的都是整数</p>
<pre><code class="language-cpp">/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
</code></pre>
<h4>skiplist和dict结合</h4>
<p>跳跃表，和dict 合作实现有序集合</p>
<p>redis 通过哈希表，实现有序集合O(1)的点查询；通过跳跃表，实现集合的有序和范围查询。</p>
<p>哈希表存放zskiplistNode的地址即可，关键成员是sds ele和double score</p>
<pre><code class="language-cpp">/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[redis]]></category><category><![CDATA[storage]]></category>
        </item>
        <item>
          <title><![CDATA[leveldb(1)—概览]]></title>
          <link>https://larrystd.github.io/posts/leveldb(1)—概览</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/leveldb(1)—概览</guid>
          <pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[leveldb 可分为五大块。db, iterator，version, memtable+log, tablefile。]]></description>
          <content:encoded><![CDATA[<p>leveldb 可分为五大块。db, iterator，version, memtable+log, tablefile。</p>
<p>leveldb 的架构图资料上有很多，可以参考<a href="https://youjiali1995.github.io/storage/leveldb-architecture/">网站</a></p>
<h3>db</h3>
<p>主要接口Open，Put/Write，Delete，Get</p>
<h4>Open</h4>
<p><code>Status DB::Open(const Options&#x26; options, const std::string&#x26; dbname, DB** dbptr)</code>
Open 需要传入options，options记录了1. 限制参数，例如max_open_files，max_file_size，block_size 2. Env* env; env提供创建文件等接口</p>
<p>open的执行过程</p>
<ol>
<li>利用manifest recover；impl->Recover(&#x26;edit, &#x26;save_manifest);
<ol>
<li>包含两部分1. versions_->Recover(save_manifest);  2. <code>RecoverLogFile(logs[i], (i == logs.size() - 1), save_manifest, edit, &#x26;max_sequence);</code></li>
<li>在dbdir 里找到logfile，根据文件名的序号排序，将排序后的logfile按序执行RecoverLogFile</li>
</ol>
</li>
<li>version和logfile replay后将状态记录给versionEdit，versionEdit的内容主要包括log_number_、last_sequence_、next_file_number_和new_files_(tablefile)。</li>
<li>最后执行VersionSet::LogAndApply，将VersionEdit SaveTo Version，然后把version append到versionSet</li>
</ol>
<h4>Write</h4>
<p>Write的执行过程</p>
<ol>
<li><code>MakeRoomForWrite(updates == nullptr);</code> 如果mem空间不够，将mem设置成imm，新建mem，触发dump</li>
<li><code>WriteBatch* write_batch = BuildBatchGroup(&#x26;last_writer);</code> 将writer放入到WriteBatchInternal::Append</li>
<li><code>log_->AddRecord(WriteBatchInternal::Contents(write_batch));</code></li>
<li><code>WriteBatchInternal::InsertInto(write_batch, mem_);</code></li>
<li><code>DB::Write</code>可以多线程执行，使用生产者消费者作为线程切换。线程同时可以是生产者，也可以是消费者。线程申请锁，把write放入队列，然后检查writer队列，如果有writer则执行具体write</li>
</ol>
<h4>Get</h4>
<p>Get的执行过程</p>
<ol>
<li>如果没有传入snapshot，设置snapshot为snapshot = versions_->LastSequence();</li>
<li>LookupKey lkey(key, snapshot); lookup key包含了seq</li>
<li>mem->Get(lkey, value, &#x26;s)</li>
<li>imm->Get(lkey, value, &#x26;s)</li>
<li>current->Get(options, lkey, value, &#x26;stats) (即Version::Get)
<ol>
<li>对L0的文件按照filenumber从大到小排序，遍历检索(L0 tablefile内部key有序，文件之间key无序, 文件有number顺序)</li>
<li>对L1以下的文件，每层使用二分查找找到指定的tablefile</li>
<li>对tablefile内部，首先使用TableCache::Get 尝试在cache中查找。cache是tablefile共享的，key是filenumber。如果tablefile没有cache，则读取文件插入cache。注意到tablecache只是pagecache上面的一层，tablecache 会cache住tablefile的metaindex_handle、index_block等元数据信息</li>
<li>tablefile内的检索过程 1. rep_->index_block->NewIterator 迭代器seek index_block 2. Iterator* block_iter = BlockReader(this, options, iiter->value()); seek block</li>
</ol>
</li>
</ol>
<p>skiplist的元素顺序</p>
<ol>
<li>元素格式| key_size | key | seq,type | value_size | value</li>
<li>从key部分之后开始排序</li>
<li>对于key相同的，包括delete标签，按照seq从大到小排序</li>
<li>mem->Get 主要调用的是SkipList<code>&#x3C;Key, Comparator>::FindGreaterOrEqual</code>，默认get key的seq是最近的seq，FindGreaterOrEqual会直接得到第一个key（也就是seq最大的key）。设置快照后get key会得到&#x3C;=某seq的key。</li>
</ol>
<p>Delete 和Write实现一致，key会有个标签标志该key是delete。</p>
<p>db检索元素使用的是迭代器的seek，例如
<code>inline void SkipList&#x3C;Key, Comparator>::Iterator::Seek(const Key&#x26; target)</code></p>
<h3>迭代器</h3>
<p>迭代器基本接口</p>
<ol>
<li>检索void Seek(const Key&#x26; target);</li>
<li>获得前后元素void Next(); void Prev();</li>
<li>获得指向的节点</li>
<li>插入元素void Insert(const Key&#x26; key);</li>
</ol>
<p>主要迭代器
SkipList::Iterator 用于操作mem和Imm table</p>
<p>TwoLevelIterator 用于检索tablefile，一次性需要把一个tablefile完全加载到内存</p>
<p>Block::Iter，操作tablefile的一个datablock，用于检索。1. 检索到RestartPoint 2. 从RestartPoint进一步检索得到key</p>
<p>MergingIterator 用于compaction。管理一层的若干tablefile，用来合并成为新的tablefile。采用多路败者树进行多个有序文件合并</p>
<p>只有skiplist需要插入修改，tablefile的修改只有append写。</p>
<h3>version</h3>
<p>versionEdit对应manifest的一条记录</p>
<p>version只记录了tablefile的分布，不记录redolog和skiplist部分。redolog和skiplist 使用seq记录版本。
seq+version构成了快照，一个快照在tablefile中对应一个版本，被快照引用的tablefile不会被回收（即使被compaction了）</p>
<p>恢复版本就是选择版本包含的tablefile进行管理；恢复快照还需要把redolog 全量恢复，其中log_number用来恢复memtable，prev_log_number用来恢复immutable。切换logfile时会向manifest写入新的log_number。</p>
<h3>memtable+log</h3>
<p>memtable就是skiplist，节点格式</p>
<p>| key_size | key | seq,type | value_size | value</p>
<p>log的记录是writebatch序列化的结果，对于write操作，格式</p>
<p>| kTypeValue, seq | key_size | key | value_size | value |  ...</p>
<p>对于delete操作，格式</p>
<p>| kTypeDeletion，seq |  key_size | key |</p>
<h3>tablefile</h3>
<p>tablefile使用TableBuilder构建</p>
<p>tablefile没有header
格式</p>
<ol>
<li>data block</li>
<li>Write filter block</li>
<li>Write metaindex block</li>
<li>Write index block</li>
<li>Write footer</li>
</ol>
<p>indexblock 每个元素是datablock的最后一个key</p>
<p>metaindex block记录除datablock, indexblock之外的block的位置，例如filter block（布隆过滤器）</p>
<p>footer
| metaindex_handle_ | index_handle_ | kTableMagicNumber |</p>
<p>metaindex_handle_，index_handle_ 记录了metaindex block和index block的位置</p>
<pre><code class="language-cpp">void Footer::EncodeTo(std::string* dst) const {
  const size_t original_size = dst->size();
  metaindex_handle_.EncodeTo(dst);
  index_handle_.EncodeTo(dst);
  dst->resize(2 * BlockHandle::kMaxEncodedLength);  // Padding
  PutFixed32(dst, static_cast&#x3C;uint32_t>(kTableMagicNumber &#x26; 0xffffffffu));
  PutFixed32(dst, static_cast&#x3C;uint32_t>(kTableMagicNumber >> 32));
  assert(dst->size() == original_size + kEncodedLength);
  (void)original_size;  // Disable unused variable warning.
}
</code></pre>
<h3>借助单元测试分析源码</h3>
<p>unittest是分析代码的有效入口，以db_test为例</p>
<h4>case1 Empty</h4>
<p>DBTest, Empty，尝试以不同option配置打开db
可选择的配置，<code>{ kDefault, kReuse, kFilter, kUncompressed, kEnd }</code></p>
<p>执行</p>
<pre><code class="language-cpp">TEST_F(DBTest, Empty) {
  do {
    ASSERT_TRUE(db_ != nullptr);
    ASSERT_EQ("NOT_FOUND", Get("foo"));
  } while (ChangeOptions());
}

  // Switch to a fresh database with the next option configuration to
  // test.  Return false if there are no more configurations to test.
  bool ChangeOptions() {
    option_config_++;
    if (option_config_ >= kEnd) {
      return false;
    } else {
      DestroyAndReopen();
      return true;
    }
  }

  enum OptionConfig { kDefault, kReuse, kFilter, kUncompressed, kEnd };

  void DestroyAndReopen(Options* options = nullptr) {
    delete db_;
    db_ = nullptr;
    DestroyDB(dbname_, Options());
    ASSERT_LEVELDB_OK(TryReopen(options));
  }

// Status TryReopen(Options* options) 需要传入Option
  Status TryReopen(Options* options) {
    delete db_;
    db_ = nullptr;
    Options opts;
    if (options != nullptr) {
      opts = *options;
    } else {
      opts = CurrentOptions();
      opts.create_if_missing = true;
    }
    last_options_ = opts;

    return DB::Open(opts, dbname_, &#x26;db_);
  }
</code></pre>
<p>cmake debug模式编译
cmake -DCMAKE_BUILD_TYPE=Debug ..</p>
<h4>case02，03 EmptyKey, EmptyValue</h4>
<p>不同option打开db，put key同时可以get到</p>
<pre><code class="language-cpp">TEST_F(DBTest, EmptyKey) {
  do {
    ASSERT_LEVELDB_OK(Put("", "v1"));
    ASSERT_EQ("v1", Get(""));
    ASSERT_LEVELDB_OK(Put("", "v2"));
    ASSERT_EQ("v2", Get(""));
  } while (ChangeOptions());
}

TEST_F(DBTest, EmptyValue) {
  do {
    ASSERT_LEVELDB_OK(Put("key", "v1"));
    ASSERT_EQ("v1", Get("key"));
    ASSERT_LEVELDB_OK(Put("key", ""));
    ASSERT_EQ("", Get("key"));
    ASSERT_LEVELDB_OK(Put("key", "v2"));
    ASSERT_EQ("v2", Get("key"));
  } while (ChangeOptions());
}
</code></pre>
<h4>case04 ReadWrite</h4>
<p>读写</p>
<pre><code class="language-cpp">TEST_F(DBTest, ReadWrite) {
  do {
    ASSERT_LEVELDB_OK(Put("foo", "v1"));
    ASSERT_EQ("v1", Get("foo"));
    ASSERT_LEVELDB_OK(Put("bar", "v2"));
    ASSERT_LEVELDB_OK(Put("foo", "v3"));
    ASSERT_EQ("v3", Get("foo"));
    ASSERT_EQ("v2", Get("bar"));
  } while (ChangeOptions());
}
</code></pre>
<h4>case05 PutDeleteGet</h4>
<pre><code class="language-cpp">TEST_F(DBTest, PutDeleteGet) {
  do {
    ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v1"));
    ASSERT_EQ("v1", Get("foo"));
    ASSERT_LEVELDB_OK(db_->Put(WriteOptions(), "foo", "v2"));
    ASSERT_EQ("v2", Get("foo"));
    ASSERT_LEVELDB_OK(db_->Delete(WriteOptions(), "foo"));
    ASSERT_EQ("NOT_FOUND", Get("foo"));
  } while (ChangeOptions());
}

#define EXPECT_LEVELDB_OK(expression) \
  EXPECT_THAT(expression, leveldb::test::IsOK())
#define ASSERT_LEVELDB_OK(expression) \
  ASSERT_THAT(expression, leveldb::test::IsOK())
</code></pre>
<h4>case06 GetFromImmutableLayer</h4>
<p>核心是三行</p>
<p>env_->delay_data_sync_.store(true, std::memory_order_release);
Put("k1", std::string(100000, 'x'));  // Fill memtable.
Put("k2", std::string(100000, 'y'));  // Trigger compaction.</p>
<pre><code class="language-cpp">TEST_F(DBTest, GetFromImmutableLayer) {
  do {
    Options options = CurrentOptions();
    options.env = env_;
    options.write_buffer_size = 100000;  // Small write buffer
    Reopen(&#x26;options);

    ASSERT_LEVELDB_OK(Put("foo", "v1"));
    ASSERT_EQ("v1", Get("foo"));

    // Block sync calls.
    env_->delay_data_sync_.store(true, std::memory_order_release);
    Put("k1", std::string(100000, 'x'));  // Fill memtable.
    Put("k2", std::string(100000, 'y'));  // Trigger compaction.
    ASSERT_EQ("v1", Get("foo"));
    // Release sync calls.
    env_->delay_data_sync_.store(false, std::memory_order_release);
  } while (ChangeOptions());
}
</code></pre>
<p>put key2时，1. 将mem变成imm 2. 后台触发compaction</p>
<p>如果delay_data_sync_ 设置true，后台compaction线程阻塞在L0->L1的DBImpl::WriteLevel0Table。imm_尚未被删除，foo还是会在imm_中读取</p>
<p>write_buffer_size 作用
DBImpl::MakeRoomForWrite 发现mem_->ApproximateMemoryUsage() > options_.write_buffer_size)，认为没有空间，然后执行</p>
<ol>
<li>s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &#x26;lfile);</li>
<li>设置Imm,imm_ = mem_;
has_imm_.store(true, std::memory_order_release);
mem_ = new MemTable(internal_comparator_);</li>
<li>MaybeScheduleCompaction();</li>
</ol>
<p>delay_data_sync_ 的作用，从取名上作用是延迟sync。其实就是datafile的sync()函数一直阻塞。</p>
<p>什么时候执行Sync()</p>
<ol>
<li>recover时，如果db不存在，执行DBImpl::NewDB()。执行manifest file->Sync();</li>
<li> DBImpl::FinishCompactionOutputFile 执行compact->outfile->Sync();
<ol>
<li>FinishCompactionOutputFile函数在minor compaction和major compaction执行完之后都会执行</li>
</ol>
</li>
<li>DBImpl::Write 执行logfile_->Sync();</li>
<li>DBImpl::WriteLevel0Table 执行BuildTable，最后执行s = file->Sync();</li>
</ol>
<p>如果delay_data_sync_ 设置true，会卡在L0->L1的DBImpl::WriteLevel0Table。imm_尚未被删除，数据还是会在imm_中读取</p>
<p>MaybeScheduleCompaction 函数逻辑</p>
<pre><code class="language-cpp">void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  if (background_compaction_scheduled_) {
    // Already scheduled
  } else if (shutting_down_.load(std::memory_order_acquire)) {
    // DB is being deleted; no more background compactions
  } else if (!bg_error_.ok()) {
    // Already got an error; no more changes
  } else if (imm_ == nullptr &#x26;&#x26; manual_compaction_ == nullptr &#x26;&#x26;
             !versions_->NeedsCompaction()) {
    // No work to be done
  } else {
    background_compaction_scheduled_ = true;
    env_->Schedule(&#x26;DBImpl::BGWork, this);
  }
}
</code></pre>
<p>PosixEnv::Schedule
作为生产者</p>
<ol>
<li>如果background_work_queue_为空，唤醒background_work_cv_（消费线程会重新申请锁</li>
<li>将任务加入到background_work_queue_，线程保护</li>
</ol>
<pre><code class="language-cpp">void PosixEnv::Schedule(
    void (*background_work_function)(void* background_work_arg),
    void* background_work_arg) {
  background_work_mutex_.Lock();

  // Start the background thread, if we haven't done so already.
  if (!started_background_thread_) {
    started_background_thread_ = true;
    std::thread background_thread(PosixEnv::BackgroundThreadEntryPoint, this);
    background_thread.detach();
  }

  // If the queue is empty, the background thread may be waiting for work.
  if (background_work_queue_.empty()) {
    background_work_cv_.Signal();
  }

  background_work_queue_.emplace(background_work_function, background_work_arg);
  background_work_mutex_.Unlock();
}

  port::Mutex background_work_mutex_;
  port::CondVar background_work_cv_ GUARDED_BY(background_work_mutex_);
  bool started_background_thread_ GUARDED_BY(background_work_mutex_);
  std::queue&#x3C;BackgroundWorkItem> background_work_queue_
      GUARDED_BY(background_work_mutex_);
</code></pre>
<p>GUARDED_BY 是一种在多线程编程中用于静态代码分析的注解（或宏），其核心作用是声明某个共享变量必须通过特定的锁（或其他同步机制）来保护，以避免数据竞争和并发安全问题。</p>
<p>BackgroundWorkItem</p>
<pre><code class="language-cpp">  // Stores the work item data in a Schedule() call.
  //
  // Instances are constructed on the thread calling Schedule() and used on the
  // background thread.
  //
  // This structure is thread-safe beacuse it is immutable.
  struct BackgroundWorkItem {
    explicit BackgroundWorkItem(void (*function)(void* arg), void* arg)
        : function(function), arg(arg) {}

    void (*const function)(void*);
    void* const arg;
  };

void PosixEnv::BackgroundThreadMain() {
  while (true) {
    background_work_mutex_.Lock();

    // Wait until there is work to be done.
    while (background_work_queue_.empty()) {
      background_work_cv_.Wait();
    }

    assert(!background_work_queue_.empty());
    auto background_work_function = background_work_queue_.front().function;
    void* background_work_arg = background_work_queue_.front().arg;
    background_work_queue_.pop();

    background_work_mutex_.Unlock();
    background_work_function(background_work_arg);
  }
}
</code></pre>
<p>启动，可以看到只有一个消费者线程</p>
<pre><code class="language-cpp">void PosixEnv::Schedule(
    void (*background_work_function)(void* background_work_arg),
    void* background_work_arg) {
  background_work_mutex_.Lock();

  // Start the background thread, if we haven't done so already.
  if (!started_background_thread_) {
    started_background_thread_ = true;
    std::thread background_thread(PosixEnv::BackgroundThreadEntryPoint, this);
    background_thread.detach();
  }
</code></pre>
<h4>case 08，GetFromVersions</h4>
<pre><code class="language-cpp">TEST_F(DBTest, GetFromVersions) {
  do {
    ASSERT_LEVELDB_OK(Put("foo", "v1"));
    dbfull()->TEST_CompactMemTable();
    ASSERT_EQ("v1", Get("foo"));
  } while (ChangeOptions());  // 会执行DestroyAndReopen();
}
</code></pre>
<p>dbfull()</p>
<pre><code class="language-cpp">  DBImpl* dbfull() { return reinterpret_cast&#x3C;DBImpl*>(db_); }
</code></pre>
<p>DBImpl::TEST_CompactMemTable()</p>
<pre><code class="language-cpp">Status DBImpl::TEST_CompactMemTable() {
  // nullptr batch means just wait for earlier writes to be done
  Status s = Write(WriteOptions(), nullptr);
  if (s.ok()) {
    // Wait until the compaction completes
    MutexLock l(&#x26;mutex_);
    while (imm_ != nullptr &#x26;&#x26; bg_error_.ok()) {
      background_work_finished_signal_.Wait();  // 主动释放锁并进入阻塞状态
    // CompactMemTable(); 执行完会调用
    // background_work_finished_signal_.SignalAll(); 唤醒
    }
    if (imm_ != nullptr) {
      s = bg_error_;
    }
  }
  return s;
}
</code></pre>
<h4>GetSnapshot</h4>
<p>db_->GetSnapshot();</p>
<pre><code class="language-cpp">TEST_F(DBTest, GetSnapshot) {
  do {
    // Try with both a short key and a long key
    for (int i = 0; i &#x3C; 2; i++) {
      std::string key = (i == 0) ? std::string("foo") : std::string(200, 'x');
      ASSERT_LEVELDB_OK(Put(key, "v1"));
      const Snapshot* s1 = db_->GetSnapshot();
      ASSERT_LEVELDB_OK(Put(key, "v2"));
      ASSERT_EQ("v2", Get(key));
      ASSERT_EQ("v1", Get(key, s1));
      dbfull()->TEST_CompactMemTable();
      ASSERT_EQ("v2", Get(key));
      ASSERT_EQ("v1", Get(key, s1));
      db_->ReleaseSnapshot(s1);
    }
  } while (ChangeOptions());
}

const Snapshot* DBImpl::GetSnapshot() {
  MutexLock l(&#x26;mutex_);
  return snapshots_.New(versions_->LastSequence());
}

  // Creates a SnapshotImpl and appends it to the end of the list.
  SnapshotImpl* New(SequenceNumber sequence_number) {
    assert(empty() || newest()->sequence_number_ &#x3C;= sequence_number);

    SnapshotImpl* snapshot = new SnapshotImpl(sequence_number);

    snapshot->next_ = &#x26;head_;
    snapshot->prev_ = head_.prev_;
    snapshot->prev_->next_ = snapshot;
    snapshot->next_->prev_ = snapshot;
    return snapshot;
  }
</code></pre>
<h4>DeletionMarkers1</h4>
<pre><code class="language-cpp">TEST_F(DBTest, DeletionMarkers1) {
  Put("foo", "v1");
  ASSERT_LEVELDB_OK(dbfull()->TEST_CompactMemTable());
  const int last = config::kMaxMemCompactLevel;
  ASSERT_EQ(NumTableFilesAtLevel(last), 1);  // foo => v1 is now in last level

  // Place a table at level last-1 to prevent merging with preceding mutation
  Put("a", "begin");
  Put("z", "end");
  dbfull()->TEST_CompactMemTable();
  ASSERT_EQ(NumTableFilesAtLevel(last), 1);
  ASSERT_EQ(NumTableFilesAtLevel(last - 1), 1);

  Delete("foo");
  Put("foo", "v2");
  ASSERT_EQ(AllEntriesFor("foo"), "[ v2, DEL, v1 ]");
  ASSERT_LEVELDB_OK(dbfull()->TEST_CompactMemTable());  // Moves to level last-2
  ASSERT_EQ(AllEntriesFor("foo"), "[ v2, DEL, v1 ]");
  Slice z("z");
  dbfull()->TEST_CompactRange(last - 2, nullptr, &#x26;z);
  // DEL eliminated, but v1 remains because we aren't compacting that level
  // (DEL can be eliminated because v2 hides v1).
  ASSERT_EQ(AllEntriesFor("foo"), "[ v2, v1 ]");
  dbfull()->TEST_CompactRange(last - 1, nullptr, nullptr);
  // Merging last-1 w/ last, so we are the base level for "foo", so
  // DEL is removed.  (as is v1).
  ASSERT_EQ(AllEntriesFor("foo"), "[ v2 ]");
}
</code></pre>
<p>Delete的实现</p>
<pre><code class="language-cpp">Status DB::Delete(const WriteOptions&#x26; opt, const Slice&#x26; key) {
  WriteBatch batch;
  batch.Delete(key);
  return Write(opt, &#x26;batch);
}

void WriteBatch::Delete(const Slice&#x26; key) {
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  rep_.push_back(static_cast&#x3C;char>(kTypeDeletion));
  PutLengthPrefixedSlice(&#x26;rep_, key);
}
</code></pre>
<p>AllEntriesFor</p>
<pre><code class="language-cpp">  std::string AllEntriesFor(const Slice&#x26; user_key) {
    Iterator* iter = dbfull()->TEST_NewInternalIterator();
    InternalKey target(user_key, kMaxSequenceNumber, kTypeValue);
    // 
    iter->Seek(target.Encode());
    std::string result;
    if (!iter->status().ok()) {
      result = iter->status().ToString();
    } else {
      result = "[ ";
      bool first = true;
      while (iter->Valid()) {
        ParsedInternalKey ikey;
        if (!ParseInternalKey(iter->key(), &#x26;ikey)) {
          result += "CORRUPTED";
        } else {
          if (last_options_.comparator->Compare(ikey.user_key, user_key) != 0) {
            break;
          }
          if (!first) {
            result += ", ";
          }
          first = false;
          switch (ikey.type) {
            case kTypeValue:
              result += iter->value().ToString();
              break;
            case kTypeDeletion:
              result += "DEL";
              break;
          }
        }
        iter->Next();
      }
      if (!first) {
        result += " ";
      }
      result += "]";
    }
    delete iter;
    return result;
  }
</code></pre>
<p>Iterator* iter = dbfull()->TEST_NewInternalIterator();</p>
<p>InternalKey target(user_key, kMaxSequenceNumber, kTypeValue);
//
iter->Seek(target.Encode());</p>
<p>这里的target 封装了seq和type</p>
<p>Iterator::Seek</p>
<p>SSTable 文件由数据块和索引块组成。索引块存储了数据块的元信息，比如每个数据块的起始键和位置。当需要查找某个键时，首先通过索引块找到对应的数据块，然后在数据块内进行查找。TwoLevelIterator负责这种两级查找过程</p>
<pre><code class="language-cpp">template &#x3C;typename Key, class Comparator>
inline void SkipList&#x3C;Key, Comparator>::Iterator::Seek(const Key&#x26; target) {
  node_ = list_->FindGreaterOrEqual(target, nullptr);
}
// 负责sst file的遍历
void TwoLevelIterator::Seek(const Slice&#x26; target) {
  // seek索引块
  index_iter_.Seek(target);
  InitDataBlock();
  if (data_iter_.iter() != nullptr) data_iter_.Seek(target);
  SkipEmptyDataBlocksForward();
}

class MergingIterator : public Iterator {
  void Seek(const Slice&#x26; target) override {
	  // 每个children是一个SkipList::Iterator或者TwoLevelIterator
    for (int i = 0; i &#x3C; n_; i++) {
      children_[i].Seek(target);
    }
    FindSmallest();
    direction_ = kForward;
  }

  void Next() override {
    assert(Valid());

    // Ensure that all children are positioned after key().
    // If we are moving in the forward direction, it is already
    // true for all of the non-current_ children since current_ is
    // the smallest child and key() == current_->key().  Otherwise,
    // we explicitly position the non-current_ children.
    if (direction_ != kForward) {
      for (int i = 0; i &#x3C; n_; i++) {
        IteratorWrapper* child = &#x26;children_[i];
        if (child != current_) {
          child->Seek(key());
          if (child->Valid() &#x26;&#x26;
              comparator_->Compare(key(), child->key()) == 0) {
            child->Next();
          }
        }
      }
      direction_ = kForward;
    }

    current_->Next();
    FindSmallest();

 private:
  // Which direction is the iterator moving?
  enum Direction { kForward, kReverse };

  void FindSmallest();
  void FindLargest();

  // We might want to use a heap in case there are lots of children.
  // For now we use a simple array since we expect a very small number
  // of children in leveldb.
  const Comparator* comparator_;
  IteratorWrapper* children_;
  int n_;
  IteratorWrapper* current_;
  Direction direction_;
};

class InternalKey {
 private:
  std::string rep_;

  InternalKey() {}  // Leave rep_ as empty to indicate it is invalid
  InternalKey(const Slice&#x26; user_key, SequenceNumber s, ValueType t) {
    AppendInternalKey(&#x26;rep_, ParsedInternalKey(user_key, s, t));
  }
</code></pre>
<h3>TODO</h3>
<p>迭代器具体使用</p>
<p>详细线程模型和并发控制</p>
<p>模块依赖和编译依赖</p>
<p>性能分析</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[storage]]></category><category><![CDATA[leveldb]]></category>
        </item>
        <item>
          <title><![CDATA[存储——分布式存储底座]]></title>
          <link>https://larrystd.github.io/posts/存储—分布式存储底座</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/存储—分布式存储底座</guid>
          <pubDate>Tue, 07 Jan 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[分布式存储底座包括分布式文件系统和分布式一致性KV存储。对外满足的条件 1. 提供数据可靠性，底座之上不需要担心数据损坏 2. 提供数据可用性，节点崩溃不影响数据读写 3. 集群级接口，多机节点可访问]]></description>
          <content:encoded><![CDATA[<p>分布式存储底座包括分布式文件系统和分布式一致性KV存储。对外满足的条件 1. 提供数据可靠性，底座之上不需要担心数据损坏 2. 提供数据可用性，节点崩溃不影响数据读写 3. 集群级接口，多机节点可访问</p>
<h3>HDFS 分布式文件系统</h3>
<p>Hadoop Distributed File System 分布式文件系统</p>
<p>由中心化节点namenode和多个datanode节点组成。namenode 维护文件目录结构，负责文件创建、打开、关闭、重命名等操作，不支持软链接/硬链接。namenode维护了文件信息，datanode只有块信息不感知文件，块大小默认是128MB。</p>
<p>对于文件系统的修改，例如创建打开文件，namenode会持久化到Editlog。为加快重启后恢复，Editlog使用checkpoint，同时secondNamenode会尽量接近namenode，以便namenode崩溃重新选主后恢复。</p>
<p>namenode借助zookeeper保证可用性，zookeeper同时记录服务信息，例如正在运行的namenode和可用的datanode地址。</p>
<p>client 使用FileStream抽象访问文件，FileStream自动和namenode/datanode交互。例如执行读操作时，FileStream自动从namenode获得文件包含的块和datanode信息，然后访问指定的datanode读对应的块。执行写操作时，FileStream从namenode拿到可写的块，然后向指定的datanode发写请求。如果使用三副本容错机制，写操作需要同时拿到三个位于不同datanode的可写的块，然后写三份等待返回。</p>
<p>三副本写有以下写策略</p>
<ol>
<li>客户端向三个datanode发三个写请求，等待三个写成功返回</li>
<li>客户端向三个datanode发三个写请求，两个返回就可认为成功，最后一个请求可以后台慢慢追赶写。也就是二三异步写。这种延迟最低。</li>
<li>客户端链式发请求，即先写第一个副本，成功后写第二个，再成功后写第三个</li>
</ol>
<p>为了保证数据完整性，数据在磁盘中存放和io链路都要进行crc校验。</p>
<h3>hdfs的缺陷</h3>
<p>hdfs的主要问题是元数据不可扩展</p>
<ol>
<li>单点namenode，元数据无法横向扩展。读需要找namenode拿数据块的layout，写需要找namenode拿可写的数据库，单namenode无法支持高iops的读写和元数据</li>
<li>由于分布式系统存在飞着的请求，加上重试，可能导致数据安全风险。例如某客户端写完两副本，第三个副本挂了；该客户端重新申请个新的第三个副本，此时挂了的副本好了，飞着的请求被处理，导致写了两次第三个副本。</li>
</ol>
<p>问题1, 可以使用通过子树划分的分布式元数据解决，分布式文件系统一般不实现link文件，对于rename操作，分布式元数据情况下需要保证多节点原子性。可以将rename操作统一打到一个节点，该节点单线程执行任务实现原子性，rename操作一般延迟较其他元数据操作差。</p>
<p>问题2, 可以使用seal 机制处理，seal后的chunk不可写。当chunk数据写完需要切chunk，或某chunk写失败需要换chunk写，原来的chunk会seal掉，seal的chunk只读不写，可以保证问题2 飞着的请求在seal 的chunk无法写操作。</p>
<p><a href="https://www.cs.purdue.edu/homes/csjgwang/CloudNativeDB/AzureStorageSOSP11.pdf">Windows Azure Storage</a> 这篇论文介绍的是更现代的分布式文件系统，主要有以下特点</p>
<ol>
<li>服务分成Front-End (FE)、Partition、Stream三个部分。Front-End服务协议层接受请求，转发到指定的Partiton；Partiton存储逻辑数据；Stream真正存储数据，分成extent和block两层，extent大小1GB，block大小4M</li>
<li><strong>partition层提供的是支持事务的数据库表/对象/文件存储</strong>。partition由若干object组成，object是固定大小存储单位，partition内部object的操作是原子的，提供事务操作（即多个object操作提供原子性）。partition记录用户文件和stream存储位置的映射（Blob表）、文件属性表和目录表(entity表)，并且保证相同partition内的表支持事务操作。（分布式文件系统元数据主要有两种，一种维护layout布局信息，另一种维护文件目录信息，这两种元数据可以隔离防止干扰？）</li>
<li>partition层有一个partition manager(master)，维护partition和节点ip信息，master由Paxos Lock保证一致性（分布式文件系统中处理元数据，还应该有一个元元数据角色，保存系统自己的配置和元信息，元元数据实现paxos协议保证可靠性）</li>
<li>partition层的数据表是KV存储，类似LSM tree结构。partition按照key的range划分（相比一致性hash分片，Range Partitions更简单，能充分利用局部性）支持分裂。（需要考虑到分裂后的两个partition之间不提供事务和原子性保证了）</li>
<li><strong>stream层才是分布式文件系统层</strong>，它提供文件语义操作。stream层分为extent(1GB)和block(4MB)两层。stream只支持append-only写，且extent支持seal。seal后的extent只读。stream层有实现paxos协议的Stream Manager(master) 维护了文件系统namespace，block等布局信息（stream master应该能支持按子树划分，实现分布式元数据）</li>
</ol>
<p>append-only系统</p>
<p>Append-only System – Having an append-only system and sealing an extent upon failure have greatly simplified the replication protocol and handling of failure scenarios. <strong>In this model, the data is never overwritten once committed to a replica</strong>, and, upon failures, the extent is immediately sealed. This model allows the consistency to be enforced across all the replicas via their commit lengths</p>
<p>An append-based system comes with certain costs. <strong>An efficient and scalable garbage collection (GC) system is crucial to keep the
space overhead low</strong>, and GC comes at a cost of extra I/O. In addition, the data layout on disk may not be the same as the virtual address space of the data abstraction stored, which led us to implement prefetching logic for streaming large data sets back
to the client.</p>
<p>append-only系统清理碎片：新建文件，将旧文件的非垃圾数据写到新文件，删除旧文件（旧文件除了最后一个chunk, 前面的chunk是seal的，GC时不用担心新增数据）</p>
<h3>zookeeper 和zab 一致性KV协议</h3>
<p>zookeeper依赖ZAB（Zookeeper Atomic Broadcast）协议实现分布式数据一致性。<strong>zab协议通过两阶段提交和多数通过两个原则保证即使leader挂了，系统仍可具备数据可靠性和强一致性</strong>。zab选择了强一致性，牺牲了可用性，即leader选举过程中，zookeeper不可用。zab协议下，集群的节点有两种角色：leader和follower。</p>
<p>两阶段提交，leader将写请求广播到follower，写请求会携带一个自增的zxid，follower 接收到写请求，将写请求写到本地事务日志，成功后再向Leader 回一个ACK确认。leader收到多数确认后，认为事务已经提交，通知follower执行消息。zxid记录了事务日志的最新更新，leader的zxid肯定最大，同时任意时刻系统超过多数follower的zxid和leader一致。</p>
<p>当leader崩溃时,zxid可以保证选举上来的follower的zxid和崩溃的leader一致，也就是本地事务日志是最新的。如果选举上来的leader发现自己本地的zxid没有执行，有两种可能1. zxid是最新的，leader尚未来得及通知执行就崩溃了，这时候应该执行；2. zxid是未提交的，需要重新进行两阶段提交。不管怎样，<strong>新leader上来对自身的zxid广播一遍，如果具有最新zxid的节点过半数，则通知他们执行；如果未过半数，则重新广播一次提交。</strong></p>
<p><strong>两阶段提交是单点串行的</strong>，也就是一个写操作执行完之后下一个才能执行，这就导致zookeeper不能执行高iops的写操作。</p>
<p>读操作需要经由leader返回版本和leader一致的follower，再从该follower里读。</p>
<p>新leader选举成功后，epoch+1，同时广播给follower，follower更新自身的epochid和leader一致。</p>
<p>当集群启动，或leader崩溃时，zookeeper需要进行leader选主</p>
<ol>
<li>集群开始启动时，每个节点状态都是follower。follower自身会维护leader信息，集群中节点的数量，定期检查leader的存在。<strong>如果leader不存在，follower 将状态改为LOOKING</strong>，准备选举</li>
<li>节点携带(myid, zxid，epochid)向集群其他节点发选主请求（包括自己），只要同意的节点超过集群节点数量的1/2，当前节点就会认为自己是leader。myid 是每个节点初始化配置的id，zxid是leader执行一次事务就会递增的id。当节点收到选主请求时，会先比较epochid，相等再比较zxid, zxid相等则比较myid，请求携带的id如果比自身大，就返回投票响应，否则拒绝投票。集群开始启动时，所有节点的zxid都为0，最终myid大的会被投票成leader</li>
<li>如果节点处于looking状态，收到投票请求，且发现对方id比自己大，节点会选择投票给对方id，并广播出去</li>
<li>如果一个节点收到选择自己作为leader的数量大于一半节点，会将自己作为leader，同时广播给集群其他节点</li>
<li>epochid和zxid小的不可能成为leader，借助两阶段提交，保证成为leader的节点必然具有所有的提交</li>
</ol>
<p>如果某个节点收到请求A，发现比自己id大投票给他，后来又收到请求B，发现比自己id大又投票给了B。会不会导致A，B同时被选为leader？</p>
<ol>
<li>什么时候出现？ 假设集群节点A、B、C、D，且四个节点zxid一致；leader挂了，节点C先发现，它向节点A、B、D发投票请求，A、B同意，D虽然反对，但不影响，C成了leader。后面节点D也发现leader挂了，于是向A、B、C发信息，A、B投票给了D，此时C已经成了leader，所以集群出现了两个leader？No，在C自认为成为leader后，需要广播到follower，follower会检查合法性，只有合法性过半数才能正式成为leader。所以如果A，B后来投票给了D，那么C的合法性检查会不通过；<strong>如果C合法性检查通过了，D再给A、B发选举请求，A、B将不予处理，C依然成为leader</strong>，C成为leader后将epoch更新通知D，D也会接受C成为leader。</li>
<li>因此只有当A，B节点检查到leader挂了，它才会进入looking状态，<strong>如果A、B检测到leader存活，其他节点对A、B发送请求选择，A、B将不予理会。</strong></li>
</ol>
<h3>raft 一致性KV协议</h3>
<p>raft 集群节点的角色有三种，leader, follower和candidate。其中candidate用于选举。</p>
<p>raft leader的写提交同样是zab的两阶段提交。因为<strong>两阶段提交能保证，leader挂了选上来的follower具有全部的已经提交的记录</strong> 。缺点是两阶段提交必须串行，不能高并发写</p>
<ol>
<li>leader issue AppendEntries RPC in parallel；leader wait for majority response</li>
<li>leader notify follower apply log</li>
</ol>
<p>raft 选举leader的协议和zab有所不同。当follower一定时间没有收到leader的心跳时，会进入选举状态</p>
<ol>
<li>raft节点维护一个term信息，当自己身份变为candidate时，会将term +1。</li>
<li>向所有节点发起 RequestVoteRPC 请求，请求包含（term，最后一条日志的任期号，最后一条日志的索引号
<ol>
<li>节点收到RequestVoteRPC请求时，先比较term， 再比较最后一条日志任期号，最后比较最后一条日志索引号。这些是保证投票给的节点具有所有已提交的日志）</li>
</ol>
</li>
<li>等待rpc期间，如果收到其他节点声明自己是 Leader的请求
<ol>
<li>该 Leader 的 term 号大于等于自己的 term 号，说明对方已经成为 Leader，则自己回退为 Follower。</li>
<li>该 Leader 的 term 号小于自己的 term 号，那么会拒绝该请求并让该节点更新 term。</li>
</ol>
</li>
</ol>
<p>raft 选举基于《只要选择出的节点具有所有已经提交的日志，选谁都行》的原则，没有zab的myid这项规则，比zab选举要快。zab等待rpc期间收到其他节点声明自己是leader的请求，由于Myid的原因可能也会否决。</p>
<p>raft 两阶段提交写操作时，如果某节点落后太多，会强制将该节点日志和leader日志拉齐；如果某节点有很多日志但是没有提交，这些没有被提交的日志会被需要提交的日志覆盖。</p>
<p><strong>如果不同节点日志中的两个条目有着相同的索引和任期号，则它们之间的所有条目都是完全一样的。</strong></p>
<h3>Paxos和multi Paxos 一致性协议</h3>
<h4>Basic Paxos</h4>
<p>paxos的角色：Proposer提案者，Acceptor 投票者，Learner学习者</p>
<p>准备阶段Prepare</p>
<ol>
<li>提案者向集群中的投票者发起提案编号为n的请求</li>
<li>投票者检查，如果发现自己之前的提案都小于n，则接受请求并承诺不会接受编号小于n的提案；否则拒绝提案</li>
</ol>
<p>如果准备阶段提案者接受到了半数以上通过，则继续批准阶段Accept</p>
<ol>
<li>提案者携带提案号n和提案值发给投票者</li>
<li>投票者确认提案编号不小于已承诺的最大编号，如果确认返回同意，否则拒绝
如果结果超半数同意，广播给集群所有的Proposer，Acceptor，Learner，Learner负责记录最终结果</li>
</ol>
<p>提议者可以同时也是决策者，提议者可以多个</p>
<p>basic paxos达成共识至少需要prepare和accept两次网络往返，高并发情况下可能导致活锁(多个提议者同时发起提案且一直重试，反复更新决策者上的提案编号，且任何一方都无法达到多数派决议来通过准备阶段)。因此，Paxos 算法主要用于理论研究，较少直接应用于工程实践。</p>
<h4>multi Paxos</h4>
<p>Multi-Paxos 通过选举出一个 Proposer 主节点，只有主节点可以发提案，避免多个Proposer同时发起提案反复影响。</p>
<p>集群中的若干Proposer 定期监测Proposer 主节点是否存在。当监测不存在时，向 Acceptors 发出选主 Proposer 申请。如果选主申请得到大多数Acceptors节点同意，该 Proposer 成为主节点。</p>
<p>TODO</p>
<h3>Tikv 和分布式KV存储</h3>
<p>zab, raft, multi paxos这些协议提供服务自身的可用性保证。hdfs的datanode如果挂了，可以通过namenode找到数据块另外的可用节点，namenode挂了，可以通过zookeeper重新选主，但是zookeeper的leader挂了，这个只能zookeeper利用协议自己去保证。</p>
<p>tikv 的记录会写入三副本，三副本按照multi-paxos组织，位于不同的三个节点。三副本其中一个作为leader，如果其中一个副本挂了（包含副本的节点挂了），会自动选出新的leader副本。<strong>单paxos以及zab, raft写入都是串行的</strong>，也就是说不论集群有多大，写入都经过一个leader。<strong>multi-paxos的最大优势是提高并发，例如一个数据子表一个paxos，可以实现多个数据子表的并行写</strong>。同时paxos自身可实现三副本自身的可用性（不依赖外部namenode就可以在崩溃后找到新的替代节点），同时利用leader实现写操作的事务支持。使用multi-paxos的好处还有，十分方便扩缩容/横向扩展，只需要扩一个follower进去, leader会自动将数据同步。</p>
<p>tikv 的设计来自google 的<a href="https://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf">spanner</a></p>
<p>At every replica that is a leader, each spanserver also implements a transaction manager to support distributed transactions. The transaction manager is used to implement a participant leader; the other replicas in the group will be referred to as participant slaves.
(tidb的paxos不是server节点级别的，而是表切片级别的，表会按照固定大小划分成切片，每个切片是支持事务的单位)</p>
<p>kv和append-only文件系统的最大区别是，**数据库的日志要求有序性，而append-only只要求原子性。**数据库的操作包括增删改，因此日志必须严格按照用户的写入顺序执行，以便通过replay日志恢复状态。因此数据库需要递增id来让请求保序（类似tcp的seq）。而append-only系统只要求数据append写入就行了，不要求写入顺序，只要求写入的完整性。kv系统的版本是顺序性日志的id，append-only文件系统的版本是写成功后，chunk的最新位置（如果master记录的某chunk最新位置和server记录的不同，以master为准）。</p>
<p>另外的区别是，append写之前一般就要通知给master定版本(告知master自己这次要写多少，写完后chunk新长度是多少)，这样append写失败了master可以知道（可能导致数据丢失）。而<strong>kv leader自己不能决定版本</strong>，需要1/2以上follower确认才能定，否则数据库认为写失败。原因在于数据节点的master 分布式元数据场景下实际是元数据的server，它本身依赖元数据master提供可用性。</p>
<p>分布式文件系统除了三副本之外，通常使用EC纠删码实现数据冗余，EC可以降低存储成本，但使用EC的<code>[offset, length]</code>区间一般要求至少是4K对齐的。</p>
<p>三副本+multi-paxos+leader写 的存储模型对kv存储有很大思考价值</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[storage]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言(6)—编译、运行和调试]]></title>
          <link>https://larrystd.github.io/posts/编程语言(6)—编译、运行和调试</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言(6)—编译、运行和调试</guid>
          <pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate>
          <description><![CDATA[写完了代码第一件事是编译，编译失败只能根据编译器通知修改代码；编译通过了第二件事是跑UT，UT不过需要进行调试。调试包括debug和release包的调试，线上进程的运行问题有时候也需要调试，调试的主要方式是调试工具和日志（包括print大法）。为了发现问题，有时候还需要添加报警日志。]]></description>
          <content:encoded><![CDATA[<p>写完了代码第一件事是编译，编译失败只能根据编译器通知修改代码；编译通过了第二件事是跑UT，UT不过需要进行调试。调试包括debug和release包的调试，线上进程的运行问题有时候也需要调试，调试的主要方式是调试工具和日志（包括print大法）。为了发现问题，有时候还需要添加报警日志。</p>
<p>提高代码的健壮性，编译器、静态检查和格式化工具、调试、单元测试、日志等是开发必不可少的</p>
<h3>C语言和C++</h3>
<h4>编译</h4>
<p>为什么需要编译？</p>
<ol>
<li>计算方面，汇编代码基本可以等同于机器码。汇编语言和机器码是一行一行指令执行，高级语言的会抽象出来if-else条件执行/while循环执行和函数执行模块，编译需要把这些模块转化成一行一行的指令；不同cpu提供的指令和寄存器不同，因而编译器的目标指令也不同（其他硬件需要和cpu兼容，一般程序只需要操纵cpu就可以同时操纵内存、磁盘、网卡等硬件）；另外，由操作系统实现中断、上下文切换等计算单元（不需要用户程序实现），也需要编译器把它们打包到编译后的二进制文件中。</li>
<li>数据方面，汇编语言没有类型概念，需要手动指定寄存器和内存物理地址传输数据，数据交换一般是cpu字长对齐的（64位cpu8字节对齐）；高级语言使用类型来确定数据内存分配大小，使用变量维护某一块内存区域，需要编译器记录类型变量的地址，使用变量转化为从某寄存器或内存地址拿数据的指令，动态内存转化为执行从空闲内存拿内存的指令。</li>
</ol>
<p>编译把.cpp 转化为 .o文件。对于多.cpp文件或使用到动态/静态库的文件（几乎所有.cpp都会使用到库文件，例如glibc），需要使用链接把多个.o文件链接成一个二进制文件。链接期间还会链接跨文件共享的外部变量/全局变量。</p>
<p>C++ 20引入了模块，模块相当于融合了头文件和源文件（库），编译、依赖和隔离机制更加清晰，但广泛使用还需要一定距离。</p>
<p>C语言和C++一般公用编译器，常用的编译器有两个，gcc和clang/llvm。常见的gcc编译指令</p>
<pre><code class="language-shell">g++ -c file.cpp  # 只编译
g++ file.cpp -o program  # 编译链接
g++ file1.cpp file2.cpp -o program  # 多文件编译

-O0  # 不进行优化（默认）。
-O1  # 基础优化，平衡编译速度和运行效率。
-O2  # 更高的优化级别，提高性能。
-O3  # 最高优化级别，可能增加编译时间和可执行文件体积。
-Os  # 优化以减小可执行文件的大小。

-g  # 生成调试信息，用于调试器（如 gdb）。
-ggdb  # 为 GNU 调试器生成更详细的调试信息。

-std=c++11  # 使用 C++11 标准。
-std=c++14  # 使用 C++14 标准。
-std=c++17  # 使用 C++17 标准。
-std=c++20  # 使用 C++20 标准。

# 警告选项
-Wall  # 启用大多数常见的警告。
-Wextra  # 启用额外的警告。
-Werror  # 将警告视为错误。
-pedantic  # 强制严格遵循标准。

# 链接选项
-l  # 指定链接库。
-L  # 指定库文件搜索路径。
-I  # 指定头文件搜索路径。
g++ file.cpp -o program -lm -L/usr/lib -I/usr/include

g++ -S file.cpp -o file.s  # 生成汇编代码

g++ -DDEBUG file.cpp -o program  # 定义宏

# 生成动态库
g++ -fPIC -c file1.cpp file2.cpp  # -fPIC 生成位置无关代码
g++ -shared -o libmylib.so file1.o file2.o  # -shared 指定生成动态库。
g++ main.cpp -L. -lmylib -o program
</code></pre>
<p>makefile是GNU推出的编译工具，GNU Autotools使用 autogen.sh + configure + make经典编译流程</p>
<ol>
<li>./autogen.sh 生成 configure 脚本和 Makefile.in 模板。</li>
<li><code>./configure [options]</code> 配置编译选项</li>
<li>make 利用makefile编译代码
这一套流程比较原始，只推荐编译老项目，不建议在新项目使用</li>
</ol>
<pre><code class="language-shell">CC = g++  # 变量定义
CFLAGS = -Wall -g
OBJ = main.o utils.o
TARGET = program

# 规则
$(TARGET): $(OBJ)
	$(CC) $(CFLAGS) -o $@ $^

main.o: main.cpp utils.h
	$(CC) $(CFLAGS) -c $&#x3C;

utils.o: utils.cpp utils.h
	$(CC) $(CFLAGS) -c $&#x3C;

# 伪目标
clean:
	rm -rf $(OBJ) $(TARGET)

# $@ 当前目标的名字。
# $&#x3C; 第一个依赖文件。
# $^ 所有依赖文件。

make  # 构建目标
make clean # 清理构建文件
</code></pre>
<p>现代的编译工具是cmake和bazel。</p>
<ol>
<li>CMake 是一个脚本，能自动生成 Makefile 或其他构建系统文件(如 Ninja 或 Visual Studio 项目文件)</li>
<li>google提供的bazel 除了是编译工具, 还可以作为包管理工具。bazel 一次编译好代码，不再使用make中间步骤</li>
</ol>
<h4>cmake 构建语法</h4>
<p>cmake构建过程一般是经典两步</p>
<ol>
<li>cmake -DCMAKE_BUILD_TYPE=Debug ..</li>
<li>make</li>
</ol>
<p>基本命令</p>
<pre><code class="language-shell">cmake_minimum_required(VERSION 3.10)  # CMake 最低版本要求

project(MyProject VERSION 1.0 LANGUAGES C CXX)  # 定义项目名称和语言

add_executable(myapp main.cpp)  # add_executable 添加可执行文件

add_library(mylibrary STATIC lib.cpp)  # add_library 添加库

target_link_libraries(myapp mylibrary) # target_link_libraries 将库链接到目标

include_directories(${CMAKE_SOURCE_DIR}/include)  # include_directories为编译器添加头文件搜索路径。

# add_dependencies 控制目标之间的构建顺序，构建顺序
# add_dependencies(&#x3C;target> &#x3C;depend1> &#x3C;depend2> ...)

set(MY_VAR "Hello")  # set 设置变量值

message(STATUS "This is a status message.")  # message 打印信息，支持不同的输出级别
message(WARNING "This is a warning message.")
message(ERROR "This is an error message.")

if(MY_VAR STREQUAL "Hello")  # if()判断条件
  message(STATUS "Hello")
elseif(MY_VAR STREQUAL "World")
  message(STATUS "World")
else()
  message(STATUS "Something else")
endif()

find_package(OpenGL REQUIRED)  # find_package 查找外部库或软件包

# find_program 搜索指定的可执行程序，并将其路径存储到指定的变量中。HINTS优先查找的路径
set(BUSTUB_CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin")
find_program(CLANG_FORMAT_BIN
        NAMES clang-format clang-format-14
        HINTS ${BUSTUB_CLANG_SEARCH_PATH})

option(MY_FEATURE "Enable MyFeature" ON)  # option 定义布尔选项，通常用于启用或禁用功能

install(TARGETS myapp DESTINATION bin)  # install 指定安装规则

# CMake 在父目录中执行 add_subdirectory() 时，CMake 会进入子目录 &#x3C;source_dir>，并寻找子目录中的 CMakeLists.txt 文件
add_subdirectory()

CMAKE_SOURCE_DIR  # 项目源代码的根目录
CMAKE_BINARY_DIR  # 构建目录
CMAKE_CURRENT_SOURCE_DIR  # 当前 CMake 脚本所在目录。
CMAKE_CURRENT_BINARY_DIR  # 当前 CMake 构建目录。
CMAKE_CXX_COMPILER  # C++ 编译器。
CMAKE_BUILD_TYPE  # 构建类型（如 Debug, Release）

STREQUAL # STREQUAL 用于比较两个字符串是否相等，区分大小写
EXISTS # 逻辑判断命令，用于检查某个文件或目录是否存在

file
# file(&#x3C;operation> &#x3C;arguments>...)
# file(READ "&#x3C;file_path>" &#x3C;variable_name>) 读取文件内容，保存到变量
# file(TO_CMAKE_PATH) 将输入路径转换为 CMake 使用的标准路径格式

enable_testing() # 启用 CTest 功能，CMake 脚本中使用 add_test() 来定义测试用例，并通过 ctest 命令执行测试。

string()
# string(&#x3C;COMMAND> &#x3C;ARGUMENTS>) 字符串处理函数
# string(CONCAT &#x3C;VAR> &#x3C;STRING1> &#x3C;STRING2> ...)  将多个字符串连接成一个字符串，并将结果存储到变量

add_custom_target # 运行命令，例如执行clang-tidy
add_custom_target(format ${BUSTUB_BUILD_SUPPORT_DIR}/run_clang_format.py
        ${CLANG_FORMAT_BIN}
        ${BUSTUB_BUILD_SUPPORT_DIR}/clang_format_exclusions.txt
        --source_dirs
        ${BUSTUB_FORMAT_DIRS}
        --fix
        --quiet
)

gtest_discover_tests # gtest_discover_tests 是 CMake 中与gtest集成的测试自动发现和注册

find_package(GTest REQUIRED)

add_executable(my_test_target test_case1.cpp test_case2.cpp)  # 添加 Google Test 测试目标
target_link_libraries(my_test_target GTest::GTest GTest::GMock)
</code></pre>
<h4>bazel构建和包管理工具</h4>
<p>Bazel 的构建过程通常由以下几个步骤组成：</p>
<ul>
<li>BUILD 文件：每个项目都有一个 <code>BUILD</code> 文件，定义了如何构建该项目。通过这个文件，Bazel 知道如何处理源代码、依赖项、编译步骤等。</li>
<li>目标（Target）：在 <code>BUILD</code> 文件中，你可以定义“目标”，例如编译一个库、一个可执行文件或一个测试。这些目标描述了项目的不同部分，Bazel 会根据目标来决定如何进行构建。</li>
<li>依赖关系：Bazel 会根据目标的依赖关系，构建出一个有向无环图（DAG），确保构建过程是有序的，并且只构建必要的部分。</li>
<li>增量构建：Bazel 会智能地检测哪些部分发生了变化，避免每次都从头开始构建。它通过文件哈希和时间戳来确定哪些目标需要重新构建。</li>
</ul>
<p>项目根目录下创建一个 <code>WORKSPACE</code> 文件，Bazel 会将其作为项目的工作空间标识。
src/BUILD</p>
<pre><code class="language-cpp">cc_binary(
    name = "myapp",
    srcs = ["main.cc"],
    deps = [
        ":myheader",  # 依赖其他目标（例如头文件或库）
    ],
)
</code></pre>
<p><code>src/main.cc</code>：</p>
<pre><code class="language-cpp">#include &#x3C;iostream>

int main() {
    std::cout &#x3C;&#x3C; "Hello, Bazel!" &#x3C;&#x3C; std::endl;
    return 0;
}
</code></pre>
<p>直接执行bazel build //src:myapp 编译</p>
<ol>
<li><code>//src:myapp</code>：表示从 <code>src</code> 目录中的 <code>BUILD</code> 文件中构建目标 <code>myapp</code>。</li>
<li><code>bazel build</code>：该命令会自动解析 <code>BUILD</code> 文件，并根据其中的规则来执行构建。</li>
</ol>
<p>目录结构</p>
<pre><code>/my-cpp-project
  ├── BUILD            # Bazel 构建文件
  ├── WORKSPACE        # Bazel 工作空间文件
  ├── src/
  │   ├── BUILD        # C++ 代码的 Bazel 构建文件
  │   └── main.cc      # C++ 源文件
  └── include/
      └── myheader.h   # C++ 头文件
</code></pre>
<p>目标匹配符</p>
<p>:	当前包的所有规则
:all	当前包的所有目标
...	递归匹配所有子包</p>
<p>可以把bazel 和go mod对比</p>
<p>构建命令</p>
<pre><code># 构建单个目标
bazel build //src/main:app

# 构建当前目录下的所有目标
bazel build :all

# 构建包内的所有目标
bazel build //src/main/...

# 构建多个指定目标
bazel build //src/main:app //tests:unit_tests

# 构建整个工作区
bazel build //...

# 排除特定目标
bazel build //... -- //experimental/...
</code></pre>
<p>bazel的输出</p>
<p>目录名称	用途
bazel-bin	二进制文件（如 cc_binary）
bazel-genfiles	生成的源代码（如协议缓冲区）
bazel-testlogs	测试日志
bazel-out	实际物理存储目录（被符号链接引用）</p>
<p>以googletest的bazel 为例</p>
<pre><code># Google Test including Google Mock
cc_library(
    name = "gtest",
    srcs = glob(
        include = [
            "googletest/src/*.cc",
            "googletest/src/*.h",
            "googletest/include/gtest/**/*.h",
            "googlemock/src/*.cc",
            "googlemock/include/gmock/**/*.h",
        ],
        exclude = [
            "googletest/src/gtest-all.cc",
            "googletest/src/gtest_main.cc",
            "googlemock/src/gmock-all.cc",
            "googlemock/src/gmock_main.cc",
        ],
    ),
    hdrs = glob([
        "googletest/include/gtest/*.h",
        "googlemock/include/gmock/*.h",
    ]),
    includes = [
        "googlemock",
        "googlemock/include",
        "googletest",
        "googletest/include",
    ],
    linkopts = select({
        ":qnx": ["-lregex"],
        ":openbsd": [
            "-lm",
            "-pthread",
        ],
        "//conditions:default": ["-pthread"],
    }),
    deps = select({
        ":has_absl": [
            "@abseil-cpp//absl/container:flat_hash_set",
            "@abseil-cpp//absl/debugging:failure_signal_handler",
			...
        ],
        "//conditions:default": [],
    }) + select({
        ":fuchsia": [
            "@fuchsia_sdk//pkg/fdio",
            "@fuchsia_sdk//pkg/syslog",
            "@fuchsia_sdk//pkg/zx",
        ],
        "//conditions:default": [],
    }),

cc_test(
    name = "gtest_samples",
    size = "small",
    srcs = [
        "googletest/samples/sample1_unittest.cc",
    ],
    linkstatic = 0,
    deps = [
        "gtest_sample_lib",
        ":gtest_main",
    ],
)

)
</code></pre>
<p><code>select</code> 函数根据特定条件（如是否启用 Abseil 库）选择不同的依赖项。</p>
<p>glob, 选中多个文件</p>
<pre><code>glob(
    include = [文件模式列表],
    exclude = [排除模式列表],  # 可选
    exclude_directories = 1,   # 默认排除目录（可选）
)
</code></pre>
<p>includes 命令的作用
编译当前包的.cpp文件时自动添加 <code>-I&#x3C;package_path>/include</code>。</p>
<pre><code class="language-cpp">includes = ["include"],      # 头文件搜索路径（相对于当前包）
</code></pre>
<p>linkopts
<code>linkopts</code> 用于指定 ​<strong>链接器选项</strong>​（Linker Flags），控制如何将目标文件、静态库或动态库链接成最终的可执行文件或共享库</p>
<p>MODULE.bazel 文件中的内容</p>
<pre><code class="language-python">bazel_dep(
    name = "abseil-cpp",  # 依赖名称
    version = "20250127.0",  # 版本号
)

bazel_dep(
    name = "platforms",
    version = "0.0.10",
)
</code></pre>
<p>通过 Bzlmod 直接从 Bazel 中央仓库（Bazel Central Registry, BCR）拉取依赖。</p>
<h4>单元测试</h4>
<p>google 的gtest库是广泛使用的单元测试框架，本地mock可以使用gmock。</p>
<p>gmock只支持mock虚函数，这个比较坑</p>
<pre><code class="language-cpp">#include &#x3C;gmock/gmock.h>
#include "data_service.h"

class MockDataService : public DataService {
public:
    MOCK_METHOD(int, fetchData, (int id), (const override));
    MOCK_METHOD(void, saveData, (int id, const std::string&#x26; data), (override));
};

// data_processor_test.cpp
#include &#x3C;gtest/gtest.h>
#include "data_processor.h"
#include "data_service_mock.h"

using testing::Return;
using testing::_;
using testing::Throw;

TEST(DataProcessorTest, ProcessHighValue) {
    MockDataService mock;
    DataProcessor processor(&#x26;mock);

    // 设置期望：fetchData(42) 返回 200
    EXPECT_CALL(mock, fetchData(42))
        .WillOnce(Return(200));

    // 期望 saveData 被调用一次，参数为 (42, "high")
    EXPECT_CALL(mock, saveData(42, "high"));

    // 调用被测方法
    int result = processor.process(42);
    ASSERT_EQ(result, 400); // 200 * 2 = 400
}

TEST(DataProcessorTest, ProcessLowValue) {
    MockDataService mock;
    DataProcessor processor(&#x26;mock);

    // 设置期望：fetchData(10) 返回 50
    EXPECT_CALL(mock, fetchData(10))
        .WillOnce(Return(50));

    // 期望 saveData 被调用一次，参数为 (10, "low")
    EXPECT_CALL(mock, saveData(10, "low"));

    int result = processor.process(10);
    ASSERT_EQ(result, 25); // 50 / 2 = 25
}

int main(int argc, char​**​ argv) {
    testing::InitGoogleMock(&#x26;argc, argv);
    return RUN_ALL_TESTS();
}
</code></pre>
<h4>静态检查和格式化</h4>
<p>Clang-Tidy 是一个基于 Clang 的 C++ 静态分析工具，用于执行代码检查、风格检测和代码优化。Clang-Tidy配置文件通常位于项目的根目录，名为 .clang-tidy</p>
<pre><code>Checks:          '*, -clang-analyzer-*'
WarningsAsErrors: 'true'
HeaderFilterRegex: '.*'
FormatStyle:     file
</code></pre>
<p>Clang-Format 用于格式化 C++ 代码，并且支持根据 .clang-format 配置文件自定义格式化规则。</p>
<pre><code>clang-format -i &#x3C;your-file.cpp>
# 指定风格
clang-format -i -style=google &#x3C;file>
</code></pre>
<p>Valgrind 内存动态检查工具，可以检查内存泄漏、内存泄漏，未初始化内存访问等</p>
<pre><code># 检查内存泄漏
valgrind --leak-check=full ./your_program
</code></pre>
<h4>自动补全</h4>
<p>使用clangd实现自动补全，命令行安装<code>sudo apt-get install clangd-10</code></p>
<p>cmake启用CMAKE_EXPORT_COMPILE_COMMANDS，会在build目录生成compile_commands.json 文件</p>
<h4>日志</h4>
<p>编码使用的printf 也是一种简单的日志调试，根据输出看预期是否正确。C++日志库也可以选择google的glog。</p>
<h4>调试工具</h4>
<p>gdb调试普通程序，需要对普通程序编译加-g选项，也就是debug编译。</p>
<p>gdb调试命令</p>
<pre><code># 运行调试器
run
run arg1 arg2

# 设置断点
break main
break 10
break 10
break 10 if x == 5  # 条件断点

# 删除断点
delete
delete 1

# 显示断点
info breakpoints
info breakpoint 1

# 启用和关闭断点
enable 2
disable 2

# 断点继续执行
continue

# 单步执行
step  # 进入函数内部执行
next  # 会跳过函数调用
finish  # 运行直到当前函数执行结束

# 查看线程堆栈
backtrace
frame 1  # 切换栈
up  # 切换到当前栈帧的上一层，即父函数的栈帧
down  # 切换到当前栈帧的下一层，即子函数的栈帧

# 查看变量值
print x
print my_struct.field
print my_array[2]

# 修改变量值
set variable x = 10

# 查看指定内存地址内容
x/4xw &#x26;x
</code></pre>
<p>gdb多线程调试</p>
<pre><code># 显示线程
info threads

# 切换线程
thread 2

# 查看线程栈
backtrace

# 打印所有线程堆栈
thread apply all backtrace

# 打印所有线程堆栈到文件
set logging on
set logging file &#x3C;filename>  # 执行这两行命令，后面gdb会把结果输出到文件
info threads
thread apply all backtrace
</code></pre>
<p>对于release 编译的调试，需要等进程运行产生core后，结合core对二进制文件进行调试。调试命令<code>gdb &#x3C;二进制文件> &#x3C;core_file></code>，core文件和二进制文件必须对应，一般来说需要保证core是调试的二进制文件生成的。</p>
<p>配置linux系统生成core文件</p>
<pre><code>ulimit -c
ulimit -c unlimited
# 设置将 core 文件保存在 /tmp/ 目录，并包括程序的名称 (%e) 和进程 ID (%p) 作为文件名。
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
</code></pre>
<p>core文件的调试只能查看进程崩溃时的状态，以下是主要使用的命令</p>
<pre><code>// 查看进程崩溃时的函数调用栈
backtrace
// 查看当前线程的栈帧和寄存器状态
info locals
info registers

# 打印所有线程堆栈到文件
set logging on
set logging file &#x3C;filename>  # 执行这两行命令，后面gdb会把结果输出到文件
info threads
thread apply all backtrace
</code></pre>
<p>gdb 可以通过<code>gdb attach pid</code>附加到进程，执行命令后，gdb会附加到目标进程，并暂停目标进程的执行，不要在线上使用！</p>
<h4>性能排查工具</h4>
<p>性能排查tips</p>
<ol>
<li>首先看cpu和磁盘利用率（或者磁盘吞吐/iops），是否到瓶颈。也就是硬件到瓶颈</li>
<li>如果1没有到，说明是软件栈瓶颈。需要看用户程序瓶颈还是内核。</li>
<li>可以使用perf 直接观察软件栈，可以打印火焰图，也可以perf top -p 查看线程情况</li>
<li>可以用strace 查看系统调用的延迟，判断内核是不是有瓶颈</li>
<li>如果程序里有链路 trace，可以直接通过 trace 看程序那部分耗时大</li>
<li>确认出问题的线程后，可以用 pstack 打印线程栈帧，看是否一直出现wait 相关的栈</li>
</ol>
<p>pstack 打印正在运行进程的堆栈信息，strace打印系统调用信息。多次执行pstack 可以看潜在的用户程序瓶颈（如有无wait调用栈），执行strace可以借助系统调用查看内核是否到瓶颈。</p>
<p>perf 是linux内核提供的强大性能排查工具。</p>
<h3>JAVA</h3>
<h4>JAVA编译和运行</h4>
<p>Java编译是将.java 文件转换为字节码（.class 文件），字节码并不是cpu可执行的汇编机器码，平台无关。编译使用java提供的编译器javac执行。</p>
<p>编译后，每个java文件都会产生一个.class文件，类似C++的.o文件。</p>
<pre><code># 编译.java文件
javac HelloWorld.java
javac Class1.java Class2.java
javac *.java

# -classpath指定编译查找的类路径。
javac -classpath /path/to/library.jar MyProgram.java
javac -classpath /path/to/library1.jar:/path/to/library2.jar MyProgram.java

# -d 指定编译输出.class 文件的目录
javac -d bin MyProgram.java

# -g添加调试信息
javac -g MyProgram.java
</code></pre>
<p>运行.class文件，只需要显示运行携带main方法的类，相关的类会自动被加载</p>
<pre><code class="language-java"># 不需要加.class后缀
java HelloWorld
</code></pre>
<h4>调试</h4>
<p>jdb是java提供的调试工具, jdb比较反人类的是，它的命令没有简写，例如next不能写作n, cont不能写作c, 以及反人类的命令stop at MyProgram:10</p>
<pre><code># 开始运行
run
# 查看帮助信息
help

# 设置断点
stop at MyProgram:10
stop in MyProgram.myMethod

Usage: stop at &#x3C;class>:&#x3C;line_number> or
       stop in &#x3C;class>.&#x3C;method_name>[(argument_type,...)]

# 查看断点（没错clear是查看断点）
clear
# 删除某个断点，不支持删除全部断点。
clear MyProgram:10

# 断点继续运行
cont

# 单步执行
next
step

# 返回到上层调用，类似gdb的finish
step up

list # 显示旁边代码

# 查看变量
print variableName
print objectInstance.memberVariable

# 查看线程信息
thread  # 当前线程信息
thread 1  # 1号线程信息
# 暂停和恢复某线程的运行
suspend [thread id(s)]
resume [thread id(s)]

# 查看线程的栈帧
where  # 当前线程信息
where 2

# 查看class和method信息
class &#x3C;className>
method &#x3C;>
</code></pre>
<p>调试正在运行的进程</p>
<pre><code># 列出正在运行的java进程
jps -l

# 连接正在运行的进程
jdb -attach &#x3C;pid>
</code></pre>
<h4>测试</h4>
<p>JUnit 是最常用的 Java 单元测试框架，使用注解 @Test 来标记测试方法，以及 @Before 和 @After 来标记测试前后的初始化和清理方法。</p>
<h3>Go</h3>
<h4>编译</h4>
<pre><code># 编译单个文件
go build main.go

# 编译整个包
go build

# 编译参数, -gcflags
go build -gcflags "-N" main.go

-N  # 禁用优化
-l  # 禁用内联优化
-l -N  # 禁用优化和内联
-m  # 输出优化决策信息
-d  # 增加调试信息
</code></pre>
<h4>调试</h4>
<p>golang推荐使用Delve 进行调试，安装<code>go install github.com/go-delve/delve/cmd/dlv@latest</code></p>
<pre><code># 调试单个文件
dlv debug main.go
# 调试某个目录
dlv debug

# 设置断点
break b
b main.go:10
# 显示已经设置的断点
breakpoints (alias: bp)

# 栈帧移动
down ------------------------ Move the current frame down.
up -------------------------- Move the current frame up.

# 继续运行直到下一个断点
continue c

# 单步执行
next  n	# 单步执行，跳过函数调用
step  s	# 单步进入函数内部

# 打印变量值
print p
p x

# 显示当前代码行及上下文
list 或 ls

goroutine ------------------- Shows or changes current goroutine
goroutines	# 查看当前所有 Goroutines
# 查看goroutine堆栈
stack &#x3C;goroutine_id>

threads	显示所有线程信息
thread (alias: tr) ---------- Switch to the specified thread.
</code></pre>
<h4>静态代码检查</h4>
<p>gopls，通常会随 Go 扩展自动安装。可以和vscode结合配置</p>
<pre><code># 静态检查
gopls check &#x3C;path-to-your-directory-or-file>
# 格式化代码
gopls format &#x3C;path-to-your-file>
# 代码补全
gopls completion &#x3C;path-to-your-file>:&#x3C;line>:&#x3C;column>

# 跳转到函数定义
gopls definition &#x3C;path-to-your-file>:&#x3C;line>:&#x3C;column>
# 跳转到引用
gopls references &#x3C;path-to-your-file>:&#x3C;line>:&#x3C;column>
</code></pre>
<h4>单元测试</h4>
<p>Go 自带的测试框架（testing 包）支持单元测试和性能测试。测试文件以 _test.go 结尾, 测试函数必须以 Test 开头，后面跟随测试的函数名。</p>
<pre><code># 运行测试
go test
go test -run TestAdd  # 只运行特定的测试函数
go test -cover  # 查看覆盖率

# 调试测试
dlv test

# 调试单个测试
dlv test -- -test.run TestFunctionName
</code></pre>
<h3>Python</h3>
<h4>编译和运行</h4>
<p>Python 会将源代码编译为一种字节码（Bytecode），存储为 .pyc 文件（位于 <code>__pycache__</code> 文件夹）。字节码会被 Python 虚拟机（PVM，Python Virtual Machine）翻译为底层的机器指令执行。</p>
<p>相比java通常把字节码打包成jar，后续由jvm执行; python一般直接保留源代码，python虚拟机直接执行源代码。python编译过程也不会进行类型检查，编译器优化行为少。</p>
<p>python语法比较灵活，表达式函数可以在全局执行，执行实现先于main模块。</p>
<h4>调试</h4>
<p>python -m pdb your_program.py 启用pdb调试</p>
<pre><code class="language-shell"># 单步运行
n next  # n
s step  # 进入当前行中的函数调用，逐步执行
c continue  # 继续执行程序，直到下一个断点
r return  # 运行到当前函数完毕

# 清理断点
clear
clear filename:lineno  # 删除某行所有断点
clear number  # 删除编号断点

enable bpnumber # 启动和关闭断点
disable bpnumber

# 打印变量值或表达式
p &#x3C;expression>

# 列出当前行附近的代码
l (list)

# 设置断点
b  break # 显示已经设置的断点
b &#x3C;line_number>
b 12
b add

# 显示当前的调用栈
w (where)
up  # 切换到调用栈的上一层（即父函数）
down  # 切换到调用栈的下一层（即子函数）

&#x3C;expression>  # 执行表达式，可以修改变量
a = 10
</code></pre>
<h4>静态检查</h4>
<p>pylint 工具检查代码，执行</p>
<pre><code class="language-shell">pylint &#x3C;your-python-file.py>
pylint &#x3C;your-project-folder>

# 生成pylint的静态检查规则
pylint --generate-rcfile > .pylintrc
</code></pre>
<p>pylint不支持自动格式化，Black可以用来做python自动化代码格式化工具</p>
<h4>单元测试</h4>
<p>unittest 库，导入待测试的模块，对需要测试的模块进行单元测试</p>
<pre><code class="language-python">import unittest
import math_utils

class TestMathUtils(unittest.TestCase):
    def test_add(self):
        # 测试 add 函数
        self.assertEqual(math_utils.add(1, 2), 3)
        self.assertEqual(math_utils.add(-1, 1), 0)
        self.assertEqual(math_utils.add(0, 0), 0)

    def test_subtract(self):
        # 测试 subtract 函数
        self.assertEqual(math_utils.subtract(10, 5), 5)
        self.assertEqual(math_utils.subtract(0, 1), -1)
        self.assertEqual(math_utils.subtract(100, 50), 50)

if __name__ == "__main__":
    unittest.main()
</code></pre>
<h4>日志</h4>
<p>配置日志处理器</p>
<pre><code class="language-python">import logging

# 创建日志器
logger = logging.getLogger("my_logger")
logger.setLevel(logging.DEBUG)

# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

# 创建文件处理器
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.WARNING)

# 定义日志格式
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 添加处理器到日志器
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 测试日志
logger.debug("这是一条调试日志")
logger.info("这是一条一般信息日志")
logger.warning("这是一条警告日志")
logger.error("这是一条错误日志")
logger.critical("这是一条严重错误日志")
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言(5)—文件IO和网络库]]></title>
          <link>https://larrystd.github.io/posts/编程语言(5)—文件IO和网络库</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言(5)—文件IO和网络库</guid>
          <pubDate>Tue, 24 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[linux系统的文件接口是十分重要的抽象, 也就是“一切都是文件”。借助文件IO，用户程序可以读写磁盘设备、网络设备、甚至管道、内存等。]]></description>
          <content:encoded><![CDATA[<p>linux系统的文件接口是十分重要的抽象, 也就是“一切都是文件”。借助文件IO，用户程序可以读写磁盘设备、网络设备、甚至管道、内存等。</p>
<h3>C语言</h3>
<p>首先介绍linux 系统调用提供的文件io</p>
<pre><code class="language-cpp">// 打开文件, 返回文件描述符。文件描述符是进程持有打开文件指针序列的索引, 通过文件描述符进程可以访问进程文件结构， 进程文件结构记录文件的Inode，以及进程当前读写文件的偏移等信息
int fd = open("file.txt", O_RDONLY);

size_t bytes = read(fd, buf, sizeof(buf))  // 从文件描述符读取数据到buf

size_t bytes = write(fd, buf, sizeof(buf))  // 将buf写入到文件描述符

close(fd);

off_t offset = lseek(fd, 0, SEEK_END);  // 调整文件读写偏移量

// 内存映射文件
#include &#x3C;sys/mman.h>
int fd = open("data.bin", O_RDWR);
void* addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 直接操作 addr 指针读写文件
munmap(addr, file_size);
</code></pre>
<p>C语言的文件IO位于stdio.h库，stdio默认启用用户态缓冲区（可通过 setvbuf 调整），可以减少系统调用次数，提升小文件效率。</p>
<pre><code class="language-cpp">typedef struct _IO_FILE FILE;
struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
};

FILE *fopen(const char *filename, const char *mode);  // 打开文件, 获得文件句柄
// mode·
// "r"：以只读方式打开文件，文件必须存在。
// "w"：以只写方式打开文件，如果文件已存在，会清空文件内容；如果文件不存在，会创建新文件。
// "a"：以追加模式打开文件，数据写入到文件末尾，文件不存在时会创建。
// "r+"：以读写模式打开文件，文件必须存在。
// "w+"：以读写模式打开文件，文件不存在时会创建，已存在时会清空文件内容。
// "a+"：以读写模式打开文件，数据写入文件末尾，文件不存在时会创建。

int fclose(FILE *stream);  // 关闭文件

int fgetc(FILE *stream)  // 读取文件, 从文件中读取一个字符。

int fgets(char *str, int num, FILE *stream)  // 从文件中读取一行，最多读取 num-1 个字符。
// 从文件中读取多个ptr对象（如结构体、数组等），每个对象大小为 size，读取 count 个对象。
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
int numbers[5];
size_t n = fread(numbers, sizeof(int), 5, file);

int fputc(int c, FILE *stream)  // 将字符 c 写入文件流

int fputs(const char *str, FILE *stream)  // 将字符串 str 写入文件，自动加上字符串的结束符 \0
// 写入多个对象到文件，每个对象大小为 size，写入 count 个对象。
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
int numbers[5] = {1, 2, 3, 4, 5};
size_t n = fwrite(numbers, sizeof(int), 5, file);

// 文件定位
// 设置文件指针的位置。offset 是偏移量，whence 是参考位置。
// SEEK_SET：从文件开头开始偏移。
// SEEK_CUR：从当前位置开始偏移
// SEEK_END：从文件末尾开始偏移
fseek(FILE *stream, long offset, int whence)：

ftell(FILE *stream)  // 获取当前文件指针的位置

int feof(FILE *stream);  // 检查文件是否已读到末尾

// 标准输入输出
// %d：输出10进制整数; %f：输出浮点数; %s：输出字符串; %c：输出字符;%x：输出十六进制整数。
int printf(const char *format, ...);
int scanf(const char *format, ...);
scanf("%d", &#x26;a); 

getchar()  // 从标准输入读取一个字符。
putchar() // 将一个字符输出到标准输出

int fscanf(FILE *stream, const char *format, ...);  // 从文件中按指定的格式读取数据，并将读取的数据存储到变量。

int fprintf(FILE *stream, const char *format, ...); // fprintf() 向文件中写入格式化的数据

// sprintf() 将格式化的数据写入字符串。
int sprintf(char *str, const char *format, ...);
sprintf(buffer, "Number: %d, Pi: %.2f", num, pi);

// sscanf() 函数用于从字符串中按指定格式读取数据
int sscanf(const char *str, const char *format, ...);
</code></pre>
<p>linu网络socket系统调用</p>
<ol>
<li>服务端，绑定（bind）到指定端口。监听（listen）端口，等待客户端连接。接受（accept）客户端的连接请求。进行数据通信（send，recv, read, write），关闭 Socket（close）。</li>
<li>客户端，创建 Socket。连接（connect）到服务器端。发送和接收数据。关闭 Socket。</li>
</ol>
<p>三次握手之前, 服务端和客户端分别阻塞在accept和connect，三次握手建立连接后, 向下执行。三次握手过程</p>
<ol>
<li>客户端发送SYN报文（SYN=1，序列号seq=x）, 客户端进入 SYN_SENT 状态</li>
<li>服务器回复SYN-ACK报文（SYN=1，ACK=1，确认号ack=x+1，序列号seq=y）, 服务器进入 SYN_RCVD 状态。客户端请求加入到SYN队列, 由net.ipv4.tcp_max_syn_backlog控制</li>
<li>客户端发送ACK报文（ACK=1，确认号ack=y+1，序列号seq=x+1）。客户端进入 ESTABLISHED 状态。服务器收到后也进入 ESTABLISHED 状态。请求进入ESTABLISHED队列，大小由net.core.somaxconn控制。
使用三次握手, 服务端可以控制连接的流控，例如服务端能力不足时，可以通过减缓发送SYN-ACK的频率，减少连接创建。同时双方都可以确定对方接受和发送连接正常。</li>
</ol>
<p>close()函数, 释放套接字文件描述符（File Descriptor），减少其引用计数。当引用计数归零时，触发 TCP 连接的关闭流程（发送 FIN 包，进入四次挥手阶段），主动关闭双向连接。TCP四次回收关闭</p>
<ol>
<li>主动方发送FIN报文（FIN=1，序列号seq=u）, 主动方从 ESTABLISHED 进入 FIN_WAIT_1 状态。此时不再发送数据，但可接收数据</li>
<li>被动方回复ACK报文（ACK=1，确认号ack=u+1，序列号seq=v）, 被动方进入 CLOSE_WAIT 状态，主动方收到后进入 FIN_WAIT_2 状态。此时被动方还可以发送数据</li>
<li>被动方数据发送完毕后，发送FIN报文（FIN=1，ACK=1，确认号ack=u+1，序列号seq=w），被动方进入 LAST_ACK 状态。</li>
<li>主动方发送ACK报文（ACK=1，确认号ack=w+1，序列号seq=u+1）, 主动方进入 TIME_WAIT 状态，等待 ​​2MSL​​（最长报文段寿命，MSL在 /proc/sys/net/ipv4/tcp_fin_timeout配置, 默认60s）后关闭。被动方收到后进入 CLOSED 状态。</li>
</ol>
<p>shutdown()，主动关闭套接字的 ​​单向或双向​​ 通信通道，立即触发 TCP 协议栈的关闭流程, 不影响文件描述符。shutdown支持允许半关闭，即只关闭一端网络</p>
<ol>
<li>shutdown(SHUT_WR), 关闭发送端, 触发两次挥手。主动方发送 FIN → 进入 FIN_WAIT_1, 被动方回复 ACK → 主动方进入 FIN_WAIT_2，被动方进入 CLOSE_WAIT。被动方需调用 close() 或 shutdown() 关闭连接，才会发送 FIN，触发完整四次挥手。若被动方不关闭，主动方会停留在 FIN_WAIT_2，直到内核超时（默认约 60 秒）</li>
<li>shutdown(SHUT_RD)，关闭接收端, 丢弃接收缓冲区的数据，不再读取新数据。​不发送任何报文​​，因此不会触发挥手流程。对方可能继续发送数据，但会被本地内核丢弃（无 RST 或 ACK 回应）。</li>
<li>shutdown(SHUT_RDWR), 触发完整四次挥手。</li>
</ol>
<p>服务端大量TIME_WAIT链接，在高并发短连接的TCP服务器上，服务器处理完请求后往往选择立刻主动正常关闭连接。或者当 connection 头部取值被设置为 close 时，基本都由「服务端」发起主动关闭连接。</p>
<ol>
<li>处理办法，在HTTP 请求的头部，connection 设置为 keep-alive，保持存活一段时间：现在的浏览器，一般都这么设置</li>
<li>缩短timewait时间为1MSL</li>
<li>如果是客户端主动关闭导致的TIMEWAIT（例如爬虫程序访问完请求立刻关闭连接），那可能导致客户端端口耗尽，可以开启net.ipv4.tcp_tw_reuse允许客户端新建连接复用timewait的链接。通过net.ipv4.ip_local_port_range扩大客户端使用端口的范围。</li>
</ol>
<pre><code class="language-cpp">#include &#x3C;sys/socket.h>
int shutdown(int sockfd, int how);

// how的取值
// SHUT_RD	关闭接收端，丢弃未读数据。
// SHUT_WR	关闭发送端，发送 FIN 包。
// SHUT_RDWR	同时关闭读和写。
</code></pre>
<pre><code class="language-cpp">// 创建socket
// domain：指定协议族，常用的有：
// AF_INET：IPv4协议。
// AF_INET6：IPv6协议。
// AF_UNIX：Unix 域协议，用于同一机器上的进程间通信。
//
// type：指定套接字类型，常见的有：
// SOCK_STREAM：面向连接的流式套接字，通常用于 TCP。
// SOCK_DGRAM：数据报套接字，通常用于 UDP。
//
// protocol：指定协议，通常为 0，表示由操作系统自动选择合适的协议。
int socket(int domain, int type, int protocol);

// bind() 用于将套接字与本地IP 地址和端口号绑定。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到任意IP地址
server_addr.sin_port = htons(PORT);        // 设置端口号
if (bind(sockfd, (struct sockaddr *)&#x26;server_addr, sizeof(server_addr)) &#x3C; 0) {
    perror("Bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

// backlog：等待连接队列的最大长度，表示操作系统在拒绝新连接之前，最多允许排队的连接请求数。
int listen(int sockfd, int backlog);

// accept() 用于接受一个传入的连接请求。该函数会阻塞，直到有客户端连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// connect() 用于客户端请求与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 从socket fd获得数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t read(int fd, void *buf, size_t count);
// 向socket fd发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t write(int fd, const void *buf, size_t count);

// 关闭一个打开的套接字
int close(int fd);
// shutdown() 可用于关闭连接的一部分（如读或写）
// how：关闭方式，可能的值有：SHUT_RD：关闭读取。SHUT_WR：关闭写入。SHUT_RDWR：关闭读取和写入。
int shutdown(int sockfd, int how);
</code></pre>
<h3>C++</h3>
<p>C++ 文件IO库, 主要是</p>
<ol>
<li>ifstream：用于从文件中读取数据。</li>
<li>ofstream：用于向文件中写入数据。</li>
<li>fstream：用于同时从文件中读取和写入数据。</li>
<li>iostream：用于输入输出流。</li>
</ol>
<pre><code class="language-cpp">// 可用文件流类的构造函数或者 open() 方法来打开文件。mode可以选择
// ios::in：以读取模式打开文件（默认）。
// ios::out：以写入模式打开文件。
// ios::app：以追加模式打开文件（写入内容会被追加到文件末尾）。
// ios::binary：以二进制模式打开文件。
// ios::trunc：如果文件已经存在，截断文件为零长度
std::ofstream outfile("example.txt", std::ios::out | std::ios::trunc);

// 检查文件流状态
is_open()  // 检查文件是否成功打开。
eof()  // 检查是否已经到达文件末尾。

// 文件指针定位
seekg(offset, direction)  // 设置读取位置。
seekp(offset, direction)  // 设置写入位置。
tellg()  // 返回当前读取位置。
tellp()  // 返回当前写入位置

#include &#x3C;iostream>
#include &#x3C;fstream>
#include &#x3C;string>

int main() {
    std::ifstream infile("example.txt");
    
    if (!infile) {
        std::cerr &#x3C;&#x3C; "Failed to open file.\n";
        return 1;
    }

    std::string line;
    while (std::getline(infile, line)) {
        std::cout &#x3C;&#x3C; line &#x3C;&#x3C; std::endl;
    }

    infile.close();
    return 0;
}

std::ofstream fout("data.txt");
fout &#x3C;&#x3C; 123 &#x3C;&#x3C; " " &#x3C;&#x3C; 3.14;  // 涉及类型转换和格式化
</code></pre>
<p>C++ 的 I/O 流（如 &#x3C;&#x3C; 和 >> 运算符）需要处理数据类型转换、格式化和错误检查，导致多次虚函数调用和条件判断。每次 I/O 操作会检查流的状态（如 fail()、eof()），增加分支判断。一般要避免使用（包括cout和cin）</p>
<p>优化C++流的方案</p>
<ol>
<li>使用二进制模式​, 避免文本解析开销。std::ofstream fout("data.bin", std::ios::binary);</li>
<li>关闭与 C 标准库的同步，提升流操作速度。<code>std::ios::sync_with_stdio(false); // 关闭与 stdio 的同步</code></li>
</ol>
<p>不建议使用C++的流，除了C语言的stdio，C++常用的流还有protobuf。protobuf 支持ZeroCopy流</p>
<pre><code class="language-cpp">#include &#x3C;google/protobuf/io/zero_copy_stream_impl.h>

// 创建 ZeroCopyOutputStream（如写入文件）
int fd = open("data.bin", O_WRONLY);
google::protobuf::io::FileOutputStream file_stream(fd);

// 直接序列化到文件流，无中间拷贝
MyMessage msg;
msg.set_bytes_data("large_data"); // 假设是大块数据
msg.SerializeToZeroCopyStream(&#x26;file_stream);
</code></pre>
<p>自定义高效流</p>
<pre><code class="language-cpp">class SharedMemoryInputStream : public google::protobuf::io::ZeroCopyInputStream {
public:
    SharedMemoryInputStream(void* shared_mem, int size) 
        : ptr_(static_cast&#x3C;const uint8_t*>(shared_mem)), size_(size), position_(0) {}

    bool Next(const void** data, int* size) override {
        if (position_ >= size_) return false;
        *data = ptr_ + position_;
        *size = size_ - position_;
        position_ = size_;
        return true;
    }

    void BackUp(int count) override { position_ -= count; }
    bool Skip(int count) override { position_ += count; return position_ &#x3C;= size_; }
    int64 ByteCount() const override { return position_; }

private:
    const uint8_t* ptr_;
    int size_;
    int position_;
};

// 使用自定义流解析数据
void* shared_mem = ...; // 共享内存地址
int mem_size = ...;
SharedMemoryInputStream stream(shared_mem, mem_size);
MyMessage msg;
msg.ParseFromZeroCopyStream(&#x26;stream);
</code></pre>
<h3>JAVA</h3>
<h4>文件IO</h4>
<p>JAVA文件流分为两种主要类型：字节流和字符流。</p>
<ol>
<li>字节流用于处理所有二进制文件</li>
<li>字符流则专门用于处理字符/文本文件</li>
</ol>
<p>字节流FileInputStream</p>
<pre><code class="language-java">// 复制图片（字节流）
try (InputStream in = new FileInputStream("input.jpg");
     OutputStream out = new FileOutputStream("output.jpg")) {
    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
}
</code></pre>
<p>字符流FileReader，会自动将字节按编码转换为字符</p>
<pre><code class="language-java">// 读取文本文件（字符流）
try (Reader reader = new FileReader("input.txt", StandardCharsets.UTF_8);
     BufferedReader br = new BufferedReader(reader)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}
</code></pre>
<p>缓冲流, 提供了一个缓冲区，可以减少对硬盘的读取次数，从而提高性能。</p>
<p>RandomAccessFile，支持随机访问文件</p>
<pre><code class="language-java">import java.io.RandomAccessFile;
import java.io.IOException;

public class RandomAccessFileExample {
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile("example.txt", "rw")) {
            file.writeUTF("Hello, World!");  // 写入字符串
            file.seek(0);  // 将文件指针移动到文件开头
            String data = file.readUTF();  // 读取字符串
            System.out.println("Read from file: " + data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<h4>标准输入输出</h4>
<p>System.out 是 Java 中标准输出流，是 PrintStream 类型的对象</p>
<p>System.in 是 Java 中标准输入流, 是 InputStream 类型的对象，读取的是byte数据。</p>
<p>对于字符数据，通常会使用 Scanner 或 BufferedReader 类接收输入</p>
<pre><code class="language-java">import java.util.Scanner;

public class ScannerInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter your name:");
        String name = scanner.nextLine();  // 读取一行字符串
        System.out.println("Enter your age:");
        int age = scanner.nextInt();
        System.out.println("Name: " + name + ", Age: " + age);
        scanner.close();
    }
}
// 格式化输出
System.out.printf(String format, Object... args);
</code></pre>
<h4>网络库</h4>
<p>JAVA NIO（New Input/Output）是Java提供的高性能、非阻塞I/O框架，位于java.nio 包, 适用于高并发网络编程。</p>
<ol>
<li>Buffer（缓冲区）​​临时存储数据，所有读写操作均通过缓冲区完成。</li>
<li>Channel（通道）​​连接数据源与缓冲区，支持双向读写。包括FileChannel：文件I/O, ServerSocketChannel：TCP服务端</li>
<li>Selector（选择器）​​单线程监听多个通道事件（如连接、读、写）。</li>
</ol>
<p>JAVA Netty​​ 是一个高性能、异步事件驱动的网络应用框架， 基于 Java NIO（Non-blocking I/O）设计</p>
<ol>
<li>基于 Reactor 模型，通过多路复用（Selector）实现非阻塞 I/O。</li>
<li>内置 HTTP/1.x、HTTP/2、WebSocket、TCP/UDP、gRPC 等协议支持。</li>
<li>零拷贝和内存优化</li>
</ol>
<p>核心抽象</p>
<ol>
<li>Channel​​ 网络连接的抽象，代表一个开放的 Socket 连接。</li>
<li>​​EventLoop​​：事件循环，处理 I/O 操作和事件（如连接、读写）。​​EventLoopGroup​​：管理一组 EventLoop，通常分为 BossGroup（接收连接）和 WorkerGroup（处理 I/O）。</li>
<li>​​Pipeline​​：处理链，由多个 ChannelHandler 组成，负责处理入站（Inbound）和出站（Outbound）事件。</li>
<li>​Handler​​：业务逻辑单元，如编解码、日志、业务处理。</li>
<li>ByteBuf, Netty 的字节容器，支持堆内/堆外内存、内存池化。</li>
</ol>
<h3>Go</h3>
<h4>文件IO</h4>
<p>golang中，io包定义了 I/O 操作的抽象接口（Reader、Writer、io.Closer）</p>
<pre><code class="language-go">type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
// 随机访问
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

type ReadWriter interface {
    Reader
    Writer
}
</code></pre>
<p>文件 I/O 操作主要通过 os、io 和 bufio 包实现</p>
<ol>
<li>os包，直接与操作系统交互，提供最底层的文件描述符（*os.File）操作。方法Open、Create、Close、Stat、Chmod、Truncate等。每次读写都是系统调用。</li>
<li>bufio包，是io包接口的一种实现，提供了缓冲区，可以减少系统调用次数，提高性能。缓冲读取（bufio.Reader、Scanner）。缓冲写入（bufio.Writer）。</li>
</ol>
<p>实现io包的抽象接口</p>
<ol>
<li>os.File， 直接与操作系统交互，提供最底层的文件描述符（*os.File）操作。方法Open、Create、Close、Stat、Chmod、Truncate等。每次读写都是系统调用。</li>
<li>net.Conn, 提供了网络IO的Read(b []byte) (n int, err error)等接口, net.TCPConn、net.UDPConn 等具体实现</li>
<li>bufio，提供了缓冲io，一般需要os.File或net.Conn的构造</li>
</ol>
<p>tcp实现的Read, Write接口</p>
<pre><code class="language-go">// src/net/net.go
func (c *conn) Read(b []byte) (int, error) {
    // 调用 netFD（网络文件描述符）的 Read 方法
    n, err := c.fd.Read(b)
    return n, err
}

// src/internal/poll/fd_unix.go
func (fd *FD) Read(p []byte) (int, error) {
    for {
        n, err := syscall.Read(fd.Sysfd, p)
        if err != nil {
            // 处理 EAGAIN（非阻塞模式下无数据）
        }
        return n, err
    }
}
</code></pre>
<p>os.File和bufio</p>
<pre><code class="language-go">// 用file构造bufio.Reader,形成带缓冲的读取
file, _ := os.Open("data.txt")
reader := bufio.NewReader(file)         // 默认 4096 字节缓冲
// 或自定义缓冲区大小
reader = bufio.NewReaderSize(file, 8192) // 8KB 缓冲
// 用file构造bufio.Writer,形成带缓冲的写入
file, _ := os.Create("output.txt")
writer := bufio.NewWriter(file)
</code></pre>
<p>打开文件, os.Open, os.OpenFile</p>
<pre><code class="language-go">file, err := os.Open("data.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close() // 确保文件关闭

file, err := os.Create("output.txt")
defer file.Close()

file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0644)
</code></pre>
<p>读取文件,  os.ReadFile, bufio.NewScanner(file), file.Read(buffer)</p>
<pre><code class="language-go">// os.ReadFile读取全部内容
data, err := os.ReadFile("data.txt") // Go 1.16+
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

// 利用bufio 包装file逐行读取
file, _ := os.Open("data.txt")
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

// 利用file.Read分块读取
file, _ := os.Open("data.txt")
buffer := make([]byte, 4096)
for {
    n, err := file.Read(buffer)
    if err == io.EOF {
        break
    }
    process(buffer[:n])
}
</code></pre>
<p>写入文件os.WriteFile, file.WriteString, writer.WriteString</p>
<pre><code class="language-go">// 写入字节到文件
data := []byte("Hello, Go!\n")
err := os.WriteFile("output.txt", data, 0644)

// 追加写入字符串到文件
file, _ := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0644)
defer file.Close()

_, err := file.WriteString("New log entry\n")

// 缓冲写入
writer := bufio.NewWriter(file)
_, err := writer.WriteString("Buffered data\n")
writer.Flush() // 确保数据写入磁盘
</code></pre>
<p>序列化数据到文件</p>
<pre><code class="language-go">type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

user := User{"Alice", 30}
data, _ := json.Marshal(user)
os.WriteFile("user.json", data, 0644)
</code></pre>
<p>golang fmt格式化</p>
<pre><code class="language-go">// 参数
%v	值的默认格式表示
%+v	带字段名的结构体表示

%c	对应 Unicode 码点的字符
%d	十进制表示
%x	十六进制（小写字母）
%X	十六进制（大写字母）

%s	字符串
%.nf: 指定小数点后 n 位
%m.nf 设置宽度和小数位数

fmt.Printf("My name is %s and I am %d years old.\n", name, age)  // 格式化输出
fmt.Fprint(os.Stdout, "Hello, world!")  // 将格式化的输出写入到指定的 io.Writer
fmt.Fprintf(file, "Hello, file!")

fmt.Scanf("%s %d", &#x26;name, &#x26;age)  // fmt.Scanf() 格式化输入
fmt.Scanln(&#x26;name)

result := fmt.Sprintf("Name: %s, Age: %d", name, age)  // 字符串格式化，返回新字符串
fmt.Sscanf(input, "%s %d", &#x26;name, &#x26;age)
</code></pre>
<h4>网络库</h4>
<p>Go 语言的 net 包是标准库中用于网络编程的核心模块，提供了简洁高效的 API 支持 TCP/IP、UDP、HTTP 等协议。net/http和net/rpc分别支持http和rpc协议。</p>
<pre><code class="language-go">import "net"
// 客户端
func main() {
	// 连接到 TCP 服务器
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error dialing:", err)
		os.Exit(1)
	}
	defer conn.Close()

	// 向服务器发送数据
	_, err = conn.Write([]byte("Hello, Server!"))
	if err != nil {
		fmt.Println("Error sending message:", err)
		return
	}

	// 读取服务器响应
	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("Error reading response:", err)
		return
	}

	// 打印接收到的响应
	fmt.Printf("Received: %s\n", string(buffer[:n]))
}

// 服务端
func main() {
	// 在本地启动一个 TCP 服务器，监听端口 8080
	listen, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error starting server:", err)
		os.Exit(1)
	}
	defer listen.Close()

	fmt.Println("Server started, waiting for connections...")

	// 接受客户端连接
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}
		defer conn.Close()

		// 读取客户端消息
		buffer := make([]byte, 1024)
		n, err := conn.Read(buffer)
		if err != nil {
			fmt.Println("Error reading from connection:", err)
		}

		// 输出接收到的消息
		fmt.Printf("Received: %s\n", string(buffer[:n]))

		// 向客户端发送响应
		_, err = conn.Write([]byte("Hello, Client!"))
		if err != nil {
			fmt.Println("Error sending response:", err)
		}
	}
}
</code></pre>
<h4>flag 命令行解析</h4>
<pre><code class="language-go">func main() {
	// 定义命令行参数
	var name string
	var age int
	var isAdmin bool

	// 通过 flag 包解析命令行标志
	flag.StringVar(&#x26;name, "name", "Guest", "Your name")
	flag.IntVar(&#x26;age, "age", 18, "Your age")
	flag.BoolVar(&#x26;isAdmin, "admin", false, "Is admin?")

	flag.Parse()

	fmt.Printf("Name: %s\n", name)
	fmt.Printf("Age: %d\n", age)
	fmt.Printf("Is Admin: %v\n", isAdmin)
}

// 执行, go run main.go -name=John -age=30 -admin=true
</code></pre>
<h3>Python</h3>
<h4>文件IO</h4>
<p>python 提供open, file, read, write, close 等原生操作文件的方法</p>
<pre><code class="language-python"># 打开文件
# 'r'：只读模式，文件必须存在。
# 'w'：写入模式，若文件已存在则覆盖，不存在则创建。
# 'a'：追加模式，在文件末尾追加内容，若文件不存在，则创建。
# 'b'：二进制模式（如读取或写入图片、音频等）
file = open('filename', mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

with open('example.txt', 'r') as file:
    content = file.read()  # 读取全部内容
    print(content)
    
    file.seek(0)  # 文件指针移动到开头
    first_10_chars = file.read(10)  # 读取前10个字符
    print(first_10_chars)

    print(file.tell())  # 输出文件指针当前位置

    file.write("Hello, World!\n")  # 写入内容

    lines = ['Line 1\n', 'Line 2\n', 'Line 3\n']
    file.writelines(lines)  # 写入多行
    file.flush()  # 立即写入数据
</code></pre>
<p>python的os 模块提供文件元数据操做的方法，如remove, stat, rename, remove, mkdir</p>
<pre><code class="language-python">os.remove('example.txt')  # 删除文件
# 获取文件状态信息
stat_info = os.stat('example.txt')
# 创建目录
os.mkdir('new_directory')
</code></pre>
<p>print() 函数用于标准输出, 支持格式化输出</p>
<pre><code class="language-python">name = "Alice"
age = 25
print(f"My name is {name}, and I am {age} years old.")
print("My name is {}, and I am {} years old.".format(name, age))
</code></pre>
<p>input()用于标准输入， input读到的类型都是字符串, 需要转换为对应类型</p>
<pre><code class="language-python">name = input("Enter your name: ")
print(f"Hello, {name}!")

age = int(input("Enter your age: "))
</code></pre>
<h4>argparse 命令行参数解析</h4>
<pre><code class="language-python">import argparse

# 创建 ArgumentParser 对象
parser = argparse.ArgumentParser(description="A simple command-line tool")

# 添加参数
parser.add_argument('name', type=str, help="Your name")
parser.add_argument('age', type=int, help="Your age")
parser.add_argument('--admin', action='store_true', help="Are you an admin?")

# 解析命令行参数
args = parser.parse_args()

# 使用参数
print(f"Name: {args.name}")
print(f"Age: {args.age}")
print(f"Admin: {args.admin}")
</code></pre>
<h4>json 序列化</h4>
<pre><code class="language-python">import json
data = {
    "name": "Alice",
    "age": 25,
    "city": "New York"
}

# 打开文件并写入 JSON 数据
with open("data.json", "w") as json_file:
    json.dump(data, json_file, indent=4)
# 从文件中读取 JSON 数据
with open("data.json", "r") as json_file:
    data = json.load(json_file)
# 对象编码
json_string = json.dumps(data, indent=4)
# 对象解码
data = json.loads(json_string)
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[存储——缓存简谈]]></title>
          <link>https://larrystd.github.io/posts/存储—缓存简谈</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/存储—缓存简谈</guid>
          <pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[缓存存放的是临时数据，相比主存，缓存往往速度更快，容量更小。]]></description>
          <content:encoded><![CDATA[<p>缓存存放的是临时数据，相比主存，缓存往往速度更快，容量更小。</p>
<p>缓存往往是多级的，由内而外，cpu cache -> 内存 -> ssd -> hdd（单机） -> 服务器/多节点/oss/nas等</p>
<p>缓存需要淘汰，因此缓存往往由链表组成，根据LRU算法淘汰</p>
<p><strong>只要有缓存就会有一致性问题</strong></p>
<h3>写缓存</h3>
<p>写缓存有两种策略，write back和write through。</p>
<p>write back情况下，缓存不会立刻更新到主存，这让write back是单对象的行为。如果是多对象操作缓存，write back必然存在缓存不可见问题。例如page cache，一个进程的写入对另一个进程可能不可见。</p>
<p><strong>一般选择的写缓存策略是write through</strong> ，相当于没有写缓存。或者选择一写，即写操作转发给一个节点执行，过程通过lease协调，避免写冲突和减少复杂性</p>
<h3>读缓存</h3>
<p>为了一致性，读写一般使用同一个缓存。<strong>读缓存需要考虑的就是缓存失效问题。</strong> 读缓存需要处理，如果写操作导致大量客户端读缓存失效，产生的缓存穿透问题。</p>
<p>nfs3 close-and-open一致性表示，文件close后，写缓存刷到后台，文件open后，本地读缓存全部失效，需要从后台重新拿。此外，nfs3通过ctime表示缓存有效性，每次读操作nfs3 都会向后台执行getattr，如果ctime和缓存一致，则直接读本地，否则读后台。</p>
<p>在有lease功能后，lease 的版本id，起到ctime的作用。数据写入服务端会lease++，客户端每次读操作会从后台获取lease，如果lease 和本地一致，则直接读本地，否则本地读缓存失效，读后台。</p>
<p>缓存在生效前需要先预热，也就是预读</p>
<ol>
<li>读缓存需要发挥提高读性能的作用</li>
<li>缓存失效避免大规模请求打到后端</li>
<li>缓存预读、更新上的优化</li>
</ol>
<h3>缓存的更新</h3>
<p>缓存本质是KV map，需要加锁维护。缓存可能遇到大量请求同时更新，更新时的加锁可能严重影响性能。</p>
<p>缓存更新有两种</p>
<ol>
<li>缓存key存在，value失效了需要更新，这种不需要加锁，很快速</li>
<li>缓存key被淘汰了/不存在，需要插入，这种更新需要加锁，比较重</li>
</ol>
<p>缓存还可以根据状态更新，典型的是cpu cacheline的MSI协议（Modified, Shared和Invalid）。这个要求每个cache line能感知到其他cache line发生写操作，同时能通知其他cache line写内存。例如cpu1能感知到cpu2发生了写cache操作，于是更新自己状态为unvalid，并让cpu2 刷到内存。java的valitale 关键字就是通过让cpu将对变量的修改直接刷内存</p>
<p>使用范围很小。</p>
<h3>缓存穿透，缓存雪崩</h3>
<p>读缓存场景，如果数据被写入会导致所有客户端读缓存失效，瞬时压力可能给到服务器。</p>
<p>如果只是写操作导致的缓存失效，则缓存穿透也可以理解
如果是过期引起的缓存失效，使用LRU同时防止缓存大规模同时过期</p>
<p>主存前面可以加队列和流控来缓解压力，架构设计上<strong>一定要避免缓存失效大量请求打到后端服务器</strong>。</p>
<p>流控可以考虑权重公平流控，后台统计每个client的请求数量，当总数量达到阈值，通过权重计算得到每个client的能放进去的请求数量（最小值和最大值，也不能没请求）</p>
<h3>4k对齐</h3>
<p>IO场景的缓存多是对齐的，例如page cache是4K对齐</p>
<p>write through策略，每笔写入都会写到后台，即使不是4K写。
客户端读缓存也设置成4K对齐的，使page cache可以命中</p>
<p>自定义fuse客户最好自行管理缓存</p>
<h3>文件元数据缓存</h3>
<p>主要的元数据缓存</p>
<ol>
<li>inode缓存，缓存inode 元数据，atime, ctime, mtime, link, mode, size等信息，这个缓存会经常更新</li>
<li>inode 数据缓存，</li>
<li>dentry缓存，缓存目录项存的文件名，方便readdir，文件创建删除时需要更新</li>
</ol>
<p>ganesha 元数据缓存，
genasha每个目录项会缓存目录中子文件name->filehandle映射，目的是方便lookup。如果lookup某个name缓存未命中， 会持有目录写锁 访问后台，拿到（name, filehandle)后释放目录写锁。
这样的原因是不给后台太多压力，防止多个请求同时打向后台（加了写锁只有一个lookup请求能打到后台），问题是产生了lookup串行化影响性能。</p>
<p>缓存应该有预读的功能，即缓存失效后尽量从后台拿到局部性多的数据。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[storage]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言——静态变量和静态函数]]></title>
          <link>https://larrystd.github.io/posts/编程语言—静态变量和静态函数</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言—静态变量和静态函数</guid>
          <pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[简谈编程语言中的静态变量和函数]]></description>
          <content:encoded><![CDATA[<p>简谈编程语言中的静态变量和函数</p>
<h3>C++的静态变量/函数</h3>
<p>C++中，静态变量生命周期贯穿程序，在程序开始运行时分配内存并初始化，程序结束运行时释放内存。</p>
<ol>
<li>在继承结构中，如果子类未定义同名静态变量，父类和子类引用相同的静态变量，父类和子类静态变量地址一样；如果子类定义了同名的静态变量，那么父类和子类具有独立的静态变量。</li>
</ol>
<pre><code class="language-cpp">#include &#x3C;iostream>

class Base {
public:
    static int count; // 静态变量声明
};

int Base::count = 0; // 静态变量定义和初始化

class Derived : public Base {
    // 派生类可以访问静态变量
};

int main() {
    Base::count = 5;
    std::cout &#x3C;&#x3C; "Base::count: " &#x3C;&#x3C; Base::count &#x3C;&#x3C; std::endl;
    std::cout &#x3C;&#x3C; "Derived::count: " &#x3C;&#x3C; Derived::count &#x3C;&#x3C; std::endl;

    std::cout &#x3C;&#x3C; "Base::count address: " &#x3C;&#x3C; &#x26;Base::count &#x3C;&#x3C; std::endl;
    std::cout &#x3C;&#x3C; "Derived::count address: " &#x3C;&#x3C; &#x26;Derived::count &#x3C;&#x3C; std::endl;
    return 0;
}

// 输出
Base::count: 5
Derived::count: 5
Base::count address: 0x55a3f58cf154
Derived::count address: 0x55a3f58cf154
</code></pre>
<ol start="2">
<li>全局静态变量在.cpp文件级别声明，只在定义它的文件中可见，其他.cpp无法通过extern声名引用。除了静态变量，C++全局变量的生命周期也和程序相同，全局变量可以被其他.cpp文件引用，链接时会使相关.cpp文件引用相同的全局变量。</li>
</ol>
<pre><code class="language-cpp">
// file1.cpp
#include &#x3C;iostream>

static int global_var = 10; // 文件级静态变量

void display() {
    std::cout &#x3C;&#x3C; "Global variable: " &#x3C;&#x3C; global_var &#x3C;&#x3C; std::endl;
}

// file2.cpp
#include &#x3C;iostream>
// extern int global_var; // 错误，无法访问 file1.cpp 的静态变量
int main() {
    // display(); // 如果 display 定义在 file1.cpp，可以调用
    return 0;
}
</code></pre>
<ol start="3">
<li>C++静态函数和上面的静态变量类似，
<ol>
<li>如果子类未定义同名静态函数，父类和子类引用相同的静态函数，父类和子类静态变量地址一样；如果子类定义了同名的静态函数，那么父类和子类具有独立的静态函数。</li>
<li>全局静态函数在.cpp文件级别声明，只在定义它的文件中可见，其他.cpp无法通过extern声名引用。C++全局函数的生命周期也和程序相同，全局函数可以被其他.cpp文件引用，链接时会使相关.cpp文件引用相同的全局变量。</li>
</ol>
</li>
</ol>
<p>静态变量/函数的符号未被导出，其他文件无法直接引用它。这是静态函数的设计初衷，用来实现模块化和封装。</p>
<pre><code class="language-cpp">#include &#x3C;iostream>

class Base {
public:
    static void display() {
        std::cout &#x3C;&#x3C; "Base static function" &#x3C;&#x3C; std::endl;
    }
};

class Derived : public Base {
public:
    // 派生类继承了 Base 的静态函数
};

int main() {
    // 通过基类调用
    Base::display(); // 输出：Base static function

    std::cout &#x3C;&#x3C; "Base::display address: " &#x3C;&#x3C;reinterpret_cast&#x3C;void*>(&#x26;Base::display) &#x3C;&#x3C; std::endl;

    // 通过派生类调用
    Derived::display(); // 输出：Base static function
    std::cout &#x3C;&#x3C; "Derived::display address: " &#x3C;&#x3C;reinterpret_cast&#x3C;void*>(&#x26;Derived::display) &#x3C;&#x3C; std::endl;
    // 通过派生类的对象调用
    Derived d;
    d.display(); // 输出：Base static function

    return 0;
}

// 输出
Base static function
Base::display address: 0x55a1967b42bf
Base static function
Derived::display address: 0x55a1967b42bf
Base static function
</code></pre>
<ol start="4">
<li>C++静态函数不能配置成虚函数virtual, const和volatile
<ol>
<li>static函数目的是编译时绑定，而virtual函数要求运行时绑定，二者不可兼容。虚函数表里也不会记录static函数</li>
<li>const 类成员函数表示该函数不能被修改类成员变量，static函数不属于对象，本质通过类名调用，不能配置成const函数</li>
<li>静态变量可以被const, volatile修饰</li>
</ol>
</li>
</ol>
<h3>JAVA的静态变量和函数</h3>
<p>JAVA 静态变量和函数的逻辑和C++类似，不一样的使JAVA的类加载机制。JAVA除了静态变量和静态函数的概念，还有静态代码块的概念。<strong>类加载时，会初始化静态变量和执行静态代码块内容</strong>。</p>
<p>但不是每个类都会被加载，有两种加载类的条件</p>
<ol>
<li>主动使用类时，如：创建类实例（new操作）。调用类的静态方法。访问类的静态字段。使用Class.forName()加载类。</li>
<li>子类初始化时，父类会被初始化。</li>
</ol>
<p>C++不需要加载类，因为编译完C++代码就没有类的信息了，更不用说加载类。</p>
<p>通过子类引用父类的静态字段： 只会初始化父类，而不会初始化子类。</p>
<pre><code class="language-java">class Parent {
    static {
        System.out.println("Parent initialized.");
    }
    static int value = 42;
}

class Child extends Parent {
    static {
        System.out.println("Child initialized.");
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Child.value); // 只输出 Parent initialized.
    }
}

// 输出
Parent initialized.
42
</code></pre>
<p><strong>常量在编译期已确定，不会触发类加载。</strong></p>
<pre><code class="language-java">class Example {
    static {
        System.out.println("Example initialized.");
    }
    static final int CONSTANT = 42;
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Example.CONSTANT); // 不会触发类加载
    }
}

// 输出
42
</code></pre>
<p>JAVA类函数默认所有实例方法都是虚函数，支持多态，final、static、private方法除外。但静态函数不支持多态。</p>
<h3>Golang 的全局函数/变量</h3>
<p>Go语言中，没有像C/C++或Java中明确的“静态变量”概念。</p>
<p>全局变量在Go中可以用于实现类似静态变量的功能。全局变量, 作用域为整个包。如下函数中，它的值在每次调用 increment 函数后都会保留。</p>
<pre><code class="language-go">package main

import "fmt"

var counter int // 全局变量

func increment() int {
    counter++
    return counter
}

func main() {
    fmt.Println(increment()) 
    fmt.Println(increment()) 
}

// 输出
1
2
</code></pre>
<p>Go的标准库提供了 sync.Once，可以确保某段代码只执行一次（类似静态初始化）。</p>
<pre><code class="language-go">package main

import (
    "fmt"
    "sync"
)

var once sync.Once
var config string

func initConfig() {
    once.Do(func() {
        fmt.Println("Initializing config...")
        config = "LoadedConfig"
    })
}

func main() {
    initConfig()
    fmt.Println(config)

    initConfig() // 不会再初始化
}
// 输出
Initializing config...
LoadedConfig
</code></pre>
<p>组合对象</p>
<pre><code class="language-go">package main

import "fmt"

// 定义结构体
type Person struct {
	Name string
	Age  int
}

// 嵌入结构体
type Employee struct {
	Person   // 匿名字段（嵌入字段）
	Position string
}

func main() {
	per := Person{Name: "Alice", Age: 20}
	emp := Employee{
		Person:   per,
		Position: "Developer",
	}
	fmt.Println(emp.Name)
	fmt.Println(emp.Age)
	fmt.Println(emp.Position)
}

// 输出
Alice
20
Developer
</code></pre>
<h3>Python的类函数和静态函数</h3>
<p>Python 没有对变量的静态修饰，只有对函数的classmethod和staticmethod修饰</p>
<p>类函数的第一个参数是 cls，表示调用该方法的类本身，而不是实例。类函数可以访问类的属性和方法，不能直接操作实例属性。类函数通过类名调用</p>
<p>Python在类内部直接定义的变量是类变量，比如下面的static_var，self定义的变量是实例变量。前者可以通过cls访问。</p>
<pre><code class="language-py">class MyClass:
    static_var = 0

    def __init__(self):
        MyClass.static_var += 1  # 每次创建实例时修改类变量

    @classmethod
    def get_static_var(cls):
        return cls.static_var

a = MyClass()
b = MyClass()
print(MyClass.static_var)  # 输出: 2
print(a.get_static_var())  # 输出: 2
</code></pre>
<p>静态方法不需要传递 self 或 cls 参数。不能访问类或实例的属性，只能执行独立的功能。同样通过类名调用。</p>
<pre><code class="language-py">class MyClass:
    @staticmethod
    def static_method():
        print("This is a static method.")

# 调用
MyClass.static_method()
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[系统——并发简谈]]></title>
          <link>https://larrystd.github.io/posts/系统—并发简谈</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/系统—并发简谈</guid>
          <pubDate>Fri, 20 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[并发可分成并发对象和并发协调两部分。并发对象可以是进程、线程、协程、以及多机并发。并发协调包括锁（进程锁、线程锁、协程锁、分布式锁），信号，信号量，条件变量，通信队列等。]]></description>
          <content:encoded><![CDATA[<p>并发可分成并发对象和并发协调两部分。并发对象可以是进程、线程、协程、以及多机并发。并发协调包括锁（进程锁、线程锁、协程锁、分布式锁），信号，信号量，条件变量，通信队列等。</p>
<p>并发编程的三个特点是<strong>原子性、有序性、可见性</strong>。<strong>原子性要求写操作要么执行完要么不执行，不能中途执行一半被干扰，并发编程中需要保证所有操作都是原子的</strong>；完全要求顺序的程序无法并发，因此并发编程的特点是原子性+部分有序。<strong>可见性是针对并发读共享变量</strong>，如果并发对象之间读共享变量，就要维护共享变量的可见性，一个并发对象修改另一个并发对象可以看到。（如果只有并发写，只要考虑原子性和顺序性，不需要可见性）</p>
<h2>并发对象</h2>
<p>传统（或者普遍）的程序执行是串行顺序的，<strong>如果把互不依赖的逻辑由单个串行改成多个并行，就产生了并发。</strong></p>
<p>常见的并发场景</p>
<ol>
<li>网络访问，例如http请求、mysql连接池，创建多个链接发送接收数据，链接之间互不影响</li>
<li>文件IO，多个线程写不同文件，线程之间互不影响。如果多线程写相同文件，线程间可能相互影响，需要对并发进行优化</li>
<li>为了利用多核能力，程序创建多进程/线程，并行计算</li>
<li>生产者消费者模型（任务队列模型），多线程从任务队列拿任务处理，任务之间互不影响</li>
</ol>
<h3>协程</h3>
<p>多线程需要处理数据更新的竞争问题，例如向数据库更新一个key，可能遇到不同线程尝试更新到不同值。即使不考虑顺序性，但由于更新key无法保证原子性，还是需要加锁维护。一个解决办法是，将key的操作绑定到线程池指定线程，也就是说，对于key的更新只交给一个固定的线程做。又比如文件系统，如果保证每个文件始终由固定的线程处理，那文件的每个io操作就天然具备了原子性。</p>
<p><strong>数据密集型应用中，执行流的耗时主要来自IO和网络</strong>。如果我们让固定的线程处理某文件的io，该线程大部分时间都会在睡眠等待数据落盘和磁盘取数据。办法是在单线程之上进行多协程调度，一旦出现IO等待，自动切换协程。这样<strong>理想情况下，线程一直执行计算和内存操作。</strong> 单线程的最高QPS就是1/（一次计算和内存的耗时）。</p>
<p>虽然golang 实现了协程自动调度，让程序员像使用线程一样方便的使用协程（golang协程使用上和线程没有区别）。但协程的手动调度还是必要存在的。**手动调度的好处是，对需要原子性的操作，保证执行完且执行期间不会协程切换。**例如更新数据表期间，协程不会切走（切走其他协程就可能修改该数据->就需要加锁）</p>
<p>协程避免回调地狱。调用一次await，函数会自动放弃的执行权，执行权交由另外随机的协程。协程避免回调地狱前提是多个函数是可以异步调用的，如果必须等待回调函数返回才能执行下一个回调，那只能用线性回调。</p>
<p>单线程多协程模型是并发同时减少锁争抢的有效模型，例如作为中央服务的服务端，可以用单线程处理某个client/session独占请求和数据，从而提供了原子性，不用对client/session独占的请求/数据加锁。</p>
<p>协程的编程模型</p>
<ol>
<li>将逻辑分为处理层、IO层、回调层。每层维护一个线程池，对于相同client请求，每层由固定的线程执行。处理层线程只处理非IO逻辑，最后把IO放到IO层；IO层协程其实是负责监听IO完成，完成后交给回调层处理回调函数。相同client请求由于每层都由固定的线程处理，不用加锁。同时单线程利用协程实现并发处理client的大量请求</li>
<li>采用async/await编程模型，利用同步的编程手法实现类似1的单线程多协程并发，但不用配置大量的回调函数</li>
</ol>
<p>await 后面跟无线程阻塞的异步函数，当协程执行到await时，会把当前协程注册事件到协程调度器（或把当前协程作为作为切换后协程的回调函数）。当await异步函数执行完毕，会通知给协程调度器，然后转而继续执行。</p>
<p>理想情况下，只有协程会阻塞，线程会一直执行处理请求的工作。线程不会进入操作系统的等待队列，从而大大提高cpu的使用率。协程模型中往往一个cpu创建一个线程就足够。</p>
<pre><code class="language-js">const fs = require('fs/promises');

async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt');
        const data2 = await fs.readFile('file2.txt');
        const data3 = await fs.readFile('file3.txt');
        console.log(data1.toString(), data2.toString(), data3.toString());
    } catch (err) {
        console.error(err);
    }
}

readFiles();
</code></pre>
<h3>多链接并发</h3>
<p>一个客户端创建多个链接，并发访问服务端数据。多链接请求比单链接好。</p>
<h2>并发协调</h2>
<h3>锁</h3>
<p>锁，可以抽象成两点</p>
<ol>
<li><strong>锁作用是对并发场景下共享资源竞争的保护。</strong></li>
<li><strong>获取锁也就是某并发对象获得对某资源的操作权</strong></li>
</ol>
<p>凡是某资源可能被多个对象操作，就要考虑使用锁来保护</p>
<p>线程锁，对多个线程操作共享资源的保护;
进程锁，对多个进程操作共享资源的保护;
分布式锁，叫机器锁容易理解，对多个机器操作共享资源的保护</p>
<p>单机文件系统，文件锁是一种进程锁; 多机共享文件系统，文件锁是一种分布式锁</p>
<p>lease也可以看做一种锁，获取lease 也是获取某对象的操作权</p>
<h4>死锁和处理</h4>
<p>持有锁就要考虑死锁，<strong>死锁产生的原因是申请释放锁顺序不一致</strong></p>
<ol>
<li><strong>如果对象不会发生，持有锁的情况申请锁，则不会有死锁</strong></li>
<li>对于锁序列A, B, C, D，如果多个对象有统一的加锁顺序，例如遵循加锁A释放、加锁B释放、加锁C释放，<strong>不会有环形加锁的逻辑，则不会产生死锁</strong>。类似是是文件系统树lookup pathwalk的顺序</li>
<li><strong>多对象加锁顺序不一致才会产生死锁</strong>
<ol>
<li>文件系统rename，hardlink 路径不是从根节点的pathwalk，可能会产生死锁。如rename需要对目录加写锁（原因是需要删除源目录文件，创建新目录文件，需要先对两个目录加锁，由于加锁顺序可以是A->B，B->A，会发生死锁），linux为了处理rename死锁，规定每个文件系统内每次只能进行一个 rename 操作。
<ol>
<li>hardlink需要对源文件加元数据锁，新hardlink文件的目录加写锁，linux处理hardlink死锁问题，规定link 的对象不能是目录</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>文件系统加锁原理</p>
<ol>
<li>写文件需加文件写锁，更新文件元数据加文件元数据写锁</li>
<li>创建文件和删除文件需加目录写锁</li>
<li>read, getattr, setattr需要对元数据加锁，因为需要更新元数据</li>
<li><strong>rename锁最重，需要对源和目的文件的目录加锁</strong></li>
</ol>
<p>lease也是一种锁，lease场景是多个机器试图操作同一服务器资源（例如多客户端试图操作共享资源</p>
<ol>
<li>lease由服务器管理，<strong>如果服务器运行正常，服务器可以撤销某客户端的lease锁</strong>，从而结束客户端的死锁状态</li>
<li>如果服务器崩溃，客户端可能处于死锁等待，这时候<strong>需要客户端设置超时，主动退避重试来避免死锁</strong></li>
<li>对于文件系统，通常情况下获取lease按照顺序，rename同样可能触发死锁，分析同上‘</li>
</ol>
<p>可以看到死锁的两种处理办法</p>
<ol>
<li>避免死锁，例如保证某操作全局唯一，采用单线程、无锁编程</li>
<li>处理死锁，超时主动/被动释放锁，退避重试</li>
</ol>
<h4>顺序性</h4>
<p>有时候还要求，从客户端无阻塞发出两个请求A->B，处理时也要按照A->B的顺序处理</p>
<p>常见的是在请求附加clientid和seqid，标注请求的顺序。</p>
<ol>
<li>服务端对某clientid，严格按照seqid顺序处理，这样强顺序会损害性能</li>
<li>客户端维护若干slot，slot内按照seqid顺序地址，将可以并发的请求通过多个slot并发发送。服务端对每个slotid 的请求顺序处理，slot外部不保证顺序</li>
<li>更宽松的，对于请求序列A,B,C，A请求失败了可以处理B请求，但B如果执行完了，不能再执行A。
，
例如操作write('a') -> read->write('a')(重试) -> write('abc')，如果遵循3，结果是read(''), write('abc')，如果执行了重试，结果则是write('aabc')。</li>
</ol>
<h4>版本序列应对一写多读场景锁</h4>
<p>版本序列是读写锁优化的有效手段</p>
<p>版本是一种缓存，可以是内存的ring。在有人持有写锁期间，可以利用版本读还未写入的数据，当写锁执行完毕，则更新版本。写操作是一种事务提交，这意味着，事务未提交成功之前，状态未改变，等同于没有事务。</p>
<h4>乐观锁应对多写场景锁</h4>
<ol>
<li>将原数据读到一个副本</li>
<li>对副本执行写操作，同时获得版本号/时间戳</li>
<li>原子提交，再次版本号/时间戳，如果一致，则提交；如果有问题，则重试1</li>
</ol>
<p>原子提交也可以换成简单操作，乐观锁的写不能太重，否则可能写期间被更新。</p>
<h4>互斥锁的实现</h4>
<p>spinlock，</p>
<ol>
<li>spinlock 主要是保护多cpu操作对象的安全，可以是cpu共享的硬件变量，为了防止死锁，加spinlock需要屏蔽当前cpu的中断</li>
</ol>
<p>mutex，存放在内存的共享变量，维护多线程的安全</p>
<p>中心化数据库/redis，zookeeper/etcd等中心化kv，实现分布式锁</p>
<h4>客户端锁和服务端锁</h4>
<p>熟悉的线程锁，进程锁是一种客户锁。在用户程序中加锁防止多线程并发修改共享变量。——如果操作系统保证每个对象只能被一个线程访问，也就是加服务端锁，就不需要客户端加锁了，当然由于性能不可能实现。</p>
<p>分布式锁保护共享变量可以在客户端层面实现，利用保证同一时刻只有一个客户端访问共享变量，则服务端不需要额外加锁。如果允许多客户端同时访问共享变量，服务端分布式锁的实现会异常复杂。</p>
<p><strong>lease也是通过客户端锁减轻服务端加锁的问题</strong>。分布式锁的损耗很大（ms级），如果经常访问的操作需要加分布式锁，会严重影响服务的性能。</p>
<p>库存，订单数量修改 加分布式锁的思路是将大量操作封装成事务，追求吞吐量。延迟要求高的操作应该避免服务加分布式锁，而是限制访问的客户端。</p>
<p><strong>分布式元数据的另一种实现是自行实现raft/poixs协议直接保证元数据集群的一致性，同时提供高性能KV存储</strong>，该方法性能比申请加分布式锁高。</p>
<p>gpfs 可以控制client和server，采用client 锁和服务锁结合的形式。例如写目录文件/rename，gpfs要求某时刻只能有一个client执行操作。但修改文件元数据应该可以允许多client操作。</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[base]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言(4)—函数和并发编程]]></title>
          <link>https://larrystd.github.io/posts/编程语言(4)—函数和并发编程</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言(4)—函数和并发编程</guid>
          <pubDate>Sat, 14 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[并发编程是提高程序处理能力的重要手段，也是编程语言必备的能力。并发的对象是函数，函数作为任务被多个并发计算对象执行。]]></description>
          <content:encoded><![CDATA[<p>并发编程是提高程序处理能力的重要手段，也是编程语言必备的能力。并发的对象是函数，函数作为任务被多个并发计算对象执行。</p>
<p>并发的单位可以是进程、线程和协程（用户态线程），并发需要保证的三个特点：原子性、顺序性和可见性。</p>
<ol>
<li>原子性表示操作要么全部成功，要么全部失败，执行中不能被中断。原子性通常使用锁（包括悲观锁、乐观锁）保证。</li>
<li>顺序性表示任务执行的顺序与任务提交时的顺序一致。顺序性通常使用队列（先进先出）保证，常见的是是生产者-消费队列保证顺序性，例如golang的channel。好的顺序设计，可以保证每个阶段只有少量线程执行，减少锁的使用。减少共享内存的使用，多用顺序性的信号通知，减少锁争抢。在分布式系统中，面对ABA问题，还会对操作和共享内存增加版本号来保证顺序性。</li>
<li>可见性表示对共享内存的修改对其他线程可见。常见的是使用内存屏障（memory barrier）保证可见性，内存屏障是CPU提供的一种同步机制，保证CPU执行到内存屏障之前的指令都执行完成，再执行内存屏障之后的指令。C++原子类的内存序就是在保证可见性。</li>
</ol>
<h3>C语言</h3>
<h4>pthread</h4>
<p>C语言中的多线程编程一般使用pthread库, pthread（POSIX线程）库是用于在类Unix系统中实现多线程编程的API实现了posix标准的线程接口。</p>
<p>pthread支持的接口</p>
<pre><code class="language-cpp">#include &#x3C;pthread.h>
// 线程创建，线程创建后立即执行，函数用void函数指针类型void *(*start_routine) (void *) 传参
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
void pthread_exit(void *retval); // 使当前线程退出，并返回一个状态。
int pthread_join(pthread_t thread, void **retval);  // 主线程等待子线程执行完毕。
pthread_t pthread_self(void);  // 返回当前线程的Id

// 初始化互斥锁, attr通常为NULL
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_lock(pthread_mutex_t *mutex); // 申请互斥锁. 返回错误码
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 释放互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);  // 销毁互斥锁, 互斥锁释放内存前需手动销毁

// 初始化条件变量, attr通常为NULL
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 阻塞当前线程，等待条件变量通知。线程唤醒后立即尝试申请锁
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);  // 唤醒一个等待条件变量的线程。
int pthread_cond_broadcast(pthread_cond_t *cond);  // 唤醒所有等待条件变量的线程。
int pthread_cond_destroy(pthread_cond_t *cond);  // 销毁条件变量

// 初始化线程属性对象
int pthread_attr_init(pthread_attr_t *attr);
// 设置线程PTHREAD_CREATE_DETACHED（分离线程）或 PTHREAD_CREATE_JOINABLE（可等待的线程）。
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_destroy(pthread_attr_t *attr);  // 销毁线程等待对象

// 设置cpu亲和力, 将线程绑定到指定的 CPU 核心上。
// cpu_set_t 是一个位图类型，每一位表示一个 CPU 核心。可以用CPU_SET(cpu, cpuset)将指定的 CPU 核心（cpu）添加到 cpuset 中
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset);
// 获取cpu亲和性
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset);

// 设置线程的调度策略和优先级
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);
// 获取线程的调度策略和优先级。
int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param);
</code></pre>
<p><code>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);</code> 需要传入锁, 表示条件变量等待函数需要先获取锁, 发现条件不满足, 才释放锁进入等待状态。这说明条件变量的设计目的是减少申请锁的冲突, 将不符合条件的线程进入等待，而不是让所有线程都竞争锁。（事实上锁竞争的线程也是忙等待，一般线程没有抢到锁会先执行一段时间的自旋等待，如果还没有抢到锁则进入锁等待队列, 释放锁的线程会唤醒处于锁等待状态的线程）</p>
<p>如果要单纯的等待-通知, 不用使用锁，可以使用信号</p>
<pre><code class="language-cpp">#include &#x3C;sys/signalfd.h>

sigset_t mask;
sigaddset(&#x26;mask, SIGUSR1);
int sfd = signalfd(-1, &#x26;mask, SFD_NONBLOCK);

// 线程等待信号
struct signalfd_siginfo info;
read(sfd, &#x26;info, sizeof(info)); // 阻塞直到收到信号
</code></pre>
<p>pthread利用锁、条件变量和数组模拟的环形缓冲区实现生产者消费者模型</p>
<pre><code class="language-cpp">#include &#x3C;pthread.h>
#include &#x3C;stdio.h>
#include &#x3C;stdlib.h>
#include &#x3C;unistd.h>

#define BUFFER_SIZE 10  // 缓冲区大小

// 定义缓冲区和相关变量
int buffer[BUFFER_SIZE];
int in = 0;  // 指向缓冲区的生产者插入位置
int out = 0; // 指向缓冲区的消费者取出位置
pthread_mutex_t mutex;  // 互斥锁
pthread_cond_t empty;   // 条件变量，表示缓冲区非空
pthread_cond_t full;    // 条件变量，表示缓冲区非满

// 生产者线程函数
void* producer(void* arg) {
    int item;
    while (1) {
        item = rand() % 100;  // 生成一个随机项

        pthread_mutex_lock(&#x26;mutex);  // 加锁，访问共享缓冲区

        // 如果缓冲区满了，等待消费者消费
        while ((in + 1) % BUFFER_SIZE == out) {
            pthread_cond_wait(&#x26;empty, &#x26;mutex);  // 等待缓冲区有空位
        }

        // 将项放入缓冲区
        buffer[in] = item;
        printf("Producer produced: %d\n", item);
        in = (in + 1) % BUFFER_SIZE;  // 更新生产者插入位置

        // 唤醒消费者线程
        pthread_cond_signal(&#x26;full);

        pthread_mutex_unlock(&#x26;mutex);  // 解锁

        sleep(1);  // 模拟生产的时间
    }
    return NULL;
}

// 消费者线程函数
void* consumer(void* arg) {
    int item;
    while (1) {
        pthread_mutex_lock(&#x26;mutex);  // 加锁，访问共享缓冲区

        // 如果缓冲区为空，等待生产者生产
        while (in == out) {
            pthread_cond_wait(&#x26;full, &#x26;mutex);  // 等待缓冲区有数据
        }

        // 从缓冲区取出项
        item = buffer[out];
        printf("Consumer consumed: %d\n", item);
        out = (out + 1) % BUFFER_SIZE;  // 更新消费者取出位置

        // 唤醒生产者线程
        pthread_cond_signal(&#x26;empty);

        pthread_mutex_unlock(&#x26;mutex);  // 解锁

        sleep(2);  // 模拟消费的时间
    }
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&#x26;mutex, NULL);
    pthread_cond_init(&#x26;empty, NULL);
    pthread_cond_init(&#x26;full, NULL);

    // 创建生产者和消费者线程
    pthread_create(&#x26;producer_thread, NULL, producer, NULL);
    pthread_create(&#x26;consumer_thread, NULL, consumer, NULL);

    // 等待线程结束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&#x26;mutex);
    pthread_cond_destroy(&#x26;empty);
    pthread_cond_destroy(&#x26;full);

    return 0;
}

// 输出
Producer produced: 83
Consumer consumed: 83
Producer produced: 86
Consumer consumed: 86
Producer produced: 77
Producer produced: 15
Consumer consumed: 77
Producer produced: 93
...
</code></pre>
<p>线程cpu绑核，</p>
<pre><code class="language-cpp">#include &#x3C;pthread.h>
#include &#x3C;stdio.h>
#include &#x3C;stdlib.h>
#include &#x3C;sched.h>
#include &#x3C;unistd.h>

void* thread_function(void* arg) {
    cpu_set_t mask;
    pthread_t thread = pthread_self();

    // 获取当前线程的 CPU 亲和性
    int result = pthread_getaffinity_np(thread, sizeof(cpu_set_t), &#x26;mask);
    if (result != 0) {
        perror("pthread_getaffinity_np");
        exit(EXIT_FAILURE);
    }

    // 输出当前线程可以运行的 CPU 核心
    for (int i = 0; i &#x3C; CPU_SETSIZE; i++) {
        if (CPU_ISSET(i, &#x26;mask)) {
            printf("Thread can run on CPU core: %d\n", i);
        }
    }

    return NULL;
}

int main() {
    pthread_t thread;
    cpu_set_t cpuset;

    // 创建线程
    pthread_create(&#x26;thread, NULL, thread_function, NULL);

    // 设置线程绑定到 CPU 核心 0 和 1
    CPU_ZERO(&#x26;cpuset);
    CPU_SET(0, &#x26;cpuset);  // 允许线程在 CPU 核心 0 上运行
    CPU_SET(1, &#x26;cpuset);  // 允许线程在 CPU 核心 1 上运行

    // 设置线程的 CPU 亲和性
    int result = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &#x26;cpuset);
    if (result != 0) {
        perror("pthread_setaffinity_np");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束
    pthread_join(thread, NULL);

    return 0;
}
// 输出
Thread can run on CPU core: 0
Thread can run on CPU core: 1
</code></pre>
<p>线程调度策略,</p>
<ol>
<li>SCHED_FIFO：先来先服务(First-In-First-Out)实时调度,</li>
<li>SCHED_RR：轮转调度（Round-Robin）实时调度，</li>
<li>SCHED_OTHER默认的操作系统决定调度线程</li>
</ol>
<pre><code class="language-cpp">#include &#x3C;pthread.h>
#include &#x3C;stdio.h>
#include &#x3C;stdlib.h>
#include &#x3C;sched.h>

void* thread_function(void* arg) {
    printf("Thread running\n");
    return NULL;
}

int main() {
    pthread_t thread;
    struct sched_param param;
    int policy;

    // 创建线程
    pthread_create(&#x26;thread, NULL, thread_function, NULL);

    // 设置调度策略为 SCHED_FIFO，并设置优先级为 10
    param.sched_priority = 10;
    pthread_setschedparam(thread, SCHED_FIFO, &#x26;param);

    // 获取并输出当前线程的调度策略和优先级
    pthread_getschedparam(thread, &#x26;policy, &#x26;param);
    printf("Thread policy: %d, Priority: %d\n", policy, param.sched_priority);

    pthread_join(thread, NULL);  // 等待线程结束
    return 0;
}

// 输出
Thread policy: 1, Priority: 10
Thread running
</code></pre>
<h4>原子操作</h4>
<p>编程语言的赋值一般是原子的，但算术操作不是原子的（需要访存、操作、写回三个操作）。c语言可以直接使用gcc提供的内建原子操作函数, 原子函数在操作函数会加内存屏障，保证常见操作的原子性、可见性。</p>
<ol>
<li>__sync_fetch_and_add, 原子地将一个值加到目标变量，返回旧值。</li>
<li>__sync_fetch_and_sub：原子地目标变量减去一个值，返回旧值。</li>
<li>__sync_lock_test_and_set：原子地设置一个值，返回旧值。</li>
</ol>
<p>原子函数的实现原理</p>
<ol>
<li>默认包含一个 ​​完整的顺序一致性屏障（Full Memory Barrier）​​，确保操作前的所有内存访问（读/写）在原子操作前完成:操作后的所有内存访问（读/写）在原子操作后开始。也就是是原子操作时可以读到最新的旧值, 原子操作后设置的新值其他线程可见</li>
<li>硬件支持，在 x86/x64 架构中，原子操作通常有对应的硬件指令，例如 xchg（Exchange）指令（带 lock 前缀），确保操作的原子性。</li>
</ol>
<p>虽然赋值操作是原子的，但C和C++没有java volatile 关键字保证可见性, 因此赋值操作还是要用原子操作__sync_lock_test_and_set。java的volatile关键字可以保证赋值操作的可见性，无须加锁。</p>
<pre><code class="language-cpp">#include &#x3C;stdio.h>
#include &#x3C;pthread.h>

int counter = 0;  // 用于计数的共享变量

void* increment(void* arg) {
    for (int i = 0; i &#x3C; 1000; i++) {
        __sync_fetch_and_add(&#x26;counter, 1);  // 原子地增加 1
    }
    return NULL;
}

int main() {
    pthread_t threads[10];

    // 创建 10 个线程
    for (int i = 0; i &#x3C; 10; i++) {
        pthread_create(&#x26;threads[i], NULL, increment, NULL);
    }

    // 等待线程结束
    for (int i = 0; i &#x3C; 10; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("Counter value: %d\n", counter);  // 输出最终值

    return 0;
}
// 输出10000
</code></pre>
<p>GCC 4.7+ 引入了更灵活的 __atomic 系列函数，允许显式控制内存顺序：</p>
<pre><code class="language-cpp">// __atomic 版本（可指定内存顺序）
type __atomic_exchange_n(type *ptr, type value, int memorder);
</code></pre>
<p>__thread, 是gcc提供的线程局部存储，每个线程操作自己的变量副本，互不影响。</p>
<ol>
<li>只能用于全局和静态变量</li>
<li>​静态初始化​​：变量必须在编译时初始化，且只能为 ​​简单数据类型​​（如 int、float、指针），不支持动态初始化或复杂类型（如结构体、动态数组）。</li>
</ol>
<pre><code class="language-cpp">static __thread int thread_local_var;  // 必须为 static 或全局变量

static __thread unsigned long task_count;  // 线程私有计数器

void process_task() {
    task_count++;  // 无锁操作
}
</code></pre>
<h3>C++</h3>
<h4>C++11的并发类</h4>
<p>C++11 提供的并发类主要包括</p>
<ol>
<li>std::thread线程, 创建立即执行, 提供join(), detach(), get_id()等方。头文件<code>#include &#x3C;thread></code></li>
<li>std::mutex互斥锁, 提供lock(), unlock(), try_lock(), <code>std::lock_guard&#x3C;std::mutex></code>, <code>std::unique_lock&#x3C;std::mutex></code>。头文件<code>#include &#x3C;mutex></code></li>
<li>std::condition_variable 条件变量，提供wait(), notify_one(), notify_all()等方法, 头文件<code>#include &#x3C;condition_variable></code></li>
</ol>
<pre><code class="language-cpp">// std::thread(Func&#x26;&#x26; f, Args&#x26;&#x26;... args)：创建一个线程并开始执行传入的函数 构造函数
// std::thread::join()：等待线程执行完毕。
// std::thread::detach()：将线程与主线程分离。
// std::thread::get_id()， 返回线程id

#include &#x3C;iostream>
#include &#x3C;thread>

void print_thread_id() {
    std::thread::id this_id = std::this_thread::get_id();
    std::cout &#x3C;&#x3C; "Thread ID: " &#x3C;&#x3C; this_id &#x3C;&#x3C; std::endl;
}

int main() {
    std::cout &#x3C;&#x3C; "Main thread ID: " &#x3C;&#x3C; std::this_thread::get_id() &#x3C;&#x3C; std::endl;

    std::thread t(print_thread_id);
    t.join();

    return 0;
}
// 输出
Main thread ID: 140509241898816
Thread ID: 140509241894656
</code></pre>
<p>线程同步</p>
<pre><code class="language-cpp">// std::mutex::lock()
// std::mutex::unlock()
// std::mutex::try_lock()：尝试锁定互斥量
// std::lock_guard&#x3C;std::mutex> guard(mtx); 自动加锁和释放锁
// std::unique_lock&#x3C;std::mutex> lck(mtx); 可手动释放锁，若未手动释放则自动释放锁
// std::condition_variable::wait() 阻塞当前线程
// std::condition_variable::notify_one()：通知一个等待的线程。
// std::condition_variable::notify_all()：通知所有等待的线程。

#include &#x3C;iostream>
#include &#x3C;thread>
#include &#x3C;queue>
#include &#x3C;mutex>
#include &#x3C;condition_variable>
#include &#x3C;chrono>

std::queue&#x3C;int> buffer;              // 共享缓冲区
std::mutex mtx;                      // 互斥锁，保护缓冲区
std::condition_variable cv;          // 条件变量，通知生产者或消费者

const int MAX_BUFFER_SIZE = 5;       // 缓冲区最大大小

// 生产者线程函数
void producer() {
    for (int i = 0; i &#x3C; 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟生产耗时
        std::unique_lock&#x3C;std::mutex> lock(mtx);

        // 如果缓冲区已满，生产者等待
        cv.wait(lock, []() { return buffer.size() &#x3C; MAX_BUFFER_SIZE; });
        buffer.push(i);
        std::cout &#x3C;&#x3C; "Produced: " &#x3C;&#x3C; i &#x3C;&#x3C; std::endl;
        cv.notify_all();
    }
}

// 消费者线程函数
void consumer() {
    for (int i = 0; i &#x3C; 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(150));  // 模拟消费耗时
        std::unique_lock&#x3C;std::mutex> lock(mtx);

        // 如果缓冲区为空，消费者等待
        cv.wait(lock, []() { return !buffer.empty(); });
        int item = buffer.front();
        buffer.pop();
        std::cout &#x3C;&#x3C; "Consumed: " &#x3C;&#x3C; item &#x3C;&#x3C; std::endl;
        cv.notify_all();
    }
}

int main() {
    // 创建生产者和消费者线程
    std::thread producer_thread(producer);
    std::thread consumer_thread(consumer);

    // 等待线程执行完毕
    producer_thread.join();
    consumer_thread.join();

    return 0;
}
// 输出
Produced: 0
Consumed: 0
Produced: 1
Consumed: 1
Produced: 2
Produced: 3
Consumed: 2
Produced: 4
...
</code></pre>
<p>异步任务</p>
<ol>
<li>std::promise和std::future。允诺（Promise）对象，用于在线程中存储一个值或异常，供其他线程通过 future 获取。未来（Future）对象，用于从 promise 中异步获取结果。提供阻塞或非阻塞方式访问数据。头文件<code>#include &#x3C;future></code></li>
<li>std::future和std::async, std::aync 直接传入函数, 返回一个future。该future可以通过get()获取函数返回值。std::async会自动创建异步执行任务</li>
</ol>
<p>std::future和std::promise联用，用于线程间传递数据</p>
<pre><code class="language-cpp">#include &#x3C;iostream>
#include &#x3C;thread>
#include &#x3C;future>

void set_value(std::promise&#x3C;int>&#x26; p) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    p.set_value(42);  // 设置 promise 的值
}

int main() {
    // 创建 promise 和 future 对象
    std::promise&#x3C;int> p;   // 可以存储一个变量
    std::future&#x3C;int> f = p.get_future();  // 可以从promise中获取存储的变量

    // 创建一个线程
    std::thread t(set_value, std::ref(p));

    std::cout &#x3C;&#x3C; "Doing some work in the main thread...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 获取 promise 设置的结果，调用 get() 会阻塞直到线程设置值
    int result = f.get();  // 阻塞，直到获取结果
    std::cout &#x3C;&#x3C; "The result from promise is: " &#x3C;&#x3C; result &#x3C;&#x3C; std::endl;
    t.join();

    return 0;
}
// 输出
Doing some work in the main thread...
The result from promise is: 42
</code></pre>
<p>std::future和std::async联用，获取异步任务执行完成后的返回值</p>
<pre><code class="language-cpp">#include &#x3C;iostream>
#include &#x3C;thread>
#include &#x3C;future>
#include &#x3C;chrono>

int calculate_square(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * x;
}

int main() {
    // 使用 std::async 启动异步任务，返回一个 std::future 对象
    std::future&#x3C;int> result = std::async(std::launch::async, calculate_square, 5);

    std::cout &#x3C;&#x3C; "Doing other work in main thread...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 获取异步任务的结果，调用 get() 会阻塞，直到结果计算完成
    int square = result.get();
    std::cout &#x3C;&#x3C; "The square of 5 is: " &#x3C;&#x3C; square &#x3C;&#x3C; std::endl;

    return 0;
}
</code></pre>
<p>在并发处理上，C++20 提供了协程和std::jthread。​jthread​​兼容 std::thread 接口​​和方法，提供了​自动 Join​​：在析构时自动调用 join()，避免未回收线程导致程序终止（传统 std::thread 析构时若未 join() 或 detach() 会触发 std::terminate）。</p>
<p>C++20 还引入了std::latch, 用于等待线程完成(这种特性加入的也太晚了)</p>
<pre><code class="language-cpp">#include &#x3C;iostream>
#include &#x3C;latch>
#include &#x3C;thread>
#include &#x3C;vector>

int main() {
    std::latch completion_latch(3);  // 初始计数器为3

    auto worker = [&#x26;](int id) {
        std::cout &#x3C;&#x3C; "Worker " &#x3C;&#x3C; id &#x3C;&#x3C; " started\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));
        completion_latch.count_down();  // 完成任务，计数器减1
    };

    std::vector&#x3C;std::jthread> threads;
    for (int i = 0; i &#x3C; 3; ++i) {
        threads.emplace_back(worker, i + 1);
    }

    completion_latch.wait();  // 主线程等待所有子线程完成
    std::cout &#x3C;&#x3C; "All workers completed!\n";

    return 0;
}
</code></pre>
<p>C++17 中引入的 std::scoped_lock, 可以一次性锁住多个互斥量，避免死锁</p>
<pre><code class="language-cpp">std::mutex mtx;

void safe_function() {
    std::scoped_lock lock(mtx); // 锁定 mtx
    // 操作共享资源...
} // 自动解锁 mtx

std::mutex mtx1, mtx2;

void safe_function() {
    std::scoped_lock lock(mtx1, mtx2); // 同时锁定 mtx1 和 mtx2
    // 操作共享资源...
} // 自动解锁 mtx2 和 mtx1（逆序）
</code></pre>
<h4>智能指针</h4>
<p><code>std::unique_ptr&#x3C;T></code>，创建对象时使用，具有对象的所有权。</p>
<p><code>std::shared_ptr&#x3C;T></code>，共享所有权, 使用引用计数维护对象生命周期, 在所有权不明确或对象析构时刻不明确时使用, 尽量使用unique_ptr</p>
<p><code>std::weak_ptr&#x3C;T></code>，弱指针，不维护对象生命周期。当对象处于观察者状态时使用，可通过尝试lock()函数判断对象是否存在，如果存在则访问。当对象A 持有对象B的shared_ptr，同时B又想持有对象A的shared_ptr时，为了避免循环引用，B应当持有A的weak_ptr 。</p>
<h4>C++线程局部变量</h4>
<p>thread_local 是 C++11 引入的关键字, 相比__thread, 前者只能修饰​​简单数据类型​​（如 int、float、指针），thread_local可用于局部变量、全局变量、类成员变量等。性能上两者均接近普通全局变量，无显著差异。</p>
<p>线程局部变量没有数据竞争，其他线程对变量的修改也不会影响当前线程的变量值</p>
<pre><code class="language-cpp">#include &#x3C;stdio.h>
#include &#x3C;pthread.h>

thread_local int thread_local_var = 0;  // 声明线程局部变量

void* thread_function(void* arg) {
    thread_local_var++;
    printf("Thread %ld: thread_local_var = %d\n", (long)arg, thread_local_var);
    return NULL;
}

int main() {
    pthread_t threads[3];

    // 创建多个线程
    for (long i = 0; i &#x3C; 3; i++) {
        pthread_create(&#x26;threads[i], NULL, thread_function, (void*)i);
    }

    // 等待线程结束
    for (int i = 0; i &#x3C; 3; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}
// 输出
Thread 0: thread_local_var = 1
Thread 1: thread_local_var = 1
Thread 2: thread_local_var = 1

class MyClass {
public:
    MyClass() { /* 构造函数 */ }
    ~MyClass() { /* 析构函数 */ }
};
thread_local MyClass obj;  // 合法，每个线程独立构造/析构
</code></pre>
<h4>C++11 的原子类</h4>
<p>std::atomic提供了线程安全的原子操作。std::atomic 是一个模板类，支持不同的数据类型（如整数、指针）。常用的方法</p>
<ol>
<li>load() 读取原子变量的值。</li>
<li>store(x) 写入值到原子变量</li>
<li>fetch_add(x) 和 fetch_sub(x) 原子加减法操作，返回旧值</li>
<li>exchange(x) 将原子对象的值替换为给定值，并返回旧值</li>
<li>compare_exchange_weak(expected, desired) 和 compare_exchange_strong(expected, desired) 执行原子比较并交换（CAS，Compare and Swap）操作。expected是当前期望的值，desired是期望修改后的值。返回true表示更新成功，false表示未更新</li>
</ol>
<pre><code class="language-cpp">
#include &#x3C;iostream>
#include &#x3C;atomic>
#include &#x3C;thread>

std::atomic&#x3C;int> counter(0);

void increment(int id) {
    for (int i = 0; i &#x3C; 10; ++i) {
        int expected = counter.load();
        while (!counter.compare_exchange_strong(expected, expected + 1)) {
            std::this_thread::yield();  // 如果失败，线程主动让出 CPU
        }
        std::cout &#x3C;&#x3C; "Thread " &#x3C;&#x3C; id &#x3C;&#x3C; " incremented counter to " &#x3C;&#x3C; counter.load() &#x3C;&#x3C; std::endl;
    }
}

int main() {
    std::thread t1(increment, 1);
    std::thread t2(increment, 2);

    t1.join();
    t2.join();

    std::cout &#x3C;&#x3C; "Final counter value: " &#x3C;&#x3C; counter.load() &#x3C;&#x3C; std::endl;
    return 0;
}
// 输出
Thread 1 incremented counter to 2
Thread 1 incremented counter to 3
Thread 1 incremented counter to 4
Thread 1 incremented counter to 5
Thread 1 incremented counter to 6
Thread 1 incremented counter to 7
</code></pre>
<h4>内存序和可见性</h4>
<p>内存序是为了保证多线程在<strong>多核CPU的可见性</strong>，是一种内存屏障。作用和JAVA的volatile类似。</p>
<ol>
<li>memory_order_seq_cst 是默认的内存顺序，相当于JAVA的volatile，对原子类的修改操作会立刻刷到内存。</li>
<li>memory_order_acquire和load连用，memory_order_release和store连用。这个保证，如果某线程执行了store+memory_order_release的操作，在其他线程执行load+memory_order_acquire时，前面的store操作是可见的</li>
</ol>
<p>由于C++没有类似java 的volatile关键字（C++的volatile 只是避免编译层的优化的重排序, 只有底层比较hack的代码告知编译期不要优化我hack的代码时才会用到, C++ 编程很少遇到），因此一旦涉及多线程共享变量，要么加锁，要么上原子类（一般不会手动使用内存屏障）。</p>
<ol>
<li>
<p>memory_order_relaxed：不保证任何顺序，只提供原子性，一般不会使用</p>
</li>
<li>
<p>memory_order_consume：保证在该原子操作之后的所有依赖操作的顺序, 这个类似memory_order_acquire, 比 memory_order_acquire 更轻量，但适用范围更窄, 只保证依赖原子类的顺序。
以下常用</p>
</li>
<li>
<p>memory_order_acquire：保证acquire之后的操作实际也在该操作后执行,不会重排序到操作之前，常修饰load操作</p>
</li>
<li>
<p>memory_order_release：保证release之前的所有操作在该操作前执行,不会重排序到操作之后，常修饰store操作
store+memory_order_release 和load+memory_order_acquire 保证写之后读之前是有序的。写-读是一个pair。release-acquire之间不是原子类的操作, 也会保证有序性。</p>
</li>
<li>
<p>memory_order_acq_rel：同时具备获取和释放语义,用来修饰load+store操作,如fetch_add</p>
</li>
<li>
<p>memory_order_seq_cst：保证严格的顺序一致性，即原子类变量多线程并发执行和单线程循环执行顺序完全一致, 原子类默认内存序</p>
</li>
</ol>
<p>内存序本身不提供“线程必须等待其他线程写了数据，才可以读”这样的同步语义，需要使用一个load循环，直到load数据。它能保证的是，只要某个线程store了数据，其他线程立刻可以load到。</p>
<pre><code class="language-cpp">#include &#x3C;iostream>
#include &#x3C;atomic>
#include &#x3C;thread>

std::atomic&#x3C;bool> ready(false);
std::atomic&#x3C;int> data(0);

void producer() {
    data.store(42, std::memory_order_relaxed);  // 写入数据
    ready.store(true, std::memory_order_release);  // 通知消费者
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {  // 等待生产者通知
        std::this_thread::yield();  // cpu从当前线程切走, 但线程不会进入等待队列，后面还可能被cpu调度
    }
    std::cout &#x3C;&#x3C; "Consumer read data: " &#x3C;&#x3C; data.load(std::memory_order_relaxed) &#x3C;&#x3C; std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    return 0;
}
</code></pre>
<h4>C++的lambda表达式和函数类型</h4>
<p>Lambda 是一种匿名闭包对象，​​每个 Lambda 表达式的类型都是唯一的闭包类型​​，无法显式声明，但可以通过 auto 或 decltype 推导。</p>
<p><code>std::function&#x3C;ReturnType(Args...)></code>可用来存储可调用对象，如函数对象，lambda表达式等。</p>
<pre><code class="language-cpp">#include &#x3C;functional>
#include &#x3C;vector>

int main() {
    std::vector&#x3C;std::function&#x3C;void()>> tasks;

    // 添加不同类型的可调用对象
    tasks.push_back([]{ std::cout &#x3C;&#x3C; "Task 1"; });   // Lambda
    tasks.push_back(&#x26;some_function);                 // 函数指针
    tasks.push_back(Functor{});                      // 仿函数, 实现operator()的对象

    for (auto&#x26; task : tasks) {
        task();  // 统一调用
    }
    return 0;
}
</code></pre>
<h4>异常处理</h4>
<p>C++异常处理基于三个关键字：try、catch、throw。</p>
<pre><code class="language-cpp">try {
    double result = divide(10, 0);
} catch (const std::runtime_error&#x26; e) {
    std::cerr &#x3C;&#x3C; "Error: " &#x3C;&#x3C; e.what() &#x3C;&#x3C; std::endl;
} catch (...) { // 捕获所有异常
    std::cerr &#x3C;&#x3C; "Unknown error!" &#x3C;&#x3C; std::endl;
}
</code></pre>
<p>异常处理的缺陷</p>
<ol>
<li>
<p>栈展开Stack Unwinding，抛出异常时，<strong>编译器需生成回溯调用栈，逐个退出函数作用域的逻辑</strong>，调用局部对象的析构函数，直到找到匹配的 catch 块。</p>
</li>
<li>
<p>编译器需为每个 try 块生成额外的元数据（如异常表），增加二进制文件体积。</p>
</li>
<li>
<p>异常改变了代码的显式执行路径，错误处理逻辑分散在 catch 块中，而非就近处理。</p>
</li>
<li>
<p>C++推荐使用错误码来进行错误处理，无额外性能开销；错误处理需要保证资源释放</p>
</li>
<li>
<p>如果调用的函数可能抛出异常，那么调用者需要捕获，防止程序崩溃。</p>
</li>
</ol>
<p>C++ noexcept关键字指定函数不抛异常，编译器可能为 noexcept 函数生成更精简的代码（无需准备栈展开逻辑）。析构函数和移动构造/赋值函数默认是noexcept函数。若 noexcept 函数意外抛异常，程序直接终止。</p>
<h3>JAVA</h3>
<h4>JAVA 并发类</h4>
<p>Thread类，继承 Thread 类并重写其 run() 方法，实现多线程。JAVA原生支持, 不需要import 库</p>
<pre><code class="language-java">class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running...");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

// 实现 Runnable 接口并重写 run() 方法, 实现多线程。不需要import 库
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Runnable thread is running...");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}
</code></pre>
<p>Callable和FutureTask接口，与 Runnable 类似，但可以返回结果和抛出异常。需要import java.util.concurrent.Callable</p>
<pre><code class="language-java">import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable&#x3C;Integer> {
    public Integer call() throws Exception {
        return 42;  // 返回结果
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        FutureTask&#x3C;Integer> futureTask = new FutureTask&#x3C;>(new MyCallable());
        Thread t1 = new Thread(futureTask);
        t1.start();

        System.out.println("Result: " + futureTask.get());
    }
}
</code></pre>
<p>线程池类, Executor 和 ExecutorService。C++标准库里没有线程池类, 需要引用第三方库。</p>
<pre><code class="language-java">import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        executor.execute(() -> System.out.println("Task 1"));
        executor.execute(() -> System.out.println("Task 2"));

        executor.shutdown();
    }
}
// 输出
Task 1
Task 2
</code></pre>
<p>CountDownLatch, latch计数器，用于等待其他线程完成。</p>
<pre><code class="language-java">import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + " finished task");
            latch.countDown();
        };

        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();

        latch.await();
        System.out.println("All tasks finished");
    }
}
</code></pre>
<p>ThreadLocal，线程内部变量</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        ThreadLocal&#x3C;Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

        Runnable task = () -> {
            threadLocal.set(threadLocal.get() + 1);
            System.out.println(Thread.currentThread().getName() + " value: " + threadLocal.get());
        };

        new Thread(task).start();
        new Thread(task).start();
    }
}
</code></pre>
<p>synchronized 锁, Java 内置的关键字，可用于方法或代码块, 用于线程同步。</p>
<pre><code class="language-java">// 同步方法,整个方法需要互斥执行
public synchronized void method() {
    System.out.println(Thread.currentThread().getName() + " is executing");
}
// 同步对象，整个this对象需要互斥执行
public void method() {
    synchronized (this) {
        System.out.println(Thread.currentThread().getName() + " is executing");
    }
}

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i &#x3C; 1000; i++) counter.increment();
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i &#x3C; 1000; i++) counter.increment();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

</code></pre>
<p>java提供volatile关键字保证变量的可见性, volatile关键字实现<strong>如果某线程只对变量进行赋值或读取操作，操作时不需要加锁</strong>。对于volatile使用场景, C++一般要使用原子类。</p>
<pre><code class="language-java">public class WorkerThread extends Thread {
    private volatile boolean running = true;

    public void stopTask() {
        running = false; // 设置为线程终止标志, 其他线程修改后立即可见, 不需要加锁
    }

    @Override
    public void run() {
        while (running) {
            // 执行任务...
        }
    }
}

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查无锁, 使用volatile保证第一次检查时的可见性
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new Singleton();    // volatile禁止重排序
                }
            }
        }
        return instance;
    }
}
</code></pre>
<h4>线程安全数据结构</h4>
<p>JAVA提供了一些并发安全的数据结构，位于java.util.concurrent包中。C++标准库不提供线程安全结构, 需要第三方库。</p>
<ol>
<li>ConcurrentHashMap
线程安全的哈希表实现，支持高效的并发读写。它将内部数据分割为多个段，每个段一个锁，可以减少锁竞争。</li>
<li>CopyOnWriteArrayList
线程安全的 List 实现</li>
<li>BlockingQueue
线程安全的队列接口，通常用于生产者-消费者模型。</li>
</ol>
<p>java.util中的HashMap，ArrayList，LinkedList等都是非并发安全的</p>
<h4>JAVA多线程和虚拟机</h4>
<p>相比C/C++ 多线程通过pthread直接在linux操作系统中起进程，JAVA的多线程需要经过JAVA虚拟机这一层。</p>
<p>java虚拟机维护了JavaThread结构，JVM是C++实现的。所以JavaThread是C++定义的类。JavaThread维护了线程的状态，一个指针指向java.lang.Thread创建的对象(oop)，另一个指针指向对应的操作系统创建的OSThread</p>
<p>java线程模型的实现取决于jvm虚拟机，只要jvm愿意，可以选择类似go使用协程来实现线程。但以市场占有率最大的HotSpot虚拟机举例，一个java线程都是直接映射到操作系统的原生线程来实现的，所有的线程调度都是由操作系统完成的。</p>
<h4>原子类</h4>
<p>volatile 关键字可以实现变量的可见性，但不提供原子性。 JAVA常见的原子类有 AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference</p>
<p>.set()设置值, .get()获取值</p>
<pre><code class="language-java">import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) {
        AtomicInteger atomicInt = new AtomicInteger(0);
        atomicInt.set(10);          // 设置值
        System.out.println("Value: " + atomicInt.get());        // 获取值
        System.out.println("Incremented: " + atomicInt.incrementAndGet());    // 增加并获取
        boolean success = atomicInt.compareAndSet(15, 100);          // 比较并设置
        System.out.println("CAS Success: " + success + ", New Value: " + atomicInt.get());
    }
}
// 输出
Value: 10
Incremented: 11
CAS Success: false, New Value: 11
</code></pre>
<h4>异常处理</h4>
<p>java 的推荐使用异常处理，因此函数调用者需要通过try-catch-finally结构来捕获异常，防止程序崩溃。</p>
<pre><code class="language-java">
try {
    // 可能抛出异常的代码
    FileInputStream file = new FileInputStream("data.txt");
} catch (FileNotFoundException e) {
    System.err.println("文件未找到: " + e.getMessage());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 无论是否发生异常，都会执行的代码（如资源释放）
    if (file != null) {
        try {
            file.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p>java 函数可以加throws关键字，声明该方法可能抛出的 ​​受检异常（Checked Exceptions）​​，函数调用者必须处理异常，否则编译器报错</p>
<pre><code class="language-java">public void readFile(String path) throws FileNotFoundException, IOException {
    FileInputStream file = new FileInputStream(path);
    // 其他操作
}
</code></pre>
<h3>Golang</h3>
<h4>并发实现</h4>
<p>Golang 在语言层面使用协程实现并发，Go内置协程调度器，能自动在协程阻塞时将协程挂起。Golang的线程是无阻塞的，这意味Golang线程不能使用线程阻塞的系统调用。</p>
<p>GMP调度器, Go调度器基于 ​​G（Goroutine）、M（Machine）、P（Processor）​​ 三者的协作：</p>
<ol>
<li>​G（Goroutine）​​：轻量级协程，初始栈仅2KB（可动态扩缩），创建和切换成本极低。</li>
<li>​​M（Machine）​​：对应操作系统的线程（OS Thread），负责实际执行代码。</li>
<li>P（Processor）​​：逻辑处理器（调度器），管理一组本地队列（存储待运行的Goroutines）。P的数量默认等于CPU核心数（可通过GOMAXPROCS调整）。</li>
</ol>
<p>Goroutine 的创建与执行​</p>
<ol>
<li>每个协程（G）由Go运行时创建，并被分配到某个P的本地队列。</li>
<li>M需要绑定一个P才能执行Goroutines。</li>
<li>当P的本地队列为空时，会从全局队列或其他P的队列中​​窃取Goroutines​​（Work Stealing）。</li>
</ol>
<p>Golang的线程不会执行任何阻塞的系统调用, 当协程阻塞时，协程主动让出执行权，M会释放绑定的P，P转去服务其他M。系统调用完成后，G尝试获取新的P继续执行（若无可用P，则G进入全局队列）。</p>
<p>Go运行时有专门的​​网络轮询器（NetPoller）​​，将阻塞的G挂起，待IO就绪后唤醒，避免占用M。</p>
<p>一个Go程序可轻松创建数十万个Goroutines，而同等规模的OS线程会耗尽资源。M的数量由调度器动态管理（通常远少于Goroutines的数量），避免频繁创建/销毁OS线程。</p>
<p>golang提供sync.WaitGroup等待协程运行完, 相当于latch。golang的无缓冲channel 可以用来实现阻塞-等待，有缓冲channel可以用来当任务队列。golang利用channel可以实现顺序性。</p>
<pre><code class="language-go">package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func producer(ch chan&#x3C;- int, wg sync.WaitGroup) {
	for i := 0; i &#x3C; 10; i++ {
		item := rand.Intn(100)
		fmt.Println("Produced:", item)
		ch &#x3C;- item              // 发送数据到channel
		time.Sleep(time.Second) // 模拟生产过程的延迟
	}
	close(ch)
	wg.Done()
}

func consumer(ch &#x3C;-chan int, wg sync.WaitGroup) {
	for item := range ch {
		fmt.Println("Consumed:", item)
		time.Sleep(2 * time.Second) // 模拟消费过程的延迟
	}
	wg.Done()

}

func main() {
	var wg sync.WaitGroup
	// 创建一个缓冲区大小为 5 的channel
	ch := make(chan int, 5)

	// 启动多个消费者
	for i := 0; i &#x3C; 3; i++ {
		go consumer(ch, wg)
		wg.Add(1)
	}

	// 启动一个生产者
	go producer(ch, wg)
	wg.Add(1)

	// 主程序运行一段时间后退出
	time.Sleep(10 * time.Second)
}

// 输出
Produced: 63
Consumed: 63
Produced: 47
Consumed: 47
Produced: 47
Consumed: 47
Produced: 99
Consumed: 99
Produced: 51
Consumed: 51
...
</code></pre>
<p>for range语句可以用来遍历channel的数组，直到channel被close。select 语句可以等待接收多个channel的数据，只要有一个channel写入了数据，select就会执行处理函数并退出。</p>
<pre><code class="language-go">package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)

	go func() {
		ch &#x3C;- 42
	}()

	select {
	case msg := &#x3C;-ch:
		fmt.Println("Received:", msg)
	case &#x3C;-time.After(1 * time.Second): // 1秒后超时, select执行fmt.Println("Timeout!")
		fmt.Println("Timeout!")
	}
}
// 输出
Received: 42
</code></pre>
<p>sync.Mutex，互斥锁。sync.RWMutex 读写锁</p>
<pre><code class="language-go">package main

import (
	"fmt"
	"sync"
)

var (
	counter int
	mu      sync.Mutex // 创建一个互斥锁
)

// 增加计数器的值
func increment() {
	mu.Lock()
	defer mu.Unlock()

	counter++
}

func main() {
	var wg sync.WaitGroup

	// 启动多个 goroutine 来增加计数器
	for i := 0; i &#x3C; 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}

	// 等待所有 goroutine 执行完毕
	wg.Wait()

	fmt.Println("Final counter:", counter)
}

import (
	"fmt"
	"sync"
)

var (
	data   int
	rwMutex sync.RWMutex
)

func read() int {
	rwMutex.RLock()
	defer rwMutex.RUnlock()
	return data
}

func write(value int) {
	rwMutex.Lock()
	defer rwMutex.Unlock()
	data = value
}

func main() {
	var wg sync.WaitGroup

	// 启动多个读 goroutine
	for i := 0; i &#x3C; 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Printf("Reader %d: data = %d\n", i, read())
		}(i)
	}

	// 启动一个写 goroutine
	wg.Add(1)
	go func() {
		defer wg.Done()
		write(42)
		fmt.Println("Writer: data updated")
	}()

	wg.Wait() // 等待所有 goroutine 完成
}
</code></pre>
<p>sync.Once 确保函数只执行一次，无论是多次调用还是多线程多次执行。可用来实现单例模式</p>
<pre><code class="language-go">import "sync"

type Singleton struct {
    // 单例对象的字段
}

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &#x26;Singleton{
            // 初始化字段
        }
    })
    return instance
}
</code></pre>
<p>sync.Cond 条件变量，执行条件变量wait前需要持有锁</p>
<pre><code class="language-cpp">func (b *Buffer) Produce(item int) {
	b.lock.Lock()
	defer b.lock.Unlock()

	for len(b.data) == bufferSize {
		b.cond.Wait()
	}
	b.data = append(b.data, item)
	fmt.Println("Produced:", item)

	b.cond.Signal()
}
</code></pre>
<h4>原子操作</h4>
<p>sync/atomic 包提供了多种原子操作函数，</p>
<ol>
<li>AddInt32, <code>func AddInt32(addr *int32, delta int32) (new int32)</code>原子地将一个 int32 值加上一个指定的值。返回操作后的 ​​新值​​</li>
<li>CompareAndSwapInt32, <code>func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)</code> 原子地比较并交换 int32 值。如果当前值等于预期值，则将其更改为新值。返回是否更改</li>
<li>LoadInt32, <code>func LoadInt32(addr *int32) (val int32)</code> 原子地读取一个 int32 值。</li>
<li>StoreInt32, <code>func StoreInt32(addr *int32, val int32)</code> 原子地写入一个 int32 值。</li>
</ol>
<p>例子, 无锁计数器</p>
<pre><code class="language-go">var counter int64

func increment() {
    atomic.AddInt64(&#x26;counter, 1) // 无需关心返回值
}

func get() int64 {
    return atomic.LoadInt64(&#x26;counter)
}
</code></pre>
<p>自旋锁</p>
<pre><code class="language-go">var lock int32

func acquireLock() {
    for !atomic.CompareAndSwapInt32(&#x26;lock, 0, 1) {
        // 自旋等待
    }
}

func releaseLock() {
    atomic.StoreInt32(&#x26;lock, 0)
}
</code></pre>
<h4>Context 协程生命周期管理</h4>
<p>channel用来协程间通信，而Context主要用来管理协程的生命周期。</p>
<h4>异常处理</h4>
<p>panic用来抛出异常，recover用来捕获异常。
panic，</p>
<ol>
<li>运行时错误自动触发​​：例如数组越界、空指针解引用等。</li>
<li>​主动调用 <code>panic(v)</code>​​：开发者可手动抛出任意类型的值（通常是 <code>error</code> 或 <code>string</code>）。</li>
</ol>
<p>行为</p>
<ol>
<li>立即​​终止当前函数​​的执行。</li>
<li>​逐层向上回溯调用栈​​，执行每个函数的 <code>defer</code> 语句。</li>
<li>若未被 <code>recover</code> 捕获，最终​<strong>​程序崩溃​</strong>​并打印堆栈信息。</li>
</ol>
<p>recover：捕获异常​​</p>
<ol>
<li>​必须在 defer 函数中调用​​：recover 仅在 defer 上下文中生效。</li>
<li>​仅在发生 panic 后生效​​：若无 panic，recover 返回 nil。</li>
</ol>
<p>Go 的设计者觉得 <code>try/catch</code> 机制的使用太泛滥了，而且<strong>从底层向更高的层级抛异常太耗费资源</strong>。他们给 Go 设计的机制也可以“捕捉”异常，但是更轻量，并且只应该作为（处理错误的）最后的手段。</p>
<pre><code class="language-cpp">import "fmt"

func riskyOperation() {
	panic("something went wrong!")
}

func safeOperation() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from panic:", r)
		}
	}()
	riskyOperation() // 触发 panic
	fmt.Println("This line will NOT be executed")
}

func main() {
	safeOperation()
	fmt.Println("Program continues normally")
}

// 输出
Recovered from panic: something went wrong!
Program continues normally
</code></pre>
<p>golang推荐使用error错误码来处理错误，产生错误的函数会返回两个变量，一个值和一个错误码；如果后者是 <code>nil</code> 就是成功，非 <code>nil</code> 就是发生了错误。</p>
<pre><code class="language-go">if value, err := pack1.Func1(param1); err != nil {
	fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
	return    // or: return err
} else {
	// Process(value)
}

type error interface {
    Error() string
}

// 自定义错误码
type MyError struct {
    Code    int
    Message string
    Details map[string]interface{}
}

func (e *MyError) Error() string {
    return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Message)
}

func ProcessRequest() error {
    return &#x26;MyError{
        Code:    400,
        Message: "invalid request",
        Details: map[string]interface{}{"field": "username"},
    }
}
</code></pre>
<h3>Python</h3>
<h4>多线程和多进程</h4>
<p>Python 的 Cpython有一个全局解释器锁（GIL）。这意味着在任何时刻，只有一个线程可以执行 Python 字节码。这简化了python的多线程管理，GIL​​单条字节码指令是原子操作​​。向x = 42, shared_list.append(item) 是线程安全的。但这让Python的多线程只能利用到单核。</p>
<p>虽然Python可以使用多进程，但多进程之间是独立的地址空间，难以访问共享变量，使用比较难受。因此threading 多线程模块是python常用的并发模块</p>
<pre><code class="language-python">import threading
import time

# 定义一个任务
def print_numbers():
    for i in range(5):
        print(i)
        time.sleep(1)

# 创建线程
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_numbers)
# 创建进程
process1 = multiprocessing.Process(target=print_numbers)
process2 = multiprocessing.Process(target=print_numbers)

thread1.start()
thread2.start()
process1.start()
process2.start()

thread1.join()
thread2.join()
process1.join()
process2.join()
</code></pre>
<p>线程锁和条件变量</p>
<pre><code class="language-python">import threading

class Counter:
    def __init__(self):
        self.value = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:  # 自动加锁/解锁
            self.value += 1

cond = threading.Condition()
shared_data = []

def producer():
    with cond:  # 自动加锁
        shared_data.append(42)
        cond.notify()  # 通知消费者

def consumer():
    with cond:  # 自动加锁
        while not shared_data:
            cond.wait()  # 自动释放锁并等待，唤醒后重新加锁
        print("Received:", shared_data.pop())
</code></pre>
<p>线程池，concurrent.futures.ThreadPoolExecutor</p>
<pre><code class="language-python">import concurrent.futures

def print_square(number):
    print(f"Square of {number} is {number * number}")

# 创建一个线程池，最多允许 5 个线程同时执行
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # 提交任务给线程池
    executor.submit(print_square, 2)
    executor.submit(print_square, 3)
    executor.submit(print_square, 4)
</code></pre>
<h4>生成器和协程</h4>
<p>生成器。python生成器是一种无栈协程，可以实现手动切换执行流。普通函数可以 yield 语句返回生成器，普通函数执行到yield后会转向执行接收yield返回值的函数，当再执行next()，线程回到原先的函数继续执行。</p>
<p>使用next() 和 send() 方法可以控制生成器的执行。</p>
<pre><code class="language-python">def simple_gen():
    value = yield "Ready"  # 初始返回值
    print("Received:", value)
    yield "End"

gen = simple_gen()
print(next(gen))  # 第一次执行生成器，收到"Ready"
print(gen.send(42))  # 执行流再次切换到simple_gen，simple_gen执行到"End

# 输出
Ready
('Received:', 42)
End
</code></pre>
<p>asyncio 是python3提供的无栈协程模块，内部实现了事件循环。</p>
<ol>
<li>async 用于定义协程函数，await 用于暂停协程并等待另一个协程完成。async定义的协程函数中不能调用可能导致线程阻塞或者等待的函数，包括阻塞读写、线程锁、sleep等。</li>
<li>await 后面跟的是io耗时的操作，表示把线程从当前协程切换走；同时await会注册事件通知，当耗时的操作执行完时，调度器会回来再执行当前协程</li>
</ol>
<p>await 同时维护了执行的先后顺序，对于async函数A await aysnc函数B，则必须等待B执行完函数A才会继续执行。这个执行逻辑避免了回调地狱，即<strong>对于回调函数链路A->B->C，可以直接在A里面写await B, B里面写 await C</strong>，代码更加简洁易懂</p>
<pre><code class="language-python">import asyncio

async def task_1():
    print("Task 1 starts")
    await asyncio.sleep(2)
    print("Task 1 ends")

async def task_2():
    print("Task 2 starts")
    await task_1()
    print("Task 2 ends")

# 并发执行两个任务
async def main():
    await asyncio.gather(task_1(), task_2())  # 需等待所有task执行完

asyncio.run(main())

# 输出
Task 1 starts
Task 2 starts
Task 1 starts
Task 1 ends
Task 1 ends
Task 2 ends
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言(3)—数据结构和算法]]></title>
          <link>https://larrystd.github.io/posts/编程语言(3)—数据结构和算法</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言(3)—数据结构和算法</guid>
          <pubDate>Tue, 10 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[数据结构(容器)库和算法库是最基础的库之一。字符串是人类语言的记录，是计算机处理的主要对象。]]></description>
          <content:encoded><![CDATA[<p>数据结构(容器)库和算法库是最基础的库之一。字符串是人类语言的记录，是计算机处理的主要对象。</p>
<h3>C和C++</h3>
<p>由于C语言不提供数据结构和算法库，这里专指C++的STL stand template library。</p>
<h4>数据结构</h4>
<p>std::array，静态数组，编译时确定数组长度，数组创建在栈。相当于C语言的<code>int[2] = {1,2}</code></p>
<p>std::vector，动态数组，可动态扩展大小。当 size() 即将超过 capacity() 时，vector 会自动分配更大的内存空间（一般是翻倍），并将原有元素​​移动或复制​​到新空间。
因此std::vector会有迭代器失效的问题，需要及时重新获取迭代器。</p>
<p>数组随机访问和更新效率高，为O(1)，但随机插入删除低。适合大量访问少量插入删除的场景。vector建议使用.at(x)索引元素，具有边界检查，如果越界会抛出异常。</p>
<pre><code class="language-cpp">#include &#x3C;vector>
#include &#x3C;iostream>

int main() {
    std::vector&#x3C;int> vec = {1, 2, 3, 4, 5};
    vec.push_back(6);

    for (int val : vec) {
        std::cout &#x3C;&#x3C; val &#x3C;&#x3C; " ";  // 输出：1 2 3 4 5 6
    }
    std::cout &#x3C;&#x3C; std::endl;

    std::cout &#x3C;&#x3C; vec[2] &#x3C;&#x3C; std::endl;  // 访问元素 3
    return 0;
}
</code></pre>
<p>std::list 是双向链表，能高效插入或删除元素，但随机访问效率低。适合维护不需要大量随机访问和更新的结构（即不需要用vector的都建议上链表）。例如队列，环形buffer等。</p>
<pre><code class="language-cpp">#include &#x3C;list>
#include &#x3C;iostream>

int main() {
    std::list&#x3C;int> lst = {1, 2, 3, 4, 5};
    lst.push_back(6);  // 在末尾插入元素
    lst.push_front(0); // 在头部插入元素

    for (int val : lst) {
        std::cout &#x3C;&#x3C; val &#x3C;&#x3C; " ";  // 输出：0 1 2 3 4 5 6
    }
    return 0;
}
</code></pre>
<p>std::deque 是双端队列，使用链接起来的若干内存块构成。deque的头部和尾部可以进行高效的插入和删除操作，同时也提供随机访问接口，但随机访问比vector慢。deque实现复杂，实际应该少用。</p>
<pre><code class="language-cpp">#include &#x3C;deque>
#include &#x3C;iostream>

int main() {
    std::deque&#x3C;int> deq = {1, 2, 3, 4, 5};
    deq.push_back(6);   // 在尾部添加元素
    deq.push_front(0);  // 在头部添加元素

    for (int val : deq) {
        std::cout &#x3C;&#x3C; val &#x3C;&#x3C; " ";  // 输出：0 1 2 3 4 5 6
    }
    return 0;
}
</code></pre>
<p>std::stack​​是可以传入底层容器的配接器，默认底层容器是std::deque，但可以指定底层容器为std::vector。同样std::queue也是个配接器，默认底层容器是std::deque，但可以指定底层容器为std::list。</p>
<pre><code class="language-cpp">std::stack&#x3C;T> s;                   // 默认使用 deque
std::stack&#x3C;T, std::vector&#x3C;T>> s;    // 显式指定 vector 作为底层容器

std::stack&#x3C;int> s1;
s1.push(1);                     // 压栈
int top = s1.top();             // 访问栈顶（1）
s1.pop();                       // 弹栈

// 使用 list 作为底层容器
std::queue&#x3C;int, std::list&#x3C;int>> q2;
q2.push(2);                     // 入队
q2.pop();                       // 出队
</code></pre>
<p>std::set 是一个​​有序关联容器​​，基于红黑树（平衡二叉搜索树）实现，元素会自动排序且不允许重复，CRUD复杂度都是O(logn)。unordered_set 是一个不排序的集合，基于哈希表实现, 复杂度为O(1)，但会面临哈希碰撞和rehash的问题。</p>
<p>std::set 创建需要传入comparator (operator &#x3C;，即<code>Less&#x3C;T></code>升序排列)。而std::unordered_set需要提供hash function和operator==。</p>
<pre><code class="language-cpp">// 按字符串长度排序
struct LengthCompare {
    bool operator()(const std::string&#x26; a, const std::string&#x26; b) const {
        return a.length() &#x3C; b.length();
    }
};
std::set&#x3C;std::string, LengthCompare> s;
auto comp = [](const std::string&#x26; a, const std::string&#x26; b) { return a &#x3C; b; }
std::set&#x3C;T, decltype(comp)> my_set(comp);

s.insert("apple");
s.insert("banana"); // "apple"（长度 5）排在 "banana"（长度 6）之前

struct Person {
    std::string name;
    int age;
};

struct PersonHash {
    size_t operator()(const Person&#x26; p) const {
        return std::hash&#x3C;std::string>()(p.name) ^ std::hash&#x3C;int>()(p.age);
    }
};

struct PersonEqual {
    bool operator()(const Person&#x26; a, const Person&#x26; b) const {
        return a.name == b.name &#x26;&#x26; a.age == b.age;
    }
};

std::unordered_set&#x3C;Person, PersonHash, PersonEqual> s2;

auto hash = [](const T&#x26; key) { /* 返回哈希值 */ };
auto equal = [](const T&#x26; a, const T&#x26; b) { /* 返回是否相等 */ };
std::unordered_set&#x3C;T, decltype(hash), decltype(equal)> s3(hash, equal);
</code></pre>
<p>哈希表的元素个数除以哈希表长度是哈希表的负载因子load factor。当负载超过负载因子时，哈希表会自动进行扩容。</p>
<ol>
<li>计算新桶数量​​：新桶数量通常为不小于 size() / max_load_factor() 的质数。</li>
<li>​​重新分配桶数组​​：分配新桶数组</li>
<li>​​重新哈希所有元素​​：将所有元素重新插入到新桶中</li>
<li>最后释放旧桶数组
rehash期间hash表会停止工作, rehash完毕后旧的迭代器会失效</li>
</ol>
<p>数组和unorder_set/unorder_map使用时建议预分配空间（reserve）以减少重新分配。</p>
<p>map类似set，只是元素是键值对pair，根据键进行排序。unordered_map类似unordered_set，是一个无序映射，使用哈希表进行存储。</p>
<pre><code class="language-cpp">#include &#x3C;unordered_map>
#include &#x3C;iostream>

int main() {
    std::unordered_map&#x3C;int, std::string> um = {{1, "one"}, {2, "two"}, {3, "three"}};

    for (const auto&#x26; pair : um) {
        std::cout &#x3C;&#x3C; pair.first &#x3C;&#x3C; ": " &#x3C;&#x3C; pair.second &#x3C;&#x3C; std::endl;
    }
    return 0;
}
</code></pre>
<p>C++ STL使用迭代器来标识容器中的元素，以及实现容器中元素的访问。迭代器支持重载<code>*</code>和<code>-></code>运算符来解引用或访问元素成员变量</p>
<ol>
<li>随机访问迭代器 (Random Access Iterator)，类似指针，通过加减算数提供随机访问。对应容器<code>std::vector、std::deque、std::array</code>。支持随机访问迭代器作为参数的算法函数，一般也支持指针作为参数。</li>
<li>双向迭代器(Bidirectional Iterator)，允许在容器中向前和向后移动。对应容器<code>std::list、std::set、std::map。</code></li>
</ol>
<p>迭代器是使用继承结构组织的，随机访问迭代器也是一种双向迭代器</p>
<p>.begin(), .end() 分别代表首尾迭代器</p>
<h4>算法库algorithm</h4>
<p>std::sort 对容器中的元素进行排序，默认升序。sort可以用std::less和std::greater 进行降序和升序排序。</p>
<pre><code class="language-cpp">
#include &#x3C;functional>
std::sort(v.begin(), v.end()); // 默认使用 std::less&#x3C;int>
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 降序
</code></pre>
<p>std::sort 内部实现原理,</p>
<ol>
<li>快速排序（Quicksort）​​：作为主要排序算法，处理大规模数据。</li>
<li>​​堆排序（Heapsort）​​：在快速排序递归深度过大时触发，避免最坏情况下的 O(n²) 时间复杂度。</li>
<li>插入排序（Insertion Sort）​​：在小规模子数组（通常 ≤16 个元素）时使用，减少递归开销。</li>
</ol>
<p>常见排序算法的稳定性</p>
<pre><code>排序算法	最佳时间复杂度	平均时间复杂度	最坏时间复杂度	空间复杂度	稳定性	原地排序	备注
​​冒泡排序​​	O(n)	        O(n²)	    O(n²)	    O(1)	        稳定	是	优化后可提前终止（如无交换时停止）。
​​选择排序​​	O(n²)	        O(n²)	    O(n²)	    O(1)	        不稳定	是	每次选择最小/最大元素交换。
​​插入排序​​	O(n)	        O(n²)	    O(n²)	    O(1)	        稳定	是	对部分有序数据效率高。
​​希尔排序​​	O(n log n)	    O(n^(3/2))	O(n²)	    O(1)	        不稳定	是	基于插入排序的改进，间隔序列影响性能。
​​归并排序​​	O(n log n)	    O(n log n)	O(n log n)	O(n)	        稳定	否	分治思想，需额外空间合并。
​​快速排序​​	O(n log n)	    O(n log n)	O(n²)	    O(log n) ~ O(n)	不稳定	是	基于分治和分区操作，平均性能最优。
​​堆排序​​	    O(n log n)	    O(n log n)	O(n log n)	O(1)	        不稳定	是	利用堆结构排序，适合大规模数据。
​​计数排序​​	O(n + k)	    O(n + k)	O(n + k)	O(n + k)	     稳定	否	适用于整数且范围较小的数据（k为数据范围）。
​​桶排序​​	    O(n + k)	    O(n + k)	O(n²)	    O(n + k)	     稳定	否	数据均匀分布时高效，依赖桶的数量和分布。
​​基数排序​​	O(d(n + k))	    O(d(n + k))	O(d(n + k))	O(n + k)	     稳定	否	按位排序（d为位数，k为基数大小），通常用于整数或字符串。
</code></pre>
<p>std::sort是不稳定的，std::stable_sort提供稳定排序，即保证相同元素的相对顺序。内部实现原理是基于 ​​归并排序（Merge Sort）​​ 的变体，并结合优化策略以优化空间。</p>
<p>std::find查找容器中的某个元素，返回第一个匹配元素的迭代器。</p>
<pre><code class="language-cpp">auto it = std::find(vec.begin(), vec.end(), 2);
if (it != vec.end()) {
    // 找到了元素
}
it == vec.end();  // 未找到元素
</code></pre>
<p>std::lower_bound，传入排序好的数组，返回<strong>第一个大于或等于指定元素</strong>的迭代器。
std::upper_bound：返回指向已排序数组中<strong>第一个大于指定元素的迭代器</strong>。
需要随机访问迭代器</p>
<pre><code class="language-cpp">auto it = std::lower_bound(vec.begin(), vec.end(), 3);
auto it = std::upper_bound(vec.begin(), vec.end(), 3);
</code></pre>
<p>std::reverse, 反转容器中的元素顺序。</p>
<pre><code class="language-cpp">std::reverse(vec.begin(), vec.end());
</code></pre>
<p>std::fill, 将容器中的所有元素设置为指定值。</p>
<pre><code class="language-cpp">std::fill(vec.begin(), vec.end(), 0);
</code></pre>
<p>std::transform, 对容器中的每个元素应用指定的操作。</p>
<pre><code class="language-cpp">std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) { return x * 2; });
</code></pre>
<p>std::copy：将容器中的元素复制到另一个容器。std::swap, 交换两个容器的内容。std::move, 移动容器，避免复制。</p>
<pre><code class="language-cpp">std::copy(vec, dest);
std::swap(vec, dest);
std::vector&#x3C;int> new_vec = std::move(vec);
</code></pre>
<p>std::count, 统计容器中某个元素出现的次数。std::count_if：统计符合特定条件的元素的数量。</p>
<pre><code class="language-cpp">int count = std::count(vec.begin(), vec.end(), 2);
int count = std::count_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; });
</code></pre>
<p>std::accumulate, 对容器中的元素进行累加。</p>
<pre><code class="language-cpp">int sum = std::accumulate(vec.begin(), vec.end(), 0);
</code></pre>
<p>std::min_element, 返回容器中最小元素的迭代器。std::max_element：返回容器中最大元素的迭代器。</p>
<pre><code class="language-cpp">auto min_it = std::min_element(vec.begin(), vec.end());
auto max_it = std::max_element(vec.begin(), vec.end());
</code></pre>
<p>std::unique, 将容器中相邻的重复元素移动到末尾，返回新逻辑结尾的迭代器，不修改容器大小​​
unique需要和sort一起连用</p>
<pre><code class="language-cpp">std::vector&#x3C;int> v = {1, 1, 2, 2, 3, 3, 3, 4};
auto last = std::unique(v.begin(), v.end());
// v 变为 {1, 2, 3, 4, 3, 3, 3, 4}，last 指向第5个元素（第一个3后的位置）
</code></pre>
<h4>字符串处理函数</h4>
<p>由于C++兼容C语言，因此支持C风格和C++风格的字符串处理算法。C风格字符串函数参数是指针，C++风格是随机访问迭代器。</p>
<p>C风格的字符串处理函数, 在C++中，需要<code>#include &#x3C;cstring></code>，C语言的字符串处理函数作为std namespace的全局函数。</p>
<p>C 语言字符串处理函数</p>
<ol>
<li>strcpy(dest, src), strncpy(dest, src, n), 字符串拷贝</li>
<li>strcat(dest, src), strncat(dest, src, n), 字符串拼接</li>
<li>strcmp(s1, s2), strncmp(s1, s2, n), 字符串比较</li>
<li>strlen(s), 字符串长度</li>
<li>strchr(s, c), strrchr(s, c), 字符串查找</li>
<li>memset, memove, memcpy 内存赋值/移动/拷贝</li>
</ol>
<pre><code class="language-cpp">// strcpy(dest, src), strncpy(dest, src, n), 字符串拷贝
char src[] = "Hello, World!";
char dest[20];
std::strcpy(dest, src);
std::strncpy(dest, src, 5);

// strcat(str1, str2) 将str2拼接到str1
std::strcat(str1, str2); 
std::strncat(str1, str2, 5);

// strlen(str)，返回 C 风格字符串的长度（不包括 \0 终止符）

// strcmp(str1, str2), strncmp(str1, str2, n) 如果str1 > str2, 返回大于0
std::strcmp(str1, str2)

// strchr(str, c)，查找字符c在字符串str中第一次出现的位置。如果找到了，返回指向该字符的指针；如果没找到，返回 nullptr。
char* ptr = std::strchr(str, 'o');

// strstr(str, sub), 查找子字符串sub在字符串str中的第一次出现。如果找到了，返回指向子字符串的指针；如果没找到，返回 nullptr。
const char* str = "Hello, World!";
const char* sub = "World";
char* ptr = std::strstr(str, sub);

// memset(str, c, n) 按字节为单位对内存初始化
char str[20];
std::memset(str, '*', 5);  // 将前 5 个字符设置为 '*'

memcpy(dest, src, n) 从src拷贝n个字节到dest
char src[] = "Hello";
char dest[10];
std::memcpy(dest, src, 6); 
</code></pre>
<p>C++ string函数</p>
<ol>
<li>append(xxx) 追加字符串</li>
<li>substr(start, length) 截取字符串, 需要拷贝</li>
<li>find(xxx) 查找子串</li>
<li>replace(start, length, xxx) 替换字符串</li>
<li>erase(start, length) 删除字符串, 是真正的删除（性能差）</li>
<li>stoi(xxx) 字符串转整数</li>
<li>std::string, .c_str()</li>
</ol>
<pre><code class="language-cpp">// size() 或 length() 获取字符串长度

// append(string)，将内容追加
std::string str = "Hello";
str.append(", World!"); 

// push_back(c)，添加一个字符
str.push_back('!');

// insert(index, string), 指定位置插入字符串
str.insert(5, ", World");

// erase(index, length), 删除指定位置区间的字符串
str.erase(5, 7);  // 从位置 5 开始删除 7 个字符

// replace(index, length, newstring), 替换字符串中的指定部分。
str.replace(7, 5, "C++");

// resize(size) 改变字符串的大小，如果增加字符则填充，减少字符则截断。
str.resize(5);

// std::find(string) 查找子字符串首次出现的位置。如果找不到返回 std::string::npos。rfind, 从字符串末尾向前查找字符串
size_t pos = str.find("World");

// find_first_of(),find_last_of()
size_t pos = str.find_first_of("o");

// substr(index, length) 提取子字符串
std::string sub = str.substr(7, 5); 

// compare 字符串比较
std::string str1 = "Hello";
std::string str2 = "World";
int result = str1.compare(str2);

// stoi(), stol(), stoll() 字符串转为整型
int num = std::stoi(str);

// to_string()，转为字符串
std::string str = std::to_string(num);

// c_str(), data() 返回 C 风格字符串（const char*）。
const char* cstr = str.c_str();
</code></pre>
<p>string_view 是C++17的特性，旨在解决substr()字符串切片会导致拷贝造成性能损失的问题</p>
<ol>
<li>string_view不管理内存，仅保存指向外部数据的指针和长度。不能通过string_view修改底层数据。</li>
<li>构造时不复制数据，可以直接传递字符串参数，避免临时对象的开销。</li>
</ol>
<pre><code class="language-cpp">void process(std::string_view text) {
    // 处理 text，无需关心数据来源
}

process("Hello");       // C字符串
process(std::string("World")); // std::string
process(sv1);          // 另一个 string_view
</code></pre>
<h3>JAVA</h3>
<h4>数据结构</h4>
<p>静态数组，</p>
<ol>
<li>​​固定长度​​：数组在初始化时指定长度，后续无法修改。</li>
<li>连续内存​​：元素在内存中连续存储，支持快速随机访问（时间复杂度 O(1)）。</li>
<li>数组本身也是对象（继承自 Object 类），存储在堆内存中。</li>
</ol>
<p>java只有8种基本类型变量(byte, short, int, long, float, double, char, boolean)存放在栈里，数组不是</p>
<pre><code class="language-java">int[] numbers;       // 声明一个整型数组,  此时 arr = null
String[] names;      // 声明一个字符串数组

numbers = new int[5]; // 初始值为 [0, 0, 0, 0, 0]

names = new String[] {"Alice", "Bob", "Charlie"};

numbers[1] = 10;             // 修改第二个元素
int length = numbers.length; // 获取数组长度（非方法，是字段）
</code></pre>
<p>ArrayList，等于C++的vector，动态数组。JAVA的List是一个接口，实现了动态数组和链表。</p>
<pre><code class="language-java">import java.util.ArrayList;

ArrayList&#x3C;String> list = new ArrayList&#x3C;>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
System.out.println(list);  // 输出: [Apple, Banana, Cherry]

int first = list.get(0);       // 获取索引 0 的元素
int last = list.get(list.size() - 1); // 获取最后一个元素
list.set(0, 20); // 将索引 0 的元素修改为 20
list.addAll(otherList); // 添加另一个集合的所有元素
</code></pre>
<p>LinkedList，双向链表，相当于C++的list</p>
<pre><code class="language-java">import java.util.LinkedList;

LinkedList&#x3C;String> list = new LinkedList&#x3C;>();
</code></pre>
<p>HashSet, 基于哈希表实现，不保证元素的顺序。</p>
<p>LinkedHashSet, 基于哈希表和链表实现，能够保证元素的插入顺序。</p>
<p>TreeSet, 基于红黑树实现，元素按自然顺序或自定义顺序排列。</p>
<p>HashMap，基于哈希表实现，允许 null 键和值。</p>
<p>LinkedHashMap，基于哈希表和链表实现，保持插入顺序（或访问顺序）</p>
<p>TreeMap，基于红黑树实现，键按自然顺序或自定义顺序排序。</p>
<p>PriorityQueue，基于堆实现的队列，元素按优先级顺序排列</p>
<p>LinkedList 也实现了 Queue 接口，支持双端队列操作。</p>
<pre><code class="language-java">import java.util.LinkedList;
import java.util.Queue;

Queue&#x3C;String> queue = new LinkedList&#x3C;>();
queue.add("Apple");
queue.add("Banana");
queue.add("Cherry");
System.out.println(queue.poll());  // 输出: Apple（出队）
System.out.println(queue);  // 输出: [Banana, Cherry]
</code></pre>
<h4>算法</h4>
<p>Arrays用于操作数组，例如.sort() 用于对静态数组进行排序，数组是基本类型数组<code>int[] arr </code>，或对象数组<code>new Integer[10]</code>.
java的.length用于获取静态数组长度</p>
<pre><code class="language-java">int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr); // 数组变为 [1, 2, 3, 4, 5]
Arrays.sort(arr, (a, b) -> b - a); // 降序排序 → [5, 4, 3, 2, 1]
// 二分查找
int index = Arrays.binarySearch(arr, 3); // 返回 2（索引）

// 填充
int[] arr = new int[5];
Arrays.fill(arr, 10); // [10, 10, 10, 10, 10]
</code></pre>
<p>Collections用于容器算法，.sort() 用于对集合（如 List）进行排序</p>
<pre><code class="language-java">import java.util.*;

public class SortExample {
    public static void main(String[] args) {
        List&#x3C;Integer> list = new ArrayList&#x3C;>(Arrays.asList(5, 2, 8, 1, 3));
        Collections.sort(list);
        System.out.println(list);  // 输出: [1, 2, 3, 5, 8]

        int index = Collections.binarySearch(list, 5);
        System.out.println(index);  // 输出: 3

        Collections.fill(list, 10);
        System.out.println(list);  // 输出: [10, 10, 10, 10, 10]

        Collections.shuffle(list);
        System.out.println(list);  // 输出: 随机打乱后的结果
    }
}
</code></pre>
<h4>字符串处理</h4>
<p>JAVA的字符串操作类主要是String, StringBuilder, StringBuffer。</p>
<p>String一旦创建，内容不可修改。任何修改操作（如拼接、替换）都会生成新对象。（如果固定内存的字符串需要修改，用字符数组更好）</p>
<pre><code class="language-java">// 创建
String str1 = "Hello"; // 字符串字面量
String str2 = new String("Hello"); // 使用构造函数

// length() 获得长度
int length = str.length();

s.charAt(0)  // 访问字符
String sub = s.substring(0, 5);  // 切片
String result = s1 + " " + s2;  // 使用 + 拼接

StringBuilder sb = new StringBuilder();  // 利用StringBuilder
sb.append("Hello");
sb.append(" ");

String trimmed = s.trim();  // 去除两端空白字符

String s = "Hello, World!";
int index = s.indexOf("World");  // 查找子字符串的索引位置

String upper = s.toUpperCase();  // 转为大写
String formatted = String.format("My name is %s and I am %d years old.", name, age);  // 字符串格式化
String s = "apple,banana,cherry";
String[] fruits = s.split(",");  // 按逗号分割字符串

// 字符数组可以修改某位置字符
String str = "Hello";
char[] charArray = str.toCharArray();
charArray[1] = 'a';
String modifiedStr = new String(charArray);
System.out.println(modifiedStr);  // 输出: Hallo
</code></pre>
<p>StringBuilder 和 StringBuffer 是用于可变字符串的类。StringBuilder只能用于单线程环境, 直接修改内部字符数组，避免频繁创建新对象。StringBuffer 所有方法均用synchronized修饰, 性能低于StringBuilder, 但多线程安全。</p>
<p>修改字符串某位置的字符</p>
<pre><code class="language-java">// String 字符串不可修改某位置字符，可以利用stringBuilder
StringBuilder sb = new StringBuilder("Hello");
sb.setCharAt(1, 'a');
System.out.println(sb.toString());  // 输出: Hallo

sb.insert(5, " World"); // "Hello" → "Hello World"
sb.deleteCharAt(4); // "Hello" → "Hell"

int length()  // 长度
String result = sb.toString();
</code></pre>
<h3>Go</h3>
<h4>数据结构</h4>
<p>golang 用语言关键字提供了容器，不必额外导入包</p>
<p>静态数组, 固定大小的同一类型的元素。Go 数组的大小在声明时确定并且内容不可更改。</p>
<pre><code class="language-go">var arr [5]int // 定义一个长度为 5 的整型数组
arr[0] = 1
arr[1] = 2
fmt.Println(arr) // 输出: [1 2 0 0 0]
</code></pre>
<p>切片slice，动态数组。[]byte表示动态字符串。slice 的切片截取操作无须拷贝,</p>
<pre><code class="language-go">// 创建切片
slice := []int{1, 2, 3, 4, 5}
s := make([]int, 3, 5) // 长度3，容量5

slice = append(slice, 6)
slice = slice[:len(slice)-1]  // 删除最后一个元素
s = s[:0] // 清空元素（容量不变）

for i, v := range slice { fmt.Printf("Index: %d, Value: %d\n", i, v) }

fmt.Println(len(s)) // 3 获取长度
fmt.Println(cap(s)) // 5 获取容量

sub := s[1:3] // 截取切片, 包含索引1，不包含索引3 → [1, 2]

// 删除元素, 将删除位置后的元素前移，时间复杂度为 ​​O(n)​​
// 会拷贝 slice[i+1:] 的元素到 slice[i:] 的位置。
slice = append(slice[:i], slice[i+1:])
// 删除元素还可以用最后一个元素覆盖删除位置
s[i] = s[len(s)-1]
s = s[:len(s)-1]

// 直接用数组创建切片
arr := [5]int{10, 20, 30, 40, 50}
s2 := arr[1:4] // [20, 30, 40]
</code></pre>
<p>map，哈希表实现的容器。map底层实现和C++的unorder_map、JAVA的hashmap不同
map的底层结构体是hmap，hmap里维护着若干个bucket数组 (即桶数组)。</p>
<ol>
<li>每个桶存储固定数量的键值对（通常为 8 对），如果超过8个键值对了, 会先把多余的pair放到一个溢出桶中</li>
<li>map负载因子的定义是map中元素的个数 / map中当前桶的个数。当装载因子 > 6.5, 或溢出桶的数量过多，就会执行哈希表扩容。扩容非一次性完成, 每次对map进行删改操作时，会触发从oldbucket中迁移到bucket的操作, 在扩容没有完全迁移完成之前，每次get或者put遍历数据时，都会先遍历oldbuckets，然后再遍历buckets。</li>
</ol>
<pre><code class="language-go">myMap := map[string]int{
    "apple":  5,
    "banana": 3,
}
m := make(map[string]int)
m3 := make(map[string]int, 100) // 预分配约 100 元素的容量

// 插入或更新元素
myMap["orange"] = 7

value, exists := m["key"] // exists 为 bool 类型，表示是否存在

for k, v := range myMap { fmt.Printf("Key: %s, Value: %d\n", k, v) }

value, exists := m["key"]
if exists {
    // 处理 value
}

length := len(m)   // 获取键值对数量
</code></pre>
<p>链表，使用container/list</p>
<pre><code class="language-go">package main

import (
    "container/list"
    "fmt"
)

func main() {
    l := list.New()
    l.PushBack(1) // 在链表尾部插入
    l.PushFront(0) // 在链表头部插入

    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value) // 输出: 0 1
    }
}
</code></pre>
<p>channel，channel可认为是一个线程安全的协程间任务队列，内部通过链表和环形链表实现，这个可以在并发编程处详细展开。</p>
<h4>算法</h4>
<p>排序，import "sort"标准库</p>
<p>golang自定义struct的排序比较麻烦, 需要对自定义的struct实现<code>Len() int</code> ,  <code>Less(i, j int) bool</code>, <code>Swap(i, j int) </code>三个接口</p>
<pre><code class="language-go">package main

import (
	"sort"
)

func main() {
	numbers := []int{5, 2, 9, 1, 5, 6}

	sort.Ints(numbers) // 排序，可以选择Ints, Strings, Stable等
}

// 自定义类型排序
type Person struct {
	Name string
	Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age &#x3C; a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

func main() {
	people := []Person{
		{"Alice", 25},
		{"Bob", 20},
		{"Charlie", 30},
	}

	sort.Sort(ByAge(people)) // 排序
}
</code></pre>
<h4>字符串处理函数</h4>
<p>Go中，string是不可变的 UTF-8 编码字符串, 无法原地修改字符串（编译失败）。[]byte是字符序列切片，可原地修改字符串。</p>
<p>golang字符串处理函数strings，如果要修改字符串, 必然会有内存拷贝。</p>
<pre><code class="language-go">s := "Hello, World!"
b := []byte(s) // 将 string 转换为 []byte

b := []byte{72, 101, 108, 108, 111}
s := string(b) // 将 []byte 转换为 string

str := "Hello"
len(str)  // 返回长度
str[1]  // 根据索引查看字符

str1 := "Hello"
str2 := "World"
result := str1 + ", " + str2  // + 拼接字符串

strings.Index(str, "World")  // 查找字符串
strings.Contains(str, "World")

// 以下都有内存拷贝
strings.Split(str, ",")  // 字符串分割，返回字符串数组
strings.TrimSpace(str)  // 去掉两侧空白

strings.ToUpper(str)  // 字符串大小写
strings.ToLower(str)

// 格式化生成新字符串
result := fmt.Sprintf("Name: %s, Age: %d", name, age)

strconv.Atoi(numStr) // 字符串转整数
</code></pre>
<p>bytes 包处理 []byte</p>
<pre><code class="language-go">
data := []byte("name:age:email")

// 分割操作（无拷贝，共享底层数组）
parts := bytes.Split(data, []byte(":")) 
</code></pre>
<h3>Python</h3>
<h4>数据结构</h4>
<p>列表，动态数组。和go一样，python列表支持切片。</p>
<pre><code class="language-python">my_list = [1, 2, 3, 4, 5]
my_list[0] = 10
my_list.append(6)
</code></pre>
<p>元组Tuple，不可变，只可被访问。</p>
<pre><code class="language-python">my_tuple = (1, 2, 3)
a, b, c = my_tuple
</code></pre>
<p>字典dict, 无序（Python 3.7 及以上版本中插入有序）、键值对、键唯一。</p>
<ol>
<li>dict的内部实现基于​​开放寻址法的哈希表​​</li>
<li>python3.7之后, 字典内部新增一个​​双向链表​​（或紧凑数组），按插入顺序记录所有键值对。</li>
</ol>
<pre><code class="language-python">my_dict = {"name": "Alice", "age": 25}
my_dict["age"] = 30

​​要求​​：对象必须实现__hash__和__eq__方法。
__hash__：返回唯一且不变的哈希值。
__eq__：判断键是否相等。

class User:
    def __init__(self, id):
        self.id = id
    
    def __hash__(self):
        return hash(self.id)
    
    def __eq__(self, other):
        return self.id == other.id

users = {User(1): "Alice", User(2): "Bob"}
</code></pre>
<p>集合set，无序、不重复。</p>
<pre><code class="language-python">my_set = {1, 2, 3, 4, 5}
my_set.add(6)
</code></pre>
<p>有序字典, 保留键值对插入顺序</p>
<pre><code class="language-python">from collections import OrderedDict

ordered_dict = OrderedDict()
ordered_dict["a"] = 1
ordered_dict["b"] = 2
</code></pre>
<p>python的 str不可变, 任何修改生成新对象（原对象不变）</p>
<pre><code class="language-python">
# 列表（可变）
lst = [1, 2, 3]
lst[0] = 10      # 直接修改 → [10, 2, 3]
lst.append(4)    # 追加元素 → [10, 2, 3, 4]

# 字符串（不可变）
s = "hello"
s[0] = "H"       # 报错：TypeError
new_s = s.replace("h", "H")  # 生成新对象 "Hello"

# 列表
lst = [1, 2, 3, 4]
sub_lst = lst[1:3]  # [2, 3]（浅拷贝）
# 字符串
s = "hello"
sub_s = s[1:3]     # "el"（新字符串）
</code></pre>
<h4>算法</h4>
<p>排序
sorted(iterable, key=None, reverse=False), 返回一个新的排序列表，不改变原列表。</p>
<pre><code class="language-python">nums = [5, 2, 9, 1]
print(sorted(nums))  # 升序: [1, 2, 5, 9]
print(sorted(nums, reverse=True))  # 降序: [9, 5, 2, 1]

words = ["apple", "banana", "cherry"]
print(sorted(words, key=len))  # 按字符串长度排序: ['apple', 'cherry', 'banana']

# list.sort(), 原地排序，直接修改列表
nums = [5, 2, 9, 1]
nums.sort()

# 字典排序
my_dict = {"b": 2, "a": 1, "c": 3}
sorted_keys = sorted(my_dict)  # 按键排序
sorted_items = sorted(my_dict.items(), key=lambda item: item[1])  # 按值排序
</code></pre>
<h4>字符串处理</h4>
<pre><code class="language-python">s = "Hello"
len(s)
s[1]
s[-1]
s[1:3]
s + " "  # 拼接
print(f"My name is {name} and I am {age} years old.")  # 格式化输出, python3支持
s.upper()，s.lower()
s.strip()  # 去除空格
s.find("World")
s.replace("World", "Python")

s.split(",")
</code></pre>
<p>对于python2, 还有个编码的问题。处理办法是，对于u开头的字符串或者unicode类型的字符串，都使用encode("utf-8") 编码为str类型后，再使用。</p>
<pre><code class="language-python"># coding=utf-8
unicode_str = u"中文"  # Unicode 字符串
byte_str = "byte"  # 字节字符串
print(type(unicode_str))  # &#x3C;type 'unicode'>
print(type(byte_str))  # &#x3C;type 'str'>

print(type(unicode_str.encode("utf-8")))  # &#x3C;type 'str'>
</code></pre>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[编程语言——映射结构]]></title>
          <link>https://larrystd.github.io/posts/编程语言—映射结构</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/编程语言—映射结构</guid>
          <pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[映射是最重要的数据结构之一，动态数组也可以看做特殊的映射。映射的优势是，它的增删改查性能都很好。增 增加key，删 删除key，改 更改某key的vakue，查 给定key查询value。]]></description>
          <content:encoded><![CDATA[<p>映射是最重要的数据结构之一，动态数组也可以看做特殊的映射。映射的优势是，它的增删改查性能都很好。增->增加key，删->删除key，改->更改某key的vakue，查->给定key查询value。</p>
<h3>映射结构</h3>
<p>C++ STL 对映射有两种实现，红黑树和哈希表。</p>
<h4>红黑树</h4>
<p>红黑树是在AVL树发展的二叉有序平衡树。AVL树通过左旋、右旋实现了高度平衡树，查询更改性能稳定O(n/2)，但频繁插入删除会造成经常的rebalance，造成插入、删除性能下降。</p>
<p>红黑树有以下要求</p>
<ol>
<li>每个节点都是红色或黑色。</li>
<li>根节点始终是黑色。</li>
<li>每个<strong>叶节点（NIL 或空节点）是黑色</strong>。（实际红黑树叶子节点是黑色的NIL，NIL不存储数据，存储数据的“叶子节点”可以为红色）</li>
<li>从任意节点到其每个叶节点的<strong>所有路径中，包含相同数量的黑色节点</strong>。</li>
<li>红色节点的两个子节点必须是黑色（红色节点不能有两个连续的红色节点）。</li>
</ol>
<p><img src="/images/rbtree.png" alt="rbtree" title="rbtree"></p>
<p>插入操作时，新插入的节点被视为红色（孩子是黑色的NIL）。如果新插入节点的父节点为黑色，插入后不需要调整。只有当插入节点的父节点为红色，才需要调整。</p>
<p>删除操作类似，删除的节点如果是红色，调整较少；只有删除的节点是是黑色，才需要重新调整</p>
<p>具体后面有时间再分析吧2333</p>
<h4>STL 哈希表</h4>
<p>哈希表的实现主要有两部分 1. hash计算索引 2. 哈希碰撞处理</p>
<p>哈希表需要根据类型key计算hash key，来确定变量在hash表的位置。C++ hash函数使用std::hash模版函数计算hash key</p>
<ol>
<li>对于整数类型，通常直接返回值本身或经过简单混淆的值（如乘以某个大素数）。</li>
<li>浮点型，std::hash 会将其二进制表示重新解释为整数，然后对该整数值计算哈希。</li>
<li>字符串类型，<code>std::hash&#x3C;std::string></code> 通常使用简单的字符串哈希算法, 例如DJB2 算法。DJB2通过<strong>逐字符累积计算哈希值</strong>，每次累积时将现有的哈希值乘以一个素数（通常是 33），再加上当前字符的 ASCII 值。</li>
<li>指针，通常直接使用指针的值（地址）</li>
</ol>
<p>哈希碰撞处理，通常使用开链法。</p>
<p>在hash表碰撞不严重的情况下，哈希表的增、删、改、查时间复杂度都是O(1)。另外，由于hash表的数组是预先分配好的，动态处理比红黑树简单很多。例如hash表可以通过key数量除以数组长度作为负载因子来判断是否应该扩容缩容。（相比下，红黑树没有扩缩容的概念？）</p>
<p>hash表的数组（slot）长度通常选择素数（减少冲突），或者选择2的幂次，容易使用按位操作（hash &#x26; (bucket_count - 1)）来快速定位索引。</p>
<p>hash表扩缩容也称为rehash，数组size为素数的扩容，size选择距离扩容前size两倍最近的素数；数组size为2的幂次的扩容，size直接乘2即可。</p>
<p>rehash单线程场景下，rehash操作在用户程序进程执行（可以在调用插入命令后触发，也可手动调用rehash函数触发）。</p>
<p>多线程情况下，rehash往往由后台线程执行，需要考虑和前台io线程的冲突。一般来说，前台io线程或后台扫描线程根据负载因子，触发rehash，rehash首先创建扩容后的数组。创建完后通知前台io线程数据正在rehash。</p>
<p>rehash是从旧表读数据，向新表插数据。本质是个增操作，单位是key</p>
<ol>
<li>增操作
<ol>
<li>如果数据在旧表不存在，则直接新hash表增加key，多线程的数据增操作很重，需要对整个hash表（或分段）加锁</li>
<li>如果数据已存在，则判断key是否rehash完毕。如果rehash完毕，在新表删除旧key然后加新key; 没有rehash则旧表删除旧key,新表增加新key；如果正在rehash，需要等rehash完抢到锁，然后在新表删除旧key然后加新key</li>
</ol>
</li>
<li>删操作
<ol>
<li>数据在旧表不存在，报错</li>
<li>数据在旧表存在，类似增操作。需要看key的rehash状态，rehash完在新表删除，否则在旧表删除</li>
</ol>
</li>
<li>改操作
<ol>
<li>数据在旧表不存在，报错</li>
<li>数据在旧表存在，如果key rehash完毕，在新表改；否则在旧表改</li>
</ol>
</li>
<li>查操作
<ol>
<li>先查询新表，新表查询不到则查询旧表。</li>
</ol>
</li>
</ol>
<p>哈希表的rehash是一个数据迁移操作，对表迁移有很大参考意义。更进一步</p>
<ol>
<li>hash表支持将某个value从key1，移动到key2，需要是原子的（即文件系统的rename）。多线程时需要对key1和key2加锁，需要避免死锁。此外，rehash时应该怎么处理？</li>
<li>hash表怎么拆分，key锁，插入锁，怎么优化。</li>
</ol>
<p>问题1, rehash时，可能出现key1在旧hash表, key2在新hash表。为了死锁避免，一张表或一个segment只同时刻允许一次rename（可使用单线程多协程实现），为了保证rename的原子性（即删除key1,增加key2两个操作需要原子性），可以使用事务。更复杂的，两张表可能不在一个机器上，需要更复杂的死锁避免，同时为了保证原子性需要分布式事务。</p>
<h4>跳跃表</h4>
<p>hash表的的数据排布是无序的，常用的有序替代品是跳跃表skiplist。redis，leveldb, rocksdb的有序内存集合都是使用跳跃表实现。由于内存数据不能持久化，redolog+跳跃表的组合是kv单机存储引擎最常见的组合。</p>
<p>使用redolog的表迁移(rehash)，可以选择让新表一直replay redolog来让数据和旧表保持一致。但这种办法不能实现彻底的热迁移，当检测到写请求少时（例如晚上），旧表会禁止用户写入，等待新表replay完毕，将新表配置成接受读写请求的表。期间会有短暂的写不可用，不可用时间主要来自等待新表replay完毕和必要的数据校验。这种表迁移比较简单，尤其当表被拆分到多个机器中（分布式表），只需要让机器1和机器2能共享相同的redolog文件。例如把redolog存储到hdfs中，当机器A的表尝试rehash到机器2时，只需要机器2 replay写到hdfs中的机器1的redolog即可。replay过程中，数据读写照常在机器1进行，直到监控到写请求少，进行短暂的禁写等待机器2replay完毕，然后通知placement该表的后续读写路由到机器2，即可。</p>
<p>越简单的实现越安全，越可控。所以生产上尤其是分布式系统中，表迁移大多数借助replay redolog实现。</p>
<h4>JAVA hashmap和线程安全hash表</h4>
<p>java 的hashmap实现和C++的unordered_map大同小异，采用头插法处理碰撞，非线程安全。</p>
<p>java的每个对象都会有hashcode函数，hashcode可被重写，默认使用对象的地址。hashcode决定了对象插入到hash表时的hash key。</p>
<p>前面说到，hash表多线程增删操作很重，需要对全表加写锁；改操作只需要对key加写锁。java hashtable的线程安全就是通过全表加锁实现。</p>
<p>为了降低锁的粒度，JAVA ConcurrentHashMap 实现segment->table两级哈希表。定位元素需要进行两次Hash，第一次 Hash 定位到 Segment，第二次 Hash 定位到元素所在的链表的头部。加锁只需要对segment加写锁，不影响其他segment。</p>
<p>使用多级hash表还可以降低存储空间的占用，可参考MMU 虚拟内存->物理内存映射的哈希表。对于两个key 10、499，一张哈希表需要512个slot，两张哈希表只需要256+26=282个slot。</p>
<p><strong>JAVA volatile关键字保证的可见性可以实现多线程查询操作无锁</strong>，通过使用volatile修饰key，get操作不用加锁。使用final修饰常量，也可以保证查操作无锁。</p>
<h4>golang的map和sync.Map</h4>
<p>golang的map是非协程安全的，go提供协程安全map sync.Map。</p>
<p>golang map 使用开链法处理冲突。初始时map的键值对存放到哈希桶中，哈希桶是一个对象，可以放置 8 个键值对。超出哈希桶后的键值对存放到溢出桶，溢出桶就是开链法的链表。</p>
<p>sync.Map的实现比较特殊，使用了两个原生map，一个叫read，仅用于读；一个叫dirty，用于存储最新写入的key-value数据。read map相当于读缓存</p>
<ol>
<li>写操作：直接写入dirty map</li>
<li>读操作：先读read map，没有再读dirty map</li>
</ol>
<h4>Python的dict</h4>
<p>Python的映射称之为字典，dict</p>
<p>python使用P开放地址法（Open Addressing） 来解决冲突。当计算出的索引位置已被占用时，通过一定的规则寻找下一个空闲位置。Python 字典采用了一种变体的开放地址法，称为 探测（Probing），通常是线性探测或伪随机探测。</p>
<p>当字典中的元素数量接近数组的容量时，数组会扩展到原来容量的两倍。</p>
<p>Python的可变对象（如列表）不可哈希，不能作为字典的键。只有不可变对象（如字符串、元组）才可以作为dict的键。</p>
<p>dict不是线程安全的</p>
<h3>其他常见数据结构的实现</h3>
<p>TODO</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[language]]></category>
        </item>
        <item>
          <title><![CDATA[linux系统(3)——系统监控和问题排查]]></title>
          <link>https://larrystd.github.io/posts/linux系统(3)—系统监控和问题排查</link>
          <guid isPermaLink="true">https://larrystd.github.io/posts/linux系统(3)—系统监控和问题排查</guid>
          <pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate>
          <description><![CDATA[本文介绍linux系统的监控工具和问题排查的一般步骤，文章大体内容 1. 用户程序一定要丰富trace和日志，并对trace做监控。正常情况下如果程序处理出现问题，会在监控告警中显示，介入排查可以通过trace和日志，判断出问题的文件、执行的线程等信息，绝大多数排查可以在这里停止，根本不用抓栈抓core。trace和日志必须包含的, 1. 时间戳 2....]]></description>
          <content:encoded><![CDATA[<p>本文介绍linux系统的监控工具和问题排查的一般步骤，文章大体内容</p>
<ol>
<li>用户程序一定要丰富trace和日志，并对trace做监控。正常情况下如果程序处理出现问题，会在监控告警中显示，介入排查可以通过trace和日志，判断出问题的文件、执行的线程等信息，绝大多数排查可以在这里停止，根本不用抓栈抓core。trace和日志必须包含的, 1. 时间戳 2. 函数名和位置 3. 线程ID和线程名, 这三个信息是排查必须</li>
<li>如果日志无法给出运行错误的原因，则需要进入机器查看。首先看有无core产生，产生core的原因一般是非法内存访问、double free、内存踩坏等或oom，因此首先去/var/log/messages 看是否oom导致，oom会记录到/var/log/messages日志。</li>
<li>如果没有core产生，但进程处理慢。可以先perf top -K -t $tid 查看耗时长的函数，原因大概是1. 被流控 2. 底层数据库/IO服务处理慢，反压上层 4. CPU/IO/网络被打满，资源不足 5. 线程数量不足，任务队列堆积 等原因，这些原因本都应该展示在监控和日志里</li>
<li>如果有oom，需要内存占用高的线程抓core，分析内存占用高和是否有内存泄漏；如果有core, 则需要分析产生core产生的原因。先在debug 环境下尝试复现core，同时调用valgrind等工具分析。分析core比较困难，可以直接找最有经验的人来协助。</li>
</ol>
<h3>系统CPU、内存、IO、网络监控</h3>
<h4>CPU和内存监控——top工具</h4>
<p>top 能整体查看系统运行情况工具。利用top，能够观察到cpu,内存的运行情况，过滤出cpu 内存占用高的进程</p>
<pre><code class="language-shell">top - 16:16:56 up 16:55,  0 users,  load average: 2.01, 1.83, 1.48
Tasks: 340 total,   1 running, 338 sleeping,   1 stopped,   0 zombie
%Cpu(s):  1.3 us,  0.3 sy,  0.0 ni, 98.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   7902.0 total,   2180.4 free,   2668.4 used,   3053.2 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   4924.6 avail Mem 
  scroll coordinates: y = 1/340 (tasks), x = 1/12 (fields)
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                               
 172367 root      20   0   31.3g 171008  54448 S   1.0   2.1   0:56.97 node                                                                  
   1832 root      20   0   11.4g  76164  43988 S   0.7   0.9   7:02.08 node                                                                  
 171284 root      20   0   13884   8968   7412 S   0.7   0.1   0:06.62 sshd                                                                  
     14 root      20   0       0      0      0 I   0.3   0.0   5:13.07 rcu_sched                                                             
   1774 root      20   0   11.3g 128480  46240 S   0.3   1.6   9:43.66 node

us(用户态进程占用)，
sy(内核进程占用), 
ni(nice低优先级进程占用), 
id(idle, cpu空闲时间占比), 
wa(wait, IO等待时间占比, 如果时间段在执行io且cpu空闲，则这段空闲cpu时间认为是wait), 
hi(hard interrupt) 硬中断执行时间,
si(soft interrupt) 操作系统软中断执行时间,
st(Steal Time) 分配给虚拟机的时间片
</code></pre>
<p>top -p $pid，输出指定进程的cpu使用情况; top -H -p $pid，输出指定进程的线程cpu使用情况</p>
<p>top 命令点击大写P、M实现按照cpu/mem 排序的进程，点击数字键1 可以看到每个cpu的占用。</p>
<h4>IO监控——iostat</h4>
<p>iostat是监控磁盘性能的工具。如果top发现cpu慢在wa，可以使用iostat看具体盘有无问题。命令<code>iostat -x 1 2</code></p>
<pre><code>avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           1.69    0.01    1.05    0.02    0.00   97.23

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dkB/s   drqm/s  %drqm d_await dareq-sz  aqu-sz  %util
loop0            0.00      0.00     0.00   0.00    0.29     1.21    0.00      0.00     0.00   0.00    0.00     0.00    0.00      0.00     0.00   0.00    0.00     0.00    0.00   0.00

r/s w/s 每秒读和写的请求数量(qos)
rrqm/s wrqm/s 每秒合并的读写请求数量
rkB/s wkB/s 每秒读写的字节数(吞吐)
r_await w_await 读写操作io平均等待时间
avgrq-sz：每个IO的平均扇区数，一个扇区512字节
await：平均每个IO所需要的时间, 
%util 磁盘的利用率
</code></pre>
<p>磁盘利用率表示io时间占cpu总时间的比例， 表示cpu处理空闲阶段且存在io操作，这段cpu时间片就认为是iowait消耗的</p>
<h4>网络监控——ifconfig, netstat, ittop</h4>
<p>ifconfig，查看网卡、ip地址、网络传输量、丢包情况等基本信息</p>
<pre><code class="language-shell">ens33: flags=4163&#x3C;UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.92.128  netmask 255.255.255.0  broadcast 192.168.92.255
        inet6 fe80::20c:29ff:fe51:9544  prefixlen 64  scopeid 0x20&#x3C;link>
        ether 00:0c:29:51:95:44  txqueuelen 1000  (Ethernet)
        RX packets 695151  bytes 132025574 (132.0 MB)
        RX errors 183  dropped 206  overruns 0  frame 0
        TX packets 863078  bytes 357598696 (357.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 19  base 0x2000
</code></pre>
<p>netstat -nultp 查看pid和监听的端口信息, 可以用来查看链接数量</p>
<pre><code class="language-shell">Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:39997         0.0.0.0:*               LISTEN      171361/code-fabdb6a 
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      130592/systemd-reso 
tcp        0      0 127.0.0.1:44791         0.0.0.0:*               LISTEN      172327/code-fabdb6a 
tcp        0      0 127.0.0.1:32819         0.0.0.0:*               LISTEN      169497/code-fabdb6a 
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      2793/cupsd
</code></pre>
<p>netstat -an | grep ":80" | wc -l, 查看端口的链接数量
netstat -r 显示路由表</p>
<p>netstat -s查看网络统计信息，统计时间是从系统启动到执行命令。如果想查最近1分钟的数据, 需要1分钟后再执行，二者数值作差</p>
<pre><code class="language-shell">Ip:
    Forwarding: 2
    2963380 total packets received
    15 with invalid addresses
    0 forwarded
    0 incoming packets discarded
    2963365 incoming packets delivered
    1762116 requests sent out
    20 outgoing packets dropped
    14 dropped because of missing route
Icmp:
    44 ICMP messages received
    0 input ICMP message failed
    ICMP input histogram:
        destination unreachable: 43
        echo requests: 1
    44 ICMP messages sent
    0 ICMP messages failed
    ICMP output histogram:
        destination unreachable: 43
        echo replies: 1
Tcp:
    7492 active connection openings
    2514 passive connection openings
    2028 failed connection attempts
    23 connection resets received
    17 connections established
    2945531 segments received
    3981690 segments sent out
    414 segments retransmitted
    2 bad segments received
    351 resets sent
</code></pre>
<p>ping和curl 查看网络和端口的连通性</p>
<p>使用iftop 统计网卡实时流量, iftop -i 指定网卡</p>
<pre><code class="language-shell">TX:             cum:   12.4KB   peak:   11.9Kb                                                                                                          rates:   9.09Kb  9.89Kb  9.89Kb
RX:                    8.64KB           8.17Kb                                                                                                                   8.17Kb  6.91Kb  6.91Kb
TOTAL:                 21.0KB           18.3Kb                                                                                                                   17.3Kb  16.8Kb  16.8Kb
</code></pre>
<h4>sar和tsar 统计历史信息</h4>
<p>以上top, netsta, iostat等工具只能统计当前时刻的信息，有时候我们需要历史统计信息，就需要sar和tsar</p>
<p>sar (System Activity Report) 是 Linux 系统中的一个性能监控工具，<strong>可以用来统计历史的cpu</strong>, 内存, io**, 网络等监控数据; tsar 是淘宝开源的性能监控工具, 同样可以用来统计历史监控数据。sar 需要安装sysstat</p>
<p>sar -u 1 5 统计cpu 信息</p>
<pre><code class="language-shell">02:17:10 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
02:17:11 PM     all      0.06      0.00      0.13      0.00      0.00     99.81
02:17:12 PM     all      0.13      0.00      0.44      0.00      0.00     99.44
02:17:13 PM     all      0.06      0.00      0.38      0.00      0.00     99.56

Average:        all      0.08      0.00      0.31      0.00      0.00     99.60
</code></pre>
<p>sar -hr 1 5 显示内存信息</p>
<pre><code class="language-shell">02:18:34 PM kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
02:18:35 PM      2.8G      4.8G      2.5G     32.8%    116.8M      2.0G      2.7G     35.2%    793.5M      3.6G      8.0k
02:18:36 PM      2.8G      4.8G      2.5G     32.8%    116.8M      2.0G      2.7G     35.2%    793.5M      3.6G      8.0k
Average:         2.8G      4.8G      2.5G     32.8%    116.8M      2.0G      2.7G     35.2%    793.5M      3.6G      8.0k
</code></pre>
<p>sar -d 1 5 查看磁盘统计, 统计每个磁盘的iops, 读写吞吐, wait, 使用率</p>
<pre><code class="language-shell">Average:          DEV       tps     rkB/s     wkB/s     dkB/s   areq-sz    aqu-sz     await     %util
Average:       dev7-0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:       dev7-1      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:       dev7-2      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
# tps Transactions Per Second, 每秒io数量
</code></pre>
<p>sar -n DEV 1 5 统计网络流量</p>
<pre><code class="language-shell">02:35:32 PM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
02:35:33 PM        lo     32.67     32.67      3.74      3.74      0.00      0.00      0.00      0.00
02:35:33 PM     ens33     16.83     15.84      2.34      2.45      0.00      0.00      0.00      0.00

rxkB/s, 每秒接收的数据量（KB）。
txkB/s, 每秒发送的数据量（KB）。
%ifutil, 网络接口利用率
</code></pre>
<p>sar -q 1 5 查看系统负载（进程数量）信息</p>
<pre><code class="language-shell">02:35:51 PM   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
02:35:52 PM         2       616      0.53      0.40      0.38         0
02:35:53 PM         0       618      0.53      0.40      0.38         0
Average:            1       617      0.53      0.40      0.38         0

runq-sz：运行队列的长度。
plist-sz：当前进程的数量。
ldavg-1、ldavg-5、ldavg-15：系统在 1、5 和 15 分钟的平均负载。
</code></pre>
<h3>进程监控</h3>
<h4>进程基本信息获取</h4>
<p>ps -ef, ps aux 获得进程的pid, 状态等信息。一般拿到进程pid后就直接top -p</p>
<p>/proc/{pid} 目录可以看到进程相关资源信息。例如
/proc/{pid}/fd 查看打开的文件信息</p>
<pre><code class="language-shell">dr-x------ 2 root       root        0 1月   3 23:22 ./
dr-xr-xr-x 9 messagebus messagebus  0 1月   3 23:22 ../
lrwx------ 1 root       root       64 1月   3 23:22 0 -> /dev/null
lrwx------ 1 root       root       64 1月   3 23:22 1 -> 'socket:[39683]'
lrwx------ 1 root       root       64 1月   3 23:22 10 -> 'socket:[269352]'
lrwx------ 1 root       root       64 1月   3 23:22 11 -> 'socket:[36858]'
</code></pre>
<p>/proc/{pid}/cgroup，查看进程cgroup配置信息</p>
<pre><code class="language-shell">13:hugetlb:/
12:perf_event:/
11:blkio:/system.slice/dbus.service
10:memory:/system.slice/dbus.service
9:rdma:/
8:pids:/system.slice/dbus.service
7:freezer:/
6:cpu,cpuacct:/system.slice/dbus.service
5:cpuset:/
4:misc:/
3:devices:/system.slice/dbus.service
2:net_cls,net_prio:/
1:name=systemd:/system.slice/dbus.service
0::/system.slice/dbus.service
</code></pre>
<p>具体的cgroup内容在/sys/fs/cgroup/下查看</p>
<pre><code class="language-shell">root@ubuntu2204:/home/tech-blog# ls /sys/fs/cgroup/
cgroup.controllers      cgroup.subtree_control  cpu.stat             io.cost.qos       memory.pressure                sys-kernel-config.mount
cgroup.max.depth        cgroup.threads          dev-hugepages.mount  io.pressure       memory.stat                    sys-kernel-debug.mount
cgroup.max.descendants  cpu.pressure            dev-mqueue.mount     io.prio.class     misc.capacity                  sys-kernel-tracing.mount
cgroup.procs            cpuset.cpus.effective   init.scope           io.stat           proc-sys-fs-binfmt_misc.mount  system.slice
</code></pre>
<h4>进程CPU内存IO使用情况</h4>
<p>使用top -p $pid 可以查看进程的cpu/内存使用情况</p>
<p>pidstat 命令可以查看进程级的统计信息
基础信息统计</p>
<pre><code class="language-shell">root@ubuntu2204:/home/tech-blog# pidstat 1 5
Linux 5.15.0-138-generic (ubuntu2204)   04/30/2025      _x86_64_        (16 CPU)

02:25:21 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
02:25:22 PM   116      1087    0.00    0.95    0.00    0.00    0.95     6  beam.smp
02:25:22 PM   114      1242    0.95    0.95    0.00    0.00    1.90     9  mysqld
02:25:22 PM     0      1931    0.00    0.95    0.00    0.00    0.95     1  node
02:25:22 PM     0      1987    0.95    0.95    0.00    0.00    1.90    12  node
02:25:22 PM     0      2424    0.95    0.95    0.00    0.00    1.90     2  Lingma
02:25:22 PM     0      5203    0.00    0.95    0.00    0.95    0.95     7  node
02:25:22 PM     0      9837    0.00    0.95    0.00    0.00    0.95     4  kworker/4:4-pm
02:25:22 PM     0     10030    0.00    0.95    0.00    0.00    0.95     4  sshd
02:25:22 PM     0     11977    1.90    0.95    0.00    0.00    2.86     2  pidstat
</code></pre>
<h3>进程性能问题排查</h3>
<p>进程出现性能问题，原因可能如下</p>
<ol>
<li>CPU 资源不足，原因可能是进程cgroup限制cpu核数导致资源不足，CPU各核心负载分配不均，进程下线程数量过多，导致有的线程抢占不到CPUhang住等。CPU资源不足导致的结果一般是进程处理能力无法上升</li>
<li>内存资源不足，可能是发生内存踩坏、double free 或内存泄漏；前者导致进程崩溃产生core，后者导致进程内存使用不断上涨，直到oom被系统kill。</li>
<li>IO资源不足，也就是读写数据库/文件变慢，这个最好排查</li>
<li>程序自身的问题，例如线程分配数量过少、存在函数耗时过长、死锁等导致性能问题</li>
</ol>
<p>一般来说问题排查可以1-2-3-4逐次进行</p>
<h4>CPU问题</h4>
<p>CPU问题主要有两个1. CPU资源不足 2. CPU负载不均，某线程占用的大量CPU导致其他线程hang住</p>
<p>首先通过top看进程当前的cpu占用，看是否接近cgroup 的cpu核数限制</p>
<ol>
<li>如果接近cgroup限制，适当调高cgroup</li>
<li>利用top查看所有的cpu核心是否打满，如果没有打满，后续应优化核心的资源均衡</li>
<li>利用top -p $pid -H查看cpu占用最高的线程</li>
<li>对cpu占用最高的线程执行, <code>perf top -K -t &#x3C;tid></code> 可以查看线程主要开销的调用栈，其中-K表示过滤调内核模块</li>
</ol>
<p>perf top -K -t $tid的示例，注意线上千万不要对进程执行perf！！！否则有进程重启的风险
<img src="/images/perf_top.png" alt="perf_top"></p>
<p>进一步的, 对线程抓热度图。命令如下</p>
<ol>
<li><code>sudo perf record -e cpu-clock -t $tid --call-graph dwarf</code>，如果要对进程执行，将-t $tid改为-p $pid, 执行完后会生成perf.data文件</li>
<li>用perf script工具对perf.data文件解析, <code>perf script -i perf.data &#x26;> perf.unfold</code></li>
<li>将perf.unfold中的符号折叠, 执行<code>./stackcollapse-perf.pl perf.unfold &#x26;> perf.folded</code></li>
<li>最后生成svg火焰图 <code>./flamegraph.pl perf.folded > perf.svg</code></li>
</ol>
<p>其中./stackcollapse-perf.pl和./flamegraph.pl 工具均来自 <a href="https://github.com/brendangregg/FlameGraph">https://github.com/brendangregg/FlameGraph</a></p>
<p>利用火焰图可以直观的观察调用栈和每个栈的执行时间，调用栈越宽的表示CPU执行时间长，可能需要优化。例如
<img src="/images/flamegraph.png" alt="frame_graph"></p>
<p>获取调用栈需要设置--call-graph dwarf, 参考 <a href="https://gaomf.cn/2019/10/30/perf_stack_traceback/">https://gaomf.cn/2019/10/30/perf_stack_traceback/</a></p>
<p>一般来说如果不是cgroup的问题，只能将进程的一些次要任务迁移走或调低，优先保证主要任务的CPU占用。</p>
<h4>内存问题</h4>
<p>内存问题也主要有两个1. 内存踩坏或double free（常发生在C/C++）2. 内存泄漏导致oom（也常发送在C/C++）。对于有GC的语言如JAVA、GO，如果内存用量持续增高，只要观察并调整相应jvm参数加快GC，就可缓解内存上涨；而对于C/C++，由于内存创建和释放完全由程序员负责，内存泄漏问题很常见，且难以排查。</p>
<p>C++ 的debug编译一定开启asan（​​Address Sanitizer）, 即<code>g++ -fsanitize=address -g xxx</code>, 在UT覆盖的情况下执行UT 时Asan可以帮助检查内存泄漏。其核心原理是通过 ​​内存插桩（Instrumentation）​​ 和 ​​影子内存（Shadow Memory）​​ 机制，实时监控程序的内存操作。Valgrind 也用于内存泄漏的工具，相比asan是编译期插桩，Valgrind是运行期插桩，运行慢但检查更全面。一般选择asan快速定位内存问题，再用Valgrind深入分析。
Asan和Valgrind都会带来性能损失，一般只用于debug 环境。</p>
<p>如果生成环境遇到内存踩坏或内存泄漏，前者一般导致进程产生core，后者在监控上能看到机器内存使用一直缓慢增长。这时我们一般首先拿到core文件，内存踩坏一般直接会产生core，内存泄漏我们使用gcore来抓core， gcore抓core可能要持续几十秒（请耐心等待）。gcore命令的好处是不需要进程重启。（执行kill -s 11会让进程直接segment fault产生core，但会导致进程重启）。如果是内存泄漏问题，可以先尝试对内存占用高的线程进行抓core</p>
<p>一个优化的抓core方法是让进程提供抓core的运维命令，进程收到命令后，fork出一个子进程继续处理任务，将父进程abort()掉，自动生成core。</p>
<p>抓到core后可以获取两种信息</p>
<ol>
<li>进程core掉时的每个线程的调用栈信息</li>
<li>进程core掉时的内存信息</li>
</ol>
<p>获取线程调用栈信息的步骤, 执行如下命令将所有调用栈打印到gdb.txt文件中</p>
<pre><code class="language-shell">gdb binary corefile

set height 100000
set logging on
thread apply all bt
</code></pre>
<p>获取进程core掉的内存信息，</p>
<ol>
<li>gdb里执行<code>info proc mappings</code>命令显示core文件中的虚拟内存映射表，同时会输出每个内存映射的start_addr, end_addr, size, offset, objfile</li>
<li>执行<code>dump binary memory result.bin $start_addr $end_addr</code> 将内存大隐刀本地result.bin文件中</li>
<li>在vim里查看result.bin文件，通过<code>:%!xxd</code>方式以16进制模式来查看，左侧是16进制地址，右侧是ASCII字符</li>
</ol>
<p>主要是看右侧的ASCII字符，看有没有相关的提示。</p>
<p>C++ 的release binary和core 会丢失代码里的类型信息、函数信息，core能看到的大多数只是一串内存地址和内存里的数据，但我们难以知道这段内存对应代码的哪个地方。一方面我们要猜测哪个地址发生了内存泄漏，另一方面也要猜测发生内存泄漏的地址位于哪部分代码。</p>
<p>我们可以通过强化tcmalloc和gdb工具辅助排查内存泄漏，tcmalloc主要是插桩，在tcmalloc分配和释放内存时做记录，gdb主要是辅助解析，例如统计申请内存大小为xxx的调用信息。利用这些记录和统计信息可以帮助内存泄漏的排查。如果C++程序里有虚函数，也可以通过虚函数记录的类型信息帮助分析（JAVA的函数默认都是虚函数，这让JAVA排查core比C++简单很多）。</p>
<h4>IO和程序自身的问题</h4>
<p>如果是程序自身的问题，1. 导致CPU占用高 2. 导致内存踩坏或内存泄漏，前面已经分析过</p>
<p><strong>用户程序必须要打印trace日志</strong>，尤其是读写文件、读写数据库等IO操作，通过trace日志可以直接判断IO问题。</p>
<p><strong>用户程序应该要让线程定期执行报备的操作</strong>，如果某个线程长期没有报备，则说明该线程要么Hang住，要么死锁，要么是其他原因阻塞住。</p>
<h4>pstack和strace</h4>
<p>sudo pstack $pid 用来抓进程/线程调用栈</p>
<p>strace -p $pid, 用来抓进程/线程的系统调用信息</p>
<p>这两个命令都十分有用，可以在发现CPU、内存、IO的异常线程时，对线程执行sudo pstack 和sudo strace， 但切记不要对进程执行这俩命令。</p>
<p>如果是线程死锁导致进程处理慢，pstack几次线程就可以判断死锁了。</p>
<h4>总结</h4>
<p>debug环境下要做到</p>
<ol>
<li>编译开启asan检查内存泄漏，有必要的话clang-format, clang-tidy, Valgrind 也要打开</li>
<li>UT覆盖率至少90%，UT可以保证单线程场景下接口的运行正确，无内存泄漏</li>
<li>上线前一定要进行多并发压测和长稳测试，主要是观察多线程情况下程序运行是否正常</li>
<li>上线前的功能测试，目的是确认程序功能正常，也就是程序逻辑是对的。如果程序逻辑不正确，上线后难以检测到（无法通过CPU，内存，QPS等指标判断异常）。<strong>因此功能覆盖率一定要100%</strong>。</li>
</ol>
<p>用户程序一定要丰富trace和日志，并对trace做监控。正常情况下如果程序处理出现问题，会在监控告警中显示，介入排查可以通过trace和日志，判断出问题的文件、执行的线程等信息，绝大多数排查可以在这里停止，根本不用抓栈抓core。trace和日志必须包含的, 1. 时间戳 2. 函数名和位置 3. 线程ID和线程名, 这三个信息是排查必须</p>
<p>如果日志无法给出运行错误的原因，则需要进入机器查看</p>
<ol>
<li>首先看有无core产生，产生core的原因一般是非法内存访问、double free、内存踩坏等或oom，因此首先去/var/log/messages 看是否oom导致，oom会记录到/var/log/messages日志。</li>
</ol>
<pre><code class="language-shell">kernel: Out of memory: Kill process 12345 (java) score 678 or sacrifice child
kernel: Killed process 12345 (java) total-vm:123456kB, anon-rss:65432kB, file-rss:0kB, shmem-rss:0kB
</code></pre>
<p>如果没有core产生，但进程处理慢。可以先perf top -K -t $tid 查看耗时长的函数，原因大概是1. 被流控 2. 底层数据库/IO服务处理慢，反压上层 4. CPU/IO/网络被打满，资源不足 5. 线程数量不足，任务队列堆积 等原因，这些原因本都应该展示在监控和日志里</p>
<p>如果有oom，需要内存占用高的线程抓core，分析内存占用高和是否有内存泄漏</p>
<p>如果有core, 则需要分析产生core产生的原因。先在debug 环境下尝试复现core，同时调用valgrind等工具分析。分析core比较困难，可以直接找最有经验的人来协助。</p>
<p>可以借助大量的问题排查分享来积累经验，例如 <a href="https://www.cnblogs.com/xingmuxin/p/11287935.html">https://www.cnblogs.com/xingmuxin/p/11287935.html</a></p>
<h3>linux系统日志</h3>
<p>linux 系统日志，默认存储在/var/log目录下</p>
<pre><code>/var/log/
├── syslog          # 通用系统日志（Debian/Ubuntu）
├── messages        # 通用系统日志（RHEL/CentOS）
├── auth.log        # 认证日志（登录、sudo）
├── kern.log        # 内核日志（硬件、驱动事件）
├── boot.log        # 系统启动日志
├── dmesg           # 内核环形缓冲区日志（启动阶段信息）
├── cron            # 定时任务日志
├── apache2/        # Apache 服务日志（访问日志、错误日志）
└── ...
</code></pre>
<p>nginx, mysql 等系统服务可能把日志写到/var/log下
/var/log/nginx/access.log	Nginx 访问日志（客户端请求）	tail -f /var/log/nginx/access.log
/var/log/mysql/error.log	MySQL 错误日志	sudo less /var/log/mysql/error.log</p>
<p>journalctl​​：查看 systemd 日志（支持服务筛选、时间范围）</p>
<pre><code class="language-bash">journalctl -u nginx.service -f    # 实时追踪 Nginx 日志
journalctl --since "2024-01-01" --until "2024-01-02"
</code></pre>
<p>dmesg​​：查看内核环形缓冲区日志。内核环形缓冲区（Kernel Ring Buffer）是Linux内核用于临时存储运行时消息（如硬件事件、驱动状态、系统错误）的内存区域。</p>
<pre><code class="language-bash">dmesg | grep "USB"           # 检查 USB 设备事件
dmesg -T                     # 显示人类可读的时间戳
</code></pre>
<p>配置日志轮转
/etc/logrotate.conf 和 /etc/logrotate.d/*</p>]]></content:encoded>
          <dc:creator><![CDATA[Larry]]></dc:creator>
          <category><![CDATA[linux]]></category>
        </item>
  </channel>
</rss>