目录
- 多线程基础
- 什么是线程
- 线程和进程的关系
- 线程的特点
- 什么是多线程编程
- 为什么要使用多线程
- 线程与CPU的执行关系
- 线程的生命周期
- 创建线程(C++11)
- 线程的可调用对象
- 传参数
- 注意事项
- join和detach的区别
- 一个线程包含什么东西
- this_thread
- 线程同步
- 线程同步机制
- 互斥锁
- 得到错误结果的线程示例:
- 通过互斥锁修正示例(互斥锁的使用方法)
- 注意事项
- lock_guard
- unique_lock
- 什么是unique_lock
- 线程间的同步方式
- 条件变量
- 啥是条件变量
- 如何定义条件变量
- wait()方法
- wait_for()方法
- 读写锁
- shared_mutex
- shared_lock
- 原子变量和原子操作
- 原子变量:
- 原子操作:
- 原子操作符
- 原子函数(不细讲)
- 信号量(学过操作系统的都学的很明白了hhh)
- 信号量的类型
- 信号量的作用
- 信号量的操作
- 信号量的应用
- 在C++中使用counting_semaphore实现信号量(C++20)
- 成员函数
多线程基础
什么是线程
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程和进程的关系
在多线程编程中,一个进程(Program)可以包含多个线程(Thread),这些线程共享相同的进程空间(包括代码段、数据段、堆等)和系统资源(如文件描述符和信号处理),但各自有独立的栈空间和线程控制块(TCB)。
线程的特点
- 轻量级:与进程相比,线程的创建和销毁成本较低,因为线程是进程的一个执行流,共享进程的大部分资源,只需要少量的额外开销来维护线程的状态和控制信息。
- 共享资源:同一进程内的线程共享进程地址空间和全局变量等资源,这使得线程间通信更加便捷。但是,这也带来了数据同步和互斥的问题,需要使用适当的同步机制来避免数据竞争和死锁等问题。
- 并发执行:多个线程可以在同一时间内并发执行,提高了程序的执行效率。但是,由于线程的执行顺序和速度受到操作系统调度策略和硬件性能的影响,因此线程的执行结果可能是不确定的。
- 独立调度:线程是独立调度的基本单位,在多线程操作系统中,调度器根据线程的优先级、状态等因素来决定线程的调度顺序和执行时间。
- 系统支持:现代操作系统通常提供了对线程的支持,包括线程的创建、销毁、调度、同步等功能的API接口。
什么是多线程编程
多线程编程是指在一个程序中创建多个线程并发的执行,每个线程执行不同的任务。线程是操作系统能够进行运算调度的最小单位,一个进程中可以包含多个线程,每个线程都是进程中的一个单一顺序的控制流。多线程编程的主要目的是为了提高程序的执行效率和响应速度,使得程序能够充分利用CPU资源。
为什么要使用多线程
- 充分利用CPU资源:多线程编程可以让程序在多个线程之间并发执行,从而充分利用CPU的多核或多处理器资源,提高程序的执行效率。
- 提高程序响应速度:多线程编程可以将占据时间长的任务放到后台去执行,使得响应用户请求的线程能够尽快处理完成,缩短响应时间,提升用户体验。
- 便于程序设计和维护:多线程编程可以将复杂的程序分解为多个相对独立的线程,每个线程负责完成特定的任务,从而简化程序设计和维护的难度。
线程与CPU的执行关系
- CPU调度单位:线程是CPU调度的最小单位。这意味着操作系统根据一定的调度算法,将CPU的执行时间分配给各个线程,使得它们能够并发执行。
- 单核CPU与多线程:在单核CPU上,多个线程实际上是并发而非真正的同时执行。这是因为CPU会在不同的线程之间快速切换,每次只执行一个线程的一部分,然后切换到另一个线程。由于切换速度非常快,用户通常感觉多个线程是同时运行的。这种技术称为时间片轮转(TimeSlicing)或多任务处理(Multitasking)。
- 多核CPU与多线程:在多核CPU上,多个线程可以真正地同时执行,因为每个线程可以被分配给不同的CPU核心去执行。这样,多个线程可以同时进行复杂的计算任务,从而大大提高整体性能。
- 线程优先级:操作系统会根据线程的优先级来决定执行顺序。优先级高的线程会获得更多的CPU时间片,从而更频繁地执行。
- 线程同步与互斥:当多个线程需要访问共享资源时,就需要考虑线程同步和互斥的问题。这是因为如果没有适当的同步机制,多个线程可能会同时修改同一个数据,导致数据不一致或错误。常用的线程同步机制包括互斥锁、条件变量、信号量等。
- 线程与进程的关系:线程是进程的一部分,每个进程可以包含多个线程。线程共享进程的内存空间和系统资源,但进程仍然是操作系统资源分配的最小单位。因此,多线程编程可以在不增加系统资源消耗的情况下提高程序的执行效率。
- 并发与并行:并发是指在同一时间段内,多个任务交替执行;而并行则是指在同一时间点,多个任务同时执行。在多核CPU上,多线程可以实现真正的并行执行;而在单核CPU上,多线程只能实现并发执行。(在计算机中并发也有同时访问的意思)
线程的生命周期
- 新建状态(New):当线程对象被创建时,它处于新建状态。此时,线程还没有开始执行,也没有分配任何资源。
- 就绪状态(Runnable):当线程对象调用了start()方法后,它进入就绪状态。此时,线程已经做好了执行的准备,等待操作系统调度执行。
- 运行状态(Running):当线程获得CPU时间片时,它进入运行状态。此时,线程开始执行其任务,直到任务完成或遇到阻塞条件。
- 阻塞状态(Blocked):当线程在执行过程中遇到某些阻塞条件(如等待l/O操作完成、等待获取某个锁等)时,它进入阻塞状态。此时,线程暂停执行,并释放CPU资源,直到阻塞条件消失并重新获得CPU时间片。
- 死亡状态(Dead):当线程执行完其任务或遇到异常导致退出时,它进入死亡状态。此时,线程的资源被回收,生命周期结束。
创建线程(C++11)
- 使用std::thread类
可以直接创建一个thread对象来启动一个新的线程,并传递一个可调用的对象(如函数,仿函数,Lambda表达式)作为线程的执行体。
#include <iostream>
#include <thread>
using namespace std;void sayhello()
{cout << "hello" << endl;
}int main()
{//main所在的这个线程就是主线程// 创建子线程打印一个hellothread t1(sayhello);//t1就是一个子线程 初始化参数叫可调对象,可以是函数,lambda表达式,仿函数t1.join();//阻塞主线程 --- 即等待线程结束cout << "this is main" << endl;return 0;
}
线程的可调用对象
在C++中,线程的可调用对象(callableobject)可以是多种类型,包括但不限于:函数指针、成员函数指针、 lambda表达式、函数对象(也称为仿函数或functor)以及绑定对象(通过std:bind创建)。以下是一些示例代码:
- 函数指针
#include <iostream>
#include <thread>
using namespace std;void fun()
{cout << "线程的函数指针调用" << endl;
}int main()
{thread t(fun);t.join();return 0;
}
- 成员函数指针
#include <iostream>
#include <thread>
using namespace std;class MyClass {
public:void fun(){cout << "线程的类成员函数指针调用" << endl;}
};int main()
{MyClass obj;thread t(&MyClass::fun,&obj);t.join();return 0;
}
- Lambda表达式
#include <iostream>
#include <thread>
using namespace std;int main()
{thread t([]() {cout << "线程lambda表达式调用"; });t.join();return 0;
}
- 仿函数
#include <iostream>
#include <thread>
using namespace std;class MyClass {
public:void operator()() {cout << "线程仿函数调用" << endl;}
};int main()
{MyClass obj;thread t(obj);t.join();return 0;
}
- 绑定对象
#include <iostream>
#include <thread>
#include <functional>
using namespace std;void fun(int a, int b)
{cout << "绑定调用" << a << " " << b << endl;
}int main()
{auto boundFunc = bind(fun, 1, 2);thread t(boundFunc);t.join();return 0;
}
传参数
- 传值
#include <iostream>
#include <thread>
using namespace std;void fun(int a)
{cout << a << "线程的函数指针调用" << endl;
}int main()
{thread t(fun,10);t.join();return 0;
}
- 传引用
需要用ref函数来包装x和y的引用。
#include <iostream>
#include <thread>
using namespace std;void fun(int& a,int& b)
{a += 5;b += 10;cout << "modified values" << ' ' << a << ' ' << b << endl;
}int main()
{int x = 5;int y = 10;thread t(fun,ref(x),ref(y));t.join();cout << "values in main thread" << ' ' << x << ' ' << y << endl;return 0;
}
注意事项
- 线程安全性:当通过引用传递数据时,必须确保对这些数据的访问是线程安全的。否则,你可能会遇到数据竞争和其他并发问题。
- 资源管理:确保你正确地管理了所有在新线程中创建或使用的资源。特别是,如果新线程使用了动态分配的内存或其他资源,你需要确保这些资源在不再需要时被正确释放。
- 异常处理:新线程中抛出的异常不会自动传播到创建该线程的线程。因此,你需要确保在新线程中正确地处理所有可能的异常。
join和detach的区别
-
join方法:
- 当一个线程调用join方法时,它会阻塞当前线程(调用join的线程),直到被调用的线程(即join的参数所指定的线程)执行完成。
- 这可以确保在主线程中,子线程执行完毕之前不会结束主线程的执行。
- 使用join可以确保线程的资源被正确地回收和清理,因为当join返回时,线程对象所代表的线程已经完成了执行。
- 如果在子线程未执行完毕的情况下尝试销毁其对应的thread对象,而该对象又未被join或detach,则会导致程序终止。
-
detach方法:
- 当调用detach方法时,线程将与其所属的线程(即调用detach的线程)分离,并在后台独立运行。
- 一旦线程被分离,它将不再受到主线程的控制,主线程也不再需要调用join来等待它的结束
- 分离的线程会自动回收其资源,当线程执行完毕后,其资源会被系统回收。
- 需要注意的是,一旦线程被分离,就无法再对其进行join操作,因为此时线程已经脱离了主线程的控制。
join和detach的主要区别在于它们对线程执行完成后的处理方式不同。join会阻塞当前线程并等待子线程执行完成,而detach则会使线程在后台独立运行并自动回收资源。
- detach 测试
当主线程执行结束时,程序就退出了。
#include <iostream>
#include <thread>
using namespace std;void fun()
{for (int i = 0; i < 1000; i++){cout << "线程一" << i << endl;}
}int main()
{thread t(fun);t.detach();cout << "this is main Thread" << endl;return 0;
}
一个线程包含什么东西
- 线程ID:每个线程在系统中都有一个唯一的标识符,用于区分不同的线程。
- 线程栈(ThreadStack):每个线程都有自己私有的栈空间,用于存储局部变量、函数调用时的参数和返回地址等信息。线程栈在创建线程时分配,并在线程结束时释放。
- 线程状态:线程的状态描述了线程当前的生命周期阶段,例如新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和终止(Terminated)等。
- 线程上下文(ThreadContext):线程上下文包含了线程执行时所需的所有信息,如CPU寄存器的内容、程序计数器(PC)的值、栈指针、信号掩码等。当线程被切换时,线程上下文会被保存,以便在之后恢复执行时能够继续执行。
- 线程函数(ThreadFunction):线程函数是线程执行的具体逻辑,它包含了线程需要执行的代码。线程函数通常是由用户定义的,并在创建线程时作为参数传递给线程创建函数。
- 线程优先级:操作系统可以根据线程优先级来决定调度哪个线程执行。优先级较高的线程会获得更多的处理器时间。
- 线程属性:线程属性用于设置线程的一些特性,如栈大小、安全属性等。这些属性可以在创建线程时设置,也可以通过其他线程管理函数进行修改。
- 线程同步原语:为了协调多个线程的执行,C++提供了多种线程同步原语,如互斥锁(Mutex),条件变量,信号量等。
this_thread
td:this_thread是C++标准库中的一个命名空间,它提供了与当前线程相关的函数和工具。在多线程编程中std:this_thread命名空间下的函数允许我们获取和操作当前线程的特定属性,如获取线程ID、使当前线程休眠等。以下是一些std::this_thread命名空间下常用的函数及其使用方法:
- 获取当前线程的ID — this_thread::get_id()
可以用于比较两个线程是否处于同一个线程。
#include <iostream>
#include <thread>
using namespace std;void fun()
{cout << this_thread::get_id() << "线程一" << endl;
}int main()
{thread t(fun);t.join();cout << "this is main Thread" << endl;cout << this_thread::get_id() << "主线程" << endl;return 0;
}
- this_thread::sleep_for()函数可以使得当前线程休眠一段时间。
线程同步
线程同步是指通过一定的机制来控制多个线程之间的执行顺序,已确保它们能够正确访问和修改共享资源。
线程同步机制
-
互斥锁(Mutex):互斥锁是最常用的线程同步机制之一。当一个线程想要访问共享资源时,它首先会尝试获取与该资源关联的互斥锁。如果锁已经被其他线程持有,则该线程将被阻塞,直到锁被释放。这样可以确保在任何时候只有一个线程能够访问共享资源。
-
条件变量(ConditionVariable):条件变量用于使线程在满足某个条件之前等待。它通常与互斥锁一起使用,以便在等待条件成立时释放锁,并在条件成立时重新获取锁。这允许线程在等待期间不占用锁,从而提高并发性能。
-
信号量(Semaphore):信号量是一种通用的线程同步机制,它允许多个线程同时访问共享资源,但限制同时访问的线程数量。信号量内部维护一个计数器,用于表示可用资源的数量。当线程需要访问资源时,它会尝试减少计数器的值;当线程释放资源时,它会增加计数器的值。当计数器的值小于零时,尝试获取资源的线程将被阻塞。
-
原子操作(AtomicOperations):原子操作是不可中断的操作,即在执行过程中不会被其他线程打断。C++11及以后的版本提供了头文件,其中包含了一系列原子操作的函数和类。这些原子操作可以用于安全地更新共享数据,而无需使用互斥锁等同步机制。
互斥锁
- 互斥(Mutex)是一种同步机制,用于保护共享资源,防止多个线程同时访问和修改同一资源,从而引发数据竞争(datarace)和不一致性。
- 当一个线程想要访问某个共享资源时,它首先会尝试获取与该资源关联的互斥锁(mutex)。如果互斥锁已经被其他线程持有(即被锁定),则该线程将被阻塞,直到互斥锁被释放(即被解锁)。一旦线程成功获取到互斥锁,它就可以安全地访问共享资源,并在访问完成后释放互斥锁,以便其他线程可以获取该锁并访问资源。
得到错误结果的线程示例:
#include <iostream>
#include <thread>
using namespace std;//共享变量
int counter = 0;//累加
void incre_counter(int times)
{for (int i = 0; i < times; ++i){//这是一个数据竞争,因为多个线程可能同时执行这行代码counter++;}
}int main()
{thread t1(incre_counter, 10000);thread t2(incre_counter, 10000);t1.join();t2.join();//输出结果不是20000,得到错误结果cout << counter << endl;return 0;
}
通过互斥锁修正示例(互斥锁的使用方法)
需要引入头文件<mutex>
-
创建互斥量
mutex mtx;
— 创建类型为mutex的变量mtx -
锁定互斥量
在访问共享资源之前,使用lock()函数锁定互斥量。
mtx.lock();
-
访问共享资源
在互斥量被锁定的期间,你可以安全地访问共享资源,因为其他试图锁定该互斥量的线程将被阻塞。 -
解锁互斥量
一旦完成对共享资源的访问,使用unlock()函数解锁互斥量
mtx.unlock();
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;//共享变量
mutex mymutex;
int counter = 0;//累加
void incre_counter(int times)
{for (int i = 0; i < times; ++i){mymutex.lock();//在访问临界资源变量之前先加锁counter++;mymutex.unlock();//完了之后解锁}
}int main()
{thread t1(incre_counter, 10000);thread t2(incre_counter, 10000);t1.join();t2.join();//输出结果是20000,得到正确结果cout << counter << endl;return 0;
}
注意事项
- 死锁:
如果线程在持有互斥量的情况下调用了一个阻塞操作(如另一个互斥量的lock()),并且这个阻塞操作永远不会完成(因为其他线程持有它需要的资源),那么就会发生死锁。避免死锁的一种方法是始终按照相同的顺序锁定互斥量,或者使用更高级的同步原语,如std:lock_guard或std:unique_lock,它们可以自动管理锁的获取和释放。 - 异常安全:
如果在锁定互斥量后抛出异常,那么必须确保互斥量被正确解锁。使用std::lock_guard或std::unique_lock可以自动处理这种情况,因为它们在析构时会释放锁。 - 不要手动解锁未锁定的互斥量:
在调用unlock0之前,必须确保互斥量已经被lock0锁定。否则,行为是未定义的。 - 不要多次锁定同一互斥量:
对于非递归互斥量(如std:mutex),不要在同一线程中多次锁定它。这会导致未定义的行为。如果需要递归锁定,请使用std:recursive_mutex。 - 使用RAII管理锁:
使用RAll(资源获取即初始化)原则来管理锁的生命周期,通过std:lock_guard或std:unique_lock来确保锁在不需要时自动释放。 - 避免长时间持有锁:
尽量缩短持有锁的时间,以减少线程之间的争用,提高程序的并发性能。 - 考虑使用更高级的同步原语:
除了std::mutex之外,C++标准库还提供了其他更高级的同步原语,如条件变量(std:condition_variable)、读写锁(std:shared_mutex)等,它们可以在特定场景下提供更高效的同步机制。
lock_guard
- lock_guard是一个模板类,位于头文件中。它符合RAll风格,它主要用于管理mutex的生命周期,确保mutex在锁定的作用域内被正确地上锁和解锁。它主要解决了手动管理mutex锁定和解锁时可能出现的问题,如:忘记解锁、异常情况下未解锁等问题。
- 你可以简单把这个东西,看成是对mutex的一种管理封装,就是说用原生的mutex,有些时候不顺手,有瑕疵。使用lock_guard比较省心,它来更好地管理你的mutex。
特点:
- 单纯使用mutex的时候,容易忘记解锁:如果程序员忘记在某个路径上调用 mtx.unlock(),会导致mutex一直保持锁定状态,其他线程将无法继续执行。
- 异常安全:在C++中,如果在持有mutex锁的情况下抛出异常,必须确保在异常发生时mutex能够被正确解锁。手动管理异常路径上的解锁容易出错。
- 应对太过复杂的逻辑:代码逻辑如果太复杂,你往往不知道在哪里解锁合适
- 自动解锁:std:lock_guard在其析构函数中自动调用unlock0,因此即使发生异常,mutex也能被正确解锁。
- 防止死锁:通过std:lock_guard,可以避免由于忘记解锁而引发的死锁问题
- 简单易用:std:lock_guard使用简单,只需在需要锁定的代码块内创建一个 std:lock_guard对象即可,无需手动调用lock0和unlock0。
unique_lock
- 因为mutex在管理方面有瑕疵,因此出现了一个互斥量封装器lock_guard来智能地管理mutex。而lock_guard只
是简单的管理,功能比较弱,有瑕疵。因此需要搞出来一个功能更强大的东西出来。而这个东西就是unique_lock。 - 本质上来说lock_guard和unique_lock都是为了更好地使用各种锁而诞生的。但是unique_lock更为灵活,功能更强大,可做的操作比较多。unique_lock有些时候也被称为灵活锁。
lock_guard示例:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;//共享变量
mutex mymutex;
int counter = 0;//累加
void incre_counter(int times)
{lock_guard<mutex> lock(mymutex);//生命周期到头才解锁for (int i = 0; i < times; ++i){counter++;}for (int i = 0; i < times; ++i){counter++;}//到这里函数结束才解锁
}int main()
{thread t1(incre_counter, 10000);thread t2(incre_counter, 10000);t1.join();t2.join();//输出结果是40000,得到正确结果cout << counter << endl;return 0;
}
什么是unique_lock
uniquelock是一个更灵活的互斥量封装器,它提供了更多的控制选项,比如延迟锁定、尝试锁定、递归锁定、定时锁定等。与std:lock_guard相比,std:unique_lock提供了更多的功能,但也需要更多的管理责任。
-
三种方式:
-
一些成员函数:
- lock() — 锁定关联的mutex
- unlock() — 解锁关联的mutex
- try_lock() — 尝试锁定mutex,如果锁定成功,返回true,负责返回false
- owns_lock() — 返回一个布尔值,指示unique_lock是否拥有mutex的所有权
如果是延迟上锁,需要显式上锁
不论哪种上锁,如果上锁了,那么将自动管理锁,当生命周期结束时会自动释放锁
如果没上锁,相当于没管理,相当于不存在锁。
线程间的同步方式
条件变量
啥是条件变量
条件变量是一种同步原语,用于在线程之间协调共享资源的访问。它允许一个线程等待特定条件的满足(如某个值的变化),而另一个线程在条件满足时通知(或唤醒)等待的线程。这种机制可以防止线程忙等待,从而提高系统效率。
特点:
- 等待和通知机制:线程可以等待某个条件的改变,而不需要一直占用CPU资源。
- 与互斥锁配合使用:通常与互斥锁一起使用,以保护共享数据的访问。
- 线程通信:实现线程之间的高效通信和协调
注意:
- 条件变量必须与互斥锁一起使用。
如何定义条件变量
在C++的多线程编程中,std:condition_variable类提供了wait和wait_for方法,用于让线程在满足特定条件之前等待。这两个方法都与互斥锁(std:mutex或std:unique_lock))一起使用,以确保线程安全地访问共享数据。
wait()方法
用于阻塞当前线程,知道另一个线程调用同一个condition_variable实例的notify_one或notify_all方法。
- 注意: 如果没有通知,即使条件为true,它也不会被执行。如果通知了,但是条件不成立,它仍然阻塞,不会执行。执行顺序为:先通知,再检测条件。
- 参数: unique_lock对象。调用wait时,这个锁会被自动释放,允许其他线程获取该锁并执行。当wait返回时,锁会被重新获取。
工作原理:
- 释放锁:当线程调用wait方法时,它会首先释放与std:unigue_lock关联的互斥锁。这是为了允许其他线程可以访问和修改与条件变量相关的共享数据。
- 进入等待队列:释放锁后,线程会进入条件变量的等待队列中,进入阻塞状态。此时线程不再消耗CPU时间,直到被唤醒。
- 等待条件或通知:线程在等待队列中等待,直到两个条件之一发生:
- 另一个线程调用了同一个条件变量的notify_one或notify_all方法,并且该线程是等待队列中的第一个线程(对于notify_one)或等待队列中的所有线程(对于notify_all)。
- 谓词函数pred返回true。
- 重新获取锁:当线程被唤醒后(无论是由于收到通知还是条件成立),它会尝试重新获取之前释放的互斥锁。如果此时锁已经被其他线程持有,则该线程会阻塞在互斥锁上,直到获得锁。
- 检查条件:获得锁后,线程会再次调用谓词函数pred来检查条件是否成立。如果条件不成立(即pred返回false),则线程会重新进入等待队列,并释放锁,继续等待。这个过程会不断重复,直到条件成立。
- 继续执行:如果条件成立(即pred返回true),则线程会退出wait方法,并继续执行后续的代码。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>//条件变量
#include <vector>
using namespace std;mutex mtx;
condition_variable cv;//条件变量
bool flag = false; //共享变量//根据共享变量组建逻辑
void myprint(int i)
{unique_lock<mutex> lck(mtx);while (!flag){cv.wait(lck);// 条件不成立,进入阻塞状态, 释放锁,等待别人的唤醒}cout << this_thread::get_id() << "-" << i << endl;
}void updateflag()
{cout << "this is update" << endl;this_thread::sleep_for(3s);unique_lock<mutex> lck(mtx);flag = true;cv.notify_all();//通知所有线程//cv.notify_one(); // 通知一个线程
}int main()
{ vector<thread> mybox;for (int i = 0; i < 10; i++){mybox.emplace_back(myprint, i);}updateflag(); //没有这步的话,线程全部阻塞等待for (auto& t : mybox){t.join();}return 0;
}
wait_for()方法
- wait_for方法与wait方法类似,但它允许你指定一个超时时间。如果在这段时间内条件没有满足,并且没有收到唤醒信号,那么wait_for会返回,并且线程会重新获取互斥锁。
- wait_for方法接受一个时间间隔作为参数,表示线程愿意等待的最长时间。这个时间间隔可以是std::chrono库中定义的任何时间单位。
- wait_for的返回值是一个cv_status枚举值,表示等待操作的结果。可能的返回值包括:
cv_status:no_timeout:表示等待操作成功完成,即在超时时间内条件被满足或收到了唤醒信号。cv_status:timeout:表示等待操作因超时而结束,条件没有被满足且没有收到唤醒信号。
读写锁
C++中的读写锁(也称为共享锁和独占锁)是一种同步机制,用于控制对共享资源的访问,允许多个线程同时读取资源,但在写入资源时只允许一个线程独占访问。
- 读操作:不会破坏数据的完整性,可以共行
- 写操作:可能会破坏数据的完整性,必须互斥
- 读写之间也要互斥
- 结论:读读之间不互斥,读写之间互斥,写写之间互斥。
特点:
图片独占锁我们使用unique_lock.
shared_mutex
shared_mutex是C++17标准引入的一种互斥锁,用于支持多读单写的并发访问方式。
它允许多个线程同时持有共享锁(读锁),但在持有(写锁)时,其他线程不能再持有任何类型的锁。
也就是说shared_mutex可以变身,它可以执行独占锁,也可以执行共享锁。
虽然在该成员函数中存在lock,unlock等函数,但是我们却不使用语法share_mutex::lock来直接调用该成员成员函数。通常使用unique_lock(或lock_guard)来管理。同样的,共享锁定则使用shared_lock来管理。
注意,这里的unique_lock和shared_lock都会自动上锁,它的构造函数包含了这种行为。
shared_lock
是一种锁管理器,用于管理shared_mutex的共享锁。它可以自动获取和释放共享锁。
#include <shared_mutex>
#include <thread>
#include <iostream>
#include <vector>
using namespace std;shared_mutex rw_mutex;//读写锁
int shared_data = 0; //共享资源void reader()
{shared_lock<shared_mutex> lock(rw_mutex); //申请共享锁cout << "Reader thread :" << this_thread::get_id() << " reads value:" << shared_data << endl;
}void writer(int value)
{unique_lock<shared_mutex> lock(rw_mutex);shared_data = value;cout << "Writer thread :" << this_thread::get_id() << " write value:" << shared_data << endl;
}int main()
{vector<thread> threads;for (int i = 0; i < 5; i++){threads.emplace_back(reader);}for (int i = 0; i < 2; i++){threads.emplace_back(writer,i);}for (auto& t : threads){t.join();}return 0;
}
原子变量和原子操作
原子变量:
原子变量是指使用std::atomic模板类定义的变量。这些变量提供了对其所表示的值进行原子操作的能力。原子变量确保在多线程环境中,对变量的读写操作是线程安全的,即操作不会被其他线程中断或干扰。
- 特征:
- 创建方法:
#include <atomic>std::atomic<int> atomicInt(0); //定义一个int类型的原子变量。名称叫atomicInt,默认值为0。
原子操作:
原子操作是指对原子变量进行的不可分割的操作。不可分割的意思是这些操作要么完全执行,要么完全不执行,不会在执行过程中被其他线程打断。这些操作包括基本的读取、写入、交换、比较并交换(CAS)以及一些算术和按位操作。
- 加载和存储操作
int value = atomicInt.load(); //原子加载
atomicInt.store(10); //原子存储
- 读写操作(等价于load和store)
int value = atomicInt;
atomicInt = 10;
- 自增和自减操作
atomicInt++;
atomicInt--;
++atomicInt;
--atomicInt;
- 其他修改操作
atomicInt += 5;
atomicInt -=3;
原子操作符
- 赋值操作符
- 用于赋值操作
- 例如
atojmicVar = value;
- 取值操作符
- 用于获取原子变量的当前值
- 例如
T value = atomicVar;
- 前置和后置递增递减操作符
- 用于将原子变量的值递增或者递减
- 例如
++atomicVar;
或--atomicVar;
或atomicVar++;
- 复合赋值操作符
- 用于对原子变量进行复合操作
- 例如
atomicVar += value;
原子函数(不细讲)
信号量(学过操作系统的都学的很明白了hhh)
信号量(Semaphore)是一种用于管理和协调多线程或多进程访问共享资源的同步机制。它通过计数器来控制对资源的访问数量,确保多个线程或进程能够安全地使用共享资源而不会发生数据竞争或死锁。传统的锁(如互斥锁)可以用来保护共享资源,但对于某些场景(如资源的计数管理),信号量提供了更灵活和高效的解决方案。
信号量的类型
信号量的作用
信号量的操作
信号量的应用
在C++中使用counting_semaphore实现信号量(C++20)
c++20更新的。
成员函数
主要是两个
- void acquire()
P操作,信号量的值如果大于0,则减1,否则阻塞当前线程 - void release(ptrdiff_t update = 1)
V操作,将信号量的值增加update,如果有等待的线程,则唤醒相应数量的线程