Skip to content

进程和线程

概念

进程

假设我们的电脑看作是一个工厂,电脑上面是可以运行各种应用程序的(浏览器、Word、音乐播放器、视频播放器....)每一个应用程序都可以看作是一个独立的工作区域,这个独立的工作区域就是我们的进程。

每个进程都会有独立的内存空间和系统资源,每个进程之间是独立的,这意味着假设有一个进程崩了,那么不会影响其他的进程。

线程

刚才我们将进程比做工厂里面一个独立的工作区域,那么每个工作区域都有员工的,一个独立的工作区域是可以有多个员工的,类似的,一个进程也可以有多个线程,线程之间进行协同工作,共享相同的数据和资源。线程是操作系统所能够调度的最小单位。

线程概念

同样都是线程,其中的一个线程能够创建其他的 6 个线程,并且有决定这些线程能够做什么的能力,那么这个线程就被称之为主线程。

在一个进程中所拥有的所有的资源,所有的线程都有权利去使用,这个就叫做“进程资源共享”。

多进程应用

理论上来讲,一个应用会对应一个进程,但是这并不是绝对的。一些大型的应用,在进行架构设计的时候,会设计为多进程应用。比较典型的就是 Chrome 浏览器。在 Chrome 浏览器中,一个标签页会对应一个进程,当前还有很多除了标签页以外的一些其他的进程。这样做的好处在于一个标签页崩溃后,不会影响其他的标签页。

多进程

和前面所提到的主线程类似,如果一个应用是多进程应用,那么也会有一个“主进程”,起到一个协调和管理其他子进程的作用。

例如,在 Node.js 里面,我们可以通过 child_process 这个模块来创建一个子进程,那么在这种情况下,启动这些子进程的 Node.js 应用实例就会被看作是主进程,child_process 就是子进程。主进程负责管理这些子进程,比如分配任务,处理通信和同步数据之类的。

Electron 多进程

Electron 是一个多进程的模型,每个Electron 应用都有一个单一的主进程,作为应用程序的入口点。主程序在NOde.js环境中运行,这意味着它具有require模块和使用所有Node.js API的能力

electron多进程

这里面 Electron 是主进程,对应的就是我们应用入口文件的 index.js,该主进程负责的任务有:

  • 管理整个 Electron 应用程序的生命周期
  • 访问文件系统以及获取操作系统的各种资源
  • 处理操作系统发出的各种事件
  • 创建并管理菜单栏
  • 创建并管理应用程序窗口

Electron Helper(Renderer)该进程就是我们窗口所对应的渲染进程。

假设在任务管理器将该进程关闭掉,我们会发现窗口不再渲染任何的东西,但是应用还存在,窗口也还存在。

这里就需要说一下,实际上在 Electron 应用中,有一个窗口进程,由窗口进程来创建的窗口,之后才是渲染进程来渲染的页面。这也是为什么我们关闭了渲染进程,但是窗口还存在的原因。

electron窗口

假设我们创建了多个窗口,那么会有多个窗口进程么?

多个窗口下仍然只有一个窗口进程,由这个窗口进程负责绘制多个窗口,不同的窗口里面会有不同的渲染进程来渲染页面。

如下图所示:

electron多窗口

最后再明确一个点,一个窗口只能对应一个渲染进程么 ?

其实也不是,哪怕是在一个窗口里面,也是可以有多个渲染进程的。如何做到?通过 webview 加载其他的页面,当使用 webview 的时候,也会对应一个渲染进程。

流程模式

上下文隔离

定义

上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑运行在所加载的 webcontent 网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和您的预加载脚本可访问的高等级权限的API 。

这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。

自 Electron 12 以来,默认情况下已启用上下文隔离,并且它是 _所有应用程序_推荐的安全设置。

迁移之前

javascript
// 上下文隔离禁用的情况下使用预加载
window.myAPI = {
  doAThing: () => {}
}
javascript
// 在渲染器进程使用导出的 API
window.myAPI.doAThing()

迁移之后

javascript
// 在上下文隔离启用的情况下使用预加载
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
  doAThing: () => {}
})
javascript
// 在渲染器进程使用导出的 API
window.myAPI.doAThing()

与Typescript一同使用

typeScript
contextBridge.exposeInMainWorld('electronAPI', {
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
typeScript
export interface IElectronAPI {
  loadPreferences: () => Promise<void>,
}

declare global {
  interface Window {
    electronAPI: IElectronAPI
  }
}
typeScript
window.electronAPI.loadPreferences()

通信

渲染进程 ➡️ 主进程

javascript
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})
javascript
const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})
html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
javascript
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
  const title = titleInput.value
  window.electronAPI.setTitle(title)
})

流程解析

renderer.js监听btn 按钮被点击 ➡️ window.electronAPI.setTitle ➡️ preload.js被调用触发ipcRenderer.send ➡️ main.js 监听到set-title触发 ipcMain.on('set-title')

渲染进程 ↔️ 主进程

javascript
const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')

async function handleFileOpen () {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (!canceled) {
    return filePaths[0]
  }
}

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  ipcMain.handle('dialog:openFile', handleFileOpen)
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})
javascript
const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  openFile: () => ipcRenderer.invoke('dialog:openFile')
})
html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer.js'></script>
</body>
</html>
javascript
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})

流程解析

renderer.js 监听btn 按钮被点击 ➡️ window.electronAPI.openFile ➡️ preload.js被调用触发ipcRenderer.invoke ➡️ main.js 监听到dialog:openFile触发 ipcMain.handle('dialog:openFile') ➡️ 选择文件,将选择文件结构用promise返回到 renderer.js

或者是使用ipcRenderer.sendSync 方式,但是这种返回的是同步的,它将阻塞渲染器进程,直到收到回复为止。

主进程 ➡️ 渲染进程

javascript
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node:path')

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
        {
          click: () => mainWindow.webContents.send('update-counter', 1),
          label: 'Increment'
        },
        {
          click: () => mainWindow.webContents.send('update-counter', -1),
          label: 'Decrement'
        }
      ]
    }

  ])

  Menu.setApplicationMenu(menu)
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  mainWindow.webContents.openDevTools()
}

app.whenReady().then(() => {
  ipcMain.on('counter-value', (_event, value) => {
    console.log(value) // will print value to Node console
  })
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})
javascript
const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
  counterValue: (value) => ipcRenderer.send('counter-value', value)
})
html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>
javascript
const counter = document.getElementById('counter')

window.electronAPI.onUpdateCounter((value) => {
  const oldValue = Number(counter.innerText)
  const newValue = oldValue + value
  counter.innerText = newValue.toString()
  window.electronAPI.counterValue(newValue)
})

流程解析

渲染进程 ➡️ 渲染进程