Git使用教程
Git是一个强大的版本控制系统,它用于跟踪文件的变化并协作开发代码。
创建新的仓库:开始使用Git,需要在项目文件夹中初始化一个新的Git仓库。进入到项目文件夹,然后
1
git init
将文件添加到仓库:在初始化了仓库之后,可以将项目文件添加到Git仓库中以进行版本控制。
使用add将修改的文件添加到暂存区:
1
git add <file1> <file2> ...
若是修改的文件较多,可直接使用
1
git add .
提交更改:文件添加到暂存区后,就可以使用commit将它们提交到仓库中。
1
git commit -m "Your commit message"
可以在提交信息中清晰地描述你所做的更改。
查看状态:你可以使用以下命令来查看当前仓库的状态,包括已修改、已暂存和未跟踪的文件。
1
git status
查看提交历史:
1
git log
比较文件差异:
1
git diff <file>
显示特定文件在工作区和暂存区之间的差异。
1
git diff <commit id>
显示当前工作目录中的文件与指定提交之间的差异
版本回退
1
git checkout <commit id>
将工作区和暂存区恢复到指定提交的状态。
创建分支:分支允许你在不影响主线开发的情况下开发新功能或修复bug。
1
git branch <branch-name>
切换分支:你可以使用以下命令切换到其他分支。
1
git checkout <branch-name>
合并分支:当你完成了在某个分支上的工作,并且希望将其合并到主分支时,你可以使用以下命令。
1
2git checkout main
git merge <branch-name>远程仓库:Git还支持与远程仓库进行交互,例如GitHub、GitLab等。你可以使用以下命令将本地仓库与远程仓库关联。
1
git remote add origin <remote-repository-url>
推送到远程仓库:一旦你与远程仓库建立了关联,你可以将本地提交推送到远程仓库。
1
git push -u origin main
一旦你将本地的提交推送到远程仓库,你可能会希望在本地仓库中获取远程仓库的最新内容,以便与其他团队成员的更改保持同步。你可以使用
git pull
命令来拉取远程仓库的内容。以下是步骤:确保当前在正确的分支上:首先,确保你在想要拉取远程仓库内容的本地分支上。
1
git checkout main
这里的 “main” 可以根据你的实际情况而定,确保你在想要拉取更新的目标分支上。
执行 git pull:运行
git pull
命令来拉取远程仓库的最新内容并将其合并到你当前所在的分支。1
git pull
这将获取远程仓库的最新提交,并尝试将其合并到你当前所在的分支中。
如果你希望拉取特定远程仓库的特定分支的更新,你可以在
git pull
命令中提供远程仓库和分支的名称。例如,如果要从名为 “origin” 的远程仓库的 “main” 分支拉取更新,可以执行以下命令:1
git pull origin main
这将从远程仓库的 “main” 分支拉取更新,并将其合并到你当前所在的分支。
git checkout
git checkout
命令在 Git 中有多种用途,它可以用于切换分支、创建分支、恢复文件和检出提交等操作。以下是 git checkout
命令的几种常见用法:
切换分支:
1
git checkout <branch-name>
这会将当前工作目录切换到指定的分支。例如,要切换到名为
main
的分支,你可以运行:1
git checkout main
你也可以使用
git switch
命令来执行相同的操作。创建新分支并切换到该分支:
1
git checkout -b <new-branch-name>
这会创建一个新的分支,并将当前工作目录切换到该分支。例如,要创建一个名为
feature
的新分支并切换到该分支,你可以运行:1
git checkout -b feature
恢复文件到指定状态:
1
git checkout <file>
这会将指定文件恢复到最近一次提交时的状态。如果你在工作区中对文件做了修改,但想要撤销这些修改并恢复到最近一次提交时的状态,可以使用这个命令。例如:
1
git checkout index.html
这会将
index.html
文件恢复到最近一次提交时的状态。检出提交:
1
git checkout <commit>
这会将你的工作目录和暂存区都恢复到指定提交时的状态,并将 HEAD 指针移动到该提交。通常情况下,你不应该在已提交的代码上工作,因为这可能会导致丢失未提交的更改。然而,这个命令在需要检查历史提交的内容或创建分离头指针时很有用。如果你只是想查看历史提交的内容,建议使用
git show <commit>
命令,这样可以避免创建分离头指针。
工作区和暂存区
工作区(Working Directory):
工作区指的是你正在工作的项目目录,其中包含了你编辑、添加和删除文件的所有内容。换句话说,工作区是你电脑上能够看到的项目目录,其中包含了项目的所有文件和子文件夹。当你修改项目中的文件时,这些修改只存在于工作区中,还没有被 Git 跟踪。
暂存区(Staging Area):
暂存区是 Git 提供的一个中间区域,用于存储你想要提交的更改。当你对工作区中的文件做出修改并准备提交时,首先需要将这些修改添加到暂存区。在暂存区中,你可以检查你要提交的更改,并决定是否需要将它们提交到版本控制系统中。
一旦你将更改添加到暂存区,Git 就会记录这些更改的状态,包括哪些文件已经被修改、哪些文件是新添加的,以及哪些文件已经准备好被提交到仓库中。暂存区允许你将多个文件的更改分成一系列逻辑上相关的提交。
TCP三次握手和四次挥手
三次握手
当两台计算机之间建立TCP连接时,它们之间会执行一个称为“三次握手”的过程。这个过程允许双方在通信开始前进行一些必要的协商,确保双方都准备好发送和接收数据。下面是TCP三次握手的详细过程:
- 第一次握手(SYN):
- 客户端向服务器发送一个特殊的TCP数据包,称为SYN包(同步序列编号)。这个包包含了一个随机生成的序列号(client_isn),用于后续的数据传输。
- 客户端将SYN标志位设置为1,表明这是一个连接请求。
- 第二次握手(SYN + ACK):
- 服务器收到客户端发送的SYN包后,会回复一个SYN包和一个ACK包(确认),称为SYN-ACK包。
- 服务器在SYN-ACK包中将SYN标志位设置为1,表示它接受了客户端的连接请求,并且同时发送一个确认序号(ACK),确认客户端的序列号加1,以表明它准备好接收数据。
- 第三次握手(ACK):
- 客户端收到服务器发送的SYN-ACK包后,会向服务器发送一个确认ACK包。
- 这个ACK包不包含任何数据,只是用来确认服务器的SYN-ACK包已经收到了。
- 客户端将确认序号设置为服务器发送的序列号加1。
完成了这个三次握手过程后,TCP连接就建立起来了,双方就可以开始通过这个连接传输数据。此时,双方都知道对方已经准备好了,并且双方也都知道对方的序列号,从而可以保证数据的可靠传输。
需要注意的是,如果在这个过程中的任何一个阶段出现了问题,比如某个数据包丢失或者超时,TCP协议会尝试重新发送数据包或者触发超时重传机制,以确保连接的建立。
四次挥手
TCP连接的四次挥手是在通信结束时,双方关闭连接的过程。它相比于三次握手有更多的细节,因为在这个过程中,每一方都需要确保对方收到了关闭请求,并且双方都知道连接已经关闭。以下是四次挥手的详细过程:
- 第一次挥手(FIN):
- 客户端或服务器其中一方决定关闭连接,发送一个FIN包给对方。
- FIN包中的FIN标志位被置为1,表示发起方已经没有数据要发送了,但仍能接收数据。
- 第二次挥手(ACK):
- 接收到FIN包的一方(假设为服务器)确认收到了关闭请求,发送一个ACK包作为确认。
- 这个ACK包通常不会携带任何数据,只是简单地确认收到了FIN包。
- 第三次挥手(FIN):
- 确认收到关闭请求的一方(服务器)也决定关闭连接,向对方发送一个FIN包。
- 这个FIN包告诉对方它也没有数据要发送了,但仍能接收数据。
- 第四次挥手(ACK):
- 收到第三次挥手的一方(假设为客户端)发送一个ACK包作为确认。
- 这个ACK包通常不会携带任何数据,只是简单地确认收到了第三次挥手的FIN包。
完成了这个四次挥手过程后,连接就完全关闭了。双方都知道对方已经关闭了连接,不会再发送数据。这样可以确保数据的可靠传输,并且释放了双方的资源,使其可以用于其他连接。
lambda表达式
C++11 引入了 lambda 表达式,它是一种用于创建匿名函数的语法。Lambda 表达式提供了一种更加简洁和灵活的方式来编写函数对象,尤其适用于需要传递函数作为参数的情况,比如 STL 算法、函数式编程、事件处理等。
Lambda 表达式的基本语法
Lambda 表达式的一般形式如下:
1 | [capture](parameters) -> return_type { body } |
capture
:捕获列表,用于捕获外部变量,可以是值捕获、引用捕获或混合捕获。parameters
:参数列表,与普通函数的参数列表类似,可以为空。return_type
:返回类型,可以省略,由编译器自动推导。body
:函数体,与普通函数体相似,可以包含一系列语句或表达式。
Lambda 表达式的用法示例
- Lambda 表达式作为函数对象:
1 | auto sum = [](int a, int b) { return a + b; }; |
- Lambda 表达式作为 STL 算法的参数:
1 | std::vector<int> numbers = {1, 2, 3, 4, 5}; |
- Lambda 表达式与标准库函数配合使用:
1 | std::vector<int> numbers = {1, 2, 3, 4, 5}; |
Lambda 表达式捕获列表
Lambda 表达式的捕获列表控制了它可以访问的外部变量。捕获列表可以为空,也可以包含一个或多个变量。捕获列表支持值捕获、引用捕获和混合捕获。
- 值捕获: 捕获外部变量的值,在 lambda 表达式创建时复制该变量的值。
1 | int x = 10; |
- 引用捕获: 捕获外部变量的引用,可以修改外部变量的值。
1 | int y = 20; |
- 混合捕获: 混合使用值捕获和引用捕获。
1 | int x = 10, y = 20; |
Lambda 表达式的返回类型推导
在 C++14 中,Lambda 表达式的返回类型可以由编译器根据其返回语句的类型自动推导。
1 | auto add = [](int a, int b) { return a + b; }; |
std::function和std::bind
std::function
和 std::bind
是 C++11 标准库中引入的两个重要组件,用于实现函数对象的封装和绑定。它们为 C++ 中的函数式编程提供了更加灵活和方便的方式。可以用于实现回调函数的思想。
std::function
std::function
是一个模板类,用于封装任意可调用对象(函数指针、函数对象、成员函数指针、lambda 表达式等),并提供一种统一的接口来调用这些对象。它可以看作是一个类型安全的函数指针的容器。
特点:
std::function
是可调用对象的包装器,它可以存储、复制和调用任何可调用对象。std::function
的类型由其模板参数确定,因此它可以表示各种不同的函数类型。std::function
对象可以在运行时被赋予不同的可调用对象,并且可以被多次复制、传递和调用。
示例:
1 | #include <iostream> |
std::bind
std::bind
是一个函数模板,用于创建一个新的可调用对象。该对象会将参数绑定到函数或函数对象上。它允许延迟绑定参数,以后再调用时传递剩余的参数。
语法:
1 | #include <functional> |
function
:要绑定的函数或函数指针。arg1, arg2, ...
:要绑定到函数的参数。
特点:
std::bind
可以绑定函数的部分或全部参数,从而创建一个新的可调用对象。std::bind
返回的可调用对象可以被多次复制、传递和调用。std::bind
可以绑定参数的位置,也可以绑定参数的值,还可以使用占位符_1
、_2
等来表示未指定的参数。
示例:
1 | #include <iostream> |
在这个示例中,std::bind
函数用来创建一个新的可调用对象 greet_function
,它绑定了 greet
函数的第一个参数为 “Alice”,第二个参数由调用者传入。而另一个可调用对象 add_function
绑定了 std::plus<int>()
函数对象,表示对两个参数进行加法运算。
假设我们有一个事件循环,当某个定时器超时时,我们需要执行一个回调函数,并传递一些参数给这个回调函数:
1 | #include <iostream> |
function与bind的结合使用
1 | #include <iostream> |
占位符std::placeholders
用来表示未绑定的参数。这些占位符在 std::placeholders
命名空间中定义。常见的占位符有:
std::placeholders::_1
:表示调用时传递的第一个参数。std::placeholders::_2
:表示调用时传递的第二个参数。- 以此类推,可以使用
_3
,_4
….等表示更多的参数。
举个例子:
1 | #include <iostream> |
1 | std::function<void(int)> boundFunction = std::bind(exampleFunction, std::placeholders::_1, 100, std::placeholders::_2); |
这行代码创建了一个新的可调用对象 boundFunction
,它绑定了 exampleFunction
的第二个参数 b
为 100,而第一个参数 a
和第三个参数 c
使用占位符 _1
和 _2
,表示它们将在调用 boundFunction
时传递。
Reactor和Proactor
Reactor模式和Proactor模式是两种常见的事件处理模式,通常用于构建高性能的并发系统。它们都是在事件驱动的系统中使用的设计模式,但它们在处理事件时的方式有所不同。下面我将详细解释它们的工作原理和区别。
Reactor模式
概念:
在Reactor模式中,有一个事件循环(Event Loop),负责监听并分发事件。该循环通过轮询或者异步IO等机制监视多个输入源(如文件描述符、套接字等),当有事件发生时,调用相关的事件处理器来处理这些事件。在Reactor模式中,事件处理是同步的,即当一个事件发生时,Reactor会调用相应的事件处理器,并且一直等待处理器完成处理,然后再继续监听新的事件。
特点:
Reactor模式的主要特点包括:
- 单线程:通常Reactor模式在单线程中运行,事件循环会按顺序处理事件。
- 同步处理:事件处理是同步的,一个事件处理器处理完事件之后,才会继续处理下一个事件。
- 高效:由于采用了非阻塞IO和事件驱动的方式,Reactor模式在高并发场景下表现出色。
单、多Reactor模式:
在单Reactor模式中,只有一个事件循环负责监听和分发事件,并且事件处理是同步的,即事件处理器会在事件循环中同步执行。这种模式通常适用于轻量级的应用或者处理较少并发连接的情况。
与单Reactor模式不同,多Reactor模式通过将事件处理分布到多个事件循环中来提高系统的并发能力。每个事件循环负责监听和处理一部分事件,从而降低了单个事件循环的负载,提高了系统的并发处理能力。通常采用主从Reactor模式:
在主从Reactor模式中,通常有一个主Reactor负责监听连接请求,并且负责创建和分配子Reactor。当主Reactor接收到连接请求时,会将连接分配给某个子Reactor,然后由子Reactor负责处理该连接的事件。这样可以避免单个Reactor负载过重,并且能够充分利用多核处理器的性能。
回调函数
在Reactor模式中,回调函数被广泛应用于处理事件。回调函数是一种在某个事件发生时被调用的函数,用于处理特定类型的事件。在Reactor模式中,当事件发生时,事件循环会调用相应的回调函数来处理事件,例如读取数据、写入数据、关闭连接等。
具体来说,在多Reactor模式中,每个事件循环都会注册一组回调函数,用于处理不同类型的事件。当事件发生时,事件循环会根据事件的类型找到对应的回调函数,并调用它来处理事件。这样可以实现事件驱动的编程模型,将事件的处理与业务逻辑分离开来,提高了代码的可维护性和可扩展性。
举个例子,假设一个Web服务器使用多Reactor模式来处理客户端连接。每个事件循环会注册一组回调函数,包括处理新连接事件、读取数据事件、写入数据事件等。当有新的客户端连接到达时,主Reactor会接收到连接请求,并将连接分配给某个子Reactor。子Reactor会调用相应的回调函数来处理该连接的读取数据事件和写入数据事件。
Proactor模式
Proactor模式与Reactor模式有所不同。在Proactor模式中,操作(通常是IO操作)被提交给一个专门的组件,称为Proactor,然后由Proactor负责执行这些操作。当操作完成时,Proactor会通知相关的事件处理器,告诉它们操作已完成,可以进行下一步处理。与Reactor模式不同,Proactor模式中的事件处理是异步的,事件处理器不需要等待操作完成,而是在操作完成后得到通知。
Proactor模式的主要特点包括:
- 异步处理:事件处理是异步的,事件处理器可以继续执行其他任务,而不必等待操作完成。
- 多线程:通常Proactor模式会使用多线程来处理IO操作,以提高系统的并发能力。
- 高性能:通过异步IO和多线程的结合,Proactor模式可以实现高性能的并发处理。
区别
- 同步 vs. 异步:Reactor模式中事件处理是同步的,而Proactor模式中事件处理是异步的。
- 单线程 vs. 多线程:Reactor模式通常在单线程中运行,而Proactor模式通常使用多线程来处理IO操作。
- 处理方式:在Reactor模式中,事件发生后Reactor负责调用事件处理器,而在Proactor模式中,操作完成后Proactor负责通知事件处理器。
static详解
static
是 C++ 中的关键字,用于声明静态成员变量和静态成员函数,以及在局部变量中具有持久性。
1. 静态变量
静态局部变量:在函数内部声明的静态变量。和全局变量一样,数据都存放在全局区。
在函数内部声明的静态变量,其生命周期跨越函数调用。它们只会在第一次函数调用时初始化,并且保留其值直到程序结束。
1
2
3
4
5
6
7
8
9
10
11
12void foo() {
static int count = 0;
count++;
std::cout << "Count: " << count << std::endl;
}
int main() {
foo(); // 输出:Count: 1
foo(); // 输出:Count: 2
foo(); // 输出:Count: 3
return 0;
}静态成员变量:静态成员变量属于类,而不是类的实例。它们被所有类的对象所共享,并且在类的所有实例中只有一个副本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <iostream>
class MyClass {
public:
static int count;
};
int MyClass::count = 0; // 静态成员变量在类外部进行初始化
int main() {
MyClass obj1;
MyClass::count = 5; // 通过类名访问静态成员变量
std::cout << "Count: " << obj1.count << std::endl; // 输出:Count: 5,因为静态成员变量是共享的
return 0;
}静态全局变量:在函数外部声明的静态变量称为静态全局变量。它们的作用域限制在声明它们的文件内,并且在程序的整个生命周期内保留其值。
说到这,顺便说说
extern
关键字。它用于声明一个变量或函数是在其他文件中定义的,而不是在当前文件中定义的。即在当前文件中声明在其他文件中定义的变量或函数,以便在当前文件中使用它们。
2. 静态函数
静态成员函数:静态函数属于类,而不是类的实例。它们可以直接通过类名调用,而无需创建类的实例。
1 | class Math { |
const详解
const
const
用于定义常量、声明常量引用以及修饰成员函数。const
的作用是告诉编译器这个东西是不可修改的,即它的值在初始化后不能再被修改。
编译器通常不为普通的const常量分配内存空间,而是将他们保存在符号表中。
1. 定义常量
1 | const int x = 5; |
定义了一个常量 x
,其值为 5。一旦定义,其值就无法修改。
2. const和引用
把const
修饰的引用称为”常用引用“,常量引用不能直接修改所引用的对象。
1 | const int ci = 1024;//ci是一个int型的常量 |
3. const和指针
常量指针:指针指向的对象不可变,但指针本身的值可以改变。
1
2
3
4
5
6int x = 5;
const int* ptr = &x; // ptr 是一个指向整型常量的指针,它所指向的对象不能被修改,但可以改变指针的值
// *ptr = 10; // 错误,不能修改 ptr 所指向的对象的值
int y = 10;
ptr = &y; // 可以修改指针 ptr 的值,使其指向不同的对象指针常量:指针本身的值不可变,但指针所指向的对象可以改变。
1
2
3
4
5int x = 5;
int* const ptr = &x; // ptr 是一个指向整型的常量指针,它的值不能改变,但可以修改其指向的对象
*ptr = 10; // 可以修改 ptr 所指向的对象的值
// ptr = &y; // 错误,无法修改指针 ptr 的值
4. const和类对象
const成员变量:
const 成员变量是指在类中声明为常量的数据成员。一旦被初始化,其值就不能再修改。它们只能在类的构造函数初始化列表中初始化,而不能在构造函数体中赋值。
const成员函数:
const只能限定类的成员函数,表示该函数不会修改类的成员变量,除非成员变量被 mutable 修饰符修饰。
const限定后,该成员函数不允许修改类的数据成员,也不允许调用非const函数,即使该函数没有修改类的数据成员,只要没有声明成const,就不能调用。
1 | #include <iostream> |
constexper
constexpr
是 C++11 引入的关键字,用于声明“常量表达式”。
常量表达式是在编译时就可以求值的表达式,即它的值可以在编译时被确定。
constexpr
可以用于变量、函数、构造函数以及类的成员函数,用于指示它们在编译时就可以被计算出来,而不是在运行时计算。
1 | //5是一个常量表达式 |
使用 constexpr
可以使得程序在编译时进行更多的优化,提高程序的性能。
智能指针
1. 智能指针
- 智能指针是一种用于管理动态分配的内存的工具,能够帮助避免内存泄漏和悬挂指针等问题。
- 智能指针是C++11标准引入的一个重要特性,它们基于RAII(资源获取即初始化)原则,利用对象生命周期的概念,在对象生命周期结束时自动释放资源。
- 智能指针实际上是一个类对象,它封装了原始指针,并在其生命周期结束时负责释放指针所指向的内存。
- 智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
- C++11引入了三种指针:
std::shared_ptr
、std::unique_ptr
、std::weak_ptr
2. std::shared_ptr
共享指针,允许多个指针指向同一块内存,它使用引用计数来跟踪有多少个指针指向了该对象。并且会在最后一个指针不再指向该内存区域时自动释放资源。
1 | #include <memory> |
创建shared_ptr
使用
std::make_shared
函数来创建std::shared_ptr
,它会自动分配内存并构造对象,同时返回一个指向该对象的std::shared_ptr
。拷贝和赋值
将一个
std::shared_ptr
赋值给另一个时,引用计数会增加。当所有指向该对象的std::shared_ptr
都被销毁时,引用计数会减少。只有当引用计数为0时,资源才会被释放。引用计数
std::shared_ptr
内部维护了一个引用计数器,用于跟踪有多少个std::shared_ptr
指向了相同的资源。可以通过use_count()
方法获取引用计数。
3. std::weak_ptr
循环引用指的是两个或多个对象彼此之间相互引用,导致它们的引用计数永远不会变为零,从而造成内存泄漏。在使用std::shared_ptr
时,循环引用是一个常见的问题,因为std::shared_ptr
的引用计数机制可能导致对象永远无法被释放。如以下示例:
1 | #include <memory> |
在这个例子中,a1
和a2
相互引用,即a1
的next
指针指向a2
,而a2
的next
指针又指向a1
。这样一来,它们之间的引用计数永远不会变为零,因为彼此都在互相引用。即使在main
函数结束时,a1
和a2
的引用计数也不会为零,导致A
类对象永远无法被销毁,造成内存泄漏。
std::weak_ptr
是std::shared_ptr
的一种弱引用,它不会增加引用计数。用于解决std::shared_ptr
的循环引用带来的内存泄漏问题。可以通过lock()
方法获得一个std::shared_ptr
,如果原来的std::shared_ptr
已经被销毁,则返回一个空指针。
上述示例中,可以将next
成员改为std::weak_ptr
类型,这样就不会导致循环引用。
1 | #include <memory> |
在这个修改后的示例中,A
类的next
成员现在是std::weak_ptr
类型,这意味着a1->next
和a2->next
不会增加A
对象的引用计数。因此,即使a1
和a2
相互引用,它们之间的循环引用也被打破了。这样在main
函数结束时,A
类对象的引用计数会变为零,对象会被正确地销毁,从而避免了内存泄漏问题。
4. std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,它确保在其生命周期内,只有一个指针可以指向该对象。当std::unique_ptr
被销毁时,它所管理的对象也会被自动释放。
- 创建unique_ptr
1 | #include <memory> |
- 移动语义
std::unique_ptr
是独占所有权的,因此它不支持拷贝语义,但支持移动语义。这意味着可以通过移动操作将资源所有权从一个std::unique_ptr
转移到另一个。
1 | #include <memory> |
- 释放资源
当std::unique_ptr
超出作用域时,它所管理的资源会被自动释放。
1 | #include <memory> |
- 自定义删除器
std::unique_ptr
支持自定义删除器,可以指定在释放资源时调用的函数或者函数对象。
1 | #include <memory> |
C++多线程
互斥锁(mutex)
互斥锁(Mutex)是一种同步机制,用于在多线程程序中保护共享资源,防止多个线程同时访问和修改共享资源而导致竞争条件的发生。互斥锁通过在对共享资源的访问前先获得锁来确保同一时刻只有一个线程能够访问共享资源,其他线程必须等待该线程释放锁后才能访问。
mutex提供了4种互斥类型:
- std::mutex:独占的互斥量,不能递归使用,不带超时功能
- std::recursive_mutex:递归互斥量,可重入,不带超时功能
- std::timed_mutex:带超时的互斥量,不能递归
- std::recursive_timed_mutex:带超时的互斥量,可以递归使用
1. 创建和初始化互斥锁
在C++中,可以使用std::mutex
类来创建和使用互斥锁。通常情况下,我们在全局范围内定义一个互斥锁对象,或者在需要保护的共享资源的类中定义一个互斥锁成员变量。
1 | #include <mutex> |
2. 加锁和解锁
在访问共享资源之前,线程需要先获取互斥锁,以确保其他线程不会同时访问该资源。获取锁时,线程会阻塞,直到它成功地获得了锁为止。使用完共享资源后,线程需要释放锁,以允许其他线程访问该资源。
1 | mtx.lock();// 加锁 |
3. lock_guard
除了lock()
和unlock()
方法外,还可以使用std::lock_guard
来自动管理锁的加锁和解锁。std::lock_guard
是一个RAII(资源获取即初始化)类型,它在创建时自动获取锁,在销毁时自动释放锁,从而避免忘记手动解锁而导致的死锁或资源泄漏。
1 | #include <mutex> |
创建一个名为 guard
的 std::lock_guard
对象,用于管理名为 mtx
的互斥锁。在 lock
对象的作用域结束时,会自动释放 mtx
互斥锁,即使在作用域内发生异常也会自动释放。这样做可以确保互斥锁在不再需要时被正确释放,避免了手动调用 lock()
和 unlock()
方法可能带来的错误和忘记释放锁的风险。
4. unique_lock
std::unique_lock
也是 C++ 标准库提供的一个 RAII 类型,用于管理互斥锁的加锁和解锁,类似于 std::lock_guard
。但与 std::lock_guard
不同的是,std::unique_lock
具有更多的灵活性和功能。它可以在创建时选择是否加锁,也可以手动释放锁,并且可以在未加锁的情况下等待条件变量。下面详细讲解 std::unique_lock
的用法:
- 创建
std::unique_lock
对象
1 | #include <mutex> |
- 使用
std::unique_lock
自动管理锁
1 | void someFunction() { |
- 手动控制加锁和解锁
std::unique_lock
允许手动控制锁的加锁和解锁。例如:
1 | std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不加锁 |
std::unique_lock
还可以在未加锁的情况下等待条件变量,从而避免了手动释放锁后再等待条件变量的复杂过程。
1 | #include <condition_variable> |
std::unique_lock
对象 lock
会自动加锁,然后等待条件变量 cv
。当条件满足时,会自动解锁并继续执行。
原子操作-atomic
有两个线程,一个要写数据,一个读数据,如果不加锁可能会造成读写值混乱,使用std::mutex
可以使得执行不会导致混乱,但是每一次循环都要加锁解锁使得程序开销很大。为了提高性能,C++11提供了原子类型std::atomic
,它提供了多线程间的原子操作。原子操作是不可分割的操作,要么完全执行,要么完全不执行,不会被其他线程中断。
原子类型是封装了一个值的类型,它的访问保证不会导致数据的竞争,并且可以用于在不同的线程之间同步内存访问。从效率上来说,原子操作要比互斥量的方式效率要高
- 创建
std::atomic
对象
1 | #include <atomic> |
创建了一个名为 atomicVariable
的 std::atomic<int>
对象,表示一个原子的整型变量。
- 原子操作
std::atomic
提供了一系列原子操作,包括读取、写入、加法、减法等。这些操作可以保证在多线程环境中的原子性,从而避免竞争条件。
1 | atomicVariable.store(10); // 将10存储到原子变量中 |
- 示例
1 | #include <iostream> |
运行结果:
1 | Final value of counter: 4000000 |
条件变量condition_varible
用于实现线程之间的条件等待和通知机制。它通常与 std::mutex
(互斥锁)一起使用,用于在某个条件满足时唤醒等待的线程。主要包括两个动作:
- 一个线程等待条件变量的条件成立而挂起(wait)
- 另一个线程使条件成立(notify_one,notify_all)
先来看一个示例:
1 | #include <iostream> |
1. 等待条件的线程
1 | void waitingThread(){ |
它会执行如下步骤:
- 获取与条件变量相关联的互斥锁。
- 进入
while
循环,检查条件是否满足。如果条件已经满足,线程会跳过等待,并继续执行后续代码。 - 如果条件尚未满足,则调用
cv.wait(lock)
函数,将当前线程置于阻塞状态,并释放互斥锁。以允许其他线程访问共享资源。 - 直到其他线程调用了与条件变量相关联的
notify_one()
或notify_all()
函数,条件变量被通知。该线程被唤醒,并会重新获取互斥锁,继续执行whie循环,检查条件是否满足。 - 如果条件满足,则线程会退出
while
循环,继续执行后续代码。
2. 设置条件并通知等待的线程
主线程负责设置条件并通知等待的线程
1 | std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁 |
- 在修改条件之前,必须先获得与条件变量关联的互斥锁,并在修改后立即释放锁。
- 然后,通过
cv.notify_one()
或cv.notify_all()
来通知等待的线程条件已经发生改变。
异步任务-async、future
已经有多线程thread了,为什么还要有async?
线程毕竟是属于比较低层次的东西,有时候使用有些不便,比如希望获取线程函数的返回结果的时候,就不能直接通过thread.join()
得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后join
,最后得到结果,这个过程是比较繁琐的。C++11 提供了**std::async()**,用于创建异步任务,即在一个新的线程中调用线程函数,并返回一个
std::future
对象,这个future
中存储了线程函数返回的结果。
简单示例:
1 | #include <iostream> |
概括std::async()的用法:
1. 创建异步任务并获取future 对象
1 | #include <future> |
创建了一个异步任务,异步任务会立即在一个新线程中执行,线程调用函数func()
,将函数的返回值赋给了future
对象fut
。
2. 获取异步任务的值
1 | auto result = fut.get(); |
需要获取异步操作的结果时,调用 get()
函数来获取 std::future
对象的值。如果异步操作还没有完成,get()
函数会阻塞当前线程,直到异步操作完成并返回结果。
如何检查异步任务是否完成:
1 | bool state = fut.valid(); |
可以调用 valid()
函数来检查 std::future
对象是否有效。如果 std::future
对象与异步操作相关联,并且异步操作尚未完成,则 valid()
函数返回 true
,否则返回 false
。
3. 异步执行策略
std::async()
函数提供的三种异步执行策略。它们决定了 std::async()
函数创建的异步任务的执行方式。
1. std::launch::async
std::launch::async
策略表示创建一个新的线程,在新的线程中异步执行指定的可调用对象。- 这意味着异步任务会立即在一个新的线程中执行,不会阻塞当前线程。
- 使用
std::launch::async
策略创建的异步任务可以实现并行执行,适用于耗时的计算任务和I/O操作等。
1 | std::future<int> fut = std::async(std::launch::async, task); |
2. std::launch::deferred
std::launch::deferred
策略表示延迟执行指定的可调用对象,直到调用get()
函数时才在调用线程中执行。- 这种策略不会创建新的线程,而是在需要时延迟执行。
- 使用
std::launch::deferred
策略创建的异步任务不会立即执行,直到调用get()
函数时才执行,适用于延迟执行和惰性求值等场景。
1 | std::future<int> fut = std::async(std::launch::deferred, task); |
3. std::launch::async | std::launch::deferred
std::launch::async | std::launch::deferred
表示由实现自行选择执行策略。- 这种策略允许实现根据具体情况自行选择执行方式,可以在新的线程中异步执行,也可以在调用线程中延迟执行。
- 使用
std::launch::async | std::launch::deferred
策略创建的异步任务有可能在新的线程中执行,也有可能在调用线程中延迟执行,具体取决于实现。
1 | std::future<int> fut = std::async(std::launch::async | std::launch::deferred, task); |