服装设计找图网站,2023年天津市施工招标公告时间,怎样制作h5,网站建设 部署与发布目录
一.什么是线程#xff1f;
线程的特点#xff1a;
线程的组成#xff1a;
二.什么是进程#xff1f;
进程的特点#xff1a;
进程的组成#xff1a;
三.线程与进程的关系#xff1a;
四.C的Thread方法的使用#xff1a;
1.创建线程#xff1a;
2.join(…目录
一.什么是线程
线程的特点
线程的组成
二.什么是进程
进程的特点
进程的组成
三.线程与进程的关系
四.C的Thread方法的使用
1.创建线程
2.join()方法
3.detach()方法
detach()方法细节
如何做到隔离线程
4.joinable()
5.native_handle()
6.hardware_concurrency()
7.线程的休眠
1std::this_thread::sleep_for() 2std::this_thread::sleep_until()
8.线程的局部存储Thread Local Storage
注意事项
五.线程的同步与互斥
1.互斥量mutex
2.C的其他锁的拓展介绍
1std::recursive_mutex
2std::timed_mutex
3std::shared_mutex
特点
3.死锁
死锁的产生通常需要满足以下四个条件
如何避免死锁
解决方法
4.条件变量Condition Variable
工作机制
5.call_once的使用
std::call_once 的工作机制:
6.atomic 原子操作
原子操作的常用方法 六.线程池的构建与使用 1.首先创建一个线程池类
1创建成员变量
2 构造函数
什么是 function 函数模板
3析构函数
4添加任务 enqueue() 方法
什么是 std::bind ?
什么是 std::forward ? 什么是 std::move
std::move 使用注意事项
2.创建 main 内 这篇博客主要讲述线程以及线程池等相关技术狂码两万六千字希望您可以耐心观看如有不足以及不解欢迎评论区留言跟我商讨本博客为学习笔记要是对您有帮助也请您给个三连支持一下话不多说进入正题 - 一.什么是线程
线程Thread是进程中的一个执行单元是操作系统能够进行运算调度的最小单位。线程是程序执行的基本单位它包含了执行所需的所有信息如程序计数器、栈和局部变量等。
优点线程的创建和销毁开销小线程之间可以方便地共享数据减少了数据传输的开销通过多线程可以充分利用多核处理器提高程序的执行效率。
缺点多个线程共享资源可能导致数据竞争和同步问题需要使用锁等机制来保护共享数据。多线程程序的调试和错误排查相对复杂可能会出现死锁、竞争条件等问题。 线程的特点 轻量级线程是比进程更小的执行单位创建和销毁的开销较小。共享资源同一进程中的多个线程可以共享进程的资源如内存空间、文件描述符等。独立执行每个线程拥有自己的执行栈和程序计数器可以独立执行任务。并发执行多个线程可以并发执行从而提高程序的执行效率。 线程的组成 线程ID每个线程都有一个唯一的标识符。程序计数器指向当前线程执行的指令。栈保存线程的局部变量和函数调用记录。寄存器存储线程的上下文信息。 二.什么是进程
进程Process是一个正在执行的程序的实例具有自己独立的内存空间和系统资源。进程是操作系统进行资源分配和调度的基本单位。
优点进程之间互不干扰安全性高。一个进程的崩溃不会影响其他进程。
缺点进程的创建和销毁需要较大的开销。进程之间的通信相对复杂通常需要使用进程间通信IPC机制。 进程的特点 资源独立每个进程都有自己的内存空间和资源进程之间相互独立互不干扰。开销大进程的创建、销毁和切换开销相对较大。更高的隔离性进程之间不直接共享内存安全性更高。 进程的组成 进程ID每个进程都有一个唯一的标识符。内存空间包括代码段、数据段、堆和栈。程序状态记录进程的当前状态就绪、运行、阻塞等。资源信息记录进程所使用的系统资源文件描述符、信号量等。 三.线程与进程的关系
进程是线程的容器一个进程可以包含多个线程。所有线程共享该进程的资源如内存但每个线程有自己的栈和寄存器。
调度与切换进程切换开销较大因为需要保存和恢复整个进程的上下文。而线程切换开销较小因为只需保存和恢复线程的上下文。
并发与并行多个进程可以并行执行多个线程在同一进程内可以并发执行。多线程程序比多进程程序更易于实现并发。
四.C的Thread方法的使用 通常建议在较大的项目中和公共代码库中使用 std:: 前缀以提高代码的可读性和可维护性。在小型项目或练习代码中如果确实没有命名冲突可以适当使用 using namespace std;但最好在源文件的开头避免使用全局命名空间污染。一般来说保持良好的命名空间管理是最佳实践。 1.创建线程 线程一共有三种创建方法 函数指针thread thread_name(函数方法名,参数1,参数2,....)函数对象thread thread_name(函数方法名(),参数1,参数2,....)Lambda表达式thread thread_name([](typename name){...}) #include iostream
#include thread
using namespace std;// 一个简单的函数作为线程的入口函数
void thone1(int Z) {for (int i 0; i Z; i) {cout 线程使用函数指针作为可调用参数\n;}
}
//引用类型变量参数
void thone11(int Z) {for (int i 0; i Z; i) {cout 线程使用函数指针作为可调用参数\n;}
}
// 可调用对象的类定义
class Threadtwo {
public:void operator()(int x) const {for (int i 0; i x; i) {cout 线程使用函数对象作为可调用参数\n;}}
};int main() {cout 线程 1 、2 、3 独立运行 endl;// 使用函数指针创建线程thread th1(thone1, 3);// 传递引用类型参数需要使用ref函数进行传递// 使用ref函数将num转换成引用类型变量int num 0;thread th11(thone11,ref(num));// 使用函数对象创建线程thread th2(Threadtwo(), 3);// 使用 Lambda 表达式创建线程thread th3([](int x) {for (int i 0; i x; i) {cout 线程使用 lambda 表达式作为可调用参数\n;}}, 3);return 0;
} thread 对象不能被复制因为线程的资源管理需要独占访问。尝试复制 std::thread 对象会导致编译错误。如果需要在多个对象间共享线程通常需要使用智能指针 std::shared_ptrstd::thread。 2.join()方法
join() 方法在 C 中用于等待线程的结束。 等待线程完成当调用 join() 方法时主线程即 main 函数所在的线程会阻塞直到被调用的线程执行完毕。 资源管理线程在执行完后会被系统资源回收。如果不调用 join()主线程在结束时可能会强行终止而被调用的线程可能还在运行导致程序的未定义行为。因此调用 join() 确保了线程资源的正确管理。 #include iostream
#include thread
using namespace std;void threadFunction() {std::cout 线程正在运行... std::endl;
}int main() {thread t(threadFunction); // 创建线程t.join(); // 等待线程完成cout 线程已结束. endl;return 0;
}3.detach()方法
detach() 方法用于将线程与其调用的线程分离使得分离后的线程与主线程独立运行。 detach()方法细节 分离线程调用 detach() 后线程会独立于主线程执行。主线程和分离的线程之间不再有直接的关系。资源管理分离的线程在完成后会自动释放其资源主线程不需要显式地调用 join() 来等待它完成。非阻塞执行主线程可以继续执行不会因等待分离线程而被阻塞。 如何做到隔离线程 线程状态管理当调用 detach() 方法时线程的状态会被设置为 可分离。这意味着线程在后台运行与创建它的线程如主线程没有绑定关系。不再访问一旦线程被分离主线程不能再调用 join() 或 joinable() 来等待或检查该线程。分离线程的生命周期不再受到主线程的影响。独立运行分离线程将继续执行直到其任务完成即使主线程已经结束。完成后线程的资源会自动被操作系统回收。 #include iostream
#include thread
#include chrono
using namespace std;void threadFunction() {cout 分离的线程正在运行... endl;this_thread::sleep_for(chrono::seconds(2)); // 模拟工作cout 分离的线程结束. endl;
}int main() {thread t(threadFunction);t.detach(); // 分离线程cout 主线程继续运行... endl;this_thread::sleep_for(chrono::seconds(1)); // 等待主线程结束cout 主线程结束. endl;return 0;
}4.joinable()
检查线程是否可以被 join()。如果线程处于可加入状态即尚未调用 join() 或 detach()返回 true否则返回 false。
#include iostream
#include thread
using namespace std;void threadFunction() {cout 线程正在运行... endl;
}int main() {thread t(threadFunction);if (t.joinable()) { // 检查线程是否可加入t.join(); // 等待线程完成}cout 线程已结束. endl;return 0;
}5.native_handle()
native_handle() 方法用于获取与线程相关的原生句柄。这个句柄通常是底层操作系统为线程分配的一个标识符允许与特定于平台的线程功能进行交互。 在计算机科学中句柄Handle是一种用于标识系统资源的抽象引用。句柄通常是一个整数或指针它提供了对底层资源的间接访问而不需要用户直接操作该资源的内部表示。句柄是一个“指针”的替代它使得程序能够通过该句柄访问某个资源而不直接引用资源的地址。 在 POSIX 系统中native_handle() 返回一个 pthread_t 类型的句柄代表一个 POSIX 线程。在 Windows 系统中native_handle() 返回一个 HANDLE 类型代表 Windows 线程。 #include iostream
#include thread
#include pthread.h // POSIX 线程库void threadFunction() {std::cout 线程正在运行... std::endl;
}int main() {std::thread t(threadFunction);// 获取原生线程句柄auto nativeHandle t.native_handle(); // 在 POSIX 系统中为 pthread_t 类型std::cout 原生线程句柄: nativeHandle std::endl;t.join(); // 等待线程完成return 0;
}6.hardware_concurrency()
返回系统可以支持的并发线程数量通常是 CPU 核心的数量。虽然这不是 std::thread 的方法但它与线程相关提供了可用的并行硬件线程数量即 CPU 核心数量。返回一个 unsigned 整数表示可用的硬件线程数量。返回值可能是 0表示无法确定。
#include iostream
#include thread
using namespace std;int main() {unsigned int numThreads thread::hardware_concurrency();cout 系统支持的并发线程数量: numThreads :endl;return 0;
}7.线程的休眠 std::this_thread::sleep_for()或std::this_thread::sleep_until()用于让线程暂停执行指定时间。 1std::this_thread::sleep_for() std::this_thread::sleep_for()用于让当前线程休眠一段时间。它接收一个表示时间长度的参数std::chrono::duration使线程暂停指定的时间。 std::this_thread::sleep_for(duration);duration传入一个std::chrono::duration对象表示线程需要休眠的时间长度。支持的时间单位包括std::chrono::seconds、std::chrono::milliseconds、std::chrono::microseconds等。 #include iostream
#include thread
#include chronoint main() {std::cout Starting 3-second sleep... std::endl;// 线程会“睡眠”并在指定时间后恢复运行std::this_thread::sleep_for(std::chrono::seconds(3));std::cout Awake after 3 seconds! std::endl;return 0;
}2std::this_thread::sleep_until() std::this_thread::sleep_until()用于让当前线程休眠至某个指定的时间点。它接收一个表示未来时间的参数std::chrono::time_point线程会暂停执行直到到达该时间点。 std::this_thread::sleep_until(time_point);time_point传入一个std::chrono::time_point对象表示线程需要休眠的时间点。time_point通常通过std::chrono::system_clock::now()获取当前时间然后加上偏移时间来指定。 #include iostream
#include thread
#include chronoint main() {auto start_time std::chrono::system_clock::now();auto wake_time start_time std::chrono::seconds(3);std::cout Sleeping until specified time point... std::endl;// 线程会休眠至从start_time算起的3秒钟后恢复时刻为wake_time// sleep_until 会直接等待到 wake_time不论当前时间距离 wake_time 还有多长时间。std::this_thread::sleep_until(wake_time);// 表示当前线程会一直等待到 3 秒后才继续执行。std::cout Awake after reaching the time point! std::endl;return 0;
}8.线程的局部存储Thread Local Storage
在C中线程局部存储Thread Local Storage简称 TLS允许每个线程有自己的独立数据副本。这对于需要在线程间共享的全局状态但又希望每个线程有其独立的值的情况非常有用。 在多线程环境中需要避免数据竞争的情况下使用线程局部存储是一种有效的方法。每个线程的任务需要保存一些状态信息但这些信息不应该被其他线程共享或干扰。 使用thread_local关键字来定义一个线程局部变量。该变量的生命周期与线程的生命周期相同线程结束时该变量的内存将自动释放。 #include iostream
#include threadthread_local int threadLocalVar 0; // 声明一个线程局部变量void threadFunction(int id) {// 每个线程都会有自己的 threadLocalVar 副本threadLocalVar id; // 设置线程局部变量std::cout Thread id : threadLocalVar threadLocalVar std::endl;
}int main() {std::thread t1(threadFunction, 1);std::thread t2(threadFunction, 2);t1.join();t2.join();return 0;
}
// Thread 1: threadLocalVar 1
// Thread 2: threadLocalVar 2注意事项 性能考虑虽然线程局部存储提供了方便但过度使用可能导致内存使用增加特别是在多线程程序中。静态存储由于线程局部变量在程序的整个运行期间都是存在的可能会导致更多的静态内存使用。跨线程访问如果线程需要共享数据仍然需要使用互斥锁等同步机制来管理对共享资源的访问。 我们还可以在结构体或类中使用thread_local使整个类的成员或特定成员成为线程局部变量。 #include iostream
#include threadstruct ThreadLocalData {thread_local static int value; // 静态成员变量为线程局部变量
};thread_local int ThreadLocalData::value 0;void threadFunction(int id) {ThreadLocalData::value id; // 修改线程局部变量std::cout Thread id : value ThreadLocalData::value std::endl;
}int main() {std::thread t1(threadFunction, 1);std::thread t2(threadFunction, 2);t1.join();t2.join();return 0;
}五.线程的同步与互斥
多个线程同时访问共享数据时可能导致数据竞争。C提供了多种同步机制如互斥锁mutex、条件变量condition_variable和原子操作atomic。
1.互斥量mutex 线程同步是指在多线程环境中控制线程的执行顺序以确保多个线程在访问共享资源时不会出现冲突。常用的同步机制有条件变量和信号量。 请看下面的例子
#include iostream
#include threadvoid print_message(int a) {for(int i 0;i 1000;i){a;}
}int main() {int a 0;std::thread thread1(print_message,std::ref(a));std::thread thread2(print_message,std::ref(a));thread1.join();thread2.join();std::cout a a std::endl;return 0;
}
当两个线程开启这两个线程会同时对a进行1操作但是如果出现例如线程1与线程二同时拿到a并对a进行操作那么同时返回就会造成a最终仅进行一次1操作这就意味着数据处理错误也就是两个线程对数据的竞争造成的错误那么如何解决这种问题呢
我们不难想到只需要在对a执行1操作仅有一个线程在执行另一个线程阻塞就可以了。这就需要提到mutex互斥量的概念了。 std::mutex 是一个简单的互斥量提供了基本的锁定机制。它确保在同一时刻只有一个线程能够访问被保护的共享资源。 要使用 std::mutex我们需要包含 mutex 头文件。 创建一个 std::mutex 对象。在访问共享资源之前调用 lock() 方法加锁。访问共享资源。调用 unlock() 方法解锁。 #include iostream
#include thread
#include mutexstd::mutex mtx;// 创建互斥量void print_message(int a) {for(int i 0;i 1000;i){mtx.lock();//加锁//访问共享资源a;mtx.unlock();//解锁}
}int main() {int a 0;std::thread thread1(print_message,std::ref(a));std::thread thread2(print_message,std::ref(a));thread1.join();thread2.join();std::cout a a std::endl;return 0;
}
需要记住我们在使用锁的时候必须要记得解锁以免出现死锁现象死锁产生的条件不可剥夺/持有并等待/互斥条件不共享/循环等待。而为了简化并且更安全的使用互斥量C给我们提供了std::lock_guard 来帮助我们。 lock_guard 的特点是锁在作用域结束时自动解锁从而无需手动调用 unlock。使用 lock_guard 可以简化代码并避免因异常或提前返回而导致的锁未释放的问题。 我们分析该锁的底层源码发现当构造函数被调用时该锁会自动加锁当析构函数被调用时该锁会自动解锁所以分析后我们明白了lock_guard创建的对象不能被复制或者移动只能在其局部作用域范围使用。 #include iostream
#include thread
#include mutexstd::mutex mtx;// 创建互斥量void print_message(int a) {for(int i 0;i 1000;i){// 使用 lock_guard 自动加锁std::lock_guardstd::mutex lock(mtx);// 修改共享变量a;// lock_guard 在作用域结束时自动解锁无需手动调用 unlock}
}int main() {int a 0;std::thread thread1(print_message,std::ref(a));std::thread thread2(print_message,std::ref(a));thread1.join();thread2.join();std::cout a a std::endl;return 0;
}
如果我们想要解锁后手动加锁就需要使用 unique_lock 。 std::unique_lock 是一个更灵活的锁管理器支持手动解锁、延迟加锁和条件变量的使用。适用于需要更复杂控制的场景。std::unique_lock 是一个更灵活的锁管理器我们可以用于复杂的控制逻辑例如在某个条件下释放锁以允许其他线程执行。[不支持拷贝因为底层代码明确写出 ...delete 这段代码(delete的作用是用于显式地禁止特定的函数或构造函数)但支持移动] try_lock()try_lock() 方法尝试锁定互斥量如果锁定成功则返回 true否则返回 false并不会阻塞当前线程。try_lock_for(std::chrono::milliseconds(...))try_lock_for() 方法尝试获取锁并在指定的时间段内进行尝试。如果在指定的时间内未能获取锁则返回 false。try_lock_until()try_lock_until() 方法尝试获取锁直到指定的时间点。如果在指定的时间点之前未能获取锁则返回 false。release()release() 方法将 unique_lock 对象的所有权转移给调用者返回互斥量的引用但不会解锁互斥量。 #include iostream
#include thread
#include mutex
#include chronostd::mutex mtx;void tryLockForExample() {std::unique_lockstd::mutex lock(mtx, std::defer_lock); // 不立即锁定if (lock.try_lock_for(std::chrono::milliseconds(100))) { // 100msstd::cout Thread std::this_thread::get_id() acquired the lock.\n;// 执行临界区代码lock.unlock(); // 手动解锁} else {std::cout Thread std::this_thread::get_id() failed to acquire the lock.\n;}
}int main() {std::thread t1(tryLockForExample);std::thread t2(tryLockForExample);t1.join();t2.join();return 0;
}2.C的其他锁的拓展介绍
1std::recursive_mutex
std::recursive_mutex 允许同一线程多次锁定同一互斥量而不会造成死锁。适用于需要递归调用的场景。
#include iostream
#include thread
#include mutexstd::recursive_mutex rmtx;
int sharedCounter 0;void recursiveIncrement(int count) {if (count 0) return;rmtx.lock();sharedCounter;recursiveIncrement(count - 1);// 递归操作rmtx.unlock();
}int main() {std::thread t1(recursiveIncrement, 5);t1.join();std::cout Final Counter: sharedCounter std::endl;return 0;
}std::recursive_mutex 允许同一线程多次加锁避免死锁。适合递归调用的场景但性能相对 std::mutex 较低。 2std::timed_mutex
std::timed_mutex 提供超时功能可以在一定时间内尝试加锁避免长时间等待。
#include iostream
#include thread
#include mutex
#include chronostd::timed_mutex tmtx;
int sharedCounter 0;void tryIncrement() {//计时超过100ms解锁if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {sharedCounter;tmtx.unlock();} else {std::cout Failed to lock. std::endl;}
}int main() {std::thread t1(tryIncrement);std::thread t2(tryIncrement);t1.join();t2.join();std::cout Final Counter: sharedCounter std::endl;return 0;
}3std::shared_mutex
std::shared_mutex 允许多个线程同时读取共享资源但在写入时会独占访问。这种锁在读多写少读取操作的次数远远超过写入操作的次数的场景中特别有效。 特点 读取频繁大多数时间系统会执行读取操作例如从数据库查询数据或从缓存中获取数据。写入不频繁写入操作相对较少通常是在数据更新或新增时进行。 在“读多写少”的场景下使用 std::shared_mutex 或类似的锁机制可以允许多个线程同时读取数据从而提高并发性能减少读取操作的延迟。通过降低写入操作的锁定时间可以减轻对共享资源的争用优化系统资源的使用。 #include iostream
#include thread
#include shared_mutex
#include vectorstd::shared_mutex smtx;
std::vectorint sharedData;void readData(int id) {std::shared_lockstd::shared_mutex lock(smtx);std::cout Reader id sees data size: sharedData.size() std::endl;
}void writeData(int value) {std::unique_lockstd::shared_mutex lock(smtx);sharedData.push_back(value);std::cout Writer added: value std::endl;
}int main() {std::thread writers[3];std::thread readers[3];// 启动写线程for (int i 0; i 3; i) {writers[i] std::thread(writeData, i);}// 启动读线程for (int i 0; i 3; i) {readers[i] std::thread(readData, i);}// 等待所有线程完成for (int i 0; i 3; i) {writers[i].join();readers[i].join();}return 0;
}3.死锁
死锁是指两个或多个线程在执行过程中因为争夺资源而造成的一种互相等待的状态。此时所有线程都无法继续执行导致程序停止运行。 死锁的产生通常需要满足以下四个条件 互斥条件至少有一个资源被一个线程持有其他线程请求该资源时必须等待。占有并等待条件一个线程至少持有一个资源并等待获取其他资源。非抢占条件已经分配给线程的资源不能被其他线程强行抢占。循环等待条件存在一种线程资源的循环等待关系。 如何避免死锁 总是以相同的顺序请求资源。使用超时来尝试获取资源。使用死锁检测算法。 #include iostream
#include thread
#include mutex
#include chronostd::mutex mtx1;
std::mutex mtx2;void thread1(){for(int i 0;i 100;i){mtx1.lock();mtx2.lock();mtx2.unlock();mtx1.unlock();}
}
void thread2(){for(int i 0;i 100;i){mtx2.lock();mtx1.lock();mtx1.unlock();mtx2.unlock();}
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();std::cout Both threads finished executing. std::endl;return 0;
}有两个互斥量 mtx1 和 mtx2 作为共享资源。thread1 首先锁定 mtx1然后尝试锁定 mtx2而 thread2 首先锁定 mtx2然后尝试锁定 mtx1。当 thread1 锁定 mtx1 后若 thread2 锁定了 mtx2thread1 将无法继续执行等待 mtx2 的释放。同时thread2 等待 mtx1 的释放导致两个线程相互等待形成死锁。 解决方法 我们可以将两个方法都先对mtx1加锁然后再mtx2加锁随后先将mtx1解锁在解锁mtx2这样在获取mtx1互斥量如果mtx1没有解锁就不会进行另外一个方法这样可以有效避免死锁。 锁的获取顺序thread1 和 thread2 都以相同的顺序首先获取 mtx1然后获取 mtx2。这意味着无论哪个线程先执行获取锁的顺序始终是一致的。 没有交叉等待死锁通常发生在两个或多个线程相互等待对方持有的锁。在这个例子中线程1和线程2都在同一时刻尝试以相同的顺序获取锁所以它们不会互相阻塞。即使一个线程持有了一个锁另一个线程也会以相同的顺序去请求锁从而避免了交叉等待的情况。 简化的示例尽管这个示例不会死锁但它仍然是一个不推荐的做法因为在更复杂的情况下可能会引入更多的互斥量且不同线程获取锁的顺序可能不一致这时就可能导致死锁。因此在实际开发中应该尽量避免嵌套锁定或者使用其他策略例如死锁检测、超时锁等来处理潜在的死锁问题。 #include iostream
#include thread
#include mutex
#include chronostd::mutex mtx1;
std::mutex mtx2;void thread1(){for(int i 0;i 100;i){mtx1.lock();mtx2.lock();mtx1.unlock();mtx2.unlock();}
}
void thread2(){for(int i 0;i 100;i){mtx1.lock();mtx2.lock();mtx1.unlock();mtx2.unlock();}
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();std::cout Both threads finished executing. std::endl;return 0;
}这样太麻烦而且还需要思考于是我们就会使用 lock() 同时锁定两个互斥量来避免死锁。 #include iostream
#include thread
#include mutex
#include chronostd::mutex mtx1;
std::mutex mtx2;void thread1(){for(int i 0; i 100; i){std::lock(mtx1, mtx2); // 同时锁定两个互斥量std::lock_guardstd::mutex lg1(mtx1, std::adopt_lock);std::lock_guardstd::mutex lg2(mtx2, std::adopt_lock);// 这里可以进行共享资源的操作}
}void thread2(){for(int i 0; i 100; i){std::lock(mtx1, mtx2); // 同时锁定两个互斥量std::lock_guardstd::mutex lg1(mtx1, std::adopt_lock);std::lock_guardstd::mutex lg2(mtx2, std::adopt_lock);// 这里可以进行共享资源的操作}
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();std::cout Both threads finished executing. std::endl;return 0;
}
4.条件变量Condition Variable
条件变量std::condition_variable是C11引入的一种用于线程同步的机制主要用于解决多个线程之间的协调问题。条件变量配合互斥锁可以让线程在特定条件下等待或被唤醒。 工作机制 等待线程调用 wait(lock,状态) 后线程会进入阻塞状态直到满足条件或被其他线程唤醒。wait 需要配合 std::unique_lockstd::mutex 一起使用以便解锁和重新锁定。唤醒线程notify_one() 会唤醒一个等待的线程notify_all 则唤醒所有等待的线程。当条件满足时调用 notify_one 或 notify_all 可以让等待的线程继续执行。 #include iostream
#include thread
#include mutex
#include condition_variable
#include queuestd::queueint q; // 消息队列(共享队列表示生产的商品)
std::condition_variable cv; // 条件变量用于线程间同步
std::mutex mtx; // 互斥锁保护共享资源// 生产者
void producer() {for (int i 0; i 10; i) {{std::unique_lockstd::mutex lock(mtx);// 共享变量q.push(i);// 通知消费者来获取cv.notify_one();std::cout Producer task: i std::endl;}std::this_thread::sleep_for(std::chrono::microseconds(100));}
}// 消费者
void consumer() {while (1) {std::unique_lockstd::mutex lock(mtx);// 如果队列为空需要等待//bool isempty q.empty();//cv.wait(lock,!isempty);// 如果为true则不阻塞往下走如果为false则阻塞等待cv.wait(lock, [](){ // lambda表达式return !q.empty();});int value q.front();q.pop();std::cout Consumer value: value std::endl;}
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
} 条件变量的优点条件变量能够有效解决线程之间的同步问题减少不必要的轮询提升多线程程序的效率。适用场景条件变量适用于各种等待和通知的场景尤其适合需要线程等待某个条件的情况如生产者-消费者模式、延迟初始化等。注意事项在使用 wait 时建议传入条件判断防止虚假唤醒。 5.call_once的使用
std::call_once 是 C11 引入的一个用于线程安全的函数主要用于确保某段代码在多线程环境中只执行一次。它通常用于初始化操作特别适合只需要执行一次的操作比如单例模式中的实例创建或资源初始化。 首先我们创建一个Log日志类并且满足单例模式全局只有一个实例对象以至于初始化操作只能执行一次饿汉模式
#include iostream
#include thread
#include mutex
#include chronoclass Log {
public:Log() {};// delete代表禁用下面两个方法Log(const Log log) delete;Log operator(const Log log) delete;// 饿汉模式声明后无需掉用构造方法直接使用即可但是切记只能有一个对象static Log GetInstance() {static Log* log nullptr;if (!log) {log new Log;}return *log;}void printLog(std::string msg) {std::cout __TIME__ msg std::endl;}
};
void print_error() {Log::GetInstance().printLog(error);
}int main() {std::thread th1(print_error);std::thread th2(print_error);th1.join();th2.join();return 0;
}
在这段代码中如果两个线程同时进行那么将会有种情况能够同时通过指针创建对象这样就会在程序内创建两个Log对象不满足我们的要求所以我们需要使用 call_once() 函数确保函数仅能调用以此。 它需要一个 std::once_flag 对象用于标记某段代码是否已经执行过。使用 std::call_once 传入 std::once_flag 和需要执行的函数确保函数只执行一次。 #include iostream
#include thread
#include mutex
#include chronoLog* log nullptr;
static std::once_flag once;// once_flag对象用于标记是否已经执行class Log {
public:Log() {};// delete代表禁用下面两个方法Log(const Log log) delete;Log operator(const Log log) delete;// 饿汉模式声明后无需掉用构造方法直接使用即可但是切记只能有一个对象static Log GetInstance() {// 确保 init() 只会被执行一次std::call_once(once,init);return *log;}static void init() {if (!log) {log new Log;}}void printLog(std::string msg) {std::cout __TIME__ msg std::endl;}
};void print_error() {Log::GetInstance().printLog(error);
}int main() {std::thread th1(print_error);std::thread th2(print_error);th1.join();th2.join();return 0;
} std::call_once 的工作机制: std::call_once 使用 std::once_flag 标记调用状态每个 std::once_flag 对象只能与 std::call_once 绑定一次。在多线程环境中std::call_once 会确保只有一个线程执行给定的函数其他线程会被阻塞直到第一次调用完成。 6.atomic 原子操作
在多线程编程中当多个线程同时访问和修改共享变量时容易出现数据竞争问题。我们在学习互斥量就使用锁的知识来处理但是这样无外乎是将一段代码锁起来而没做到变量的独立。
C标准库提供了std::atomic类模板用于确保对变量的访问是原子的即不会在一个线程中修改变量的过程中被另一个线程中断。 std::atomic可以用于多种类型的数据例如整数、布尔值、指针等。它提供了多种原子操作避免了手动加锁的复杂性。 std::atomic操作的效率通常高于锁但并不适合复杂的同步场景。 #include iostream
#include atomic
#include thread
#include vectorstd::atomicint counter 0; // 使用std::atomic包装一个整型变量void increment(int n) {for (int i 0; i n; i) {counter.fetch_add(1, std::memory_order_relaxed); // 原子加1}
}int main() {int num_threads 10;int increments_per_thread 1000;std::vectorstd::thread threads;// 创建多个线程来并发地执行increment函数for (int i 0; i num_threads; i) {threads.emplace_back(increment, increments_per_thread);}// 等待所有线程完成for (auto th : threads) {th.join();}std::cout Final counter value: counter.load() std::endl;return 0;
}原子操作的常用方法 fetch_add(val): 原子加法增加指定的值并返回旧值。fetch_sub(val): 原子减法减少指定的值并返回旧值。store(val): 将一个值存储到 atomic对象中。load(): 从atomic对象中读取值。exchange(val): 原子地设置新值并返回旧值。compare_exchange_weak(expected,val)【适合循环使用】 / compare_exchange_strong(expected,val)【适合单词执行】: 比较并交换值。在修改atomic变量的值前先检查变量的当前值是否符合预期值(expected)如果符合就执行交换操作否则不做任何修改。如果变量的当前值等于expected则将其修改为给定的新值 (desired)并返回true表示成功。如果变量的当前值不等于expected则修改expected为当前的实际值并返回false表示操作失败。 #include iostream
#include atomic
#include threadstd::atomicint value(100);void try_change_value(int expected, int new_value) {int temp expected;if (value.compare_exchange_weak(temp, new_value)) {std::cout Thread std::this_thread::get_id() successfully changed value to new_value std::endl;} else {std::cout Thread std::this_thread::get_id() failed to change value. Current value is value.load() std::endl;}
}int main() {std::thread t1(try_change_value, 100, 200);std::thread t2(try_change_value, 100, 300);t1.join();t2.join();std::cout Final value: value.load() std::endl;return 0;
}// Thread 28892 successfully changed value to 200
// Thread 20096 failed to change value. Current value is 200
// Final value: 200 六.线程池的构建与使用 1.首先创建一个线程池类
1创建成员变量
class ThreadPool {
private:std::vectorstd::thread threads; // 线程池std::queuestd::functionvoid() tasks; // 任务队列std::mutex mtx; // 保护任务队列的互斥锁std::condition_variable cv; // 条件变量用于线程同步bool stop; // 标志位指示线程池是否应该停止
}
2 构造函数
ThreadPool(int numThreads) :stop(false) {// 根据参数创建 numThreads 个线程到线程池当中for (int i 0; i numThreads; i) {// 创建线程至线程池内threads.emplace_back([this] {//使用线程在队列拿任务while (1) {std::unique_lockstd::mutex lock(mtx);// 如果队列为空需要等待如果为true则不阻塞往下走如果为false则阻塞等待cv.wait(lock, [this] {return !tasks.empty() || stop;});// 如果线程状态停止并且任务队列为空直接返回if (stop tasks.empty()) {return;}//线程没有终止需要取列表最左边的任务std::functionvoid() task(std::move(tasks.front()));tasks.pop();lock.unlock();task();}}); // 直接构造新变量以此节省资源而push_back是通过拷贝构造函数来实现的}
} 首先传递参数来创建 numThreads 个线程将它们加入到 threads 向量中。每个线程执行一个匿名函数 [this] { ... }。 在线程的循环中 使用 std::unique_lockstd::mutex 加锁保护 tasks 队列。使用 cv.wait(lock, [this] { return !tasks.empty() || stop; }); 等待条件变量 条件如果 tasks 不为空或 stop 为 true条件满足继续执行。否则阻塞线程直到条件满足。底层代码是一个while循环的递归操作如果 stop 为 true 且 tasks 为空则退出线程。取出队列中的任务并移除tasks.pop()。解锁 mtx防止任务执行期间持锁影响其他线程。执行任务 task()。 什么是 function 函数模板 std::function 是 C11 标准库中引入的一个通用函数包装器用于存储、传递和调用任意可调用的目标对象包括普通函数、lambda 表达式、函数对象、以及成员函数指针等。std::function 可以让我们在编写通用代码时不必关心具体的函数类型从而提高代码的灵活性和可扩展性。 #include functional // 引入头文件
std::function返回类型(参数类型列表) 函数变量名;例 #include iostream
#include functional
int add(int a, int b) {return a b;
}
int main() {std::functionint(int, int) func add;std::cout Result of add: func(3, 5) std::endl; // 输出 8return 0;
}包装 lambda 表达式 #include iostream
#include functional
int main() {std::functionint(int, int) func [](int a, int b) {return a * b;};std::cout Result of lambda: func(3, 5) std::endl; // 输出 15return 0;
}使用 std::function 作为参数进行回调 #include iostream
#include functional
void executeOperation(const std::functionint(int, int) operation, int x, int y) {std::cout Result: operation(x, y) std::endl;
}
int main() {std::functionint(int, int) add [](int a, int b) { return a b; };std::functionint(int, int) multiply [](int a, int b) { return a * b; };executeOperation(add, 3, 5); // 输出 8executeOperation(multiply, 3, 5); // 输出 15return 0;
}3析构函数
~ThreadPool() {{std::unique_lockstd::mutex lock(mtx);stop true;}//通知所有线程将任务队列内的所有任务取完cv.notify_all();for (auto t : threads) {t.join();}
}将 stop 设置为 true通知线程池中的所有线程退出。调用 cv.notify_all() 唤醒所有等待线程。使用 join() 等待所有线程完成工作。 4添加任务 enqueue() 方法
templateclass T, class... Args
void enqueue(T t, Args... args) {//将一个函数与特定参数绑定起来以便在后续调用时可以通过调用返回的可调用对象来执行该函数std::functionvoid() task std::bind(std::forwardT(t), std::forwardArgs(args)...);{std::unique_lockstd::mutex lock(mtx);tasks.emplace(std::move(task));}cv.notify_one();
}enqueue 是一个模板函数用于将任务添加到线程池。使用 std::bind 和 std::function 将传入的任务 t 及其参数 args 绑定成一个可调用的 task 对象。task 被放入任务队列 tasks 中加入互斥锁 mtx 确保线程安全。调用 cv.notify_one() 唤醒一个等待中的线程。 什么是 std::bind ? std::bind 是一个函数模板它的作用是将一个函数及其参数进行绑定生成一个新的函数对象该函数对象可以被调用执行时使用绑定的参数。 std::bind(可调用对象, 参数列表);#include iostream
#include functional
void print_sum(int a, int b) {std::cout Sum: a b std::endl;
}
int main() {// 绑定 print_sum 函数和参数 2, 3生成一个新函数对象 add_two_and_threeauto add_two_and_three std::bind(print_sum, 2, 3);add_two_and_three(); // 输出 Sum: 5return 0;
}在这个例子中std::bind 将 print_sum 函数与参数 2 和 3 绑定生成了一个新的可调用对象 add_two_and_three后续调用时会执行 print_sum(2, 3)。 什么是 std::forward ? std::forward 是 C11 引入的一个工具用于实现“完美转发”perfect forwarding。它的作用是将函数参数原封不动地传递给另一个函数同时保持参数的“值类别”左值或右值。 在模板函数中如果我们需要保持传递参数的左值或右值属性就需要用到 std::forward。这样可以避免不必要的拷贝提升效率。 #include iostream
#include utilityvoid print(int n) { std::cout Left value: n std::endl; }
void print(int n) { std::cout Right value: n std::endl; }templatetypename T
void wrapper(T t) {print(std::forwardT(t)); // 保持 t 的值类别
}int main() {int x 5;wrapper(x); // 输出 Left value: 5wrapper(10); // 输出 Right value: 10return 0;
}当 wrapper 接受左值 x 时std::forward 将其原样传递为左值。当 wrapper 接受右值 10 时std::forward 将其原样传递为右值。 什么是 std::move std::move 是 C11 引入的标准库函数用于将对象转换为右值引用。它本身并不真正“移动”对象而是强制将一个左值转换为右值引用这样编译器就可以优先选择调用可“移动”的版本的构造函数或赋值运算符而非复制版本。 std::move 用于将左值转换为右值引用以便调用移动构造或移动赋值等高效的移动操作避免拷贝。这样可以优化性能尤其是在处理大型对象或容器时。 #include iostream
#include vector
#include utility // 包含std::moveint main() {std::vectorint v1 {1, 2, 3};std::vectorint v2;// 使用 std::move 将 v1 的资源移动到 v2v2 std::move(v1);std::cout v1 size: v1.size() std::endl; // 输出 v1 size: 0std::cout v2 size: v2.size() std::endl; // 输出 v2 size: 3return 0;
}std::move(v1) 将 v1 转换为右值引用使得赋值操作 v2 std::move(v1) 使用了 v1 的移动构造函数或移动赋值运算符而不是拷贝构造函数或拷贝赋值运算符。这意味着 v1 的数据会被“移动”到 v2之后 v1 的数据将变为空但对象依然有效。结果v1 不再包含数据v2 获得了 v1 的资源。这种操作避免了内存分配和复制提高了效率。 std::move 使用注意事项 std::move 仅仅是一个类型转换工具它不会真正移动对象。使用 std::move 后原对象仍然存在但处于未定义状态只能做销毁或重新赋值不能继续使用原数据。确保只在不再需要使用原对象时才使用 std::move否则可能导致意外问题。 2.创建 main 内
int main() {ThreadPool pool(4);for (int i 0; i 10; i) {pool.enqueue([i] {{std::unique_lockstd::mutex lock(mtxall);std::cout task: i is running std::endl;}std::this_thread::sleep_for(std::chrono::seconds(1));{std::unique_lockstd::mutex lock(mtxall);std::cout task: i is done std::endl;}});}return 0;
}创建 ThreadPool pool(4);初始化一个包含 4 个线程的线程池。向线程池中提交 10 个任务每个任务输出当前任务编号 i 的状态。 使用 std::unique_lockstd::mutex lock(mtxall); 保护 std::cout确保任务编号和状态按顺序输出不发生竞态。线程池自动执行任务。 #include iostream
#include thread
#include mutex
#include condition_variable
#include queue
#include vector
#include functional // 函数模版std::mutex mtxall; // 互斥锁保护共享资源class ThreadPool {
private:std::vectorstd::thread threads; // 线程池std::queuestd::functionvoid() tasks; // 消息队列(共享队列表示生产的商品)std::mutex mtx; // 互斥锁保护共享资源std::condition_variable cv; // 条件变量用于线程间同步bool stop; // 线程池状态public:ThreadPool(int numThreads) :stop(false) {for (int i 0; i numThreads; i) {// 创建线程至线程池内threads.emplace_back([this] {//使用线程在队列拿任务while (1) {std::unique_lockstd::mutex lock(mtx);// 如果队列为空需要等待如果为true则不阻塞往下走如果为false则阻塞等待cv.wait(lock, [this] {return !tasks.empty() || stop;});// 如果线程状态停止并且任务队列为空直接返回if (stop tasks.empty()) {return;}//线程没有终止需要取列表最左边的任务std::functionvoid() task(std::move(tasks.front()));tasks.pop();lock.unlock();// 解锁task();}}); // 直接构造新变量以此节省资源而push_back是通过拷贝构造函数来实现的}}~ThreadPool() {{std::unique_lockstd::mutex lock(mtx);stop false;}//通知所有线程将任务队列内的所有任务取完cv.notify_all();for (auto t : threads) {t.join();}}templateclass T,class... Argsvoid enqueue(T t,Args... args) { // 在函数模版内右值引用是万能引用// 获取任务std::functionvoid()task std::bind(std::forwardT(t), std::forwardT(args)...);// 将任务放入任务列表内{std::unique_lockstd::mutex lock(mtx);tasks.emplace(std::move(task));}cv.notify_one();}
};int main() {ThreadPool pool(4);for (int i 0; i 10; i) {pool.enqueue([i] {// 为了避免线程竞争而打印乱码加入锁就可以解决或者使用printf()函数打印{std::unique_lockstd::mutex lock(mtxall);std::cout task: i is runing std::endl;}std::this_thread::sleep_for(std::chrono::seconds(1));{std::unique_lockstd::mutex lock(mtxall);std::cout task: i is done std::endl;}});}return 0;
}