跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 线程控制详解:POSIX 线程库与多线程编程实践

介绍 Linux 下 POSIX 线程库(pthread)的使用,涵盖线程创建、销毁、等待、取消及分离。通过代码示例讲解全局变量共享、线程局部存储、堆空间共享特性,分析线程 ID 与 LWP 区别,并对比 C++11 thread 与 pthread 的关系。重点阐述主线程退出对进程的影响及资源回收机制,提供实用的多线程编程指导。

Stephaine Walsh发布于 2026/3/27更新于 2026/7/2551 浏览
Linux 线程控制详解:POSIX 线程库与多线程编程实践

POSIX 线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以'pthread_'打头的。要使用这些函数库,要通过引入头文件<pthread.h>。链接这些线程函数库时要使用编译器命令的'-lpthread'选项。

创建线程

int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);

功能:创建一个新的线程。

参数:

  • thread:返回线程 ID(输出型参数),这个 id 是指 lwp 吗?并不是,lwp 不需要暴露给用户,至于是什么下文会揭晓。
  • attr:设置线程的属性(优先级,栈大小之类),attr 为 NULL 表示使用默认属性。
  • start_routine:回调函数(函数指针类型),线程启动后要执行的函数。
  • arg:传给线程启动函数的参数。

RETURN VALUE On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

返回值:成功返回 0;失败返回错误码。

错误检查:传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量 errno 赋值以指示错误。pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做)。而是将错误代码通过返回值返回。pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads 函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的 errno 变量的开销更小。

举个例子:

// 新线程
void *thread_routine(void *args) {
    std::string threadname = static_cast<const char *>(args);
    while (true) {
        printf("new thread...\n");
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n; // 避免警告
    // 主线程
    sleep(2);
    while (true) {
        printf("main thread...\n");
        sleep(1);
    }
    return 0;
}

运行该程序可以看到有两个执行流,这证明我们确实创建出了一个线程。

既然我们说 id 不是指 lwp,那么又该如何证明呢?

pthread_t id;
int n = pthread_create(&id, nullptr, thread_routine, (void*)"thread-1");
(void)n;
printf("main create a new thread , new thread id is 0x%lx\n", id); // 不建议是%ld,因为数字会很大

文章配图

怎么理解这个'ID'呢?这个'ID'是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。

由于每个进程有自己独立的内存空间,故此'ID'的作用域是进程级而非系统级 (内核不认识)。

其实 pthread 库也是通过内核提供的系统调用(例如 clone)来创建线程的,而内核会为每个线程创建系统全局唯一的'ID'来唯一标识这个线程。

LWP 是什么呢?LWP 得到的是真正的线程 ID。之前使用 pthread_self 得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程 ID,线程栈,寄存器等属性。在 ps -aL 得到的线程 ID,有一个线程 ID 和进程 ID 相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈是在共享区(堆栈之间),因为 pthread 系列函数都是 pthread 库提供给我们的。而 pthread 库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

全局变量共享

int gval = 100;

// 新线程
void *thread_routine(void *args) {
    std::string threadname = static_cast<const char *>(args);
    while (true) {
        printf("new thread...: gval: %d, &gval %p \n", gval, &gval);
        gval++;
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n; // 避免警告
    // 主线程
    sleep(2);
    while (true) {
        printf("main thread... : gval: %d, &gval %p \n", gval, &gval);
        sleep(1);
    }
    return 0;
}

在进程章节,我们也是做了类似的工作,但当时出现的情况是父子进程的虚拟地址是相同的,但打印出来的值是不同的;

在线程章节,我们会看到二者的虚拟地址是相同的,并且全局变量值是跟随着一方的改动而进行改动。

线程局部存储
thread_local int gval = 100;
  • 每个线程都会单独维护一份该变量的副本;
  • 不同线程中,该变量的值和地址都是相互独立的;
  • 线程之间不会共享 thread_local 变量。
  • 即使是同一个全局变量声明为 thread_local,不同线程中访问它时,也会得到不同的虚拟地址。

那么又是如何做到的呢?

  • thread_local 是 C++11 引入的关键字,由编译器(如 GCC / G++)在底层进行处理。
  • 编译器会将该变量放入 线程局部存储区,保证每个线程有自己的一份局部副本。
  • 与普通 static 或 global 不同,thread_local 并不会被所有线程共享。
  • 在 ELF 符号表中,thread_local 变量会有特殊标记,用来区分其存储方式。

函数共享

std::string fun() { return "我是另一个函数"; }

// 新线程
void *thread_routine(void *args) {
    std::string threadname = static_cast<const char *>(args);
    while (true) {
        printf("new thread...: gval: %d, &gval %p, %s \n", gval, &gval, fun().c_str());
        gval++;
        sleep(1);
    }
    return nullptr; // 如果线程执行完了自己的入口函数,表明该线程退出
}

int main() {
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n; // 避免警告
    // 主线程
    sleep(2);
    while (true) {
        printf("main thread... : gval: %d, &gval %p, %s \n", gval, &gval, fun().c_str());
        sleep(1);
    }
    return 0;
}

同样可以观察到两个线程重入同一个函数。那么是否也可以理解函数重入了?!

堆空间共享 (原则上)

全局上申请堆空间:

int *data = new int(10);

新线程内部申请空间:

新线程函数内部申请堆空间,data 变量是在栈上开辟的,只有这个线程能访问这个局部变量,知道堆空间的起始虚拟地址。那么我只要让其他线程知道这部分堆空间其实虚拟地址就可以了?这并不难做到啊!因此说原则上堆空间是共享的

一个线程崩溃,引起进程退出

// 新线程
void *thread_routine(void *args) {
    std::string threadname = static_cast<const char *>(args);
    int cnt = 0; // Added for compilation
    while (true) {
        cnt--;
        if (cnt == 3) {
            // 野指针节引用
            printf("%s is dead...\n", threadname.c_str());
            int *p = nullptr;
            *p = 0;
        }
        sleep(1);
    }
    return nullptr; // 如果线程执行完了自己的入口函数,表明该线程退出
}

线程等待

引入 -- 主线程先退,进程被回收

// 新线程
void *thread_routine(void *args) {
    std::string threadname = static_cast<const char *>(args);
    while (true) {
        printf("new thread...\n");
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n; // 避免警告
    // 主线程
    sleep(2);
    while (true) {
        printf("main thread...\n");
        sleep(1);
        break;
    }
    return 0;
}

为什么主线程退出,整个进程会被进行回收呢?

main 结束,表示主线程结束,同时也表示,当前进程结束 -- 释放资源,进程结束,所有线程全部退出,哪怕没有执行完。

在多线程代码中,为了避免主线程先退出,因此主线程需要对其他线程进行等待,类似 wait,从而解决新线程得的内存泄漏问题 (类似僵尸问题)。


int pthread_join(pthread_t thread, void **retval); 功能:等待线程结束

参数:thread:等待哪个线程 value_ptr:输出型参数,获得的是新线程函数的返回值 void*

RETURN VALUE On success, pthread_join() returns 0; on error, it returns an error number.

返回值:成功返回 0;失败返回错误码

// 每个线程都有对应的名字
std::vector<pthread_t> tids;

void *thread_routine(void *args) {
    std::string name = static_cast<const char *>(args);
    printf("new thread is running , name is : %s\n", name.c_str());
    sleep(1);
    return nullptr; // 如果线程执行完了自己的入口函数,表明该线程退出
}

int main() {
    // 创建 10 个线程
    for (int i = 0; i < 10; i++) {
        pthread_t tid;
        char idbuffer[64];
        snprintf(idbuffer, 64, "thread-%d", i + 1); // Fixed variable name
        int n = pthread_create(&tid, nullptr, thread_routine, idbuffer);
        (void)n;
        tids.push_back(tid);
    }
    sleep(1);
    for (auto &tid : tids) printf("main create a new thread, new thread id is : 0x%lx\n", tid);
    // 多线程代码中,我们往往想让谁最后退出??主线程! --- 线程也要进行等待,类似进程 wait!
    // 要对新线程进行等待:也会造成类似僵尸进程的问题!
    // 等待回收多个线程
    for (auto &tid : tids) {
        pthread_join(tid, nullptr);
        printf("thread end..., 退出的线程是:%lu\n", tid);
    }
    printf("wait new thread success...\n");
    return 0;
}

在上面这段程序中,主线程创建了 10 个线程,并在最后进行了阻塞回收防止线程资源泄漏,并且我们想让每一个被创建出来的线程都打印出来各自的名字。于是,定义了 idbuffer 数组。

文章配图

按常理来说,即使每个线程都不一定一创建就会运行,但是我也要看到 thread 由 1 置 10 才对,为什么会出现这种情况呢?我们来仔细剖析下里面的原因:

在多线程程序中,每个线程都应该拥有自己对应的 name。然而,从上图的输出结果可以看到,本应依次显示 thread-1 到 thread-9 的线程名,却出现了多个 thread-10。

造成这种情况的原因是:在新线程函数体中,线程的 name 是通过传入的 args 参数赋值的。但是,在部分线程执行到赋值操作之前,args 的内容已经被修改。具体来说,args 对应的是 idbuffer,而 idbuffer 在被复用时,被后来创建的线程(如 thread-10)写入了新的内容。

结果就是:线程 1 ~ 9 在真正执行到 name = args 之前,idbuffer 已经被 thread-10 覆盖;因此,剩下的线程在赋值时,读取到的都是 thread-10 的内容;最终导致输出中出现了多个 'thread-10'。

那么该如何解决呢?只要让每一个线程占有自己申请的空间就行了!

for (int i = 0; i < 10; i++) {
    pthread_t tid;
    char *name = new char[64]; // Fixed semicolon
    snprintf(name, 64, "thread-%d", i + 1);
    int n = pthread_create(&tid, nullptr, thread_routine, name);
    (void)n;
    tids.push_back(tid);
}

多线程全局变量共享! 多线程其他函数共享! 原则上,堆空间也是共享的!

线程崩溃问题:健壮性降低:编写多线程需要更全面更深入地考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出任何一个线程调用 exit,都会导致进程退出,变相的导致所有线程退出.

线程终止

void pthread_exit(void *retval);

功能:线程终止

参数:retval 不要指向一个局部变量。

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者 (自身)

需要注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

只终止某个线程而不终止整个进程,可以有三种方法:

  1. pthread_exit(nullptr)
  2. return nullptr
  3. 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。

在线程等待中,我们说过第二个参数可以拿到线程的退出信息,而这样做的目的就可以根据返回值的信息让主线程进行判断,从而决定下一步要怎么做。

// 新线程
pthread_exit((void*)0);
// 主线程
int m = pthread_join(tid, &ret);

线程的退出信息?

还记得在进程章节我们讲过退出信息 = 信号 + 退出码,我当前能拿到退出码,可是怎么知道信号呢?实际上并不需要考虑信号,因为多线程这里,没机会考虑所谓的异常,因为一旦异常,整个进程就退出来,根本就执行不到 pthread_join 啊!

线程的返回值是数字 0,但谁说返回信息只能是内置类型了?可以返回一个类吗?返回一个函数?(应用层面传参同样可以是任意类型,类对象也是可以的 (这点一定要体会到!))

返回一个类:

// 返回一个类
class Task {
public:
    Task() : _x(0), _y(0), _result(0), _code(0) { }
    Task(int x, int y) : _x(x), _y(y), _result(0), _code(0) { }
    void Div() {
        if (_y == 0) {
            _code = 1; // 除 0 了
            return;
        }
        _result = _x / _y;
    }
    void Print() {
        std::cout << "result: " << _result << "[" << _code << "]" << std::endl;
    }
private:
    int _x;
    int _y;
    int _result;
    int _code;
};
class Result { };

// 需求:线程我也想要自己的"全局变量", 只想自己看到
void *start_routine(void *args) {
    std::string name = static_cast<const char *>(args);
    Task *t = static_cast<Task *>(args);
    while (true) {
        std::cout << "我是一个新线程," << std::endl;
        sleep(1);
        break;
    }
    t->Div();
    sleep(2);
    return (void *)t; // 给线程返回一个类对象
}

int main() {
    pthread_t tid;
    // 未来 pthread 库中,表明线程控制块的起始地址
    // 给线程传参
    Task *t = new Task(20, 10);
    pthread_create(&tid, nullptr, start_routine, (void *)t);
    sleep(2);
    while (true) {
        sleep(1);
        std::cout << "我是一个主线程," << std::endl;
        break;
    }
    void *ret = nullptr;
    int n = pthread_join(tid, &ret);
    Task *result = (Task *)ret;
    result->Print();
    return 0;
}

返回一个函数:

// 定义一个函数类型
typedef int (*MathFunction)(int, int);
int add(int a, int b) { return a + b; }

// 线程执行的函数,返回一个函数指针
void *thread_function(void *args) {
    std::string name = static_cast<const char *>(args);
    printf("new thread is created , name is %s\n", name.c_str());
    MathFunction result;
    result = &add;
    return (void *)result; // 返回函数指针
}

int main() {
    pthread_t tid;
    MathFunction func;
    // 创建新线程
    int n = pthread_create(&tid, nullptr, thread_function, (void *)"thread-1");
    (void)n;
    // 等待线程结束并获取返回的函数指针
    MathFunction ret;
    pthread_join(tid, (void **)&ret);
    printf("new thread wait success...\n");
    // 使用返回的函数进行计算
    int a = 10, b = 5;
    int result = ret(a, b);
    printf("计算结果:%d\n", result);
    return 0;
}

线程取消

int pthread_cancel(pthread_t thread); 功能:取消一个执行中的线程 参数:thread:线程 ID

RETURN VALUE On success, pthread_cancel() returns 0; on error, it returns a nonzero error number. 返回值:成功返回 0;失败返回错误码

主线程进行线程取消:

// 线程取消
void *thread_routine(void *args) {
    std::string name = static_cast<const char *>(args);
    while (true) {
        printf("new thread name : %s\n", name.c_str());
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    sleep(5);
    // 取消线程
    pthread_cancel(tid);
    printf("new thread 被取消...\n");
    return 0;
}

新线程还没运行到 return nullptr,此时就被主线程取消了,那么 pthread_join 的第二个参数带出来的信息又是啥?

线程被其他线程取消,返回值是 -1,#define PTHREAD_CANCELED ((void *)-1)。等待是成功的,但是返回信息默认被系统设置为 -1

若 thread 线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。

获取自身线程 id

pthread_t pthread_self(void); 功能:返回一个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的'ID'。

既然可以让主线程取消其他线程,那么线程是否能自己取消自己呢?是可以做到的,但是意义不大。

pthread_cancel(pthread_self());

线程分离

默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

int pthread_detach(pthread_t thread);

既可以让主线程自己设置,其他线程也可以设置分离

应用场景:

因为真正的软件是一个死循环啊,主线程不会退,让其他线程自动被释放

多个不同的执行流都是为了完成进程这个任务的

在怎么分离也脱不开"线程"概念,也脱不开虚拟地址等

无论是 joinable 还是分离都是修改 task struct 内的一个宏值,代表不同的标志

系统调用号获取 lwp

#define get_lwp_id() syscall(SYS_gettid)
__thread int lwpid;

// 用系统调用号拿到自己的 id
void *thread_routine(void *args) {
    lwpid = get_lwp_id();
    std::string name = static_cast<const char *>(args);
    int cnt = 10;
    while (true) {
        std::cout << "new thread create... " << "new thread id is: " << lwpid << std::endl;
        sleep(1);
        break;
    }
    return (void *)10;
}

int main() {
    lwpid = get_lwp_id();
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    void *ret;
    int n = pthread_join(tid, &ret);
    (void)n;
    std::cout << "join success " << "main thread id is: " << lwpid << std::endl;
    sleep(10);
    return 0;
}

线程库及 pthread_join 第二个参数

文章配图

C++11 thread vs pthread

在 Linux 中,C++ 的多线程本质就是对 pthread 库在做封装!!!

而在 macos,unix,windows 等都有自己对线程库的实现啊!C++ 是否也需要对应封装一遍呢?

需要的,一款软件被发明出来,首要任务是什么?有人用,一直有人用,增大自己的客户覆盖群.

C++ 为什么要支持多线程啊?如何保证自己的多线程被更多人使用?

需要支持跨平台啊!

为什么需要支持跨平台?一种平台,背后就是一批用户!!!跨平台性之争,本质是用户之争!!

如何做到跨平台?

每个平台的多线程封装,都做一份!!!

这就是为什么一个语言支持新特性,需要很长时间且很难的原因啊!

线程 ID、进程地址空间布局

  • pthread_create 函数会产生一个线程 ID,存放在第一个参数指向的地址中。该线程 ID 和前面说的线程 ID(LWP) 不是一回事。
  • 前面讲的线程 ID 属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,属于 NPTL 线程库的范畴。线程库的后续操作,就是根据该线程 ID 来操作线程的。

pthread_t 到底是什么类型呢?取决于实现。对于 Linux 目前实现的 NPTL 实现而言,pthread_t 类型的线程 ID,本质上就是一个进程地址空间上的一个地址。

文章配图

文章配图

总结

本文详细介绍了 Linux 线程编程的核心知识,重点讲解了 POSIX 线程库 (pthread) 的使用方法。主要内容包括:线程创建与终止(pthread_create/pthread_exit)、线程等待与资源回收(pthread_join)、线程取消(pthread_cancel)以及线程分离(pthread_detach)。文章通过代码示例演示了线程间共享全局变量、函数和堆空间的特性,分析了线程局部存储 (thread_local) 的实现原理,并对比了线程 ID 与 LWP 的区别。最后探讨了 C++ 多线程与 pthread 库的关系,指出 C++11 线程库本质是对各平台原生线程库的封装。全文深入浅出地讲解了 Linux 多线程编程的关键技术点,为开发者提供了实用的线程编程指导。

文章配图

目录

  1. POSIX 线程库
  2. 创建线程
  3. 全局变量共享
  4. 线程局部存储
  5. 函数共享
  6. 堆空间共享 (原则上)
  7. 一个线程崩溃,引起进程退出
  8. 线程等待
  9. 引入 -- 主线程先退,进程被回收
  10. 线程终止
  11. 线程取消
  12. 获取自身线程 id
  13. 线程分离
  14. 系统调用号获取 lwp
  15. 线程库及 pthread_join 第二个参数
  16. C++11 thread vs pthread
  17. 线程 ID、进程地址空间布局
  18. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 用 Rokid 灵珠平台做一个 AI Glasses 作业助手
  • Webnovel Writer:基于 Claude Code 的长篇网文 AI 创作系统
  • AI 驱动游戏:鸿蒙生态的机会在哪里?
  • RAG 系统知识投毒攻击研究:One Shot Dominance 论文解读
  • Python 开根号的 5 种常用方法及场景选择
  • Layui 下解决 Unity WebGL Tab 切换黑屏
  • 语言学习通用心法:享受过程、持续输入与词汇积累
  • 低成本运行 Claude Code:通过 LiteLLM 接入 GitHub Copilot Chat API
  • Python pip 安装报错:Preparing metadata 失败的处理
  • 智能梯控系统设计方案:跨品牌群控与 AI 调度架构解析
  • 豆包 Seedream 4.0 多图融合评测:主体一致性与 4K 生成能力解析
  • Python 开发环境安全:为何不应在下载目录直接运行脚本
  • GESP C++ 一级真题解析:手机电量显示
  • Angular入门启蒙03,Angular 环境搭建全解析:Node.js、npm 与 Angular CLI 安装配置
  • Qt 与鸿蒙适配的核心逻辑
  • Kali Linux 2025 部署与 OpenVAS 安全扫描实战
  • 发那科机器人与西门子 PLC 通讯方案:网关与 Modbus TCP 配置及代码
  • 华为昇腾 910B 使用 LLaMA-Factory 微调 Qwen3.5-32B 模型指南
  • C++ multiset 核心原理与实战指南
  • 绿联 NAS 配置 WebDAV 公网访问及 RaiDrive 挂载指南

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online