Q4: 如何理解 Node.js 的"单线程"?它是如何处理高并发的?

11 阅读3分钟

Node.js 面试题详细答案 - Q4

Q4: 如何理解 Node.js 的"单线程"?它是如何处理高并发的?

Node.js 单线程的理解

主线程是单线程的
// 主线程是单线程的
console.log('主线程开始')

// 这些操作在主线程中顺序执行
for (let i = 0; i < 1000000; i++) {
  // CPU 密集型任务会阻塞主线程
}

console.log('主线程结束')

// 如果上面的循环很耗时,会阻塞后续代码执行
但 Node.js 不是完全单线程的
// Node.js 实际上使用线程池处理 I/O 操作
const fs = require('fs')
const crypto = require('crypto')

// 文件 I/O 操作在线程池中执行
fs.readFile('large-file.txt', (err, data) => {
  console.log('文件读取完成')
})

// 加密操作在线程池中执行
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', (err, derivedKey) => {
  console.log('加密完成')
})

console.log('主线程继续执行') // 立即执行,不等待 I/O 操作

高并发处理机制

1. 事件循环 + 非阻塞 I/O
const http = require('http')

const server = http.createServer((req, res) => {
  // 模拟异步 I/O 操作
  setTimeout(() => {
    res.end(`请求 ${req.url} 处理完成`)
  }, 100)
})

server.listen(3000, () => {
  console.log('服务器启动,可以处理大量并发请求')
})

// 一个主线程可以处理数千个并发连接
// 因为 I/O 操作是非阻塞的
2. 连接处理示例
const http = require('http')

let connectionCount = 0

const server = http.createServer((req, res) => {
  connectionCount++
  console.log(`当前连接数: ${connectionCount}`)

  // 模拟数据库查询
  setTimeout(() => {
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(
      JSON.stringify({
        message: 'Hello World',
        connectionId: connectionCount,
      })
    )
  }, 50)
})

server.listen(3000, () => {
  console.log('服务器启动在端口 3000')
})

// 可以同时处理数千个连接
// 每个连接的处理都是非阻塞的

线程池的使用

查看线程池信息
const os = require('os')

console.log('CPU 核心数:', os.cpus().length)
console.log('线程池大小:', process.env.UV_THREADPOOL_SIZE || 4)

// 默认线程池大小为 4
// 可以通过环境变量调整
// UV_THREADPOOL_SIZE=8 node app.js
线程池操作示例
const fs = require('fs')
const crypto = require('crypto')

// 这些操作会使用线程池
const operations = [
  () => fs.readFile('file1.txt', () => console.log('文件1读取完成')),
  () => fs.readFile('file2.txt', () => console.log('文件2读取完成')),
  () =>
    crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () =>
      console.log('加密1完成')
    ),
  () =>
    crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () =>
      console.log('加密2完成')
    ),
  () =>
    crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () =>
      console.log('加密3完成')
    ),
]

// 同时执行多个操作
operations.forEach((op) => op())

console.log('主线程继续执行')

高并发 vs 多线程

传统多线程模型
// 传统多线程模型(Java/C#)
// 每个请求创建一个线程
// 线程创建和切换开销大
// 内存占用高

// 伪代码示例
function handleRequest(request) {
  const thread = new Thread(() => {
    // 处理请求
    processRequest(request)
  })
  thread.start()
}
Node.js 单线程模型
// Node.js 单线程模型
// 一个线程处理所有请求
// 通过事件循环和非阻塞 I/O 实现高并发
// 内存占用低

const server = http.createServer((req, res) => {
  // 非阻塞处理
  processRequestAsync(req, res)
})

性能对比示例

// 模拟高并发测试
const http = require('http')

const server = http.createServer((req, res) => {
  // 模拟异步操作
  setTimeout(() => {
    res.end('OK')
  }, 10)
})

server.listen(3000, () => {
  console.log('服务器启动')

  // 模拟 1000 个并发请求
  for (let i = 0; i < 1000; i++) {
    const req = http.request('http://localhost:3000', (res) => {
      console.log(`请求 ${i} 完成`)
    })
    req.end()
  }
})

限制和解决方案

CPU 密集型任务的问题
// 问题:CPU 密集型任务会阻塞主线程
function cpuIntensiveTask() {
  let result = 0
  for (let i = 0; i < 1000000000; i++) {
    result += i
  }
  return result
}

// 解决方案1:使用 setImmediate 分解任务
function cpuIntensiveTaskAsync(callback) {
  let result = 0
  let i = 0

  function processChunk() {
    const start = Date.now()
    while (i < 1000000000 && Date.now() - start < 5) {
      result += i
      i++
    }

    if (i < 1000000000) {
      setImmediate(processChunk)
    } else {
      callback(result)
    }
  }

  processChunk()
}
使用 Worker Threads
// 解决方案2:使用 Worker Threads
const { Worker, isMainThread, parentPort } = require('worker_threads')

if (isMainThread) {
  // 主线程
  const worker = new Worker(__filename)
  worker.postMessage('start')
  worker.on('message', (result) => {
    console.log('计算结果:', result)
  })
} else {
  // 工作线程
  parentPort.on('message', (msg) => {
    let result = 0
    for (let i = 0; i < 1000000000; i++) {
      result += i
    }
    parentPort.postMessage(result)
  })
}

总结

  • 主线程单线程:JavaScript 执行是单线程的
  • 线程池:I/O 操作使用线程池处理
  • 高并发:通过事件循环和非阻塞 I/O 实现
  • 优势:内存占用低,开发简单
  • 限制:CPU 密集型任务会阻塞主线程
  • 解决方案:任务分解、Worker Threads、集群模式