第2章 线程管理
2.1 线程的基本操作
每个程序至少有一个执行main()函数的线程, 其他线程与主线程同时运行。 如main()函数执行完成退出一样, 线程执行完函数也会退出。 为线程创建std::thread() 对象后, 需要等待该线程结束。 那么, 先启动线程。
2.1.1 启动线程
一些情况下, 任务函数对象需要通过某种通讯机制进行参数的传递, 或者执行一系列独立操作, 通过通讯机制传递信号让线程信号停止。 先放下这些特殊情况不谈, 简单来说, 使用C++线程库启动线程, 就是构造std::thread对象:
void do_some_work();
std::thread my_thread(do_some_work);
这里需要包含头文件, std::thread可以通过有函数操作符类型的实例进行构造:
class background_task
{
public:void operator()() const{do_something();do_something_else();}
};background_task f;
std::thread my_thread(f);
代码中, 提供的函数对象会复制到新线程的存储空间中, 函数对象的执行和调用都在线程的内存空间中进行。
注意:当把函数对象传入线程构造函数中时,如果你传递了一个临时变量, 而不是一个命名的变量。 C++编译器会将其解析为函数声明, 而不是雷翔对象的定义。
std::thread my_thread(background_task());
这相当对声明了一个名为my_thread的函数, 这个函数带有一个函数(函数指针执行没有参数并返回background_task对象的函数), 返回一个std::thread对象的函数。
使用在前面命名函数对象的方式, 或使用多组括号(1), 或者使用统一的初始化语法(2), 都可以避免该问题。
std::thread my_thread((background_task())); //(1)
std::thread my_thread{background_task()}; //(2)
Lambda表达式也能避免这个问题。 Lambda表达式允许使用一个可以捕获局部变量的局部函数(可以避免传递参数)。 之前的例子可以改写为Lambda表达式的方式:
std::thread my_thread([]{do_something();do_something_else();});
线程启动后是要等待线程结束, 还是让其自动运行。 当std::thread()对象销毁之前还没有做出决定, 程序就会终止(std::thread的析构函数会调用std::terminate())。因此, 即使是有异常存在, 也需要确保线程能够正确汇入(joined)或分离(detached)。
如果不等待线程汇入, 就必须保证线程结束之前, 访问数据的有效性。这不是一个新问题–单线程代码中, 对象销毁之后再去访问, 会产生未定义行为–不过, 线程的生命周期增加了这个问题发生的几率。
这种情况很可能发生在线程还没借宿, 函数已经退出的时候, 这时线程函数还持有函数局部变量的指针或引用。
代码2.1 函数已经返回, 线程依旧访问局部变量
struct func
{int& i;func(int& i_) : i(i_) {}void operator() (){for (unsigned j=0 ; j<1000000 ; ++j){do_something(i); // (1) 潜在访问隐患:空引用(dangling reference)}}
};void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach(); // (2) 不等待线程结束
} // (3) 新线程可能还在运行
代码中, 已经决定不等待线程(使用了detach()(2)), 所以当oops()函数执行完成时(3), 线程中的函数可能还在运行。 如果线程还在运行, 就会去调用do_something(i)(1), 这时就会访问已经销毁的变量。 如同一个单线程程序–允许在函数完成后继续拥有局部变量的指针或引用。 当然, 这种情况发生时, 错误并不明显, 会使多线程更容易出错。 运行顺序参考下表2.1。
表2.1 分离( leaving it
to run)线程在局部变量销毁后, 仍对该变量进行访问。
主线程 | 新线程 |
---|---|
使用some_local_state构造my_func | |
开启新线程my_thread | |
启动 | |
调用func::operator() | |
将my_thread分离 | 执行func::operator();可能会在do_something中调用some_local_state的引用 |
销毁some_local_state | 持续运行 |
退出oops函数 | 持续执行func::operator();可能会在do_something中调用some_local_state的引用 --> 导致未定义行为 |
这种情况的常规处理方法:将数据复制到线程中。 如果使用一个可调用的对象作为线程函数, 这个对象就会复制到线程中, 而后原始对象会立即销毁。 如代码2.1所示, 但对于对象中包含的指针和引用还需谨慎。 使用访问局部变量的函数去创建线程是一个糟糕的注意(bad idea)。 | |
此外, 可以通过join()函数来确保线程在主函数完成前结束。 |
2.1.2 等待线程完成
如需等待线程, 需要使用join()。 将代码2.1的my_thread.detach()替换为my_thread.join(), 就可以确保局部变量在线程完成后才销毁。 因为主线程并没有做什么事, 使用独立的线程去执行函数变得意义不大。 但在实际中, 原始线程要么有自己的工作要做, 要么会启动多个子线程来做一些有用的工作, 并等待这些线程结束。
当你需要对等待中的线程有更灵活的控制时, 如: 看一下某个线程是否结束, 或者只等待一段时间(超过时间就判定为超时)。 想要做到这些, 需要使用其他机制来完成, 比如条件变量和future。 调用join(), 还可以清理了线程相关的内存, 这项std::thread对象将不再与已完成的线程有任何关联。 这意味着, 只能对一个线程使用一次join(), 一旦使用过join(), std::thread对象就不能再次汇入了。 当对使用joinable()时, 将返回flase。
2.1.3 特殊情况下的等待
如果想要分离线程, 可以在线程启动后, 直接使用detach()进行分离。 如果等待线程, 则需要细心挑选使用join()的位置。 当在线程运行后产生的一场, 会在join()调用之前抛出, 这样就会跳过join()。
避免应用被抛出的异常所终止。 通常, 在无异常的情况下使用join()时, 需要在异常处理过程中调用join(), 从而避免生命周期的问题。
代码2.2 等待线程完成
struct func; // 定义在代码2.1中
void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);try{do_something_in_current_thread();}catch(...){t.join(); // (1)throw;}t.join(); // (2)
}
代码2.2中使用了try/catch块确保线程退出后函数才结束。 当函数正常退出后, 会执行(2)处。 当执行过程中抛出异常, 程序会执行到(1)。 如果线程在函数之前结束–就要查看是否因为线程函数使用了局部变量的引用–而后确定一下程序可能会退出的途径。有一个叫RAII(Resource Acquisition Is Initialization)的机制可以解决这个问题,提供一个类, 在析构函数中使用join()。
代码2.3 使用RAII等待线程完成
class thread_guard
{std::thread& t;
public:explicit thread_guard(std::thread& t_):t(t_){}~thread_guard(){if(t.joinable()) // (1){t.join(); // (2)}}thread_guard(thread_guard const&)=delete; // (3)thread_guard& operator=(thread_guard const&)=delete;
};struct func; // 定义在代码2.1中void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);do_something_in_current_thread();
} // (4)
线程执行到(4)时,局部对象就要被逆序销毁了。 因此, thread_guard对象g是第一个被销毁的, 这时线程在析构函数中被加入(2)到原始线程中。 即使do_something_in_current_thread抛出一个异常, 这个销毁依然会发生。
在thread_guard析构函数的测试中, 首先判断线程是否可汇入(1)。如果可汇入, 会调用join()(2)进行汇入。拷贝构造函数和拷贝复制操作标记为=delete(3), 是为了不让编译器自动生成。 直接对对象进行拷贝或复制是很危险的, 因为这可能会弄对已汇入的线程。 通过删除生命, 任何尝试给thread_guard对象复制的操作都会引发一个编译错误。 参阅附录A的A.2节。
如果不想等待线程结束, 可以分离线程, 从而避免异常。 不过, 这就打破了线程与std::thread对象的联系, 即使线程仍然在后台运行着, 分离操作也能确保在std::thread对象销毁时不调用std::terminate()。
2.1.4 后台运行线程
使用detach()会让线程在后台运行, 这就意味着与主线程不能直接交互。 如果线程分离, 就不可能有std::thread对象使用它, 分离线程的确在后台运行, 所以分离的线程不能汇入。 不过C++运行库保证, 当线程退出时, 相关资源的能够正确回收。
分离线程通常为守护线程(daemon threads)。 UNIX中守护线程,是指没有任何显示的接口, 并在后台运行的线程, 这种线程的特点就是长时间运行。线程的生命周期可能会从应用的起始到结束, 可能会在后台监视文件系统, 还有可能对缓存进行清理, 或者对数据结构进行优化。另外, 分离线程只能确定线程什么时候结束, 发后即忘(fire and forget)的任务使用到就是分离线程。
如2.1.2节所示, 调用std::thread成员函数detach成员函数detach()来分离一个线程。 之后, 相应的std::thread对象与实际执行的线程无关, 并且这个线程也无法汇入:
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
为了从std::thread对象中分离线程, 不能对没有执行线程的std::thread对象使用detach(), 并且要使用同样的方式进行检查–当std::thread对象使用t.joinable()返回的是true, 就可以使用t.detach()。
试想如何能让一个文字处理应用同事编辑多个文档。 无论是用户界面, 还是在内部应用内部进行, 都有很多的解决方法。 虽然, 这些窗口看起来是完全独立的, 每个窗口都有自己独立的菜单选项, 但他们却运行在同一个应用实例中。 一种内部处理方式是, 让每个文档处理窗口拥有自己的线程。 每个线程运行同样的代码, 并个隔离不同窗口处理的数据。 如此这般, 打开一个文档就要启动一个新线程。 因为是对独立文档进行操作, 所以没有必要等待其他线程完成, 这里就可以让文档处理窗口运行在分离线程上。
代码2.4使用分离线程处理文档
void edit_document(std::string const& filename)
{open_document_and_display_gui(filename);while(!done_editing()){user_command cmd=get_user_input();if(cmd.type==open_new_document){std::string const new_name=get_filename_from_user();std::thread t(edit_document,new_name); // (1)t.detach(); // (2)}else{process_user_input(cmd);}}
}
如果用户选择打开一个新文档, 需要启动一个新线程去打开新文档(1), 并分离线程(2)。 与当前线程做出的操作一样, 新线程只不过是打开另一个文件而已。 所以, edit_document函数可以复用, 并通过传参的形式打开新的文件。
这个例子也展示了传参启动线程的方法: 不仅可以向std::thread构造函数(1)传递函数名, 还可以传递函数所需的参数(实参)。 当然, 也有其他方法可以完成这项功能, 比如: 使用带有数据的成员函数, 代替需要传参的普通函数。
2.2 传递参数
如代码2.4所示, 向可调用对象或函数传递参数很简单, 只需要将这些参数作为std::thread构造函数的附加函数即可。这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代码创建了一个调用(3, “hello”)的线程。 注意, 函数f需要一个std::string对象作为第二个参数, 但这里使用的是字符串的字面值, 也就是char const*类型, 线程的上下文完成字面值想std::string的转化。 需要特别注意, 指向动态变量的指针作为参数的情况, 代码如下:
void f(int i,std::string const& s);
void oops(int some_param)
{char buffer[1024]; //(1)sprintf(buffer, "%i",some_param);std::thread t(f,3,buffer); // (2)t.detach();
}
buffer(1)是一个指针变量, 指向局部变量, 然后此局部变量通过buffer传递到新线程中(2)。此时, 函数oops可能会在buffer转换成std::string之前结束, 从而导致未定义的行为。 因为, 无法保证隐式转换的操作和std::thread构造函数的拷贝操作的顺序, 有可能std::thread的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到std::thread构造函数之前, 就将字面值转化为std::string:
void f(int i,std::string const& s);
void not_oops(int some_param)
{char buffer[1024];sprintf(buffer,"%i",some_param);std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬空指针t.detach();
}
相反的情形(期望传递一个非常量引用, 但复制了整个对象)倒是不会出现, 因为会出现编译错误。 比如, 尝试使用线程更新引用传递的数据结构:
void update_data_for_widget(widget_id w, widget_data& data); // (1)
void oops_again(widget_id w)
{widget_data data;std::thread t(updata_data_for_widget, w, data); // (2)display_status();t.join();process_widget_data(data);
}
虽然updata_data_for_widget(1)的第二个参数期待传入一个引用, 但std::thread的构造函数(2)并不知晓, 构造函数无视函数参数类型, 盲目地拷贝已提供的变量。 不过, 内部代码会将拷贝的参数以右值的方式进行传递, 这是为了哪些支持移动的类型, 而后会尝试以右值为实参调用updata_data_for_widget。 但因为函数期望的是一个非常量引用作为参数(而非右值), 所以会在编译时出错。 对于熟悉std::bing的开发者来说, 问题的解决方法很简单:可以使用std::ref将参数转换成引用的形式。 因此可将线程的调用改为以下形式:
std::thread t(update_data_for_widget, w, std::ref(data));
这样update_data_for_widget就会收到data的引用, 而非data的拷贝副本, 这样代码就能顺利的通过编译了。
如果熟悉std::bind, 就应该不会对以上传参的语法感到陌生, 因为std::thread构造函数和std::bind的操作在标准库中以相同的机制进行定义。 比如, 你也可以传递一个成员函数指针作为线程函数, 并提供一个合适的对象指针作为第一个参数:
class X
{public:void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x); // 1
另一种有趣的情形是, 提供的参数仅支持移动(move),不能拷贝。 "移动"是指原始对象中的数据所有权转移给另一个对象, 从而这些数据就不再原始对象中保存(类似于剪切)。 std::unique_ptr就是这样一种类型, 这种类型为动态分配的对象提供内存自动管理机制(类似垃圾回收机制)。 同一时间内, 只允许一个std::unique_ptr实例指向一个对象, 并且当这个实例销毁时, 指向的对象也将被删除。 移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权后, 就会留下一个空指针。 使用移动操作可以将对象转换成函数可接受的实参类型, 或满足函数返回值类型要求。 当原对象是临时变量时, 则自动进行移动操作, 但当原对象是一个命名变量, 移动的时候就需啊哟使用std::move()进行显示移动。下面的代码展示了std::move的用法, 展示了std::move 是如何移动态对象的所有权到线程中去的:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(jprocess_big_object, std::move(p));
通过在std::thread构造函数中执行std::move§, big_object对象的所有权首先被转移到新创建线程的内部存储中, 之后再传递给process_big_object函数。
C++标准线程库中和std::unique_ptr在所属权上相似的类有好几种, std::thread为其中之一。 虽然, std::thread不像std::unique_ptr能占有动态对象的所有权, 但是它能占用其他资源: 每个实例都负责管理一个线程。 线程的所有权可以在多个std::thread实例中转移, 这依赖于std::thread实例的可移动且不可复制性。 不可复制性表示某一时间点, 一个std::thread实例只能关联一个执行线程。 可移动性是的开发者可以自己决定, 哪个实例拥有线程实际执行的所有权。
相关文章:
第2章 线程管理
2.1 线程的基本操作 每个程序至少有一个执行main()函数的线程, 其他线程与主线程同时运行。 如main()函数执行完成退出一样, 线程执行完函数也会退出。 为线程创建std::thread() 对象后, 需要等待该线程结束。 那么, 先启动线程。…...

机器学习第二十六周周报 ARIMA Clustering model
文章目录 week26 ARIMA & Clustering model摘要Abstract一、龙格库塔方法二、文献阅读1. 题目2. abstract3. 网络架构3.1 ARIMA模型3.2 K-means聚类3.3 评估标准 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程4.3.1 数据来源4.3.2 使用ARIMA模型预测4.3.3 K-means聚类…...
支持XP系统的最新firefox浏览器
都2024年了,还XP系统?事情是这样的,我要维护一下非常老的项目,系统部署在windows server 2003下面。升级系统是不太可能了。只能在老系统上维护,老的系统上自带的IE 6.0浏览器,在当前几乎是不可用状态&…...

Pytorch从零开始实战17
Pytorch从零开始实战——生成对抗网络入门 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——生成对抗网络入门环境准备模型定义开始训练总结 环境准备 本文基于Jupyter notebook,使用Python3.8,Pytorch1.8cpu…...
openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c
文章目录 openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c概述笔记END openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c 概述 DSA签名(摘要算法SHA256), DSA验签(摘要算法SHA256) 签名 : 用发送者的私钥进行签名. 验签 : 用发送者的公…...

vue2使用 element表格展开功能渲染子表格
默认样式 修改后 样式2 <el-table :data"needDataFollow" border style"width: 100%"><el-table-column align"center" label"序号" type"index" width"80" /><el-table-column align"cent…...

一个简单的ETCD GUI工具
使用ETCD没有好用的GUI工具,随手用c#写了一个, 做得好玩的一个ETCD GUI工具,后面加上CLI 工具,类似于 redis Cli工具一样,简化在 Linux下面的操作,不知道有没有必要, git 地址如下,…...

vue2 使用pdf.js 实现pdf预览,并可复制文本
需求:pdf预览,并且可以选中pdf的内容进行复制。 在ruoyi的vue前端项目中用到,参考了网上不少文章,因为大部分没给具体的pdf.js版本,导致运行过程中报各种api 错误,经过尝试以下版本可用,…...
REPLACE INTO
简介 在数据库中,REPLACE INTO 是一种用于插入或更新数据的(DML) SQL 语句。它与 INSERT INTO 语句类似,但具有一些特殊的行为。 语法 REPLACE INTO table_name (column1, column2, ...) VALUES (value1, value2, ...); repla…...

idea 安装免费Ai工具 codeium
目录 概述 ide安装 使用 chat问答 自动写代码 除此外小功能 概述 这已经是我目前用的最好免费的Ai工具了,当然你要是有钱最好还是用点花钱的,比如copilot,他可以在idea全家桶包括vs,还有c/c的vs上运行,还贼强&am…...
关于C#中的Select与SelectMany方法
Select 将序列中的每个元素投影到新表单。 实例1 IEnumerable<int> squares Enumerable.Range(1, 10).Select(x > x * x);foreach (int num in squares) {Console.WriteLine(num); } /*This code produces the following output:149162536496481100 */ 实例2 str…...

CentOS上安装Mellanox OFED
打开Mellanox官网下载驱动 Linux InfiniBand Drivers 点击下载链接跳转至 Tgz解压缩执行 ./mlnxofedinstall发现缺少模块 # ./mlnxofedinstall Logs dir: /tmp/MLNX_OFED_LINUX.11337.logs General log file: /tmp/MLNX_OFED_LINUX.11337.logs/general.log Verifying KMP rpm…...

无/自监督去噪(1)——一个变迁:N2N→N2V→HQ-SSL
目录 1. 前沿2. N2N3. N2V——盲点网络(BSNs,Blind Spot Networks)开创者3.1. N2V实际是如何训练的? 4. HQ-SSL——认为N2V效率不够高4.1. HQ-SSL的理论架构4.1.1. 对卷积的改进4.1.2. 对下采样的改进4.1.3. 比N2V好在哪ÿ…...
【24.1.19】
24.1.19 本周工作内容下周工作计划 本周工作内容 本周的话主要的一个工作还是第三部分页面部分的完成工作,那就先来汇报一下第三部分的工作进度,第三部分的页面工作呢已经完成啦,就在刚刚提交啦全部的代码,那么这一部分的工作呢也…...

使用mamba替换conda和anaconda配置环境安装软件
使用mamba替换miniconda和anaconda,原因是速度更快,无论是创建新环境还是激活环境 conda、mamba、anaconda都是蟒蛇的意思… 下载mambaforge wget https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh ba…...

鸿蒙开发系列教程(四)--ArkTS语言:基础知识
1、ArkTS语言介绍 ArkTS是HarmonyOS应用开发语言。它在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开…...
Pix2Pix理论与实战
本文为🔗365天深度学习训练营 中的学习记录博客 原作者:K同学啊|接辅导、项目定制 我的环境: 1.语言:python3.7 2.编译器:pycharm 3.深度学习框架Pytorch 1.8.0cu111 一、引入 在之前的学习中,我们知道…...
[GN] 后端接口已经写好 初次布局前端需要的操作(例)
提示:前端项目一定要先引入组件 配置。再编码!!!! 文章目录 使用 vue-cli 脚手架初始化前端工程化配置引入Vue前端组件库 -- arco前后端联调引入Md 编辑器组件 使用 vue-cli 脚手架初始化 使用安装脚手架工具…...

AIGC:人工智能驱动的数据分析新时代
AIGC:人工智能驱动的数据分析新时代 随着人工智能技术的迅猛发展,我们正迎来数据分析的新时代,其中AIGC(Artificial Intelligence with Generative Capabilities)的应用成为引领潮流的重要方向。本文将深入探讨几个关…...
Windows Qt C++ VTK 借助msys环境搭建
本示例仅仅是搭建环境,后续使用还得大佬指导。 Qt 6.6.0 MinGW 64bit 借助msys2 来安装VTK 包,把*.dll 链接进来,就可以用了。 先安装VTK 包。 Package: mingw-w64-x86_64-vtk - MSYS2 Packages 执行 pacman 命令:pacman -…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门  {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...