当前位置: 首页 > news >正文

C++并发编程(一):线程基础

简介

本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。

教程视频链接如下:线程基础:视频教程

文档链接如下:线程基础:笔记文档

理论上直接看 up 提供的笔记文档即可,我这里主要是记录一些我自己觉得可能需要补充的点。

线程发起

void thread_work1(std::string str) {std::cout << "str is " << str << std::endl;
}
//1 通过()初始化并启动一个线程
std::thread t1(thead_work1, hellostr);

解释一下这行代码:

std::thread t1(thread_work1, hellostr);

1、创建 std::thread 对象:t1 是一个 std::thread 类型的对象,它代表了一个即将或正在执行的线程。

2、指定线程要执行的任务:通过构造函数,我们告诉 t1 线程应该执行哪个函数。在这个例子中,任务是调用 thread_work1 函数。

3、传递参数给任务:thread_work1 函数需要一个 std::string 类型的参数。在构造 t1 时,我们通过传递 hellostr 变量来提供这个参数。hellostr 是一个已经定义并初始化的 std::string 对象,其值将被传递给 thread_work1 函数。

4、启动线程:当 std::thread 对象被构造时,它会自动启动一个新线程来执行指定的任务(即调用 thread_work1 函数,并将 hellostr 作为参数传递)。这个过程是异步的,意味着主线程(即创建 t1 的线程)将继续执行其后续指令,而不会等待新线程完成。

5、线程管理:一旦线程被启动,它就在自己的执行上下文中独立运行。但是,std::thread 对象 t1 提供了管理这个线程的手段,比如通过调用 t1.join() 来等待线程完成,或者通过调用 t1.detach() 来分离线程(让它在后台运行,而 t1 对象不再管理它)。

注意:普通函数的函数名就是这个函数实际的地址。

我们可以来试验一下这个事情:

#include <iostream> 
#include <string>
#include <thread> # 注意使用多线程时要包含这个头文件using namespace std;//线程函数
void thread_work1(std::string str) {std::cout << "str is " << str << std::endl;
}int main() {string hellostr = "hello world";//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);return 0;
}

在这里插入图片描述

可以看见运行结果是程序崩溃了,为什么呢?

当我们定义完线程 t1 之后它就会自动初始化并开始运行,那么在后台就会开始执行 thread_work1 这个线程函数然后输出 hello world。但是程序此时继续往下走时我们并没有将这个线程挂起或者停靠结果主线程就结束了,主线程结束就必须要回收 hellostr 这个字符串资源,那么就可能会存在这个资源已经被释放了,那么在 thread_work1 里虽然依然能够调用资源但是有可能会出问题(只是当前例子没出)。

因为在代码中我们可以看到传参是通过值传递,也就是把外部的局部变量作为一个拷贝拷贝给线程函数 thread_work1 ,所以在这个例子中字符串依然可以正常输出,那为什么会崩溃?就是因为主线程退出了,而子线程则有可能是还在运行的,那么就会崩溃。

但要注意,即使我们能够人为的保证让主线程在子线程执行完之后再结束主线程(比如让主线程睡上几秒)上述的崩溃问题也依然存在。

这是因为11新标准的这套线程 API 做了优化,当编译器发现我们启动了一个线程但却没有把这个线程做善后的工作(比如join或者detach掉),那么就会出现主线程在回收资源的时候就会调用这个线程的析构函数,析构函数内部就会执行一个很生硬的 terminate 函数,这个函数就会强制终止,这个函数的强制终止是会调用 assert 断言导致崩溃的。

线程等待

因此为了防止这样的崩溃问题,我们可以加入一个 join 函数:

int main() {string hellostr = "hello world";//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);//主线程等待子线程退出t1.join();return 0;
}

这个 t1.join 会让主线程去等待子线程 t1 执行完了主线程再继续往下执行。

此时再运行将不会发生问题。

仿函数(函数对象)作为参数

当我们用仿函数作为参数传递给线程时,也可以达到启动线程执行某种操作的含义。

但是要注意一些问题,我们可以来看个例子:

class background_task {
public:// 实现了括号运算符的类就称可以创建函数对象void operator()() {cout << "background_task called" << endl;}
};int main() {//t2 被当作函数对象的定义,其类型为返回 thread,参数为 background_taskthread t2(background_task());//t2.join(); 编译错误return 0;
}

线程对象 t2 会去执行 background_task 这个类的函数对象,当我们使用 background_task() 的时候会调用这个类的构造函数,结果是会生成一个对象,这个对象传递给了这个线程 t2 作为参数,同时因为我们重载了括号运算符,所以这个对象可以直接执行,也就是可以把对象当成函数来使用,这也是函数对象的意义。

但是可以看到在调用 join 函数时,出现编译错误:

在这里插入图片描述

这是因为编译器会将 thread t2(background_task()); 这行代码解释成了一个函数指针, 返回一个 std::thread 类型的值, 这个函数指针的参数也为一个函数指针, 这个函数指针返回值则为background_task, 参数为void。可以理解为如下:

"std::thread (*)(background_task (*)())"

这样看有点不太好懂,因为我们说编译器会将 t2 当成一个函数指针,不妨拆解一下上面这行代码:

std::thread 是返回值,(*) 是一个函数指针,()是参数列表,最后 background_task (*)() 是该函数指针的参数,也是一个函数指针。

再对比一下函数指针的声明形式:

返回类型 (*指针变量名)(参数类型列表);

这样应该就好明白多了。

我们明明是想定义一个线程对象 t2,却被编译器解释成了一个函数指针,这肯定是不行的。

解决方案如下:

//可多加一层()
//此时编译器就会认为其是一个对象,进行正常的线程初始化并启动了
std::thread t2((background_task()));
t2.join();//可使用{}方式进行变量初始化
std::thread t3{ background_task() };
t3.join();

此时编译就正常了。

lambda表达式

lambda 表达式也可以作为线程的参数传递给thread:

std::thread t4([](std::string  str) { std::cout << "str is " << str << std::endl;},  hellostr);t4.join();

线程detach

线程允许采用分离的方式在后台独自运行,C++ concurrent programing书中称其为守护线程。

方式是使用 detach 函数,该函数会让线程在后台运行,它不会受主线程的影响,主线程可以直接执行自己的任务,子线程就和主线程分离了,它们会各自使用自己的资源。

但是这里要注意:分离的时候,一旦子线程需要用到主线程的资源时,由于主线程运行结束,资源释放了,那么子线程在获取主线程资源时就容易产生问题,来看下面的例子。

struct func {//对于类和结构体的成员属性是引用的话,那么可以在构造函数初始化列表中初始化int& _i;func(int& i) : _i(i) {}void operator()() {for (int i = 0; i < 3; i++) {_i = i;std::cout << "_i is " << _i << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}}
};void oops() {int some_local_state = 0;//使用函数对象创建 func 类的对象并调用函数func myfunc(some_local_state);//创建并启动线程std::thread functhread(myfunc);//演示隐患,子线程还在访问局部变量 some_local_state,但局部变量可能会随着 } 结束而回收或随着主线程退出而回收functhread.detach();
}int main() {// detach 注意事项oops();//防止主线程退出过快,需要停顿一下,让子线程跑起来detachstd::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}

主线程在执行完 oops() 后又睡了一秒然后就退出了。

虽然主线程退出了,但是子线程还在执行。不过这里要注意,因为主线程就是进程存在的主要形式,进程包括主线程以及其衍生的一众子线程,因此主线程如果结束了那么整个进程也就结束了,那自然而然所有的子线程即使是在 detach 的状态也会被操作系统给全部回收掉。

上面的例子存在隐患,因为some_local_state是局部变量, 当oops调用结束后局部变量some_local_state就可能被释放了,而线程还在detach后台运行,容易出现崩溃。

所以当我们在线程中使用局部变量的时候可以采取几个措施解决局部变量的问题

1、通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。

2、将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。

3、将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。

比如下面的修改:

void use_join() {int some_local_state = 0;func myfunc(some_local_state);std::thread functhread(myfunc);functhread.join();
}// join 用法
use_join();

异常处理

当我们启动一个线程后,如果主线程产生崩溃,会导致子线程也会异常退出(因为主进程就是依赖于主线程存在的),也就是之前说的调用terminate。如果子线程在进行一些重要的操作比如将充值信息入库等,那么丢失这些信息是很危险的。所以常用的做法是捕获异常,并且在异常情况下保证子线程稳定运行结束后,主线程再抛出异常结束运行。如下面的逻辑:

void catch_exception() {int some_local_state = 0;func myfunc(some_local_state);std::thread  functhread{ myfunc };try {//本线程做一些事情,假设可能引发崩溃std::this_thread::sleep_for(std::chrono::seconds(1));}catch (std::exception& e) {//一旦引发崩溃,那么就会在这里被捕获住//捕获到异常后主线程不能马上崩溃,要先等子线程运行结束functhread.join();//子线程运行结束后,此时主线程再来处理该异常throw;}//如果没有异常那就正常继续下去即可functhread.join();
}

但是用这种方式编码,会显得臃肿,可以采用 RAII 技术,保证线程对象析构的时候等待线程运行结束,回收资源。

介绍一下 RAII 技术:

在这里插入图片描述

那么我们就来写一个简单的线程守卫:

class thread_guard {
private:std::thread& _t;
public:explicit thread_guard(std::thread& t):_t(t){}~thread_guard() {//join只能调用一次if (_t.joinable()) {_t.join();}}thread_guard(thread_guard const&) = delete;thread_guard& operator=(thread_guard const&) = delete;
};

可以这么使用:

void auto_guard() {int some_local_state = 0;func my_func(some_local_state);std::thread  t(my_func);thread_guard g(t);//本线程做一些事情std::cout << "auto guard finished " << std::endl;
}
auto_guard();

慎用隐式转换

C++中会有一些隐式转换,比如 char* 转换为 string 等。这些隐式转换在线程的调用上可能会造成崩溃问题:

void danger_oops(int som_param) {char buffer[1024];sprintf(buffer, "%i", som_param);//在线程内部将char const* 转化为std::stringstd::thread t(print_str, 3, buffer);t.detach();std::cout << "danger oops finished " << std::endl;
}

当我们定义一个线程变量thread t时,传递给这个线程的参数buffer会被保存到thread的成员变量中。

而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str。

而此时 buffer 可能随着 } 运行结束而释放了。

改进的方式很简单,我们将参数传递给thread时显示转换为string就可以了,这样thread内部保存的是string类型。

void safe_oops(int some_param) {char buffer[1024];sprintf(buffer, "%i", some_param);std::thread t(print_str, 3, std::string(buffer));t.detach();
}

关于为什么参数会像我说的这样保存和调用,我在之后会按照源码给大家讲一讲。

引用参数

当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,如果采用如下调用会编译失败:

void change_param(int& param) {param++;
}
void ref_oops(int some_param) {std::cout << "before change , param is " << some_param << std::endl;//需使用引用显示转换std::thread  t2(change_param, some_param);t2.join();std::cout << "after change , param is " << some_param << std::endl;
}

即使函数 change_param 的参数为 int& 类型,我们传递给 t2 的构造函数为 some_param, 也不会达到在change_param 函数内部修改关联到外部some_param的效果。因为 some_param 在传递给 thread 的构造函数后会转变为右值保存,右值传递给一个左值引用会出问题,所以编译出了问题。

改为如下调用就可以了:

void ref_oops(int some_param) {std::cout << "before change , param is " << some_param << std::endl;//需使用引用显示转换std::thread  t2(change_param, std::ref(some_param));t2.join();std::cout << "after change , param is " << some_param << std::endl;
}

绑定类成员函数

有时候我们需要绑定一个类的成员函数:

class X
{
public:void do_lengthy_work() {std::cout << "do_lengthy_work " << std::endl;}
};
void bind_class_oops() {X my_x;std::thread t(&X::do_lengthy_work, &my_x);t.join();
}

这里大家注意一下,如果thread绑定的回调函数是普通函数,可以在函数前加 & 或者不加 & ,因为编译器默认将普通函数名作为函数地址,如下两种写法都正确:

void thead_work1(std::string str) {std::cout << "str is " << str << std::endl;
}
std::string hellostr = "hello world!";
//两种方式都正确
std::thread t1(thead_work1, hellostr);
std::thread t2(&thead_work1, hellostr);

但是如果是绑定类的成员函数,必须添加 & 。

使用move操作

有时候传递给线程的参数是独占的,所谓独占就是不支持拷贝赋值和构造,但是我们可以通过std::move的方式将参数的所有权转移给线程,如下:

void deal_unique(std::unique_ptr<int> p) {std::cout << "unique ptr data is " << *p << std::endl;(*p)++;std::cout << "after unique ptr data is " << *p << std::endl;
}
void move_oops() {auto p = std::make_unique<int>(100);std::thread  t(deal_unique, std::move(p));t.join();//不能再使用p了,p已经被move废弃// std::cout << "after unique ptr data is " << *p << std::endl;
}

相关文章:

C++并发编程(一):线程基础

简介 本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。 教程视频链接如下&#xff1a;线程基础&#xff1a;视频教程 文档链接如下&#xff1a;线程基础&#xff1a;笔记文档 理论上直接看 up 提供的笔记文档即可&#xff0c;我这里主要是记录一些我自己…...

enq: HW - contention事件来啦

业务系统反应数据库慢&#xff0c;根据时间查看awr报告。 先看一眼事件名称 HW enqueue 用于序列化超出段高水位线的空间分配。如果同时向对象添加大量数据&#xff0c;则多个进程可能同时尝试在高水位线上方分配空间&#xff0c;从而导致争用。 既然是控制资源并发的enq&…...

MyBatis补充

控制类和dao层接口以及mapper中的xml是怎样的关联的&#xff1f; 在Mybatis中&#xff0c;控制类和dao层接口是通过mapper的xml文件进行连接的。 控制类调用dao层接口中的方法&#xff0c;通过接口实现进行访问数据库操作。dao层接口定义数据库操作的方法&#xff0c;提供给控制…...

系统架构师(每日一练16)

每日一练 答案与解析 1.软件测试一般分为两个大类:动态测试和静态测试。前者通过运行程序发现错误&#xff0c;包括()等方法;后者采用人工和计算机辅助静态分析的手段对程序进行检测&#xff0c;包括()等方法。答案与解析 问题1 A.边界值分析、逻辑覆盖、基本路径 B.桌面检查、…...

实践致知第17享:电脑忽然黑屏的常见原因及处理方法

一、背景需求 小姑电话说&#xff1a;最近&#xff0c;电脑忽然就黑屏了&#xff08;如下图所示&#xff09;&#xff0c;但是等待几十秒甚至一分钟&#xff0c;电脑就能自然恢复了&#xff0c;这种状况一天能出现三四次&#xff0c;怎么办&#xff1f; 二、分析诊断 电脑黑屏…...

微信小程序--实现地图定位---获取经纬度

(1) (2) (3) html: <view class"titleTwo" style"border: none;"><view class"fontSize30 invoiceTile">企业地址</view><view class"invoiceRight" bind:tap"tapChooseAddress" data-maptype"…...

【Python系列】使用 `isinstance()` 替代 `type()` 函数

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

【多模态大模型】 BLIP-2 in ICML 2023

一、引言 论文&#xff1a; BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models 作者&#xff1a; Salesforce Research 代码&#xff1a; BLIP-2 特点&#xff1a; 该方法分别使用冻结的图像编码器&#xff08;ViT-L/…...

HPC高性能计算平台

随着技术的发展和数据量的爆炸性增长&#xff0c;企业面临的挑战日益复杂&#xff0c;对计算能力的需求也在不断增加。这些问题的解决超出了传统计算方法的能力范围&#xff0c;高性能计算&#xff08;HPC&#xff09;正是为解决这类问题而生。 高性能计算&#xff08;HPC&…...

前端常用的几个工具网站

觉得不错的前端工具类网站 1、Grid布局生成 https://cssgrid-generator.netlify.app 2、拟物按钮样式生成 https://neumorphism.io 3、玻璃形态效果 在线制作CSS玻璃形态 4、一些Button、checkBox、switch、card的css样式 零代码 - 精美CSS样式库 5、CSS阴影生成 在线创建…...

支付功能之代收代付

有很多老板问小编&#xff1a;“这个分账功能好是好&#xff0c;也能搞定项目中的二清问题和税务纠纷&#xff0c;但还是太复杂了&#xff0c;每次要添加被分账对象都需要提交材料进行审核&#xff0c;太繁琐了&#xff0c;有没有更方便快捷的支付产品来解决资金问题&#xff1…...

QPixmap

pixel[ˈpɪksl]像素 QPixmap 是 Qt 框架中用于处理图像的一个类。它主要用于在屏幕上显示和处理图像&#xff0c;提供了许多实用的功能&#xff0c;如加载、保存、缩放、旋转、合并等。 绘制 从文件加载&#xff1a;从指定文件加载图像。 QPixmap pixmap(":/images/exam…...

Laravel门面之下:构建自定义门面应用的艺术

Laravel门面之下&#xff1a;构建自定义门面应用的艺术 在Laravel框架中&#xff0c;门面&#xff08;Facade&#xff09;提供了一种将类静态调用与面向对象代码解耦的优雅方式。门面是一个全局可访问的类&#xff0c;它为底层复杂的服务提供了一个简单的接口。然而&#xff0…...

智启万象 | 2024 Google 开发者大会直播攻略

8 月 7 日上午 9:30 2024 Google 开发者大会 主旨演讲直播将准时开启 想要在线上探索大会精彩内容&#xff1f; 快查收这份观看指南&#xff01; 8 月 7 日上午 9:30 2024 Google 开发者大会开幕 锁定大会官网观看主旨演讲现场直播&#xff01; 本次大会内容将同步于多个…...

技巧:print打印内容到控制台时信息显示不全

# 请求一个接口&#xff0c;res是响应内容&#xff0c;使用res.text打印的信息不全 #使用流式处理响应 #如果你需要流式处理大的响应&#xff0c;确保你在处理响应内容的同时不会提前结束流。resself.request_base(select_api,change_datachange_data)print("")# pri…...

3.表的操作

目录 创建表 创建表案例&#xff1a; 查看表结构 修改表 1.增加新列 2.修改列的属性 3.删除列 4.修改表名 5.修改列 删除表 创建表 语法&#xff1a; CREATE TABLE [IF NOT EXISTS] table_name(field1 datatype1 [COMMENT 注释信息],field2 datatype2 [COMMENT 注释…...

AI回答:C#项目编译后生成部分文件的主要职责

【引入】以ConsoleApp1为例&#xff0c;请问C#编译之后以下文件有啥用 1.bin\runtimes 文件夹存放什么&#xff0c;有什么用&#xff1f; bin\runtimes 文件夹存放了项目的运行时相关文件&#xff0c;这些文件包括了各种目标平台的运行时库。 2.bin\生成的exe文件可以在别的电脑…...

RPC通信的简单流程

远程调用者假设需要调用Login方法&#xff0c;将调用的信息通过muduo库&#xff0c;同时进行了序列化和反序列化&#xff0c;发送到Rpcprovider上&#xff0c;RpcProvider通过对象和方法表来确定需要调用哪个服务对象的哪个方法。 UserRpcServiceRpc和UseRpcServiceRpcStub是继…...

前端发版(发包)缓存,需要强制刷新问题处理

问题原因&#xff1a; 浏览器问题 一、创建初始版本文件(public/version.json) { "version": "1722240835844" }二、设置版本判断&#xff08;version.js&#xff09; import axios from "axios";const isNewVersion () > {let baseUrl …...

洛谷练习(8.4/8.5)

题目 P2036 PERKET题目描述思路代码 P3799 小 Y 拼木棒题目描述思路代码 P1010 幂次方题目描述思路代码 P1498 南蛮图腾题目描述思路代码 P1928 外星密码题目描述思路代码 P2036 PERKET 题目描述 比较苦度和酸度的最小差值 思路 搜索最小差值 代码 void dfs(int sd,int k…...

DLMS/COSEM中的信息安全:加密算法(下)1

4.公钥算法 4.1概述 一般来说,公钥密码系统使用难以解决的问题作为算法的基础。RSA算法基于非常大的整数的素因子分解。椭圆曲线密码学(ECC)是基于求解椭圆曲线离散对数问题(ECDLP)的难度。与RSA相比,ECC提供了相似的安全级别,但密钥大小明显减少。ECC特别适用于嵌入式…...

ES6中的Promise、async、await,超详细讲解!

Promise是es6引入的异步编程新解决方案&#xff0c;Promise实例和原型上有reject、resolve、all、then、catch、finally等多个方法&#xff0c;语法上promise就是一个构造函数&#xff0c;用来封装异步操作并可以获取其成功或失败的结果&#xff0c;本篇文章主要介绍了ES6中的P…...

Modbus poll和Modbus Mbslave的使用

读取Modbus Mbslave中的数据 首先创建COM1和COM2端口 然后 using System.IO.Ports; ​ namespace 通信 {internal class Program{static void Main(string[] args){Console.WriteLine("Hello, World!");SerialPort serialPort new SerialPort("COM1",960…...

树莓集团的全球化征程:数字媒体产业的本土与国际布局

在全球数字化转型的浪潮中&#xff0c;树莓集团正稳步推进数字媒体产业从本土到国际的全球化布局。在数字媒体产业这一新兴且充满活力的领域中&#xff0c;树莓集团不仅在国内市场树立了标杆&#xff0c;更以其独特的全球化战略布局&#xff0c;引领着行业的未来趋势。 本土深耕…...

LeetCode面试150——274H指数

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及代码实现 3.1 排序 3.2 排序时间优化(计数排序) 3.3 二分查找 参考文献 1 题目描述 给你一个整数数组 citations &#xf…...

【Linux】Linux重定向指南:探索输出重定向与追加重定向的奥秘!

欢迎来到 CILMY23 的博客 &#x1f3c6;本篇主题为&#xff1a;Linux重定向指南&#xff1a;探索输出重定向与追加重定向的奥秘&#xff01; &#x1f3c6;个人主页&#xff1a;CILMY23-CSDN博客 &#x1f3c6;系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 | 贪…...

Spring AI -快速开发ChatGPT应用

Spring AI介绍 Spring AI是AI工程师的一个应用框架&#xff0c;它提供了一个友好的API和开发AI应用的抽象&#xff0c;旨在简化AI应用的开发工序&#xff0c;例如开发一款基于ChatGPT的对话、图片、音频等应用程序。 Spring AI已经集成了OpenAI的API&#xff0c;因此我们不需…...

Modern C++ 智能指针

Why&#xff1f; 原始指针存在缺陷&#xff0c;不符合现代编程语言的需要。 原始指针的缺陷&#xff1a; 指针指向一片内存&#xff0c;使用者无法得知到底是指向了什么&#xff0c;是数组还是对象&#xff1f;使用完指针是否需要销毁&#xff1f;什么时候销毁&#xff1f;如…...

Python的100道经典练习题,每日一练,必成大神!!!

Python的100道经典练习题是一个广泛而深入的学习资源&#xff0c;可以帮助Python初学者和进阶者巩固和提升编程技能 完整的100多道练习题可在下面图片免沸获取哦~ 整理了100道Python的题目&#xff0c;如果你是一位初学者&#xff0c;这一百多道题可以 帮助你轻松的使用Python…...

代码回滚命令

定位到当前分支 git branch回滚到指定的commit git reset --hard 85da0cb8322accad143cpush到远程分支 git push --force...