实战:Zig 编写高性能 Web 服务(2)

news2024/12/26 21:57:06

1.1 编写 HTTP server

我们从python -m http.server 8000启动得到灵感,先确定好目标:

  • 编写一个HTTP/1.1 http server
  • zig version 0.12.0

使用zig init搭建项目的前置工作你先自行搭建好,不会的翻看前面铺垫的章节熟悉zig的项目结构。

关键文件build.zig:

const std = @import("std");

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "zig-http-server",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
        // .use_llvm = false,
        // .use_lld = false,
    });

    const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize });
    exe.root_module.addImport("xev", xev.module("xev"));

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    b.installArtifact(exe);

    // This *creates* a Run step in the build graph, to be executed when another
    // step is evaluated that depends on it. The next line below will establish
    // such a dependency.
    const run_cmd = b.addRunArtifact(exe);

    // By making the run step depend on the install step, it will be run from the
    // installation directory rather than directly from within the cache directory.
    // This is not necessary, however, if the application depends on other installed
    // files, this ensures they will be present and in the expected location.
    run_cmd.step.dependOn(b.getInstallStep());

    // This allows the user to pass arguments to the application in the build
    // command itself, like this: `zig build run -- arg1 arg2 etc`
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build run`
    // This will evaluate the `run` step rather than the default, which is "install".
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

build.zig.zon文件:

.{
    .name = "zig-http-server",
    .version = "0.0.0",
    .dependencies = .{
        .libxev = .{
            .url = "https://codeload.github.com/mitchellh/libxev/tar.gz/b3f9918776b8700b337b7ebe769060328fe246b0",
            .hash = "122044caf67c7833c7110dc93531031899e459a6818ed125a0bcfdb0b5243bd7700b",
        },
    },
    .paths = .{
        "",
    },
}

马上开始我们的编程之旅,打开src/main.zig文件:

const std = @import("std");
const xev = @import("xev");

const net = std.net;
const Allocator = std.mem.Allocator;

const CompletionPool = std.heap.MemoryPoolExtra(xev.Completion, .{});
const ClientPool = std.heap.MemoryPoolExtra(Client, .{});

const xev = @import("xev");

我们采用了第三方的类库:mitchellh/libxev,libxev 是一个跨平台、高性能的事件循环库,提供了非阻塞 IO、定时器、事件等的抽象,并且能够在 Linux(io_uring 或 epoll)、macOS(kqueue)以及 Wasm + WASI 上运行。它既可以作为 Zig 语言的 API,也可以作为 C 语言的 API 使用。

我们需要建一个Client的结构体,代表一个网络客户端。这个客户端结构体包含了几个字段,如id(客户端ID)、socket(网络套接字)、loop(事件循环)、arena(内存分配器)、client_pool(客户端内存池)和completion_pool(完成操作内存池)。此外,它还有一个用于读取数据的缓冲区read_buf。

Client结构体还定义以下方法:

  • work:这个方法启动客户端的工作流程,它创建了一个Completion对象,并使用socket.read方法开始读取数据。
  • readCallback:这个回调函数在数据读取完成后被调用。它处理读取的结果,如果成功,则打印出读取的数据,并准备一个HTTP响应。然后,它使用socket.write方法发送响应。
  • writeCallback:这个回调函数在响应发送完成后被调用。它关闭套接字连接。
  • shutdownCallback:这个回调函数在套接字关闭后被调用。它调用socket.close方法来关闭套接字。
  • closeCallback:这个回调函数在套接字关闭完成后被调用。它清理资源,包括销毁Completion对象和Client对象本身。
  • destroy:这个方法用于手动销毁客户端资源。

这个Client结构体实现了一个简单的HTTP服务器,它能够读取客户端的请求,并返回一个包含"Hello, World!"消息的HTTP响应。每个客户端都有自己的内存分配器和内存池,用于管理内存分配和释放。当客户端连接关闭时,所有分配的内存都会被清理。代码片段如下:

const Client = struct {
    id: u32,
    socket: xev.TCP,
    loop: *xev.Loop,
    arena: std.heap.ArenaAllocator,
    client_pool: *ClientPool,
    completion_pool: *CompletionPool,
    read_buf: [4096]u8 = undefined,

    const Self = @This();

    pub fn work(self: *Self) void {
        const c_read = self.completion_pool.create() catch unreachable;
        self.socket.read(self.loop, c_read, .{ .slice = &self.read_buf }, Client, self, Client.readCallback);
    }

    pub fn readCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.ReadBuffer,
        r: xev.TCP.ReadError!usize,
    ) xev.CallbackAction {
        const self = self_.?;
        const n = r catch |err| {
            std.log.err("read error {any}", .{err});
            s.shutdown(l, c, Client, self, shutdownCallback);
            return .disarm;
        };
        const data = buf.slice[0..n];

        std.log.info("{s}", .{data});

        const httpOk =
            \\HTTP/1.1 200 OK
            \\Content-Type: text/plain
            \\Server: xev-http
            \\Content-Length: {d}
            \\Connection: close
            \\
            \\{s}
        ;

        const content_str =
            \\Hello, World! {d}
        ;

        const content = std.fmt.allocPrint(self.arena.allocator(), content_str, .{self.id}) catch unreachable;
        const res = std.fmt.allocPrint(self.arena.allocator(), httpOk, .{ content.len, content }) catch unreachable;

        self.socket.write(self.loop, c, .{ .slice = res }, Client, self, writeCallback);

        return .disarm;
    }

    fn writeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.WriteBuffer,
        r: xev.TCP.WriteError!usize,
    ) xev.CallbackAction {
        _ = buf; // autofix
        _ = r catch unreachable;

        const self = self_.?;
        s.shutdown(l, c, Client, self, shutdownCallback);

        return .disarm;
    }

    fn shutdownCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        r: xev.TCP.ShutdownError!void,
    ) xev.CallbackAction {
        _ = r catch {};

        const self = self_.?;
        s.close(l, c, Client, self, closeCallback);
        return .disarm;
    }

    fn closeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        socket: xev.TCP,
        r: xev.TCP.CloseError!void,
    ) xev.CallbackAction {
        _ = l;
        _ = r catch unreachable;
        _ = socket;

        var self = self_.?;
        self.arena.deinit();
        self.completion_pool.destroy(c);
        self.client_pool.destroy(self);
        return .disarm;
    }

    pub fn destroy(self: *Self) void {
        self.arena.deinit();
        self.client_pool.destroy(self);
    }
};

 我们还需要定义名为Server的结构体,它代表了一个网络服务器。这个服务器结构体包含了几个字段,如loop(事件循环)、gpa(全局内存分配器)、completion_pool(完成操作内存池)、client_pool(客户端内存池)和conns(当前连接数)。

Server结构体定义了一个方法:

  • acceptCallback:这个回调函数在服务器接受到新的客户端连接时被调用。它创建了一个新的Client对象,并初始化它的各个字段,包括客户端ID、事件循环、网络套接字、内存分配器和内存池。然后,它调用client.work()方法来开始处理客户端请求。

这个回调函数的目的是接受新的客户端连接,并为每个连接创建一个Client实例来处理它。在创建了新的客户端实例后,它会增加conns计数器来记录当前的连接数。最后,它返回xev.CallbackAction.rearm,这意味着服务器会继续监听新的连接。

这个Server结构体实现了一个简单的网络服务器,它能够接受客户端连接,并为每个连接创建一个Client对象来处理通信。服务器使用内存池来管理客户端实例的内存分配,以提高性能和减少内存碎片。代码片段如下:

const Server = struct {
    loop: *xev.Loop,
    gpa: Allocator,
    completion_pool: *CompletionPool,
    client_pool: *ClientPool,
    conns: u32 = 0,

    fn acceptCallback(
        self_: ?*Server,
        l: *xev.Loop,
        // we ignore the completion, to keep the accept loop going for new connections
        _: *xev.Completion,
        r: xev.TCP.AcceptError!xev.TCP,
    ) xev.CallbackAction {
        const self = self_.?;
        var client = self.client_pool.create() catch unreachable;
        client.* = Client{
            .id = self.conns,
            .loop = l,
            .socket = r catch unreachable,
            .arena = std.heap.ArenaAllocator.init(self.gpa),
            .client_pool = self.client_pool,
            .completion_pool = self.completion_pool,
        };
        client.work();

        self.conns += 1;

        return .rearm;
    }
};

1.2 进入main实现

main整体流程是:

  1. 初始化线程池和事件循环。
  2. 创建一个TCP套接字,并绑定到指定的地址和端口。
  3. 开始监听传入的连接。
  4. 初始化完成池和客户端池。
  5. 创建一个服务器结构体,包含所有必要的组件。
  6. 注册一个接受连接的回调函数。
  7. 运行事件循环,等待连接和处理事件。

http服务处理的就是多线程,所以我们借助了第三方类库xev来管理线程池。

var thread_pool = xev.ThreadPool.init(.{});
    defer thread_pool.deinit();
    defer thread_pool.shutdown();
  •  我们创建了一个xev.ThreadPool类型的变量thread_pool,并初始化它。xev是一个库,ThreadPool是这个库中的一个类型,用于管理线程池。
  • defer关键字用于注册一个函数,这个函数会在当前作用域结束时被调用。这里我们注册了thread_pool.deinit()和thread_pool.shutdown(),确保线程池在程序结束时被正确关闭和清理。
const port = 3000;
const addr = try net.Address.parseIp4("0.0.0.0", port);
var socket = try xev.TCP.init(addr);

std.log.info("Listening on port {}", .{port});

try socket.bind(addr);
try socket.listen(std.os.linux.SOMAXCONN);

绑定socket接口的方式,可以看出zig的简洁之道,和C一样。

socket.accept(&loop, c, Server, &server, Server.acceptCallback);

这行代码调用socket.accept方法,用于接受传入的连接。这个方法需要几个参数:

  • &loop:事件循环的引用,用于注册接受连接的事件。
  • c:完成事件的引用,用于在连接被接受时通知服务器。
  • Server:服务器结构体的类型,用于类型检查。
  • &server:服务器结构体的引用,用于在接受连接时传递给回调函数。
  • Server.acceptCallback:服务器结构体中的一个函数,当连接被接受时会被调用。
try loop.run(.until_done);

这行代码启动事件循环,并开始处理事件。run方法会阻塞当前线程,直到事件循环被关闭或者发生错误。.until_done是一个枚举值,表示事件循环应该运行直到所有任务都完成。

然后在项目直接运行zig build run就可以启动。main.zig完整代码如下:

const std = @import("std");
const xev = @import("xev");

const net = std.net;
const Allocator = std.mem.Allocator;

const CompletionPool = std.heap.MemoryPoolExtra(xev.Completion, .{});
const ClientPool = std.heap.MemoryPoolExtra(Client, .{});

pub fn main() !void {
    var thread_pool = xev.ThreadPool.init(.{});
    defer thread_pool.deinit();
    defer thread_pool.shutdown();

    var loop = try xev.Loop.init(.{
        .entries = 4096,
        .thread_pool = &thread_pool,
    });
    defer loop.deinit();

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const port = 3000;
    const addr = try net.Address.parseIp4("0.0.0.0", port);
    var socket = try xev.TCP.init(addr);

    std.log.info("Listening on port {}", .{port});

    try socket.bind(addr);
    try socket.listen(std.os.linux.SOMAXCONN);

    var completion_pool = CompletionPool.init(alloc);
    defer completion_pool.deinit();

    var client_pool = ClientPool.init(alloc);
    defer client_pool.deinit();

    const c = try completion_pool.create();
    var server = Server{
        .loop = &loop,
        .gpa = alloc,
        .completion_pool = &completion_pool,
        .client_pool = &client_pool,
    };

    socket.accept(&loop, c, Server, &server, Server.acceptCallback);
    try loop.run(.until_done);
}

const Client = struct {
    id: u32,
    socket: xev.TCP,
    loop: *xev.Loop,
    arena: std.heap.ArenaAllocator,
    client_pool: *ClientPool,
    completion_pool: *CompletionPool,
    read_buf: [4096]u8 = undefined,

    const Self = @This();

    pub fn work(self: *Self) void {
        const c_read = self.completion_pool.create() catch unreachable;
        self.socket.read(self.loop, c_read, .{ .slice = &self.read_buf }, Client, self, Client.readCallback);
    }

    pub fn readCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.ReadBuffer,
        r: xev.TCP.ReadError!usize,
    ) xev.CallbackAction {
        const self = self_.?;
        const n = r catch |err| {
            std.log.err("read error {any}", .{err});
            s.shutdown(l, c, Client, self, shutdownCallback);
            return .disarm;
        };
        const data = buf.slice[0..n];

        std.log.info("{s}", .{data});

        const httpOk =
            \\HTTP/1.1 200 OK
            \\Content-Type: text/plain
            \\Server: xev-http
            \\Content-Length: {d}
            \\Connection: close
            \\
            \\{s}
        ;

        const content_str =
            \\Hello, World! {d}
        ;

        const content = std.fmt.allocPrint(self.arena.allocator(), content_str, .{self.id}) catch unreachable;
        const res = std.fmt.allocPrint(self.arena.allocator(), httpOk, .{ content.len, content }) catch unreachable;

        self.socket.write(self.loop, c, .{ .slice = res }, Client, self, writeCallback);

        return .disarm;
    }

    fn writeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.WriteBuffer,
        r: xev.TCP.WriteError!usize,
    ) xev.CallbackAction {
        _ = buf; // autofix
        _ = r catch unreachable;

        const self = self_.?;
        s.shutdown(l, c, Client, self, shutdownCallback);

        return .disarm;
    }

    fn shutdownCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        r: xev.TCP.ShutdownError!void,
    ) xev.CallbackAction {
        _ = r catch {};

        const self = self_.?;
        s.close(l, c, Client, self, closeCallback);
        return .disarm;
    }

    fn closeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        socket: xev.TCP,
        r: xev.TCP.CloseError!void,
    ) xev.CallbackAction {
        _ = l;
        _ = r catch unreachable;
        _ = socket;

        var self = self_.?;
        self.arena.deinit();
        self.completion_pool.destroy(c);
        self.client_pool.destroy(self);
        return .disarm;
    }

    pub fn destroy(self: *Self) void {
        self.arena.deinit();
        self.client_pool.destroy(self);
    }
};

const Server = struct {
    loop: *xev.Loop,
    gpa: Allocator,
    completion_pool: *CompletionPool,
    client_pool: *ClientPool,
    conns: u32 = 0,

    fn acceptCallback(
        self_: ?*Server,
        l: *xev.Loop,
        // we ignore the completion, to keep the accept loop going for new connections
        _: *xev.Completion,
        r: xev.TCP.AcceptError!xev.TCP,
    ) xev.CallbackAction {
        const self = self_.?;
        var client = self.client_pool.create() catch unreachable;
        client.* = Client{
            .id = self.conns,
            .loop = l,
            .socket = r catch unreachable,
            .arena = std.heap.ArenaAllocator.init(self.gpa),
            .client_pool = self.client_pool,
            .completion_pool = self.completion_pool,
        };
        client.work();

        self.conns += 1;

        return .rearm;
    }
};

1.3 运行效果:

1.3 学习总结

理解上面代码的关键是理解Zig中的基本概念,如defer、try、结构体、方法和事件循环的工作原理。同时,理解xev库的使用也很重要,因为本次http-server项目大量依赖于这个库的多线程管理能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1792582.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小米用田忌赛马的方式,逼得苹果降价超2000元应对,确实厉害

苹果的iPhone15降价2300多元,成为618的大热门,之前不少人士认为迫使苹果如此大幅度降价的原因是因为另一家手机企业的竞争,而日前有人士认为是小米用田忌赛马的方式,迫使苹果降价应对。 小米这次大幅度降价的手机并非是最新款的小…

设计模式-策略模式(行为型)

行为型-策略模式 了解策略模式 策略模式是一种行为型设计模式,在策略模式中定义了一系列算法或者策略,并将这些策略封装到独立的类中,使得可以相互替换。在使用时,可以指定响应的策略使用。 角色 策略接口:对于某种…

【计算机网络】计算机网络的概念

计算机网络的概念 导读一、计算机网络的概念1.1 个人理解1.2 通信设备与线路1.2.1 集线器1.2.2 交换机1.2.3 路由器 1.3 计算机网络的进一步理解1.4 互联网1.5 网的不同含义 二、计算机网络的不同定义2.1 广义观点2.2 资源共享观点2.3 用户透明性观点 结语 导读 大家好&#x…

【机器学习】基于OpenCV和TensorFlow的MobileNetV2模型的物种识别与个体相似度分析

在计算机视觉领域,物种识别和图像相似度比较是两个重要的研究方向。本文通过结合深度学习和图像处理技术,基于OpenCV和TensorFlow的MobileNetV2的预训练模型模,实现物种识别和个体相似度分析。本文详细介绍该实验过程并提供相关代码。 一、名…

【Python】ERROR: Could not find a version that satisfies the requirement

成功解决“ERROR: Could not find a version that satisfies the requirement”错误的全面指南 一、引言 在Python开发中,经常需要通过pip工具来安装各种依赖包。然而,有时在尝试安装某个包时,可能会遇到“ERROR: Could not find a version …

批量提取 Word 文档中的全部图片

步骤 1、打开 WinRAR 任选一个现成的压缩包双击打开 WinRAR ,或从开始菜单打开 WinRAR 2、直接把要提取图片的 Word 文档拖入 WinRAR 菜单区域 1 → 2 → 3,WinRAR 资源管理目录中的 media 就是该 Word 文档所要提取的全部图片所在文件夹 按住&#x…

python书上的动物是啥

Python的创始人为Guido van Rossum。1989年圣诞节期间,在阿姆斯特丹,Guido为了打发圣诞节的无趣,决心开发一个新的脚本解释程序,做为ABC语言的一种继承。之所以选中Python作为程序的名字,是因为他是一个叫Monty Python…

CATIA进阶操作——创成式曲面设计入门(1)线架设计,三维点、直线、平面、曲线

目录 引出三维空间点生成三维直线三维平面三维曲线总结异形弹簧新建几何体草图编辑,画一条样条线进行扫掠,圆心和半径画出曲面上的螺旋线再次选择扫掠,圆心和半径 其他自定义信号和槽1.自定义信号2.自定义槽3.建立连接4.进行触发 自定义信号重…

c++简略实现共享智能指针Shared_Ptr<T>

重点: 1.引用计数在堆上(原本应为原子变量) 2.引用计数增加减少需要加锁保证线程安全。 3.内部实现Release函数用于释放资源 4.未实现,增加自定义删除器可以将Release修改为模板函数,传入可调用参数。对于shared_p…

【Multi-Feature FAS】《Face Anti-Spoofing Based on Multi-Feature Fusion》

文章目录 原文贡献 / 相关工作作者的方法评价 原文 [1]杨敏.基于多特征融合的人脸防伪技术研究[D].武汉大学,2019. 贡献 / 相关工作 针对攻击对象存在背景依赖和非刚性运动造成的深度信息缺失问题,采用边缘信息放大差异 各种 loss 数据库 评价指标 本节将会对…

队列及其应用

实验内容 请设计一个简单的模拟银行排队系统,要求程序具有以下4项菜单: 1.取号。选择该菜单后,为客户产生一个排队号。 2.叫号。选择该菜单后,显示可服务的客户排队号。 3.查看队伍。从队首到队尾列出所有排队客户的排队号。 4.退…

94、python-第三阶段-4-数据计算-map方法

直接运行会报错,需要配置下python环境变量 from pyspark import SparkConf,SparkContext import os os.environ[PYSPARK_PYTHON]"D:/Program Files/Python/Python3.13/python.exe" conf SparkConf().setMaster("local[*]").setAppName("…

【数据结构】二叉树的层序遍历~动画超详解

目录 1 什么是层序遍历2 二叉树层序遍历的基本思路3 二叉树层序遍历的实现 1 什么是层序遍历 我们从字面意思就明白,所谓层序,就是一层一层按顺序去遍历一个二叉树,这和我们之前了解的按前中后序遍历方式完全不同 比方说这颗二叉树: 前序遍历: 层序遍历: 2 二叉树层序遍历的…

通过 AI Edge Torch 生成式 API 在设备上使用自定义大语言模型

作者 / 首席工程师 Cormac Brick,软件工程师 Haoliang Zhang 我们很高兴地发布 AI Edge Torch 生成式 API,它能将开发者用 PyTorch 编写的高性能大语言模型 (LLM) 部署至 TensorFlow Lite (TFLite) 运行时,从而无缝地将新的设备端生成式 AI 模…

申请医疗设备注册变更时,需要补充考虑网络安全的情况有哪些?

在申请医疗器械设备注册变更时,需要补充网络安全的情况主要包括以下几点: 网络安全功能更新:如果医疗器械的自研软件发生网络安全功能更新,或者合并网络安全补丁更新的情形,需要单独提交一份自研软件网络安全功能更新…

计算机网络ppt和课后题总结(下)

常用端口总结 计算机网络中,端口是TCP/IP协议的一部分,用于标识运行在同一台计算机上的不同服务。端口号是一个16位的数字,范围从0到65535。通常,0到1023的端口被称为“熟知端口”或“系统端口”,它们被保留给一些标准…

springboot项目中第三方jar包打包进jar包

springboot项目中,如果手动引入了jar包,打包时不会将手动引入的第三方jar包打包进价包里,如何处理? 若第三方的jar包的lib和src同级,则maven打包时默认不会将lib下的jar包打包进jar包,处理方式有两种&#…

康谋技术 | 自动驾驶:揭秘高精度时间同步技术(二)

在自动驾驶中,对车辆外界环境进行感知需要用到很多传感器的数据(Lidar,Camera,GPS/IMU),如果计算中心接收到的各传感器消息时间不统一,则会造成例如障碍物识别不准等问题。 为了对各类传感器进…

数据结构与算法-12_二叉搜索树

文章目录 1.概述2.实现定义节点查询Comparable最小最大新增前驱后继删除找小的找大的找之间小结 3.习题E01. 删除节点-Leetcode 450E02. 新增节点-Leetcode 701E03. 查询节点-Leetcode 700E04. 验证二叉搜索树-Leetcode 98E05. 求范围和-Leetcode 938E06. 根据前序遍历结果构造…

【面试题】创建两个线程交替打印100以内数字(一个打印偶数一个打印奇数)

阅读导航 一、问题概述二、解决思路三、代码实现四、代码优化 一、问题概述 面试官:C多线程了解吗?你给我写一下,起两个线程交替打印0~100的奇偶数。就是有两个线程,一个线程打印奇数另一个打印偶数,它们交替输出&…