最新资讯

  • 第3章 多线程服务器的适用场合与常用编程模型

第3章 多线程服务器的适用场合与常用编程模型

2026-01-29 17:17:22 栏目:最新资讯 3 阅读

本章主要讲我个人在多线程开发方面的一些粗浅经验。总结了一两种常用的线程模型,归纳了进程间通信与线程同步的最佳实践,以期用简单规范的方式开发功能正确、线程安全的多线程程序。本章假定读者已经有多线程编程的知识与经验(本书不是一篇入门教程)。

文中的“多线程服务器”是指运行在Linux操作系统上的独占式网络应用程序。硬件平台为Intel x86-64系列的多核CPU,单路或双路SMP服务器(每台机器一共拥有四个核或八个核,十几GB内存),机器之间用千兆以太网连接。这大概是目前民用PC服务器的主流配置。不考虑做分布式存储,只考虑分布式计算,系统的规模大约是几十台服务器到几百台服务器之间。

我将要谈的“网络应用程序”的基本功能可以简单归纳为“收到数据,算一算,再发出去”。在这个简化了的模型里,似乎看不出用多线程的必要,单线程应该也能做得很好。“为什么需要写多线程程序”这个问题容易引发口水战,我放到§ 3.5讨论。请允许我先假定“多线程编程”这一背景。

“服务器”这个词有时指程序,有时指进程,有时指硬件(无论虚拟的或真实的),请注意按上下文区分。另外,本书不考虑虚拟化的场景,当我说“两个进程不在同一台机器上”时,指的是逻辑上不在同一个操作系统里运行,虽然物理上可能位于同一机器虚拟出来的两台“虚拟机”上。

3.1 进程与线程

“进程(process)”是操作系统里最重要的两个概念之一(另一个是文件),粗略地讲,一个进程是“内存中正在运行的程序”。本书的进程指的是Linux操作系统通过fork()系统调用产生的那个东西,或者Windows下CreateProcess()的产物,不是Erlang里的那种“轻量级进程(Actor)”。

每个进程有自己独立的地址空间(address space),“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》[ERL]把“进程”比喻为“人”,我觉得十分精当,为我们提供了一个思考的框架。

每个人有自己的记忆(memory),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。

有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:

容错 万一有人突然死了

扩容 新人中途加进来

负载均衡 把甲的活儿挪给乙做

退休 甲要修复bug,先别派新任务,等他做完手上的事情就把他重启等等各种场景,十分便利。

“线程”这个概念大概是在1993年以后才慢慢流行起来的,距今不到20年,比不得有40年光辉历史的Unix操作系统。线程的出现给Unix添了不少乱,很多C库函数(strtok()、ctime())不是线程安全的,需要重新定义(§ 4.2);signal的语意也大为复杂化。据我所知,最早支持多线程编程的(民用)操作系统是Solaris 2.2和Windows NT 3.1,它们均发布于1993年。随后在1995年,POSIX threads标准确立。

线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。

“多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值。Alan Cox说过:“A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的。)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最高效的,这正好也是下一节展示的编程模型。

3.2 单线程服务器的常用编程模型

[UNP]对此有很好的总结(第6章的IO模型、第30章的客户端/服务器设计范式),这里不再赘述。据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数“non-blocking IO+IO multiplexing”(非阻塞I/O+I/O多路复用)这种模型,即Reactor模式[注1:http://www.cs.wustl.edu/-schmidt/PDF/Reactor1-93.pdf, Reactor2-93.pdf,Reactor.pdf],我知道的有:

● lighttpd,单线程服务器。(Nginx与之类似,每个工作进程有一个event loop。)

● libevent,libev。

● ACE,Poco C++libraries。

● Java NIO,包括Apache Mina和Netty。

● POE(Perl)。

● Twisted(Python)。

相反,Boost.Asio和Windows I/O Completion Ports实现了Proactor模式[注2:https://www.cs.wustl.edu/-schmidt/PDF/proactor.pdf],应用面似乎要窄一些。此外,ACE也实现了Proactor模式。

在“non-blocking IO+IO multiplexing”(非阻塞I/O+I/O多路复用)这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:

//代码仅为示意,没有完整考虑各种情况
while(!done)
{
    int timeout_ms = max(1000, getNextTimedCallback());
    int retval = ::poll(fds, nfds, timeout_ms);
    if(retval < 0) {
        //处理错误,回调用户的error handler
    }
    else {
        //处理到期的timers,回调用户的timer handler
        if(retval > 0) {
            //处理IO事件,回调用户的IO event handler
        }
    }
}

这里select(2)/poll(2)有伸缩性方面的不足,Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品[注3https://www.kegel.com/c10k.html C10K问题]。

Reactor模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立(connect(2)/accept(2))甚至DNS解析[注4:gethostbyname(3)是阻寒的,对陌生域名解析的耗时可长达数秒。]都可以用非阻塞方式进行,以提高并发度和吞吐量(throughput),对于IO密集的应用是个不错的选择。lighttpd就是这样,它内部的fdevent结构十分精妙,值得学习。

基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。现代的语言有一些应对方法(例如coroutine),但是本书只关注C++这种传统语言,因此就不展开讨论了。

3.3 多线程服务器的常用编程模型

这方面我能找到的文献[注5:https://cs.uwaterloo.ca/~brecht/pubs.html https://inria.hal.science/file/index/docid/674475/filename/paper.pdf]不多,大概有这么几种(见§ 6.6更详细的讨论):

1.每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引入NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳。

2.使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。

3.使用non-blocking IO+IO multiplexing(非阻塞IO + IO多路复用)。即Java NIO的方式。

4.Leader/Follower等高级模式。在默认情况下,我会使用第3种,即non-blocking IO+one loop per thread模式来编写多线程C++网络服务程序。

在默认情况下,我会使用第3种,即non-blocking IO+one loop per thread(非阻塞IO+每线程单循环)模式来编写多线程C++网络服务程序。

-----------------------------分隔线------------------------------

笔记:

//BlockingQueue_test.cpp

#include "muduo/base/BlockingQueue.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/Thread.h"

#include 
#include 
#include 
#include 
#include 

class Test
{
public:
Test(int numThreads)
: latch_(numThreads)
{
    printf("numThreads=================%d
", numThreads);
    for (int i = 0; i < numThreads; ++i)
        {
            char name[32];
            snprintf(name, sizeof name, "work thread %d", i);
            threads_.emplace_back(new muduo::Thread(
                std::bind(&Test::threadFunc, this), muduo::string(name)));
        }
    printf("threads_.size=================%ld
", threads_.size());
    for (auto& thr : threads_)
        {
            thr->start();
        }
}

void run(int times)
{
    printf("waiting for count down latch  times=======%d
", times);
    latch_.wait();
    printf("all threads started
");
    for (int i = 0; i < times; ++i)
        {
            char buf[32];
            snprintf(buf, sizeof buf, "hello %d", i);
            queue_.put(buf);   //添加任务
            printf("tid=%d, put data = %s, size = %zd
", muduo::CurrentThread::tid(), buf, queue_.size());
        }
}

void joinAll()
{
    //启动所有线程
    printf("joinAll===============threads_.size = %ld
", threads_.size());
    for (size_t i = 0; i < threads_.size(); ++i)
        {
            queue_.put("stop");
        }

    for (auto& thr : threads_)
        {
            thr->join();
        }
}

private:

void threadFunc()
{
    printf("threadFunc=====tid=%d, %s started
",
        muduo::CurrentThread::tid(),
        muduo::CurrentThread::name());

    latch_.countDown();
    bool running = true;
    while (running)
        {
            std::string d(queue_.take());  //执行任务
            printf("threadFunc=====tid=%d, get data = %s, size = %zd
", muduo::CurrentThread::tid(), d.c_str(), queue_.size());
            running = (d != "stop");
        }

    printf("threadFunc=====tid=%d, %s stopped
",
        muduo::CurrentThread::tid(),
        muduo::CurrentThread::name());
}

muduo::BlockingQueue queue_;
muduo::CountDownLatch latch_;
std::vector> threads_;
};

void testMove()
{
    muduo::BlockingQueue> queue;
    queue.put(std::unique_ptr(new int(42)));
    printf("testMove====queue.size===%ld
",queue.size());
    std::unique_ptr x = queue.take();
    printf("testMove====queue.size===%ld took %d
",queue.size(), *x);
    *x = 123;
    queue.put(std::move(x));
    printf("testMove====queue.size===%ld
",queue.size());
    std::unique_ptr y = queue.take();
    printf("testMove====queue.size===%ld took %d
", queue.size(),  *y);
}

int main()
{
printf("pid=%d, tid=%d
", ::getpid(), muduo::CurrentThread::tid());
    Test t(5);
    t.run(100);
    t.joinAll();

    testMove();

    printf("number of created threads %d
", muduo::Thread::numCreated());
}

//编译命令:
// g++ -o BlockingQueue_test BlockingQueue_test.cpp 
//    -I/usr/local/include 
//    -L/usr/local/lib 
//    -lmuduo_net -lmuduo_base -lpthread

Qt工程编译的配置 *.pro文件

QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

# 添加头文件搜索路径
INCLUDEPATH += /usr/local/include

#引用静态库
LIBS += -L/usr/local/lib/ -lmuduo_net
LIBS += -L/usr/local/lib/ -lmuduo_base

# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += 
        BlockingQueue_test.cpp 

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

编译运行:

-----------------------------分隔线------------------------------

3.3.1 one loop per thread

此种模型下,程序里的每个IO线程有一个event loop(或者叫Reactor),用于处理读写和定时事件(无论周期性的还是单次的),代码框架跟§ 3.2一样。

libev的作者说[注6:http://cvs.schmorp.de/ibev/ev.POd#THREADS_AND_COROUTINES]:

One loop per thread is usually a good model. Doing this is almost never wrong,sometimes a better-performance model exists,but it is always a good start.

这种方式的好处是:

● 线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。

● 可以很方便地在线程间调配负载。

● IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。

Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可。对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。

对于non-trivial(非平凡的)的服务端程序,一般会采用non-blocking IO+IO multiplexing,每个connection/acceptor都会注册到某个event loop上,程序里有多个event loop,每个线程至多有一个event loop。

多线程程序对event loop提出了更高的要求,那就是“线程安全”。要允许一个线程往别的线程的loop里塞东西[注7:比方说主 I〇 线程收到一个新建连接,分配给某个子IO线程处理。],这个loop必须得是线程安全的。如何实现一个优质的多线程Reactor?可参考第8章。

3.3.2 线程池

不过,对于没有IO而光有计算任务的线程,使用event loop有点浪费,我会用一种补充方案,即用blocking queue实现的任务队列(TaskQueue):

typedef boost::function Functor;
BlockingQueue taskQueue;  //线程安全的阻塞队列
void workerThread()
{
    while(running)  //running 变量是个全局标志
    {
       Functor task = taskQueue.take();  //this blocks 
       task(); //在产品代码中需要考虑异常处理
    }
}

用这种方式实现线程池特别容易,以下是启动容量(并发数)为的线程池:

int N = num_of_computing_threads;
for(int i = 0; i < N; ++i)
{
    create_thread(&workerThread);//伪代码:启动线程
}

使用起来也很简单:

Foo foo; //Foo有calc()成员函数
boost::function task = boost::bind(&Foo::calc, &foo);
taskQueue.post(task);

上面十几行代码就实现了一个简单的固定数目的线程池,功能大概相当于Java中的ThreadPoolExecutor的某种“配置”。当然,在真实的项目中,这些代码都应该封装到一个class中,而不是使用全局对象。另外需要注意一点:Foo对象的生命期,第1章详细讨论了这个问题。

muduo的线程池[注8:muduo/base/ThreadPool.{h,cc}]比这个略复杂,因为要提供stop()操作。

除了任务队列,还可以用BlockingQueue实现数据的生产者消费者队列,即T是数据类型[注9:例如 std::string或google::protobuf::Message*]而非函数对象,queue的消费者(s)从中拿到数据进行处理。

BlockingQueue是多线程编程的利器,它的实现可参照Java util.concurrent里的(ArraylLinked)BlockingQueue【阻塞队列】。这份Java代码可读性很高,代码的基本结构和教科书一致(1个mutex,2个condition variables),健壮性要高得多。如果不想自己实现,用现成的库更好。muduo里有一个基本的实现,包括无界的BlockingQueue和有界的BoundedBlockingQueue两个class。有兴趣的读者还可以试试Intel Threading Building Blocks里的concurrent_queue,【并发队列】性能估计会更好。

3.3.3 推荐模式

总结起来,我推荐的C++多线程服务端编程模式为:one(event)loop per thread+thread pool。

● event loop(也叫IO loop)用作IO multiplexing,配合non-blocking IO和定时器。

● thread pool用来做计算,具体可以是任务队列或生产者消费者队列。

以这种方式写服务器程序,需要一个优质的基于Reactor模式的网络库来支撑,muduo正是这样的网络库。

程序里具体用几个loop、线程池的大小等参数需要根据应用来设定,基本的原则是“阻抗匹配”,使得CPU和IO都能高效地运作,具体的例子见p.80。

此外,程序里或许还有个别执行特殊任务的线程,比如logging,这对应用程序来说基本是不可见的,但是在分配资源(CPU和IO)的时候要算进去,以免高估了系统的容量。

-------------------------分隔线---------------------------

笔记:

//TheadPool_test.cpp

#include "muduo/base/ThreadPool.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/CurrentThread.h"
#include "muduo/base/Logging.h"

#include 
#include    //usleep

void print()
{
    printf("tid = %d
", muduo::CurrentThread::tid());
}

void printString(const std::string& str)
{
    LOG_INFO << str;
    usleep(100 * 1000);
}

void test(int maxSize)
{
    LOG_WARN << "Test ThreadPool with max queue size = " << maxSize;
    muduo::ThreadPool pool("MainThreadPool");
    pool.setMaxQueueSize(maxSize);
    pool.start(5);

    LOG_WARN << "Adding";
    pool.run(print);
    pool.run(print);
    for(int i = 0; i < 100; ++i)
    {
        char buf[32];
        snprintf(buf, sizeof buf, "task %d", i);
        pool.run(std::bind(printString, std::string(buf)));
    }
    LOG_WARN << "Done";
    muduo::CountDownLatch latch(1);
    pool.run(std::bind(&muduo::CountDownLatch::countDown, &latch));
    latch.wait();
    pool.stop();
}

/*
 * Wish we could do this in the future.
void testMove()
{
  muduo::ThreadPool pool;
  pool.start(2);

  std::unique_ptr x(new int(42));
  pool.run([y = std::move(x)]{ printf("%d: %d
", muduo::CurrentThread::tid(), *y); });
  pool.stop();
}
*/

void longTask(int num)
{
    LOG_INFO << "longTask " << num;
    muduo::CurrentThread::sleepUsec(3000000);
}

void test2()
{
    LOG_WARN << "Test ThreadPool by stoping early.";
    muduo::ThreadPool pool("ThreadPool");
    pool.setMaxQueueSize(5);
    pool.start(3);

    muduo::Thread thread1([&pool]()
    {
        for(int i = 0; i < 20; ++i)
        {
            pool.run(std::bind(longTask, i));
        }
    }, "thread1");
    thread1.start();

    muduo::CurrentThread::sleepUsec(5000000);
    LOG_WARN << "stop pool";
    pool.stop(); //early stop

    thread1.join();
    //run() after stop()
    pool.run(print);
    LOG_WARN << "test2 Done";
}

int main()
{
    test(0);
    test(1);
    test(5);
    test(10);
    test(50);
    test2();
    return 0;
}

编译运行:

-------------------------分隔线---------------------------

3.4 进程间通信只用TCP

Linux下进程间通信(IPC)的方式数不胜数,光[UNPv2]列出的就有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals)等等,更不必说Sockets了。同步原语(synchronization primitives)也很多,如互斥器(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、文件锁(record locking)、信号量(semaphore)等等。

如何选择呢?根据我的个人经验,贵精不贵多,认真挑选三四样东西就能完全满足我的工作需要,而且每样我都能用得很熟,不容易犯错。

进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unix domain协议),其最大的好处在于:可以跨主机,具有伸缩性。反正都是多进程了,如果一台机器的处理能力不够,很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上,程序改改host:port配置就能继续用。相反,前面列出的其他IPC都不能跨机器[注10:比如共享内存效率最高,但受网络带宽及延迟限制,无论如何也不能高效地共享两台物理机器的内存。],这就限制了scalability。

在编程上,TCP sockets和pipe都是操作文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等。不同的是,TCP是双向的,Linux的pipe是单向的,进程间双向通信还得开两个文件描述符,不方便[注11:可以用 socketpair(2)替代。];而且进程要有父子关系才能用pipe,这些都限制了pipe的使用。在收发字节流这一通信模型下,没有比Sockets/TCP更自然的IPC了。当然,pipe也有一个经典应用场景,那就是写Reactor/event loop时用来异步唤醒select(或等价的poll/epoll_wait)调用[注12:在 Linux下,可以用eventfd(2)代替,效率更高。],Sun HotSpot JVM在Linux就是这么做的[注13:https://blog.csdn.net/haoel/article/details/2224055]。

TCP port由一个进程独占,且操作系统会自动回收(listening port和已建立连接的TCP socket都是文件描述符,在进程结束时操作系统会关闭所有文件描述符)。这说明,即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的mutex就有这个风险)。还有一个好处,既然port是独占的,那么可以防止程序重复启动,后面那个进程抢不到port,自然就没法初始化了,避免造成意料之外的结果。

两个进程通过TCP通信,如果一个崩溃了,操作系统会关闭连接,另一个进程几乎立刻就能感知,可以快速failover。当然应用层的心跳也是必不可少的(§ 9.3)。

与其他IPC相比,TCP协议的一个天生的好处是“可记录、可重现”。tcpdump和Wireshark是解决两个进程间协议和状态争端的好帮手,也是性能(吞吐量、延迟)分析的利器。我们可以借此编写分布式程序的自动化回归测试。也可以用tcpcopy [注14:http://code.google.com/p/tcpcopy/]之类的工具进行压力测试。TCP还能跨语言,服务端和客户端不必使用同一种语言。试想如果用共享内存作为IPC,C++程序如何与Java通信,难道用JNI吗?

另外,如果网络库带“连接重试”功能的话,我们可以不要求系统里的进程以特定的顺序启动,任何一个进程都能单独重启。换句话说,TCP连接是可再生的,连接的任何一方都可以退出再启动,重建连接之后就能继续工作,这对开发牢靠的分布式系统意义重大。

使用TCP这种字节流(byte stream)方式通信,会有marshal/unmarshal的开销,这要求我们选用合适的消息格式,准确地说是wire format,目前我推荐Google Protocol Buffers。见§ 9.6关于分布式系统消息格式的讨论。

有人或许会说,具体问题具体分析,如果两个进程在同一台机器,就用共享内存,否则就用TCP,比如MS SQL Server就同时支持这两种通信方式。试问,是否值得为那么一点性能提升而让代码的复杂度大大增加呢?何况TCP的local吞吐量一点都不低,见§6.5.1的测试结果。TCP是字节流协议,只能顺序读取,有写缓冲;共享内存是消息协议,a进程填好一块内存让b进程来读,基本是“停等(stop wait)”方式。要把这两种方式揉到一个程序里,需要建一个抽象层,封装两种IPC。这会带来不透明性,并且增加测试的复杂度。而且万一通信的某一方崩溃,状态reconcile也会比sockets麻烦。(数据刚写到一半,怎么办?)为我所不取。再说了,你舍得让几万块买来的SQL Server和其他应用程序分享机器资源吗?生产环境下的数据库服务器往往是独立的高配置服务器,一般不会同时运行其他占资源的程序。

TCP本身是个数据流协议,除了直接使用它来通信外,还可以在此之上构建RPC/HTTP/SOAP之类的上层通信协议,这超过了本章的范围。另外,除了点对点的通信之外,应用级的广播协议也是非常有用的,可以方便地构建可观可控的分布式系统,见§7.11。

分布式系统中使用TCP长连接通信

§ 3.1提到,分布式系统的软件设计和功能划分一般应该以“进程”为单位。从宏观上看,一个分布式系统是由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信。本章讨论分布式系统中单个服务进程的设计方法,第9章将谈一谈整个系统的设计。我提倡用多线程,并不是说把整个系统放到一个进程里实现,而是指功能划分之后,在实现每一类服务进程时,在必要时可以借助多线程来提高性能。对于整个分布式系统,要做到能scale out,即享受增加机器带来的好处。

使用TCP长连接的好处有两点:一是容易定位分布式系统中的服务之间的依赖关系。只要在机器上运行netstat-tpna|grep:port就能立刻列出用到某服务的客户端地址(Foreign列),然后在客户端的机器上用netstat或lsof命令找出是哪个进程发起的连接。这样在迁移服务的时候能有效地防止出现outage。TCP短连接和UDP则不具备这一特性。二是通过接收和发送队列的长度也较容易定位网络或程序故障。在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动。如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障。以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子。

3.5 多线程服务器的适用场合

“服务器开发”包罗万象,本书所指的“服务器开发”的含义请见本章开头,用一句话形容是:跑在多核机器上的Linux用户态的没有用户界面的长期运行[注15:长期运行”的意思不是指程序7x24不重启,而是程序不会因为无事可做而退出,它会等着下一个请求的到来。例如 wget 不是长期运行的,httpd 是长期运行的。]的网络应用程序,通常是分布式系统的组成部件。

开发服务端程序的一个基本任务是处理并发连接,现在服务端网络编程处理并发连接主要有两种方式:

● 当“线程”很廉价时,一台机器上可以创建远高于CPU数目的“线程”。这时一个线程只处理一个TCP连接(甚至半个),通常使用阻塞IO(至少看起来如此)。例如,Python gevent、Go goroutine、Erlang actor。这里的“线程”由语言的runtime自行调度,与操作系统线程不是一回事。

● 当线程很宝贵时,一台机器上只能创建与CPU数目相当的线程。这时一个线程要处理多个TCP连接上的IO,通常使用非阻塞IO和IO multiplexing。例如,libevent、muduo、Netty。这是原生线程,能被操作系统的任务调度器看见。

在处理并发连接的同时,也要充分发挥硬件资源的作用,不能让CPU资源闲置。以上列出的库不是每个都能做到这一点。既然本书讨论的是C++编程,那么只考虑后一种方式,这是在Linux下使用native语言编写用户态高性能网络程序的最成熟的模式。本节主要讨论的是这些“线程”应该属于一个进程(以下模式2),还是分属多个进程(模式3)。

与前文相同,本节的“进程”指的是fork(2)系统调用的产物。“线程”指的是pthread_create()的产物,因此是宝贵的那种原生线程。而且我指的Pthreads是NPTL的,每个线程由clone(2)产生,对应一个内核的task_struct。

首先,一个由多台机器组成的分布式系统必然是多进程的(字面意义上),因为进程不能跨OS边界。在这个前提下,我们把目光集中到一台机器,一台拥有至少4个核的普通服务器。如果要在一台多核机器上提供一种服务或执行一个任务,可用的模式有:(这里的“模式”不是pattern,而是model,不巧它们的中译文是一样的。)

1.运行一个单线程的进程;

2.运行一个多线程的进程;

3.运行多个单线程的进程;

4.运行多个多线程的进程。

这些模式之间的比较已经是老生常谈,简单地总结如下。

● 模式1是不可伸缩的(scalable),不能发挥多核机器的计算能力。

● 模式3是目前公认的主流模式。它有以下两种子模式:

3a简单地把模式1中的进程运行多份[注16:如果能用多个 TCP port 对外提供服务的话,]

3b主进程+woker进程,如果必须绑定到一个TCP port,比如httpd+fastcgi

● 模式2是被很多人所鄙视的,认为多线程程序难写,而且与模式3相比并没有什么优势。

● 模式4更是千夫所指,它不但没有结合2和3的优点,反而汇聚了二者的缺点。

本文主要想讨论的是模式2和模式3b的优劣,即:什么时候一个服务器程序应该是多线程的。从功能上讲,没有什么是多线程能做到而单线程做不到的,反之亦然,都是状态机嘛(我很高兴看到反例)。从性能上讲,无论是IO bound还是CPU bound的服务,多线程都没有什么优势。

Paul E.McKenney在《Is Parallel Programming Hard,And,If So,What Can You Do About It?》[注17:http://kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html]第3.5节指出,“As a rough rule of thumb,use the simplest tool that will get the job done.”比方说,使用速率为50MB/s的数据压缩库、在进程创建销毁的开销是800µs、线程创建销毁的开销是50µs的前提下,考虑如何执行压缩任务:

● 如果要偶尔压缩1GB的文本文件,预计运行时间是20s,那么起一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时。

● 如果要经常压缩500kB的文本数据,预计运行时间是10ms,那么每次都起进程似乎有点浪费了,可以每次单独起一个线程去做。

● 如果要频繁压缩10kB的文本数据,预计运行时间是200µs,那么每次起线程似乎也很浪费,不如直接在当前线程搞定。也可以用一个线程池,每次把压缩任务交给线程池,避免阻塞当前线程(特别要避免阻塞IO线程)。

由此可见,多线程并不是万灵丹(silver bullet),它有适用的场合。那么究竟什么时候该用多线程?在回答这个问题之前,我先谈谈必须用单线程的场合。

3.5.1 必须用单线程的场合

据我所知,有两种场合必须使用单线程:

1.程序可能会fork(2);

2.限制程序的CPU占用率。

只有单线程程序能 fork(2)根据后面 § 4.9 的分析,一个设计为可能调用fork(2)的程序必须是单线程的,比如后面§ 3.5.3中提到的“看门狗进程”。多线程程序不是不能调用fork(2),而是这么做会遇到很多麻烦,我想不出做的理由。

一个程序fork(2)之后一般有两种行为:

1.立刻执行exec(),变身为另一个程序。例如shell和inetd;又比如lighttpd fork()出子进程,然后运行fastcgi程序。或者集群中运行在计算节点上的负责启动job的守护进程(即我所谓的“看门狗进程”)。

2.不调用exec(),继续运行当前程序。要么通过共享的文件描述符与父进程通信,协同完成任务;要么接过父进程传来的文件描述符,独立完成工作,例如20世纪80年代的Web服务器NCSA httpd。

这些行为中,我认为只有“看门狗进程”必须坚持单线程,其他的均可替换为多线程程序(从功能上讲)。

单线程程序能限制程序的CPU占用率 这个很容易理解,比如在一个8核的服务器上,一个单线程程序即便发生busy-wait(无论是因为bug,还是因为overload),占满1个core,其CPU使用率也只有12.5%.[注18:本书中,当句尾是百分号“%”时,句号改用句点“.”,以避免与千分号“%0”相混淆。]在这种最坏的情况下,系统还是有87.5%的计算资源可供其他服务进程使用。

因此对于一些辅助性的程序,如果它必须和主要服务进程运行在同一台机器的话(比如它要监控其他服务进程的状态),那么做成单线程的能避免过分抢夺系统的计算资源。比方说如果要把生产服务器上的日志文件压缩后备份到NFS上,那么应该使用普通单线程压缩工具(gzip/bzip2)。它们对系统造成的影响较小,在8核服务器上最多占满1个core。如果有人为了“提高速度”,开启了多线程压缩或者同时起多个进程来压缩多个日志文件,有可能造成的结果是非关键任务耗尽了CPU资源,正常客户的请求响应变慢。这是我们不愿意看到的。

3.5.2 单线程程序的优缺点

从编程的角度,单线程程序的优势无须赘言:简单。程序的结构一般如§ 3.2所言,是一个基于IO multiplexing的event loop。或者如云风所言[注19:http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html],直接用阻塞IO。event loop的典型代码框架见§ 3.2。

Event loop有一个明显的缺点,它是非抢占的(non-preemptive)。假设事件a的优先级高于事件b,处理事件a需要1ms,处理事件b需要10ms。如果事件b稍早于a发生,那么当事件a到来时,程序已经离开了poll(2)调用,并开始处理事件b。事件a要等上10ms才有机会被处理,总的响应时间为11ms。这等于发生了优先级反转。这个缺点可以用多线程来克服,这也是多线程的主要优势。

多线程程序有性能优势吗

前面我说,无论是IO bound还是CPU bound的服务,多线程都没有什么绝对意义上的性能优势。这句话是说,如果用很少的CPU负载就能让IO跑满,或者用很少的IO流量就能让CPU跑满,那么多线程没啥用处。举例来说:

● 对于静态Web服务器,或者FTP服务器,CPU的负载较轻,主要瓶颈在磁盘IO和网络IO方面。这时候往往一个单线程的程序(模式1)就能撑满IO。用多线程并不能提高吞吐量,因为IO硬件容量已经饱和了。同理,这时增加CPU数目也不能提高吞吐量。

● CPU跑满的情况比较少见,这里我只好虚构一个例子。假设有一个服务,它的输入是n个整数,问能否从中选出m个整数,使其和为0(这里n<100,m>0)。这是著名的subset sum问题,是NP-Complete的。对于这样一个“服务”,哪怕很小的n值也会让CPU算死。比如n=30,一次的输入不过200字节(32-bit整数),CPU的运算时间却能长达几分钟。对于这种应用,模式3a是最适合的,能发挥多核的优势,程序也简单。

也就是说,无论任何一方早早地先到达瓶颈,多线程程序都没啥优势。

说到这里,可能已经有读者不耐烦了:你讲了这么多,都在说单线程的好处,那么多线程究竟有什么用?

3.5.3 适用多线程程序的场景

我认为多线程的适用场景是:提高响应速度,让IO和“计算”相互重叠,降低latency。虽然多线程不能提高绝对性能,但能提高平均响应性能。

一个程序要做成多线程的,大致要满足:

● 有多个CPU可用。单核机器上多线程没有性能优势(但或许能简化并发业务逻辑的实现)。

● 线程间有共享数据,即内存中的全局状态。如果没有共享数据,用模型3b就行。虽然我们应该把线程间的共享数据降到最低,但不代表没有。

● 共享的数据是可以修改的,而不是静态的常量表。如果数据不能修改,那么可以在进程间用shared memory,模式3就能胜任。

● 提供非均质的服务。即,事件的响应有优先级差异,我们可以用专门的线程来处理优先级高的事件。防止优先级反转。

● latency和throughput同样重要,不是逻辑简单的IO bound或CPU bound程序。换言之,程序要有相当的计算量。

● 利用异步操作。比如logging。无论往磁盘写log file,还是往log server发送消息都不应该阻塞critical path。

● 能scale up。一个好的多线程程序应该能享受增加CPU数目带来的好处,目前主流是8核,很快就会用到16核的机器了。

● 具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点之后会急速下降。线程数目一般不随负载变化。

● 多线程能有效地划分责任与功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是把所有逻辑都塞到一个event loop里,不同类别的事件之间相互影响。

这些条件比较抽象,这里举两个具体的(虽然是虚构的)例子。

假设要管理一个Linux服务器机群,这个机群里有8个计算节点,1个控制节点。机器的配置都是一样的,双路四核CPU,千兆网互联。现在需要编写一个简单的机群管理软件(参考LLNL的SLURM [注20:https://computing.llnl.gov/linux/slurm/]),这个软件由3个程序组成:

1.运行在控制节点上的master,这个程序监视并控制整个机群的状态。

2.运行在每个计算节点上的slave,负责启动和终止job,并监控本机的资源。[注21:slave的实现要点见http://www.slideshare.net/chenshuo/zurg-part-1.]

3.供最终用户使用的client命令行工具,用于提交job。

根据前面的分析,slave是个“看门狗进程”,它会启动别的job进程,因此必须是个单线程程序。另外它不应该占用太多的CPU资源,这也适合单线程模型。master应该是个模式2的多线程程序:

● 它独占一台8核的机器,如果用模型1,等于浪费了87.5%的CPU资源。

● 整个机群的状态应该能完全放在内存中,这些状态是共享且可变的。如果用模式3,那么进程之间的状态同步会成大问题。而如果大量使用共享内存,则等于是掩耳盗铃,是披着多进程外衣的多线程程序。因为一个进程一旦在临界区内阻塞或crash,其他进程会全部死锁。

● master的主要性能指标不是throughput,而是latency,即尽快地响应各种事件。它几乎不会出现把IO或CPU跑满的情况。

● master监控的事件有优先级区别,一个程序正常运行结束和异常崩溃的处理优先级不同,计算节点的磁盘满了和机箱温度过高这两种报警条件的优先级也不同。如果用单线程,则可能会出现优先级反转。

● 假设master和每个slave之间用一个TCP连接,那么master采用2个或4个IO线程来处理8个TCP connections能有效地降低延迟。

● master要异步地往本地硬盘写log,这要求logging library有自己的IO线程。

● master有可能要读写数据库,那么数据库连接这个第三方library可能有自己的线程,并回调master的代码。

● master要服务于多个clients,用多线程也能降低客户响应时间。也就是说它可以再用2个IO线程专门处理和clients的通信。

● master还可以提供一个monitor接口,用来广播推送(pushing)机群的状态,这样用户不用主动轮询(polling)。这个功能如果用单独的线程来做,会比较容易实现,不会搞乱其他主要功能。

● master一共开了10个线程:

▶ 4个用于和slaves通信的IO线程。

▶ 1个logging线程。

▶ 1个数据库IO线程。

▶ 2个和clients通信的IO线程。

▶ 1个主线程,用于做些背景工作,比如job调度。

▶ 1个pushing线程,用于主动广播机群的状态。

● 虽然线程数目略多于core数目,但是这些线程很多时候都是空闲的,可以依赖OS的进程调度来保证可控的延迟。

综上所述,master用多线程方式编写是自然且高效的。

再举一个TCP聊天服务器的例子,这里的“聊天”不完全指人与人聊天,也可能是机器与机器“聊天”。这种服务的特点是并发连接之间有数据交换,从一个连接收到的数据要转发给其他多个连接。因此我们不能按模式3的做法,把多个连接分到多个进程中分别处理(这会带来复杂的进程间通信),而只能用模式1或者模式2。如果纯粹只有数据交换,那么我想模式1也能工作得很好,因为现在的CPU足够快,单线程应付几百个连接不在话下。

如果功能进一步复杂化,加上关键字过滤、黑名单、防灌水等等功能,甚至要给聊天内容自动加上相关连接,每一项功能都会占用CPU资源。这时就要考虑模式2了,因为单个CPU的处理能力显得捉襟见肘,顺序处理导致消息转发的延迟增加。这时我们考虑把空闲的多个CPU利用起来,自然的做法是把连接分散到多个线程上,例如按round-robin的方式把1000个客户连接分配到4个IO线程上。这样充分利用多核加速。具体的例子见§ 6.6的方案9,以及p.260的实现。

线程的分类

据我的经验,一个多线程服务程序中的线程大致可分为3类:

1.IO线程,这类线程的主循环是IO multiplexing,阻塞地等在select/poll/epoll_wait系统调用上。这类线程也处理定时事件。当然它的功能不止IO,有些简单计算也可以放入其中,比如消息的编码或解码。

2.计算线程,这类线程的主循环是blocking queue,阻塞地等在condition variable上。这类线程一般位于thread pool中。这种线程通常不涉及IO,一般要避免任何阻塞操作。

3.第三方库所用的线程,比如logging,又比如database connection。

服务器程序一般不会频繁地启动和终止线程。甚至,在我写过的程序里,create thread只在程序启动的时候调用,在服务运行期间是不调用的。

在多核时代,要想充分发挥CPU性能,多线程编程是不可避免的,“鸵鸟算法”不是办法。在学会多线程编程之前,我也一直认为单线程服务程序才是王道。在接触多线程编程之后,经过一段时间的训练和适应,我已能比较自如地编写正确且足够高效的多线程程序。学习多线程编程还有一个好处,即训练异步思维,提高分析并发事件的能力。这对设计分布式系统帮助巨大,因为运行在多台机器上的服务进程本质上是异步的。熟悉多线程编程的话,很容易就能发现分布式系统在消息和事件处理方面的race condition。

3.6“多线程服务器的适用场合”例释与答疑

《多线程服务器的适用场合》一文在博客[注22:http://blog.csdn.net/Solstice/article/details/5334243]登出之后,有热心读者提出质疑,我自己也觉得原文没有把道理说通、说透,下面用一些实例来解答读者的疑问。为方便阅读,本节以问答体呈现。以下“连接、端口”均指TCP协议。

1.Linux能同时启动多少个线程?

对于32-bit Linux,一个进程的地址空间是4GiB,其中用户态能访问3GiB左右,而一个线程的默认栈(stack)大小是10MB,心算可知,一个进程大约最多能同时启动300个线程。如果不改线程的调用栈大小的话,300左右是上限,因为程序的其他部分(数据段、代码段、堆、动态库等等)同样要占用内存(地址空间)。

对于64-bit系统,线程数目可大大增加,具体数字我没有测试过,因为我在实际项目中一台机器上最多只用到过几十个用户线程,其中大部分还是空闲的。

下面的第2问关于线程数目的讨论以32-bit Linux为例。

2.多线程能提高并发度吗?

如果指的是“并发连接数”,则不能。

由问题1可知,假如单纯采用thread per connection的模型,那么并发连接数最多300,这远远低于基于事件的单线程程序所能轻松达到的并发连接数(几千乃至上万,甚至几万)。所谓“基于事件”,指的是用IO multiplexing event loop的编程模型,又称Reactor模式,在前文中已有介绍。

那么采用前文中推荐的one loop per thread呢?至少不逊于单线程程序。实际上单个event loop处理1万个并发长连接并不罕见,一个multi-loop的多线程程序应该能轻松支持5万并发链接。

小结:thread per connection不适合高并发场合,其scalability不佳。one loop per thread的并发度足够大,且与CPU数目成正比。

3.多线程能提高吞吐量吗?

对于计算密集型服务,不能。

假设有一个耗时的计算服务,用单线程算需要0.8s。在一台8核的机器上,我们可以启动8个线程一起对外服务(如果内存够用,启动8个进程也一样)。这样完成单个计算仍然要0.8s,但是由于这些进程的计算可以同时进行,理想情况下吞吐量可以从单线程的1.25qps(query per second)上升到10qps。(实际情况可能要打个八折——如果不是打对折的话。)

假如改用并行算法,用8个核一起算,理论上如果完全并行,加速比高达8,那么计算时间是0.1s,吞吐量还是10qps,但是首次请求的响应时间却降低了很多。实际上根据Amdahl’s law,即便算法的并行度高达95%,8核的加速比也只有6,计算时间为0.133s,这样会造成吞吐量下降为7.5qps。不过以此为代价,换得响应时间的提升,在有些应用场合也是值得的。

再举一个例子,如果要在一台8核机器上压缩100个1GB的文本文件,每个core的处理能力为200MB/s。那么“每次起8个进程,每个进程压缩1个文件”与“依次压缩每个文件,每个文件用8个线程并行压缩”这两种方式的总耗时相当,因为CPU都是满载的。但是第2种方式能较快地拿到第一个压缩完的文件,也就是首次响应的延时更小。

这也回答了问题4。

如果用thread per request的模型,每个客户请求用一个线程去处理,那么当并发请求数大于某个临界值T′时,吞吐量反而会下降,因为线程多了以后上下文切换的开销也随之增加(分析与数据请见《A Design Framework for Highly Concurrent Systems》[注23:by Matt Welsh et al. http://www.cs.berkeley.edu/~culler/papers/events.pdf

])。thread per request是最简单的使用线程的方式,编程最容易,简单地把多线程程序当成一堆串行程序,用同步的方式顺序编程,比如在Java Servlet 2.x中,一次页面请求由一个函数HttpServlet.service(HttpServletRequest req,HttpServletResponse resp)同步地完成。

为了在并发请求数很高时也能保持稳定的吞吐量,我们可以用线程池,线程池的大小应该满足“阻抗匹配原则”,见问题7。

线程池也不是万能的,如果响应一次请求需要做比较多的计算(比如计算的时间占整个response time的1/5强),那么用线程池是合理的,能简化编程。如果在一次请求响应中,主要时间是在等待IO,那么为了进一步提高吞吐量,往往要用其他编程模型,比如Proactor,见问题8。

4.多线程能降低响应时间吗?

如果设计合理,充分利用多核资源的话,可以。在突发(burst)请求时效果尤为明显。

例1:多线程处理输入 以memcached服务端为例。memcached一次请求响应大概可以分为3步:

1.读取并解析客户端输入;

2.操作hashtable;

3.返回客户端。

在单线程模式下,这3步是串行执行的。在启用多线程模式时,它会启用多个输入线程(默认是4个),并在建立连接时按round-robin法把新连接分派给其中一个输入线程,这正好是我说的one loop per thread模型。这样一来,第1步的操作就能多线程并行,在多核机器上提高多用户的响应速度。第2步用了全局锁,还是单线程的,这可算是一个值得继续改进的地方。

比如,有两个用户同时发出了请求,这两个用户的连接正好分配在两个IO线程上,那么两个请求的第1步操作可以在两个线程上并行执行,然后汇总到第2步串行执行,这样总的响应时间比完全串行执行要短一些(在“读取并解析”所占的比重较大的时候,效果更为明显)。请继续看下面这个例子。

例2:多线程分担负载 假设我们要做一个求解Sudoku的服务[注24:见笔者的博客《谈谈数独》( http://blog.csdn.net/Solstice/article/details/2096209)。

],这个服务程序在9981端口接受请求,输入为一行81个数字(待填数字用0表示),输出为填好之后的81个数字(1~9),如果无解,输出“NO ”。

由于输入格式很简单,用单个线程做IO就行了。先假设每次求解的计算用时为10ms,用前面的方法计算,单线程程序能达到的吞吐量上限为100qps;在8核机器上,如果用线程池来做计算,能达到的吞吐量上限为800qps。下面我们看看多线程如何降低响应时间。

假设1个用户在极短的时间内发出了10个请求,如果用单线程“来一个处理一个”的模型,这些reqs会排在队列里依次处理(这个队列是操作系统的TCP缓冲区,不是程序里自己的任务队列)。在不考虑网络延迟的情况下,第1个请求的响应时间是10ms;第2个请求要等第1个算完了才能获得CPU资源,它等了10ms,算了10ms,响应时间是20ms;依此类推,第10个请求的响应时间为100ms;这10个请求的平均响应时间为55ms。

如果Sudoku服务在每个请求到达时开始计时,会发现每个请求都是10ms响应时间;而从用户的观点来看,10个请求的平均响应时间为55ms,请读者想想为什么会有这个差异。

下面改用多线程:1个IO线程,8个计算线程(线程池)。二者之间用Block-ingQueue沟通。同样是10个并发请求,第1个请求被分配到计算线程1,第2个请求被分配到计算线程2,依此类推,直到第8个请求被第8个计算线程承担。第9和第10号请求会等在BlockingQueue里,直到有计算线程回到空闲状态其才能被处理。(请注意,这里的分配实际上由操作系统来做,操作系统会从处于waiting状态的线程里挑一个,不一定是round-robin的。)这样一来,前8个请求的响应时间差不多都是10ms,后2个请求属于第二批,其响应时间大约会是20ms,总的平均响应时间是12ms。可以看出这比单线程快了不少。

由于每道Sudoku题目的难度不一,对于简单的题目,可能1ms就能算出来,复杂的题目最多用10ms。那么线程池方案的优势就更明显,它能有效地降低“简单任务被复杂任务压住”的出现概率。

以上举的都是计算密集的例子,即线程在响应一次请求时不会等待IO。下面谈谈更复杂的情况。

5.多线程程序如何让IO和“计算”相互重叠,降低latency(延迟)?

基本思路是,把IO操作(通常是写操作)通过BlockingQueue(阻塞队列)交给别的线程去做,自己不必等待。

例1:日志(logging)在多线程服务器程序中,日志(logging)至关重要,本例仅考虑写log file的情况,不考虑log server。

在一次请求响应中,可能要写多条日志消息,而如果用同步的方式写文件(fprintf或fwrite),多半会降低性能,因为:

● 文件操作一般比较慢,服务线程会等在IO上,让CPU闲置,增加响应时间。

● 就算有buffer,还是不灵。多个线程一起写,为了不至于把buffer写错乱,往往要加锁。这会让服务线程互相等待,降低并发度。(同时用多个log文件不是办法,除非你有多个磁盘,且保证log files分散在不同的磁盘上,否则还是要受到磁盘IO瓶颈的制约。)

解决办法是单独用一个logging线程,负责写磁盘文件,通过一个或多个BlockingQueue对外提供接口。别的线程要写日志的时候,先把消息(字符串)准备好,然后往queue里一塞就行,基本不用等待。这样服务线程的计算就和logging线程的磁盘IO相互重叠,降低了服务线程的响应时间。

尽管logging很重要,但它不是程序的主要逻辑,因此对程序的结构影响越小越好,最好能简单到如同一条printf语句,且不用担心其他性能开销。而一个好的多线程异步logging库能帮我们做到这一点,见第5章。(Apache的log4cxx和log4j都支持AsyncAppender这种异步logging方式。)

例2:memcached客户端 假设我们用memcached来保存用户最后发帖的时间,那么每次响应用户发帖的请求时,程序里要去设置一下memcached里的值。这一步如果用同步IO,会增加延迟。

对于“设置一个值”这样的write-only idempotent操作,我们其实不用等memcached返回操作结果,这里也不用在乎set操作失败,那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的memcached的客户端,对于set操作,调用方只要把key和value准备好,调用一下asyncSet()函数,把数据往BlockingQueue上一放就能立即返回,延迟很小。剩下的事就留给memcached客户端的线程去操心,而服务线程不受阻碍。

其实所有的网络写操作都可以这么异步地做,不过这也有一个缺点,那就是每次asyncWrite()都要在线程间传递数据。其实如果TCP缓冲区是空的,我们就可以在本线程写完,不用劳烦专门的IO线程。Netty就使用了这个办法来进一步降低延迟。

以上都仅讨论了“打一枪就跑”的情况,如果是一问一答,比如从memcached取一个值,那么“重叠IO”并不能降低响应时间,因为你无论如何要等memcached的回复。这时我们可以用别的方式来提高并发度,见问题8。(虽然不能降低响应时间,但也不要浪费线程在空等上。)

以上的例子也说明,BlockingQueue是构建多线程程序的利器。另见§ 12.8.3。

6.为什么第三方库往往要用自己的线程?

event loop模型没有标准实现。如果自己写代码,尽可以按所用Reactor的推荐方式来编程。但是第三方库不一定能很好地适应并融入这个event loop framework,有时需要用线程来做一些串并转换。比方说检测串口上的数据到达可以用文件描述符的可读事件,因此可以方便地融入event loop。但是检测串口上的某些控制信号(例如DCD)只能用轮询(ioctl(fd,TIOCMGET,&flags))或阻塞等待(ioctl(fd,TIOCMIWAIT,TIOCM_CAR));要想融入event loop,需要单独起一个线程来查询串口信号翻转,再转换为文件描述符的读写事件(可以通过pipe(2))。

对于Java,这个问题还好办一些,因为thread pool在Java里有标准实现,叫ExecutorService。如果第三方库支持线程池,那么它可以和主程序共享一个ExecutorService,而不是自己创建一堆线程。(比如在初始化时传入主程序的obj。)对于C++,情况麻烦得多,Reactor和thread pool都没有标准库。

例1:libmemcached只支持同步操作 libmemcached支持所谓的“非阻塞操作”,但没有暴露一个能被select/poll/epoll的file describer,它的memcached_fetch始终会阻塞。它号称memcached_set可以是非阻塞的,实际意思是不必等待结果返回,但实际上这个函数会阻塞地调用write(2),仍可能阻塞在网络IO上。

如果在我们的Reactor event handler里调用了libmemcached的函数,那么latency就堪忧了。如果想继续用libmemcached,我们可以为它做一次线程封装,按问题5例2的办法,用额外的线程专门做memcached的IO,而程序主体还是Reactor。甚至可以把memcached的“数据就绪”作为一个event,注入我们的event loop中,以进一步提高并发度。(例子留待问题8讲。)

万幸的是,memcached的协议非常简单,大不了可以自己写一个基于Reactor的客户端,但是数据库客户端就没那么幸运了。

例2:MySQL的官方C API不支持异步操作 MySQL的官方客户端[注25:非官方的 libdrizzle 似乎支持异步操作,见 https://github.com/chaoslawful/drizzle-nginx-module]只支持同步操作,对于UPDATE/INSERT/DELETE之类只要行为不管结果的操作(如果代码需要得知其执行结果,则另当别论),我们可以用一个单独的线程来做,以降低服务线程的延迟。可仿照前面memcached_set的例子,不再赘言。麻烦的是SELECT,如果要把它也异步化,就得动用更复杂的模式了,见问题8。

相比之下,PostgreSQL的C客户端libpq的设计要好得多,我们可以用PQsend-Query()来发起一次查询,然后用标准的select/poll/epoll来等待PQsocket。如果有数据可读,那么用PQconsumeInput处理之,并用PQisBusy判断查询结果是否已就绪。最后用PQgetResult来获取结果。借助这套异步API,我们可以很容易地为libpq写一套wrapper,使之融入程序所用的event loop模型中。

7.什么是线程池大小的阻抗匹配原则?

我在前文中提到“阻抗匹配原则”,这里大致讲一讲。

如果池中线程在执行任务时,密集计算所占的时间比重为P(0

假设C=8,P=1.0,线程池的任务完全是密集计算,那么T=8。只要8个活动线程就能让8个CPU饱和,再多也没用,因为CPU资源已经耗光了。

假设C=8,P=0.5,线程池的任务有一半是计算,有一半等在IO上,那么T=16。考虑操作系统能灵活、合理地调度sleeping/writing/running线程,那么大概16个“50%繁忙的线程”能让8个CPU忙个不停。启动更多的线程并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。

如果P<0.2,这个公式就不适用了,T可以取一个固定值,比如5×C。另外,公式里的C不一定是CPU总数,可以是“分配给这项任务的CPU数目”,比如在8核机器上分出4个核来做一项任务,那么C=4。

8.除了你推荐的Reactor+thread pool,还有别的non-trivial多线程编程模型吗?

有,Proactor。如果一次请求响应中要和别的进程打多次交道,那么Proactor模型往往能做到更高的并发度。当然,代价是代码变得支离破碎,难以理解。

这里举HTTP proxy为例,一次HTTP proxy的请求如果没有命中本地cache,那么它多半会:

1.解析域名(不要小看这一步,对于一个陌生的域名,解析可能要花几秒的时间);

2.建立连接;

3.发送HTTP请求;

4.等待对方回应;

5.把结果返回给客户。

这5步中跟2个server发生了3次round-trip,每次都可能花几百毫秒:

1.向DNS问域名,等待回复;

2.向对方的HTTP服务器发起连接,等待TCP三路握手完成;

3.向对方发送HTTP request,等待对方response。

而实际上HTTP proxy本身的运算量不大,如果用线程池,池中线程的数目会很庞大,不利于操作系统的管理调度。

这时我们有两个解决思路:

1.把“域名已解析”、“连接已建立”、“对方已完成响应”做成event,继续按照Reactor的方式来编程。这样一来,每次客户请求就不能用一个函数从头到尾执行完成,而要分成多个阶段,并且要管理好请求的状态(“目前到了第几步?”)。

2.用回调函数,让系统来把任务串起来。比如收到用户请求,如果没有命中本地缓存,那么需要执行:

a.立刻发起异步的DNS解析startDNSResolve(),告诉系统在解析完之后调用DNSResolved()函数;

b.在DNSResolved()中,发起TCP连接请求,告诉系统在连接建立之后调用connectionEstablished();

c.在connectionEstablished()中发送HTTP request,告诉系统在收到响应之后调用httpResponsed();

d.最后,在httpResponsed()里把结果返回给客户。

.NET大量采用的BeginInvoke/EndInvoke操作也是这个编程模式。当然,对于不熟悉这种编程方式的人,代码会显得很难看。有关Proactor模式的例子可参看Boost.Asio的文档,这里不再多说。

Proactor模式依赖操作系统或库来高效地调度这些子任务,每个子任务都不会阻塞,因此能用比较少的线程达到很高的IO并发度。

Proactor能提高吞吐,但不能降低延迟,所以我没有深入研究。另外,在没有语言直接支持的情况下[注26:有的语言能通过库扩展,例如 http://jscex.info/zh-cn/],Proactor模式让代码非常破碎,在C++中使用Proactor是很痛苦的。因此最好在“线程”很廉价的语言中使用这种方式,这时runtime往往会屏蔽细节,程序用单线程阻塞IO的方式来处理TCP连接。

9.模式2和模式3a该如何取舍?

§ 3.5中提到,模式2是一个多线程的进程,模式3a是多个相同的单线程进程。

我认为,在其他条件相同的情况下,可以根据工作集(work set)的大小来取舍。工作集是指服务程序响应一次请求所访问的内存大小。

如果工作集较大,那么就用多线程,避免CPU cache换入换出,影响性能;否则,就用单线程多进程,享受单线程编程的便利。举例来说

● 如果程序有一个较大的本地cache,用于缓存一些基础参考数据(in-memory look-up table),几乎每次请求都会访问cache,那么多线程更适合一些,因为可以避免每个进程都自己保留一份cache,增加内存使用。

● memcached这个内存消耗大户用多线程服务端就比在同一台机器上运行多个memcached instance要好。(但是如果你在16GiB内存的机器上运行32-bit memcached,那么此时多instance是必需的。)

● 求解Sudoku用不了多大内存。如果单线程编程更方便的话,可以用单线程多进程来做。再在前面加一个单线程的load balancer,仿lighttpd+fastcgi的成例。

线程不能减少工作量,即不能减少CPU时间。如果解决一个问题需要执行一亿条指令(这个数字不大,不要被吓到),那么用多线程只会让这个数字增加。但是通过合理调配这一亿条指令在多个核上的执行情况,我们能让工期提早结束。这听上去像统筹方法,其实也确实是统筹方法。

本文地址:https://www.yitenyun.com/2214.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 #kubernetes #笔记 #平面 #容器 #linux #学习方法 香港站群服务器 多IP服务器 香港站群 站群服务器 #Trae #IDE #AI 原生集成开发环境 #Trae AI #人工智能 #微信 #分阶段策略 #模型协议 #运维 #学习 #科技 #深度学习 #自然语言处理 #神经网络 #华为云 #部署上线 #动静分离 #Nginx #新人首发 #github #git #harmonyos #docker #鸿蒙PC #物联网 #websocket #fastapi #html #css #Conda # 私有索引 # 包管理 #进程控制 #开源 #开发语言 #云原生 #iventoy #VmWare #OpenEuler #kylin #低代码 #爬虫 #音视频 #hadoop #hbase #hive #zookeeper #spark #kafka #flink #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #后端 #数据库 #langchain #内网穿透 #网络 #cpolar #vscode #mobaxterm #计算机视觉 #分布式 #华为 #FTP服务器 #node.js #MobaXterm #ubuntu #缓存 #sql #AIGC #agi #ARM服务器 # GLM-4.6V # 多模态推理 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #tcp/ip #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #word #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #qt #C++ #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #http #项目 #高并发 #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #pytorch #AI编程 #pycharm #ssh #算法 #大数据 #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #Harbor #unity #c# #游戏引擎 #mcu #flask #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #内存治理 #django #c++ #性能优化 #经验分享 #安卓 #android #腾讯云 #前端 #nginx #serverless #diskinfo # TensorFlow # 磁盘健康 #散列表 #哈希算法 #数据结构 #leetcode #golang #java #redis #mysql #ci/cd #jenkins #gitlab #ide #区块链 #测试用例 #生活 #我的世界 #web安全 #安全 #Ansible # 自动化部署 # VibeThinker #Ubuntu服务器 #硬盘扩容 #命令行操作 #VMware #网络协议 #儿童书籍 #儿童诗歌 #童话故事 #经典好书 #儿童文学 #好书推荐 #经典文学作品 #uni-app #小程序 #notepad++ #自动化 #ansible #vllm #大模型 #Streamlit #Qwen #本地部署 #AI聊天机器人 #ModelEngine #架构 #mvp #个人开发 #设计模式 #vue.js #spring boot #jvm #aws #云计算 #DisM++ # 系统维护 #京东云 #语音识别 #ai #prometheus #Android #Bluedroid #智能手机 #udp #everything #jar #需求分析 #数学建模 #2026年美赛C题代码 #2026年美赛 #centos #java-ee #文心一言 #AI智能体 #Ascend #MindIE #银河麒麟 #系统升级 #信创 #国产化 #LLM #iBMC #UltraISO #游戏 #mmap #nio #flutter #gpu算力 #编辑器 #计算机网络 #金融 #mcp #金融投资Agent #Agent #研发管理 #禅道 #禅道云端部署 #中间件 #课程设计 #深度优先 #DFS #svn #n8n #windows #阿里云 #RAID #RAID技术 #磁盘 #存储 #c语言 #stm32 #unity3d #服务器框架 #Fantasy #elasticsearch #设备驱动 #芯片资料 #网卡 #信息与通信 #transformer #AI写作 #iphone #spring #凤希AI伴侣 #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #AI #大模型学习 #journalctl #程序人生 #科研 #博士 #MC #wordpress #雨云 #LobeChat #vLLM #GPU加速 #json #链表 #链表的销毁 #链表的排序 #链表倒置 #判断链表是否有环 #电脑 #grafana #SSH反向隧道 # Miniconda # Jupyter远程访问 #web #webdav #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #ping通服务器 #读不了内网数据库 #bug菌问答团队 #网络安全 #MCP #MCP服务器 #风控模型 #决策盲区 #epoll #高级IO #数据仓库 #php #VS Code调试配置 #javascript #AI论文写作工具 #学术论文创作 #论文效率提升 #MBA论文写作 #asp.net #面试 #react.js #1024程序员节 #LoRA # RTX 3090 # lora-scripts #牛客周赛 #fiddler #ddos #信息可视化 #claude code #codex #code cli #ccusage #数据集 #远程桌面 #远程控制 #驱动开发 #bash #lua #测试工具 #压力测试 #azure #YOLO #目标检测 #时序数据库 #DeepSeek #蓝耘智算 #ida #版本控制 #Git入门 #开发工具 #代码托管 #里氏替换原则 #制造 #个人博客 #蓝桥杯 #arm开发 #嵌入式 #嵌入式硬件 #arm #nas #守护进程 #复用 #screen #ONLYOFFICE #MCP 服务器 #嵌入式编译 #ccache #distcc #es安装 #毕设 #STUN # TURN # NAT穿透 #sizeof和strlen区别 #sizeof #strlen #计算数据类型字节数 #计算字符串长度 #进程 #操作系统 #进程创建与终止 #shell #智能路由器 #ollama #llm #ssl #企业微信 #信号处理 #tcpdump #机器学习 #embedding #RustDesk #IndexTTS 2.0 #本地化部署 #oracle #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #模版 #函数 #类 #笔试 #树莓派4b安装系统 #毕业设计 #车辆排放 #paddleocr #我的世界服务器搭建 #minecraft #生信 #Spring AI #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #WEB #laravel #流量监控 #pencil #pencil.dev #设计 #Playbook #AI服务器 #CPU利用率 #sqlite #PyCharm # 远程调试 # YOLOFuse #RAG #全链路优化 #实战教程 #openresty #Triton # PyTorch # CUDA #流媒体 #NAS #飞牛NAS #监控 #NVR #EasyNVR #SSH保活 #Miniconda #远程开发 #openlayers #bmap #tile #server #vue #SSH Agent Forwarding # 容器化 #vuejs #eBPF #单片机 #openEuler #Hadoop #todesk #chatgpt #nacos #银河麒麟aarch64 #uvicorn #uvloop #asgi #event #搜索引擎 #homelab #Lattepanda #Jellyfin #Plex #Emby #Kodi #TensorRT # Triton # 推理优化 #zabbix #信令服务器 #Janus #MediaSoup #wsl #鸿蒙 #建筑缺陷 #红外 #MS #Materials #microsoft #数码相机 #SSH #X11转发 #debian #改行学it #创业创新 #程序员创富 #sqlserver #密码学 #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #agent #ai大模型 #apache #vue上传解决方案 #vue断点续传 #vue分片上传下载 #vue分块上传下载 #risc-v #ms-swift # 一锤定音 # 大模型微调 #deepseek #tdengine #涛思数据 #adb #推荐算法 #SSH公钥认证 # 安全加固 #tensorflow #cpp #claude #dify #PowerBI #企业 #log #opencv #数据挖掘 #macos #screen 命令 #上下文工程 #langgraph #意图识别 #指针 #fpga开发 #LVDS #高速ADC #DDR #东方仙盟 # GLM-TTS # 数据安全 #API限流 # 频率限制 # 令牌桶算法 #游戏机 #JumpServer #堡垒机 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #飞牛nas #fnos #黑群晖 #虚拟机 #无U盘 #纯小白 #处理器 #支付 #微信小程序 #Gunicorn #WSGI #Flask #并发模型 #容器化 #Python #性能调优 #vim #gcc #yum #ESP32 #传感器 #MicroPython #蓝湖 #Axure原型发布 #RK3576 #瑞芯微 #硬件设计 #振镜 #振镜焊接 #teamviewer #llama #语言模型 #ceph #ambari #单元测试 #集成测试 #jupyter #ai编程 #Linux #TCP #Socket网络编程 # 目标检测 #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #web server #请求处理流程 #UDP套接字编程 #UDP协议 #网络测试 #SRS #直播 #910B #昇腾 #milvus #springboot #知识库 #Host #渗透测试 #SSRF #RSO #机器人操作系统 #glibc #chrome #Anaconda配置云虚拟环境 #MQTT协议 #C语言 #tomcat #集成学习 #https #政务 #fabric #postgresql #可信计算技术 #rocketmq #selenium #winscp #openHiTLS #TLCP #DTLCP #商用密码算法 #系统架构 #Clawdbot #个人助理 #数字员工 # 双因素认证 #ui #分类 #服务器繁忙 #前端框架 #powerbi #PyTorch #模型训练 #星图GPU #Docker #cursor #rustdesk #p2p #连接数据库报错 #YOLOFuse # Base64编码 # 多模态检测 #源码 #闲置物品交易系统 #IPv6 #DNS #C# # REST API # GLM-4.6V-Flash-WEB #web3.py #视频去字幕 #C #bootstrap #SPA #单页应用 #麒麟OS #YOLOv8 # Docker镜像 #swagger #IndexTTS2 # 阿里云安骑士 # 木马查杀 #jetty #visual studio code #prompt #mamba #mariadb #esp32教程 #计算机 #OPCUA #开源软件 #rust #LangGraph #CLI #JavaScript #langgraph.json #firefox #CMake #Make #C/C++ # 高并发部署 #Anything-LLM #IDC服务器 #私有化部署 #vps #raid #raid阵列 #算力一体机 #ai算力服务器 #青少年编程 #simulink #matlab #JAVA #Java #intellij-idea #database #idea #webpack #学术写作辅助 #论文创作效率提升 #AI写论文实测 #电气工程 #PLC #负载均衡 #reactjs #web3 #maven #rdp #几何学 #拓扑学 #AB包 #海外服务器安装宝塔面板 #翻译 #开源工具 #ComfyUI # 推理服务器 #libosinfo #Dify #ARM架构 #鲲鹏 #微服务 #1panel #vmware #客户端 #DIY机器人工房 #性能 #优化 #RAM #酒店客房管理系统 #论文 #说话人验证 #声纹识别 #CAM++ #windows11 #系统修复 #.net #大模型入门 #AI大模型 #select #其他 #typescript #npm #PTP_1588 #gPTP #智慧校园解决方案 #智慧校园一体化平台 #智慧校园选型 #智慧校园采购 #智慧校园软件 #智慧校园专项资金 #智慧校园定制开发 #模型上下文协议 #MultiServerMCPC #load_mcp_tools #load_mcp_prompt #机器人 #硬件工程 #Windows #gitea #Jetty # CosyVoice3 # 嵌入式服务器 #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #万悟 #联通元景 #智能体 #镜像 #scala #结构体 #webrtc #idm #Android16 #音频性能实战 #音频进阶 #流程图 #论文阅读 #论文笔记 #健身房预约系统 #健身房管理系统 #健身管理系统 #SMTP # 内容安全 # Qwen3Guard #Reactor #clickhouse #Coze工作流 #AI Agent指挥官 #多智能体系统 #gateway #Comate #扩展屏应用开发 #android runtime #SSE # AI翻译机 # 实时翻译 #eclipse #spring cloud #servlet #r-tree #聊天小程序 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #无人机 #Deepoc #具身模型 #开发板 #未来 #scrapy #UOS #海光K100 #统信 #NFC #智能公交 #服务器计费 #FP-增长 #CANN #wpf #交互 #Fun-ASR # 语音识别 # WebUI #Proxmox VE #虚拟化 #CUDA #部署 #昇腾300I DUO #GPU服务器 #8U #硬件架构 #NPU #浏览器自动化 #python #Qwen3-14B # 大模型部署 # 私有化AI #cosmic #c++20 #opc ua #opc #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #vp9 #程序员 #矩阵 #线性代数 #AI运算 #向量 #SSH免密登录 #运维开发 #智能一卡通 #门禁一卡通 #梯控一卡通 #电梯一卡通 #消费一卡通 #一卡通 #考勤一卡通 #GB28181 #SIP信令 #SpringBoot #视频监控 #SSH跳板机 # Python3.11 #漏洞 #WT-2026-0001 #QVD-2026-4572 #smartermail #ranger #MySQL8.0 #统信UOS #服务器操作系统 #win10 #qemu #TTS私有化 # IndexTTS # 音色克隆 #UDP的API使用 #数据采集 #浏览器指纹 #存储维护 #screen命令 #视觉检测 #visual studio #系统管理 #服务 # Connection refused #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #Modbus-TCP #iot #智能家居 #求职招聘 #源代码管理 #黑客技术 #网安应急响应 #管道Pipe #system V #elk #微PE # GLM # 服务连通性 #Nacos #gRPC #注册中心 #win11 #chat #pdf #edge #迭代器模式 #观察者模式 #c #YOLO26 #muduo #TcpServer #accept #高并发服务器 #SAP #ebs #metaerp #oracle ebs # 高并发 #能源 #智慧城市 #go #实时音视频 #业界资讯 # GPU集群 #postman #勒索病毒 #勒索软件 #加密算法 #.bixi勒索病毒 #数据加密 #超算服务器 #算力 #高性能计算 #仿真分析工作站 #国产化OS #LangFlow # 轻量化镜像 # 边缘计算 #SSH跳转 #html5 #weston #x11 #x11显示服务器 #计算几何 #斜率 #方向归一化 #叉积 #samba #数模美赛 # 批量管理 #excel #copilot #硬盘克隆 #DiskGenius #媒体 #vivado license #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #交通物流 #ArkUI #ArkTS #鸿蒙开发 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #音乐分类 #音频分析 #ViT模型 #Gradio应用 #IO #hibernate #测评 #CCE #Dify-LLM #Flexus #JNI #CPU #连锁药店 #连锁店 #puppeteer #LabVIEW知识 #LabVIEW程序 #LabVIEW功能 #labview #KMS #slmgr #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #spine # keep-alive #POC #问答 #交付 #xlwings #Excel #mybatis #系统安全 #ipmitool #BMC #迁移重构 #数据安全 #代码迁移 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #restful #ajax #nfs #iscsi #文件管理 #文件服务器 #范式 #kmeans #聚类 #ecmascript #elementui #文件IO #输入输出流 #ET模式 #非阻塞 # 大模型 # 模型训练 #scanf #printf #getchar #putchar #cin #cout #大语言模型 #pandas #matplotlib #全能视频处理软件 #视频裁剪工具 #视频合并工具 #视频压缩工具 #视频字幕提取 #视频处理工具 #企业级存储 #网络设备 #Smokeping #多模态 #微调 #超参 #LLamafactory #策略模式 #pve #OSS #duckdb #zotero #WebDAV #同步失败 #代理模式 #工具集 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #cesium #可视化 #数据分析 #排序算法 #jdk #排序 #欧拉 #SMP(软件制作平台) #EOM(企业经营模型) #应用系统 #aiohttp #asyncio #异步 #软件 #本地生活 #电商系统 #商城 # IndexTTS 2.0 # 自动化运维 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #.netcore #儿童AI #图像生成 #tornado # 模型微调 #数组 #麒麟 # 水冷服务器 # 风冷服务器 #Aluminium #Google #Shiro #反序列化漏洞 #CVE-2016-4437 #Zabbix #CosyVoice3 #语音合成 #大模型部署 #mindie #大模型推理 #n8n解惑 #大模型开发 #rabbitmq #Go并发 #高并发架构 #Goroutine #系统设计 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #esp32 arduino #net core #kestrel #web-server #asp.net-core #AI技术 #HistoryServer #Spark #YARN #jobhistory #EMC存储 #NetApp存储 #产品运营 #模拟退火算法 #简单数论 #埃氏筛法 #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #eureka #mongodb #x86_64 #数字人系统 #5G #汇编 #yolov12 #研究生life #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #asp.net上传大文件 #rtsp #转发 #土地承包延包 #领码SPARK #aPaaS+iPaaS #数字化转型 #智能审核 #档案数字化 #xss #Termux #Samba #SSH别名 #三维 #3D #三维重建 #CVE-2025-61686 #路径遍历高危漏洞 #信创国产化 #达梦数据库 #IntelliJ IDEA #Spring Boot #neo4j #NoSQL #SQL #Llama-Factory # 大模型推理 #ShaderGraph #图形 #log4j #VMware Workstation16 #GPU #AutoDL ##租显卡 #HeyGem # 服务器IP # 端口7860 # 远程访问 # 服务器IP配置 #进程等待 #wait #waitpid #边缘计算 #大模型教程 #全文检索 # 代理转发 # 跳板机 #echarts #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman # GPU租赁 # 自建服务器 #VibeVoice # 语音合成 # 云服务器 #markdown #建站 #结构与算法 #游戏美术 #技术美术 #游戏策划 #游戏程序 #用户体验 #web服务器 # 公钥认证 #阻塞队列 #生产者消费者模型 #服务器崩坏原因 #MinIO服务器启动与配置详解 #ue5 #代理 #平板 #零售 #智能硬件 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #遛狗 #CTF #TLS协议 #HTTPS #漏洞修复 #运维安全 #VMWare Tool #arm64 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #SSH复用 # 远程开发 # 远程运维 #DHCP #autosar #插件 #DAG #服务器解析漏洞 #注入漏洞 #nvidia #串口服务器 #Modbus #MOXA #GATT服务器 #蓝牙低功耗 # ControlMaster #硬件 #safari #WinDbg #Windows调试 #内存转储分析 #cnn #memory mcp #Cursor #网路编程 #百万并发 #intellij idea #计组 #数电 #googlecloud #vnstat # 远程连接 #重构 #Xshell #Finalshell #生物信息学 #组学 #memcache #大剑师 #nodejs面试题 #攻防演练 #Java web #红队 #C2000 #TI #实时控制MCU #AI服务器电源 # 树莓派 # ARM架构 #ansys #ansys问题解决办法 #anaconda #虚拟环境 # 网络延迟 #远程软件 # OTA升级 # 黄山派 # ARM服务器 #ngrok #WRF #WRFDA #视频 #HarmonyOS #ip #代理服务器 # IP配置 # 0.0.0.0 #雨云服务器 #Minecraft服务器 #教程 #MCSM面板 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #门禁 #梯控 #智能梯控 #超时设置 #客户端/服务器 #网络编程 #挖矿 #Linux病毒 #turn #sql注入 #机器人学习 #路由器 # 服务器配置 # GPU #跳槽 #galeweather.cn #高精度天气预报数据 #光伏功率预测 #风电功率预测 #高精度气象 #视觉理解 #Moondream2 #多模态AI #线性回归 #Gateway #认证服务器集成详解 #AI-native #贴图 #材质 #设计师 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #框架搭建 #状态模式 #dba #Tokio #CA证书 #华为od #华为机试 #react native #TTS #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #知识 #量子计算 #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 #ASR #SenseVoice # 批量部署 #JT/T808 #车联网 #车载终端 #模拟器 #仿真器 #开发测试 #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 # TTS服务器 # 键鼠锁定 #agentic bi #论文复现 #opc模拟服务器 #远程连接 #服务器线程 # SSL通信 # 动态结构体 #node #protobuf #报表制作 #职场 #数据可视化 #用数据讲故事 #语音生成 #Keycloak #Quarkus #AI编程需求分析 #证书 #后端框架 #AITechLab #cpp-python #CUDA版本 # 数字人系统 # 远程部署 #参数估计 #矩估计 #概率论 #lvs #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #canvas层级太高 #canvas遮挡问题 #盖住其他元素 #苹果ios手机 #安卓手机 #调整画布层级 #蓝牙 #LE Audio #BAP #Ubuntu #ESP32编译服务器 #Ping #DNS域名解析 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #YOLO11 #Node.js # child_process #sentinel #scikit-learn #随机森林 #运维工具 #安全威胁分析 #网络攻击模型 #pyqt #仙盟创梦IDE #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #面向对象 #基础语法 #标识符 #常量与变量 #数据类型 #运算符与表达式 #Discord机器人 #云部署 #程序那些事 #动态规划 #r语言 #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #Linly-Talker # 数字人 # 服务器稳定性 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #flume #3d # 黑屏模式 #主板 #总体设计 #电源树 #框图 #Minecraft #PaperMC #我的世界服务器 #领域驱动 #前端开发 #STDIO传输 #SSE传输 #WebMVC #WebFlux #自由表达演说平台 #演说 #Claude #传统行业 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #国产开源制品管理工具 #Hadess #一文上手 #入侵 #日志排查 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #零代码平台 #AI开发 #UDP #人大金仓 #Kingbase #gnu #Spring AOP #小艺 #搜索 #环境搭建 #图像识别 #软件工程 #ffmpeg #双指针 #租显卡 #训练推理 #产品经理 #就业 #ipv6 #多进程 #python技巧 #wps #Linux多线程 #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #国产操作系统 #V11 #kylinos #KMS激活 # 硬件配置 #numpy #coffeescript #CSDN #Langchain-Chatchat # 国产化服务器 # 信创 #composer #symfony #java-zookeeper #pjsip #Syslog #系统日志 #日志分析 #日志监控 #Autodl私有云 #深度服务器配置 #blender #warp #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #人脸识别sdk #视频编解码 #人脸识别 #AI生成 # outputs目录 # 自动化 #Prometheus #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #FASTMCP #交换机 #三层交换机 #vue3 #人脸核身 #活体检测 #身份认证与人脸对比 #微信公众号 #高斯溅射 #内存接口 # 澜起科技 # 服务器主板 # 显卡驱动备份 #React安全 #漏洞分析 #Next.js #Puppet # IndexTTS2 # TTS #云开发 #广播 #组播 #并发服务器 #云服务器 #个人电脑 #AI智能棋盘 #Rock Pi S #高仿永硕E盘的个人网盘系统源码 #MC群组服务器 #gpu #nvcc #cuda #VPS #搭建 #递归 #线性dp #农产品物流管理 #物流管理系统 #农产品物流系统 #农产品物流 #unix #编程 #c++高并发 #支持向量机 #企业存储 #RustFS #对象存储 #高可用 #CS2 #debian13 #BoringSSL #音诺ai翻译机 #AI翻译机 # Ampere Altra Max #模块 #sklearn #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 # 权限修复 #考研 #群晖 #音乐 #SQL注入主机 # 鲲鹏 #ssm #http头信息 #uip #k8s #国产PLM #瑞华丽PLM #瑞华丽 #PLM #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL #SMARC #ARM #TCP服务器 #开发实战 # 智能运维 # 性能瓶颈分析 #L2C #勒让德到切比雪夫 #空间计算 #原型模式 #Kylin-Server #服务器安装 #devops #戴尔服务器 #戴尔730 #装系统 #多线程 #性能调优策略 #双锁实现细节 #动态分配节点内存 #nosql #junit #汽车 #ThingsBoard MCP #文件上传漏洞 #大学生 #大作业 #电梯 #电梯运力 #电梯门禁 #数据访问 # 服务器IP访问 # 端口映射 #vncdotool #链接VNC服务器 #如何隐藏光标 #A2A #GenAI #bug #HBA卡 #RAID卡 #FHSS #bond #服务器链路聚合 #网卡绑定 #自动化运维 #程序开发 #程序设计 #计算机毕业设计 #C++ UA Server #SDK #跨平台开发 # ProxyJump #机器视觉 #6D位姿 #nodejs #mssql #TFTP #NSP #下一状态预测 #aigc #outlook #错误代码2603 #无网络连接 #2603 #算力建设 #性能测试 #LoadRunner #智能制造 #供应链管理 #工业工程 #库存管理 #N8N #b树 #海外短剧 #海外短剧app开发 #海外短剧系统开发 #短剧APP #短剧APP开发 #短剧系统开发 #海外短剧项目 #RK3588 #RK3588J #评估板 #核心板 #嵌入式开发 #SSH密钥 #练习 #基础练习 #循环 #九九乘法表 #计算机实现 #数字孪生 #三维可视化 #dynadot #域名 #ETL管道 #向量存储 #数据预处理 #DocumentReader #工厂模式 #esb接口 #走处理类报异常 #le audio #低功耗音频 #通信 #连接 #经济学 #smtp #smtp服务器 #PHP #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #声源定位 #MUSIC #windbg分析蓝屏教程 #AI视频创作系统 #AI视频创作 #AI创作系统 #AI视频生成 #AI工具 #文生视频 #AI创作工具 #Buck #NVIDIA #交错并联 #DGX #PyTorch 特性 #动态计算图 #张量(Tensor) #自动求导Autograd #GPU 加速 #生态系统与社区支持 #与其他框架的对比 #AI 推理 #NV #npu #ServBay #安全架构 #AI工具集成 #容器化部署 #分布式架构 #目标跟踪 #快递盒检测检测系统 #xshell #host key #FL Studio #FLStudio #FL Studio2025 #FL Studio2026 #FL Studio25 #FL Studio26 #水果软件 #智能电视 #内网 #clawdbot #编程助手 #图像处理 #vertx #vert.x #vertx4 #runOnContext #CMC #分布式数据库 #集中式数据库 #业务需求 #选型误 #Matrox MIL #二次开发 #rsync # 数据同步 #网络配置实战 #Web/FTP 服务访问 #计算机网络实验 #外网访问内网服务器 #Cisco 路由器配置 #静态端口映射 #网络运维 #防火墙 #claudeCode #content7 #工作 #单例模式 #懒汉式 #恶汉式 #odoo # 串口服务器 # NPort5630 #appche #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #ftp #sftp #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #OpenHarmony #CS336 #Assignment #Experiments #TinyStories #Ablation #Python办公自动化 #Python办公 #cpu #webgl #星际航行 #Socket #套接字 #I/O多路复用 #字节序 #RWK35xx #语音流 #实时传输 #ossinsight #超算中心 #PBS #lsf #反向代理 #鼠大侠网络验证系统源码 #AI部署 # ms-swift #PN 结 #cocos2d #图形渲染 #adobe #数据迁移 #测速 #iperf #iperf3 #pxe #可再生能源 #绿色算力 #风电 #ARM64 # DDColor # ComfyUI #节日 #express #cherry studio #free #vmstat #sar #gmssh #宝塔 #漏洞挖掘 #Exchange #小智 #系统安装 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #MinIO #TRO #TRO侵权 #TRO和解 #游戏服务器断线 #若依 #期刊 #SCI #地理 #遥感 #Fluentd #Sonic #日志采集 #taro #AI应用编程 #AI Agent #开发者工具 #EN4FE #Archcraft #图论 #okhttp #计算机外设 #Karalon #AI Test #remote-ssh #glances #电子电气架构 #系统工程与系统架构的内涵 #自动驾驶 #Routine #健康医疗 #高考 #工程实践 #强化学习 #策略梯度 #REINFORCE #蒙特卡洛 #AI应用 #百度 #ueditor导入word #轻量化 #低配服务器 #Beidou #北斗 #SSR #gpt #API #bigtop #hdp #hue #kerberos #阿里云RDS #poll #vrrp #脑裂 #keepalived主备 #高可用主备都持有VIP #docker安装seata #寄存器 #信息安全 #信息收集 #Rust #项目申报系统 #项目申报管理 #项目申报 #企业项目申报 #H3C #dubbo #生产服务器问题查询 #日志过滤 # AI部署 #语义搜索 #嵌入模型 #Qwen3 #AI推理 #材料工程 #VMware创建虚拟机 #stl #IIS Crypto #远程更新 #缓存更新 #多指令适配 #物料关联计划 #挖漏洞 #攻击溯源 #sglang #DooTask #防毒面罩 #防尘面罩 #tcp/ip #网络 #决策树 #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #运营 #UEFI #BIOS #Legacy BIOS #程序定制 #毕设代做 #课设 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #wireshark #KMS 激活 # 服务器迁移 # 回滚方案 #开关电源 #热敏电阻 #PTC热敏电阻 #UDP服务器 #recvfrom函数 #云计算运维 #高精度农业气象 #ICE #文本生成 #CPU推理 #WAN2.2 #Coturn #TURN #VSCode # SSH #日志模块 #dash #正则表达式 # HiChatBox # 离线AI #银河麒麟服务器系统 #区间dp #贪心算法 #二进制枚举 #短剧 #短剧小程序 #短剧系统 #微剧 #统信操作系统 #人形机器人 #人机交互 #xml #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #DDD #tdd #题解 #图 #dijkstra #迪杰斯特拉 #CNAS #CMA #程序文件 #数据报系统 #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 # GPU服务器 # tmux #网络安全大赛 #idc #云服务器选购 #Saas #线程 #lucene #实时检测 #卷积神经网络 #旅游 #具身智能 # Qwen3Guard-Gen-8B #密码 #HarmonyOS APP #晶振 #AI电商客服 #nmodbus4类库使用教程 #docker-compose #spring ai #oauth2 #Cpolar #国庆假期 #服务器告警 #rtmp #dreamweaver #OpenManage #fs7TF #hdfs #华为od机试 #华为od机考 #华为od最新上机考试题库 #华为OD题库 #华为OD机试双机位C卷 #od机考题库 #AI+ #coze #AI入门 #ROS # 局域网访问 # 批量处理 #resnet50 #分类识别训练 #IFix #运维 #cascadeur # 高温监控 #OBC # 环境迁移 #隐私合规 #网络安全保险 #法律风险 #风险管理 #React #Next #CVE-2025-55182 #RSC #gerrit #2025年 #FRP #AI教程 # DIY主机 # 交叉编译 #自动化巡检 #0day漏洞 #DDoS攻击 #漏洞排查 #异步编程 #系统编程 #Pin #http服务器 #istio #服务发现 #基金 #股票 #娱乐 #敏捷流程 #AE #rag #AI赋能盾构隧道巡检 #开启基建安全新篇章 #以注意力为核心 #YOLOv12 #AI隧道盾构场景 #盾构管壁缺陷病害异常检测预警 #隧道病害缺陷检测 #ARMv8 #内存模型 #内存屏障 #jquery #学术生涯规划 #CCF目录 #基金申请 #职称评定 #论文发表 #科研评价 #顶会顶刊 #fork函数 #进程创建 #进程终止 #分子动力学 #化工仿真 #静脉曲张 #腿部健康 #运动 #session #clamav #外卖配送 #JADX-AI 插件 #命令模式 #边缘AI # Kontron # SMARC-sAMX8 #语义检索 #向量嵌入 #boltbot #人脸活体检测 #live-pusher #动作引导 #张嘴眨眼摇头 #苹果ios安卓完美兼容 #starrocks #L6 #L10 #L9 #OpenAI #故障 #软件需求 #个性化推荐 #BERT模型 #tekton #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #因果学习 #Qwen3-VL # 服务状态监控 # 视觉语言模型 #新浪微博 #传媒 #隐函数 #常微分方程 #偏微分方程 #线性微分方程 #线性方程组 #非线性方程组 #复变函数 #DuckDB #协议 #思爱普 #SAP S/4HANA #ABAP #NetWeaver #Ward #4U8卡 AI 服务器 ##AI 服务器选型指南 #GPU 互联 #GPU算力 #Arduino BLDC #核辐射区域探测机器人 #esp32 #mosquito #效率神器 #办公技巧 #自动化工具 #Windows技巧 #打工人必备 #Python3.11 #Spire.Office