Typescript 第八章 异步编程,并行和并发(JavaScript事件循环,异步流,多线程类型安全)
Typescript第八章 异步编程,并发和并行
异步API,比如说回调,promise和流。
JavaScript引擎在一个线路中多路复用任务,而其他任务则处于空闲状态。这种事件循环是JavaScript引擎的标准线程模型。
多路复用是指在一个线程中同时处理多个任务
异步编程让程序难以理解。我们不能一行一行的分析程序。
Typescript提供了工具,通过类型可以追踪异步操作,借助内置的async/await可以把熟悉的同步思想运用到异步程序上。使用Typescript还可以为多线程程序指定严格的的消息传递协议。
我们先讨论一下JavaScript引擎中具体是如何运作的,弄清楚为什么看似单个的线程中可以暂停和恢复执行。
8.1 JavaScript的事件循环
下面代码怎么执行?
setTimeout(()=>console.log("A"),1)// 时间到了就加入事件队列等待执行setTimeout(()=>console.log("B"),2)console.log("C");
JavaScript和C语言中sleep才用的并发模型,以及java把作业调度到另外一个线程中不同。
概括的说,JavaScriptVM采用下述方式模拟并发
- JavaScript主线程调用XMLHTTPRequest(处理Ajax请求),setTimeout,readFile等异步API。这些API由JavaScript平台提供,我们自己不能创建
- 调用原生的异步API之后,控制权返回主线程,继续向下执行,就像从未调用异步API一样。
- 异步操作执行完毕后,平台在事件队列中添加一个任务。每个线程都有自己的队列,异步操作的结果就通过队列发回主线程。任务中有关于调用的元信息,还有主线程中回调函数的引用。
- 主线程的调用堆栈清空后,平台将检查事件队列中有没有待处理的任务。如果有等待处理的任务,平台着手处理,触发一个函数调用,把控制权返还给主线程中的那个函数。调用那个函数之后,如果调用堆栈又变空了,平台再次检查事件队列中有没有可以处理的任务了。这个循环一直运转下去,直到调用栈和事件队列都空,而且所有原生的异步API调用都已结束。
8.2 处理回调
JavaScript异步程序的核心基础就是回调。回调其实就是常规的函数,只是作为参数传给另一个函数。就像在同步程序中一样,另一个函数在做完操作(处理网络请求等)之后调用回调函数。异步代码调用的回调也是函数,而且类型签名中没有标明函数是异步调用的。
NodeJS的原生API。例如fs.readFile(采用异步方式从磁盘中读取文件的内容)和dns.resolveCname(采用异步方式解析CNAME记录),按照约定,回调的第一个参数是错误或null,第二个参数是结果或null。(错误优先)。
readFile的类型签名如下:
function readFile(path:string,options:{encoding:string,flag?:string},callback:(err:Error|null,data:string|null)=>void):void{}
注意:readFile和callback的类型没有什么特别之处,都是常规的JavaScript函数。签名中没有特别标明readFile是异步的,也没有指出在调用readFile之后控制权会立即传给下一行代码(不等待readFile的结果)。
pnpm add @types/node -D安装nodejs的类型声明
下面写一个案例读取文件
import * as fs from 'fs'fs.readFile("log.txt",{encoding:'utf-8'},(error,data)=>{if(error){console.log(error);return}console.log("success!:"+data);})// 采用并发写入fs.appendFile("log.txt","笑死我了",error=>{if(error){console.log("error"+error);}})
这里API是异步的,不能把API在代码中调用顺序理解为执行文件系统的操作顺序。readFile虽然在前面调用但是读取出来的访问日志可能没有后面新增的那行日志,具体有没有要看运行这段代码时文件系统有多繁忙。
Nodejs一般约定,如果函数的最后一个参数是一个接受两个参数的函数,而且顺序为Error|null和T|null类型,那么这个函数通常是异步的。
无论如何,从类型上是看不出来的。
回调函数容易产生一个问题,回调地狱
setTimeout(()=>{console.log(1);setTimeout(()=>{console.log(2);setTimeout(()=>{console.log(3);console.log("啊");},1)},1)},1)
按顺序执行的操作往往是一环扣一环,前一步成功才执行下一步,除非遇到错误,我们要自己手动维护执行的顺序。按一定顺序执行的回调很容易出错。
有序的操作只是我们想借助异步任务执行的一种操作,此外我们可能还想并行运行几个函数,获知全部函数何时运行完毕,或者让几个函数竞争,只获取第一个结束的函数返回结果等。
- 使用回调函数可执行简单的异步任务
- 虽然回调适合处理简单的任务,但是如果异步任务变多,很容易一团乱麻。
8.3 promise(我tm来了!!!)
我们不是第一批遇到这些限制的程序员。本节说明promise,这个概念对异步进行抽象,方便任务编排,排列任务等。即使你以前使用过promise或future,也能更好的理解他们的原理。
// 自行分析console.log("start");async function async1() {console.log("async1");let result = await async2()console.log(result);}async function async2() {console.log("async2");return Promise.resolve("wohaole")}async1()console.log("end");
下面举个例子,指出我们想如何使用Promise:先向文件中添加一些内容,然后再把文件中的内容读取出来:
function appendAndReadPromise(path:string,data:string):Promise<string>{return appendPromise(path,data).then(()=>readPromise(path)).catch(error=>console.log(error);)}
注意:这里没有回调地狱,我们把想执行的一些异步任务,变成了易于理解的线性链条:前一个任务完成后才能执行下一个任务,倘若失败,则跳到catch子句。假如是基于回调的API,那么写出的代码可能是下面这样:
function appendAndRead(path: string,data: string,cb: (error: Error | null, result: string | null | any) => void) {appendFile(path, data, err => {if (err) {return cb(err, null)}readFile(path, (err, result) => {if (err) {return cb(err, null)}cb(null, result)})})}
针对这个设想,我们手动实现PromiseAPI
new Promise接受一个函数,我们称为执行器(executor)。在Promise的实现中,执行器接受两个参数,一个是resolve函数,一个是reject函数。
type Executor = (resolve: Function,reject: Function) => voidclass Promise {constructor(f:Executor){}}
那么,resolve和reject是如何运行的呢?下面通过示例说明一下。假设我们把Nodejs中的一个回调API(例如:fs.readFile)改造成基于Promise的API,Nodejs内置的fs.readFileAPI时这样使用的:
import {readFile} from 'fs'readFile(path,(error,result)=>{//..})
import {readFile} from 'fs'function readFilePromise(path:string):Promise<string>{return new Promise((resolve,reject)=>{readFile(path,(err,result)=>{if(err){reject(err)}resolve(result)})})}
可见,resolve的参数是什么类型取决于具体使用的API(这里,其参数的类型就是result的类型),而reject的参数始终是Error类型。因此,我们要更行类型,把不安全的Function改为更具体的类型:
type Executor<T,E extends Error> = (resolve: (result:T)=>void,reject: (error:E)=>void) => voidclass Promise<T,E extends Error> {constructor(f:Executor<T,E>){}}
class Promise<T,E extends Error> {constructor(f:Executor<T,E>){}then<U,F extends Error>(g:(result:T)=>Promise<U,F>):Promise<U,F>{//}catch<U,F extends Error>(g:(error:E)=>Promise<U,F>):Promise<U,F>{//}}
then和catch以不同的方式排列Promise:then把成功从一个Promise获得的结果映射到一个新Promise上,catch则把错误映射到一个新的Promise上,从被拒绝的状态中走出去。(这种风格和上节中的Option设计模式一样都受函数式编程语言Haskell中的Monad设计模式影响)
then的使用方法
let a:()=>Promise<string,TypeError> = //..let b:(s:string)=>Promise<number,never> = //let c:()=>Promise<boolean,RangeError>=//a().then(b).catch(e=>c()).then(result=>console.info("Done",result)).catch(e=>console.log("error",e))
此外,我们还有处理Promise真正抛出异常的情况(例如,throw Error(‘foo’)),为此,在实现then和catch要把代码放在try/catch中,在cattch中分支处理被拒绝的情况,然后事情并没有那么简单,这里还涉及一些其他问题
- Promise对象都有可能被拒,而通过静态检查发现不了这个问题。
- Promise对象被拒不一定是因为有错。Typescript别无选择,只能继承JavaScript的行为,而在JavaScript中,通过throw可以抛出一切。
考虑这两点,我们要放宽对Promise类型的要求,不指定错误类型
type Executor<T> = (resolve: (result: T) => void,reject: (error: unknown) => void) => voidclass Promise<T> {constructor(f: Executor<T>) { }then<U>(g: (result: T) => Promise<U>): Promise<U> {// }catch<U>(g: (error: unknown) => Promise<U>): Promise<U>{// }}let a = new Promise((resolve,reject)=>{resolve(123)})a.then((result)=>{// })
至此,Promise封装好了,内部具体实现,自行研究。
8.4 async和await
promise对于异步代码所做的抽象十分强大,JavaScript自身(当然包括Typescript)也有相应的句法:async和awai,使用这种句法,可以像同步操作那样处理异步操作
await可以视为.then在语言上的语法糖。使用await处理Promise对象时,要把相关的代码放在async块中。这种情况下不再使用.catch,而是把await放在常规的try/catch中。
// 老写法function getUser(){getUserId(18).then(user=>getLocation(user)).then(location=>console.log(location)).catch(error=>console.log(error)).finally(()=>console.log("done"))}
async function getUser() {try {let user = await getUserId(18)let location = await getLocation(user)console.log(location);} catch (error) {console.log("error");} finally{console.log("done");}}
async和await是JavaScript特性,这里就不深入探究了。
8.5 异步流
promise对象是便于建模,排列和编排未来的值,但是如果有多个值在未来的不同时刻产出。这种情况并不少见,比如从文件系统中读取文。
这样的操作有不同的建模方式,最为常见的是事件发射器(Nodejs EventEmitter)或响应式编程库(RxJS)。这两种方式之间的区别就像回调和promise对象一样:事件简单,轻量,而响应式编程库更强大,可以编排和排列事件流。
事件发射器
事件发射器提供的API用于在通道中发射事件,并监控通道中的事件:
interface Emitter{emit(channel:string,value:unknown):voidon(channel:string,f:(value:string)=>void):void}
发射器是JavaScript中一种常见的设计模式,使用DOM事件,或Nodejs EventEmitter模块中可能涉及。
type RedisClient = {on<E extends keyof Events>(event:E,f:(arg:Events[E])=>void):voidemit<E extends keyof Events>(event:E,arg:Events[E]):void}type RedisClient = {on(event:'ready',f:()=>void):voidon(event:'error',f:(e:Error)=>void):voidon(event:'reconnecting',f:(params:{attempt:number,delay:number})=>void):void}// 优化type Events = {ready:voiderror:Errorreconnecting:{attempt:number,delay:number}}type RedisClient = {on<E extends keyof Events>(event:E,f:(arg:Events[E])=>void):void}
把事件名称和参数提取到结构中,然后映射该结构,生成监听器和发射器,这是Typescript中常见的模式。
8.6 多线程类型安全
目前我们讨论的异步程序基本上运行在一个CPU线程中,不过,一些CPU密集型任务可能需要并行,把一项任务分到多个线程中。这么做可能是为了提升速度,可能是想让主线程空闲出来,继续相应后序操作。本节介绍编写安全的并行程序。涵盖浏览器和服务器。
8.6.1 在浏览器中:使用Web职程
浏览器大都支持web职程(worker)处理多线程。未免某些操作(例如CPU密集型任务)阻塞主线程,导致UI无响应,我们可以在JavaScript主线程中创建一些职称(严格受限的后后台代码,而Promise和setTimeout等异步PAI只是并发运行代码。职程在另一个cpu中运行代码)。web职程可以处理网络请求,文件系统写入等操作,不过有一些小限制。
Web职程是浏览器提供的API,设计人员对安全性提出了更高的要求,这里的“安全性”指的是,内存安全。如果多个线程读取同一块内存,很容易遇到各种并发问题。例如不确定性,死锁。
在tsconfig.json中加入“lib”:[“dom”,”es20120”,”webworker”]支持webworker
// 主线程代码let worker = new Worker("./workerScript.js")worker.postMessage("some data")// 接受其他线程worker.onmessage = e=>{console.log(e.data);}
// 并行线程,只能在浏览器环境才能用onmessage = e=>{console.log("child",e.data);// 发给主线程 postMessage("receiver"+e.data)}
这样传递消息很简单,但是没有类型,无法确保正确处理可能发送的所有消息类型。
类型安全的协议
我们知道如何在两个线程之间传递消息,那么,若想指明一个命令始终收到特定事件的响应怎么做呢?
我们可以选择在职程 中定义函数,把参数发给该函数,再把结果发送回来。
我们构建一个计算引擎,让他支持三种运算:求矩阵的行列式,计算两个矩阵的点积和求逆矩阵。
type Matrix = number[][]type MatrixProtocol = {determinant:{in:[Matrix],out:number},'dot-product':{in:[Matrix,Matrix]out:Matrix},invert:{in:[Matrix],out:Matrix}}
我们将在主线程中定义矩阵,运算则交给职程。我们对不安全的操作(职程发送和接受不带类型的消息)进行包装,把带类型的API开放给使用方。我们定义一个简单请求响应协议Protocol,列出职程可执行的操作,并为预期的输入和输出声明类型。
type Protocol = { // 1[command:string]:{in:unknown[],out:unknown}}function createProtocol<P extends Protocol>(script:string){ //2return <K extends keyof P>(command:K)=>(...args:P[K]['in'])=>{return new Promise<P[K]['out']>((resolve,reject)=>{let worker = new Worker(script)worker.onerror = rejectworker.onmessage = event => resolve(event.data.data)worker.postMessage({command,args})})}}
// let runWithMatrixProtocol = createProtocol<MatrixProtocol>('MatricWorkerScript.js')let parallelDeterminant = runWithMatrixProtocol('determinant')parallelDeterminant([[1,2],[3,4]]).then(determinant=>console.log(determinant))
type Matrix = number[][]type MatrixProtocol = {determinant:{in:[Matrix],out:number},'dot-product':{in:[Matrix,Matrix]out:Matrix},invert:{in:[Matrix],out:Matrix}}type Protocol = { // 1[command:string]:{in:unknown[],out:unknown}}function createProtocol<P extends Protocol>(script:string){ //2return <K extends keyof P>(command:K)=>{return (...args:P[K]['in'])=>{return new Promise<P[K]['out']>((resolve,reject)=>{let worker = new Worker(script)worker.onerror = rejectworker.onmessage = event => resolve(event.data.data)worker.postMessage({command,args})})}}}// let runWithMatrixProtocol = createProtocol<MatrixProtocol>('MatricWorkerScript.js')let parallelDeterminant = runWithMatrixProtocol('determinant')parallelDeterminant([[1,2],[3,4]]).then(determinant=>console.log(determinant))
8.6.2 在nodejs使用子进程
// mainimport {fork} from 'child_process'let child = fork("./child.js")child.on("message",data=>{})child.send({type:'syn',data:[3]})
相关文章:
Typescript 第八章 异步编程,并行和并发(JavaScript事件循环,异步流,多线程类型安全)
Typescript第八章 异步编程,并发和并行 异步API,比如说回调,promise和流。 JavaScript引擎在一个线路中多路复用任务,而其他任务则处于空闲状态。这种事件循环是JavaScript引擎的标准线程模型。 多路复用是指在一个线程中同时处…...
c++ 打印当前时间(精确到毫秒)
打印时间精确到毫秒好实现,但是那种对用户可读性不好,更适合开头记一次结尾记一次,打印中间减出来的程序运行时间。 但是因为一些情况,我开多线程开的不方便打印结束时间,同事跟我说那你把开始时间打印一下࿰…...

mapstruct 错误 java.lang.NoSuchMethodError: Ljava/lang/Double 错误
问题描述 在使用 mapstruct 的过程中遇到错误 java.lang.NoSuchMethodError: Ljava/lang/Double 错误 问题解决 maven clean, 然后 maven install Build -> Rebuild Project 执行 maven install 时, 如果报错 找不到 xxx 类, 但 ctrl鼠标左键 发现可以点进去这个类, 那…...

SpringBoot+AOP+Redission实战分布式锁
文章目录 前言一、Redission是什么?二、使用场景三、代码实战1.项目结构2.类图3.maven依赖4.yml5.config6.annotation7.aop8.model9.service 四、单元测试总结 前言 在集群环境下非单体应用存在的问题:JVM锁只能控制本地资源的访问,无法控制…...

Linux系统---进程概念
文章目录 冯诺依曼体系结构操作系统(OS)进程的理解 进程状态 进程优先级 环境变量 进程地址空间 Linux2.6内核进程调度队列 一、冯诺依曼体系结构 我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。 如图…...
ELK常用语法和线上问题排查
ELK常用语法及线上问题排查 速查问题 1.全文搜索 在搜索栏输入目标值,查询所有字段中包含该值的文档 案例:用户反馈了一串id或者其他同事给了一个jobid,还没有头绪怎么查的时候,直接全局搜索 jobid 2.字段 限定字段全文搜索…...
session-cookies 三个缓存 localStorage、sessionStorage、Cookies。
session-cookies session-cookies This plugin is used to summarize the browser’s three caches localStorage, sessionStorage, Cookies.The plugin is designed to be quick and easy to use. Below is a summary of some apis. session-cookies 这个插件是用来汇总浏览…...
LA@行列式性质
文章目录 行列式性质🎈转置不变性质交换性质多重交换移动(抽出插入)👺 因子提取性质拆和性质倍加性质 手算行列式的主要方法原理:任何行列式都可以化为三角行列式 行列式性质🎈 设行列式 ∣ A ∣ d e t ( a i j ) |A|\mathrm{det}(a_{ij}) …...

【java安全】原生反序列化利用链JDK7u21
文章目录 【java安全】原生反序列化利用链JDK7u21前言原理equalsImpl()如何调用equalsImpl()?HashSet通过反序列化间接执行equals()方法如何使hash相等? 思路整理POCGadget为什么在HashSet#add()前要将HashMap的value设为其他值? 【java安全】…...
[SQL智慧航行者] - SQL刷题规划
前言: 不知不觉, 慢慢的把一些基础知识整理到了现在. SQL挖掘机系列可能就要到此结束了. 目前是规划把这部分SQL练习部分放到SQL刷题的专栏, 就不放到SQL专栏了. 因为后续如果接触更多的知识点和题目也好分类. 请大家如果关注的话, 想看SQL知识点 --> SQL专栏; 想看SQL刷…...

概率论与数理统计:第一章:随机事件及其概率
文章目录 概率论Ch1. 随机事件及其概率1.基本概念(1)随机试验、随机事件、样本空间(2)事件的关系和运算①定义:互斥(互不相容)、对立②运算法则:德摩根率 (3)概率的定义(4)概率的性质(5)概率计算排列组合 2.等可能概型1.古典概型 (离散)2.几何概型 (连续…...
elementUi重置Select选择器样式、option、deep、vue3、plus
文章目录 前言select选框样式(element-plus)option选项面板样式(element-plus) 前言 样式标签属性为<style scoped lang"scss"></style> select选框样式(element-plus) // 设置选择框的背景样式 ::v-deep .el-input__wrapper {background: transparent;b…...

jar命令的安装与使用
场景: 项目中经常遇到使用WinR软件替换jar包中的文件,有时候存在WinRAR解压替换时提示没有权限,此时winRAR不能用还有有什么方法替换jar包中的文件。 方法: 使用jar命令进行修改替换 问题: 执行jar命令报错jar 不…...
windows系统多线程多进程下应用函数CloseHandle的一些细节
Windows CloseHandle()函数调用之后,对应的内核对象的句柄计数器减去1。如果此内核对象的句柄的计数器计数变为0,系统会回收此内核对象。所以,如果创建了内核对象(例如可调用了CreateThread创建一个线程内核对象。此对象创建成功后此线程内核…...

微服务——elasticsearch
初识ES——什么是elasticsearch elasticsearch的发展 初识ES——正向索引和倒排索引 初识ES——es与mysql的概念对比 类比到mysql中是表结构约束 概念对比 初始ES——安装es和kibana 1.部署单点es 1.1创建网络 要安装es容器和kibana容器并让他们之间相连,这里…...

paddlenlp:社交网络中多模态虚假媒体内容核查
初赛之环境配置篇 一、背景二、任务三、数据集1、初赛阶段2、评分标准 四、环境操作五、写在最后 一、背景 随着新媒体时代信息媒介的多元化发展,各种内容大量活跃在媒体内中,与此同时各类虚假信息也充斥着社交媒体,影响着公众的判断和决策。…...
Centos系统有哪些特点呢
CentOS 是一个基于 Red Hat Enterprise Linux (RHEL) 的免费的开源操作系统,由 CentOS 项目维护。CentOS 项目是一个社区驱动的项目,旨在为用户提供一个稳定的、可扩展的 Linux 发行版。 CentOS 系统具有以下特点: 稳定性:CentOS 系统非常稳定,即使是在高负载的环境下也是如此…...

一文学会git常用命令和使用指南
文章目录 0. 前言1.分支分类和管理1. 分支分类规范:2. 最佳实践3. 分支命名规范示例:4. 分支管理方法: 2. commit 注释规范1. 提交注释结构:2. 提交注释的准则: 3. git 常用命令1. git pull 核心用法2. git push 命令1…...
[PyTorch][chapter 46][LSTM -1]
前言: 长短期记忆网络(LSTM,Long Short-Term Memory)是一种时间循环神经网络,是为了解决一般的RNN(循环神经网络)存在的长期依赖问题而专门设计出来的。 目录: 背景简介 LSTM C…...

寄存器详解(二)
目录 内存中字的存储 示例: 数据段寄存器DS与[address] 字的传送 数据段简介 CPU提供的栈机制 栈段寄存器SS和栈顶指针寄存器SP PUSH AX指令的完整描述 示例图 POP AX指令的完整描述 示例图 栈顶超界问题 示例一: 示例二: 内存中字…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
【git】把本地更改提交远程新分支feature_g
创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...