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

C++ --- 多线程的使用

目录

一.什么是线程?

线程的特点:

线程的组成:

二.什么是进程?

进程的特点:

进程的组成:

三.线程与进程的关系:

四.C++的Thread方法的使用:

1.创建线程:

2.join()方法:

3.detach()方法:

detach()方法细节:

如何做到隔离线程?

4.joinable():

5.native_handle():

6.hardware_concurrency():

7.线程的休眠:

(1)std::this_thread::sleep_for():

 (2)std::this_thread::sleep_until():

8.线程的局部存储(Thread Local Storage): 

注意事项:

五.线程的同步与互斥:

1.互斥量(mutex):

2.C++的其他锁的拓展介绍:

(1)std::recursive_mutex:

(2)std::timed_mutex:

(3)std::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)是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。线程是程序执行的基本单位,它包含了执行所需的所有信息,如程序计数器、栈和局部变量等。

优点:线程的创建和销毁开销小,线程之间可以方便地共享数据,减少了数据传输的开销,通过多线程可以充分利用多核处理器,提高程序的执行效率。

缺点:多个线程共享资源可能导致数据竞争和同步问题,需要使用锁等机制来保护共享数据。多线程程序的调试和错误排查相对复杂,可能会出现死锁、竞争条件等问题。

线程的特点:

  1. 轻量级:线程是比进程更小的执行单位,创建和销毁的开销较小。
  2. 共享资源:同一进程中的多个线程可以共享进程的资源,如内存空间、文件描述符等。
  3. 独立执行:每个线程拥有自己的执行栈和程序计数器,可以独立执行任务。
  4. 并发执行:多个线程可以并发执行,从而提高程序的执行效率。

线程的组成:

  • 线程ID:每个线程都有一个唯一的标识符。
  • 程序计数器:指向当前线程执行的指令。
  • :保存线程的局部变量和函数调用记录。
  • 寄存器:存储线程的上下文信息。

二.什么是进程?

进程(Process)是一个正在执行的程序的实例,具有自己独立的内存空间和系统资源。进程是操作系统进行资源分配和调度的基本单位。

优点:进程之间互不干扰,安全性高。一个进程的崩溃不会影响其他进程。

缺点:进程的创建和销毁需要较大的开销。进程之间的通信相对复杂,通常需要使用进程间通信(IPC)机制。

进程的特点:

  1. 资源独立:每个进程都有自己的内存空间和资源,进程之间相互独立,互不干扰。
  2. 开销大:进程的创建、销毁和切换开销相对较大。
  3. 更高的隔离性:进程之间不直接共享内存,安全性更高。

进程的组成:

  • 进程ID:每个进程都有一个唯一的标识符。
  • 内存空间:包括代码段、数据段、堆和栈。
  • 程序状态:记录进程的当前状态(就绪、运行、阻塞等)。
  • 资源信息:记录进程所使用的系统资源(文件描述符、信号量等)。

三.线程与进程的关系:

进程是线程的容器:一个进程可以包含多个线程。所有线程共享该进程的资源(如内存),但每个线程有自己的栈和寄存器。

调度与切换:进程切换开销较大,因为需要保存和恢复整个进程的上下文。而线程切换开销较小,因为只需保存和恢复线程的上下文。

并发与并行:多个进程可以并行执行,多个线程在同一进程内可以并发执行。多线程程序比多进程程序更易于实现并发。

四.C++的Thread方法的使用:

通常建议在较大的项目中和公共代码库中使用 std:: 前缀,以提高代码的可读性和可维护性。在小型项目或练习代码中,如果确实没有命名冲突,可以适当使用 using namespace std;,但最好在源文件的开头避免使用全局命名空间污染。一般来说,保持良好的命名空间管理是最佳实践。 

1.创建线程:

线程一共有三种创建方法:

  1. 函数指针:thread thread_name(函数方法名,参数1,参数2,....);
  2. 函数对象:thread thread_name(函数方法名(),参数1,参数2,....);
  3. 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_ptr<std::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()用于让线程暂停执行指定时间。

(1)std::this_thread::sleep_for():

std::this_thread::sleep_for()用于让当前线程休眠一段时间。它接收一个表示时间长度的参数(std::chrono::duration),使线程暂停指定的时间。

std::this_thread::sleep_for(duration);

 duration:传入一个std::chrono::duration对象,表示线程需要休眠的时间长度。支持的时间单位包括std::chrono::secondsstd::chrono::millisecondsstd::chrono::microseconds等。

#include <iostream>
#include <thread>
#include <chrono>int 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;
}

 (2)std::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 <chrono>int 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 <thread>thread_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 <thread>struct 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 <thread>void 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 <mutex>std::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 <mutex>std::mutex mtx;// 创建互斥量void print_message(int& a) {for(int i = 0;i < 1000;i++){// 使用 lock_guard 自动加锁std::lock_guard<std::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 <chrono>std::mutex mtx;void tryLockForExample() {std::unique_lock<std::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++的其他锁的拓展介绍:

(1)std::recursive_mutex:

std::recursive_mutex 允许同一线程多次锁定同一互斥量而不会造成死锁。适用于需要递归调用的场景。

#include <iostream>
#include <thread>
#include <mutex>std::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 较低。

(2)std::timed_mutex:

std::timed_mutex 提供超时功能,可以在一定时间内尝试加锁,避免长时间等待。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::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;
}

(3)std::shared_mutex:

std::shared_mutex 允许多个线程同时读取共享资源,但在写入时会独占访问。这种锁在读多写少(读取操作的次数远远超过写入操作的次数)的场景中特别有效。

特点:
  1. 读取频繁:大多数时间,系统会执行读取操作,例如从数据库查询数据或从缓存中获取数据。
  2. 写入不频繁:写入操作相对较少,通常是在数据更新或新增时进行。

在“读多写少”的场景下,使用 std::shared_mutex 或类似的锁机制,可以允许多个线程同时读取数据,从而提高并发性能,减少读取操作的延迟。通过降低写入操作的锁定时间,可以减轻对共享资源的争用,优化系统资源的使用。

#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>std::shared_mutex smtx;
std::vector<int> sharedData;void readData(int id) {std::shared_lock<std::shared_mutex> lock(smtx);std::cout << "Reader " << id << " sees data size: " << sharedData.size() << std::endl;
}void writeData(int value) {std::unique_lock<std::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.死锁:

死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的状态。此时,所有线程都无法继续执行,导致程序停止运行。

死锁的产生通常需要满足以下四个条件:

  1. 互斥条件:至少有一个资源被一个线程持有,其他线程请求该资源时必须等待。
  2. 占有并等待条件:一个线程至少持有一个资源,并等待获取其他资源。
  3. 非抢占条件:已经分配给线程的资源不能被其他线程强行抢占。
  4. 循环等待条件:存在一种线程资源的循环等待关系。

如何避免死锁?

  • 总是以相同的顺序请求资源。
  • 使用超时来尝试获取资源。
  • 使用死锁检测算法。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::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;
}

有两个互斥量 mtx1mtx2 作为共享资源。thread1 首先锁定 mtx1,然后尝试锁定 mtx2,而 thread2 首先锁定 mtx2,然后尝试锁定 mtx1。当 thread1 锁定 mtx1 后,若 thread2 锁定了 mtx2thread1 将无法继续执行,等待 mtx2 的释放。同时,thread2 等待 mtx1 的释放,导致两个线程相互等待,形成死锁。

解决方法:

我们可以将两个方法都先对mtx1加锁,然后再mtx2加锁,随后先将mtx1解锁,在解锁mtx2,这样在获取mtx1互斥量如果mtx1没有解锁就不会进行另外一个方法,这样可以有效避免死锁。

  • 锁的获取顺序thread1thread2 都以相同的顺序首先获取 mtx1,然后获取 mtx2。这意味着,无论哪个线程先执行,获取锁的顺序始终是一致的。

  • 没有交叉等待:死锁通常发生在两个或多个线程相互等待对方持有的锁。在这个例子中,线程1和线程2都在同一时刻尝试以相同的顺序获取锁,所以它们不会互相阻塞。即使一个线程持有了一个锁,另一个线程也会以相同的顺序去请求锁,从而避免了交叉等待的情况。

  • 简化的示例:尽管这个示例不会死锁,但它仍然是一个不推荐的做法,因为在更复杂的情况下,可能会引入更多的互斥量,且不同线程获取锁的顺序可能不一致,这时就可能导致死锁。因此,在实际开发中,应该尽量避免嵌套锁定,或者使用其他策略(例如死锁检测、超时锁等)来处理潜在的死锁问题。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::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 <chrono>std::mutex mtx1;
std::mutex mtx2;void thread1(){for(int i = 0; i < 100; i++){std::lock(mtx1, mtx2); // 同时锁定两个互斥量std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lg2(mtx2, std::adopt_lock);// 这里可以进行共享资源的操作}
}void thread2(){for(int i = 0; i < 100; i++){std::lock(mtx1, mtx2); // 同时锁定两个互斥量std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);std::lock_guard<std::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)是C++11引入的一种用于线程同步的机制,主要用于解决多个线程之间的协调问题。条件变量配合互斥锁可以让线程在特定条件下等待或被唤醒。

工作机制:

  • 等待线程:调用 wait(lock,状态) 后,线程会进入阻塞状态,直到满足条件或被其他线程唤醒。wait 需要配合 std::unique_lock<std::mutex> 一起使用,以便解锁和重新锁定。
  • 唤醒线程notify_one() 会唤醒一个等待的线程,notify_all 则唤醒所有等待的线程。当条件满足时,调用 notify_onenotify_all 可以让等待的线程继续执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::queue<int> q; // 消息队列(共享队列,表示生产的商品)
std::condition_variable cv; // 条件变量,用于线程间同步
std::mutex mtx; // 互斥锁,保护共享资源// 生产者
void producer() {for (int i = 0; i < 10; i++) {{std::unique_lock<std::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_lock<std::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 是 C++11 引入的一个用于线程安全的函数,主要用于确保某段代码在多线程环境中只执行一次。它通常用于初始化操作,特别适合只需要执行一次的操作,比如单例模式中的实例创建或资源初始化。

 首先我们创建一个Log日志类并且满足单例模式(全局只有一个实例对象,以至于初始化操作只能执行一次)(饿汉模式):

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>class 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 <chrono>Log* 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 <vector>std::atomic<int> 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::vector<std::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 <thread>std::atomic<int> 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::vector<std::thread> threads; // 线程池std::queue<std::function<void()>> 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_lock<std::mutex> lock(mtx);// 如果队列为空,需要等待(如果为true则不阻塞往下走,如果为false则阻塞等待)cv.wait(lock, [this] {return !tasks.empty() || stop;});// 如果线程状态停止并且任务队列为空,直接返回if (stop && tasks.empty()) {return;}//线程没有终止,需要取列表最左边的任务std::function<void()> task(std::move(tasks.front()));tasks.pop();lock.unlock();task();}}); // 直接构造新变量以此节省资源,而push_back是通过拷贝构造函数来实现的}
}

首先传递参数来创建 numThreads 个线程,将它们加入到 threads 向量中。每个线程执行一个匿名函数 [this] { ... }。 

在线程的循环中:

  • 使用 std::unique_lock<std::mutex> 加锁(保护 tasks 队列)。
  • 使用 cv.wait(lock, [this] { return !tasks.empty() || stop; }); 等待条件变量:
    • 条件:如果 tasks 不为空或 stoptrue,条件满足,继续执行。
    • 否则,阻塞线程直到条件满足。(底层代码是一个while循环的递归操作)
  • 如果 stoptruetasks 为空,则退出线程。
  • 取出队列中的任务并移除(tasks.pop())。
  • 解锁 mtx,防止任务执行期间持锁,影响其他线程。
  • 执行任务 task()
什么是 function 函数模板? 

std::function 是 C++11 标准库中引入的一个通用函数包装器,用于存储、传递和调用任意可调用的目标对象,包括普通函数、lambda 表达式、函数对象、以及成员函数指针等。std::function 可以让我们在编写通用代码时不必关心具体的函数类型,从而提高代码的灵活性和可扩展性。

#include <functional> // 引入头文件
std::function<返回类型(参数类型列表)> 函数变量名;

例:

#include <iostream>
#include <functional>
int add(int a, int b) {return a + b;
}
int main() {std::function<int(int, int)> func = add;std::cout << "Result of add: " << func(3, 5) << std::endl; // 输出 8return 0;
}

 包装 lambda 表达式:

#include <iostream>
#include <functional>
int main() {std::function<int(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::function<int(int, int)>& operation, int x, int y) {std::cout << "Result: " << operation(x, y) << std::endl;
}
int main() {std::function<int(int, int)> add = [](int a, int b) { return a + b; };std::function<int(int, int)> multiply = [](int a, int b) { return a * b; };executeOperation(add, 3, 5);       // 输出 8executeOperation(multiply, 3, 5);  // 输出 15return 0;
}

(3)析构函数:

~ThreadPool() {{std::unique_lock<std::mutex> lock(mtx);stop = true;}//通知所有线程将任务队列内的所有任务取完cv.notify_all();for (auto& t : threads) {t.join();}
}
  • stop 设置为 true,通知线程池中的所有线程退出。
  • 调用 cv.notify_all() 唤醒所有等待线程。
  • 使用 join() 等待所有线程完成工作。

(4)添加任务 enqueue() 方法:

template<class T, class... Args>
void enqueue(T &&t, Args&&... args) {//将一个函数与特定参数绑定起来,以便在后续调用时可以通过调用返回的可调用对象来执行该函数std::function<void()> task = std::bind(std::forward<T>(t), std::forward<Args>(args)...);{std::unique_lock<std::mutex> lock(mtx);tasks.emplace(std::move(task));}cv.notify_one();
}
  • enqueue 是一个模板函数,用于将任务添加到线程池。
  • 使用 std::bindstd::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: 5"return 0;
}

在这个例子中,std::bindprint_sum 函数与参数 23 绑定,生成了一个新的可调用对象 add_two_and_three,后续调用时会执行 print_sum(2, 3)。 

什么是 std::forward ?

std::forward 是 C++11 引入的一个工具,用于实现“完美转发”(perfect forwarding)。它的作用是将函数参数原封不动地传递给另一个函数,同时保持参数的“值类别”(左值或右值)。

在模板函数中,如果我们需要保持传递参数的左值或右值属性,就需要用到 std::forward。这样可以避免不必要的拷贝,提升效率。

#include <iostream>
#include <utility>void print(int& n) { std::cout << "Left value: " << n << std::endl; }
void print(int&& n) { std::cout << "Right value: " << n << std::endl; }template<typename T>
void wrapper(T&& t) {print(std::forward<T>(t));  // 保持 t 的值类别
}int main() {int x = 5;wrapper(x);       // 输出 "Left value: 5"wrapper(10);      // 输出 "Right value: 10"return 0;
}
  • wrapper 接受左值 x 时,std::forward 将其原样传递为左值。
  • wrapper 接受右值 10 时,std::forward 将其原样传递为右值。
 什么是 std::move ?

std::move 是 C++11 引入的标准库函数,用于将对象转换为右值引用。它本身并不真正“移动”对象,而是强制将一个左值转换为右值引用,这样编译器就可以优先选择调用可“移动”的版本的构造函数或赋值运算符,而非复制版本。

std::move 用于将左值转换为右值引用,以便调用移动构造或移动赋值等高效的移动操作,避免拷贝。这样可以优化性能,尤其是在处理大型对象或容器时。

#include <iostream>
#include <vector>
#include <utility> // 包含std::moveint main() {std::vector<int> v1 = {1, 2, 3};std::vector<int> 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_lock<std::mutex> lock(mtxall);std::cout << "task: " << i << " is running" << std::endl;}std::this_thread::sleep_for(std::chrono::seconds(1));{std::unique_lock<std::mutex> lock(mtxall);std::cout << "task: " << i << " is done" << std::endl;}});}return 0;
}
  • 创建 ThreadPool pool(4);,初始化一个包含 4 个线程的线程池。
  • 向线程池中提交 10 个任务,每个任务输出当前任务编号 i 的状态。
    • 使用 std::unique_lock<std::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::vector<std::thread> threads; // 线程池std::queue<std::function<void()>> 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_lock<std::mutex> lock(mtx);// 如果队列为空,需要等待(如果为true则不阻塞往下走,如果为false则阻塞等待)cv.wait(lock, [this] {return !tasks.empty() || stop;});// 如果线程状态停止并且任务队列为空,直接返回if (stop && tasks.empty()) {return;}//线程没有终止,需要取列表最左边的任务std::function<void()> task(std::move(tasks.front()));tasks.pop();lock.unlock();// 解锁task();}}); // 直接构造新变量以此节省资源,而push_back是通过拷贝构造函数来实现的}}~ThreadPool() {{std::unique_lock<std::mutex> lock(mtx);stop = false;}//通知所有线程将任务队列内的所有任务取完cv.notify_all();for (auto& t : threads) {t.join();}}template<class T,class... Args>void enqueue(T &&t,Args&&... args) { // 在函数模版内,右值引用是万能引用// 获取任务std::function<void()>task = std::bind(std::forward<T>(t), std::forward<T>(args)...);// 将任务放入任务列表内{std::unique_lock<std::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_lock<std::mutex> lock(mtxall);std::cout << "task: " << i << " is runing" << std::endl;}std::this_thread::sleep_for(std::chrono::seconds(1));{std::unique_lock<std::mutex> lock(mtxall);std::cout << "task: " << i << " is done" << std::endl;}});}return 0;
}

相关文章:

C++ --- 多线程的使用

目录 一.什么是线程&#xff1f; 线程的特点&#xff1a; 线程的组成&#xff1a; 二.什么是进程&#xff1f; 进程的特点&#xff1a; 进程的组成&#xff1a; 三.线程与进程的关系&#xff1a; 四.C的Thread方法的使用&#xff1a; 1.创建线程&#xff1a; 2.join(…...

百度笔试(10.29)

判断s字符串是否可以通过添加某些字符变成t字符串&#xff0c;s是否是t的子序列 s input() n int(input()) st set() res [] for idx in range(n):t input()if t in st or len(t) < len(s):continuest.add(t)i,j 0,0while i < len(s) and j < len(t):if s[i] …...

数据库版本更新后,如何迁移数据?

数据库版本更新后迁移数据是一个需要谨慎处理的过程。以下是一般步骤和最佳实践&#xff1a; 1. **备份数据**&#xff1a; 在进行任何迁移之前&#xff0c;确保对现有数据库进行完整备份。这可以是物理备份&#xff08;如数据库文件的拷贝&#xff09;或逻辑备份&#xff…...

Chrome与火狐的安全功能全面评估

在当今数字化时代&#xff0c;网络安全已成为用户最为关注的问题之一。作为两款广受欢迎的浏览器&#xff0c;Chrome和火狐&#xff08;Firefox&#xff09;都提供了多种安全功能来保护用户的在线隐私和数据安全。本文将全面评估这两款浏览器的安全功能&#xff0c;帮助用户更好…...

微服务设计模式 - 重试模式(Retry Pattern)

微服务设计模式 - 重试模式&#xff08;Retry Pattern&#xff09; 定义 重试模式&#xff08;Retry Pattern&#xff09;是一种微服务中的设计模式&#xff0c;用于在临时性失败&#xff08;如网络故障或暂时不可用的服务&#xff09;发生时&#xff0c;自动重新尝试请求&…...

DNS配置

1.搭建dns服务器能够对自定义的正向或者反向域完成数据解析查询。 2.配置从DNS服务器&#xff0c;对主dns服务器进行数据备份。 正反向解析 [rootlocalhost ~]# vim /etc/named.conf options {listen-on port 53 { 192.168.111.130; };directory "/var/named&quo…...

【Linux指令】---获取进程的PID

获取进程的PID getpid()函数...

在centos中安装cmake

安装依赖工具: sudo yum install -y epel-release sudo yum groupinstall -y "Development Tools" sudo yum install -y wget wget 下载 CMake 3.24 的压缩包: wget https://github.com/Kitware/CMake/releases/download/v3.24.0/cmake-3.24.0-linux-x86_64.tar.gz …...

【补补漏洞吧 | 02】等保测评ZooKeeperElasticsearch未授权访问漏洞补漏方法

一、项目背景 客户新系统上线&#xff0c;因为行业网络安全要求&#xff0c;需要做等保测评&#xff0c; 通过第三方漏扫工具扫描系统&#xff0c;漏扫报告显示ZooKeeper和 Elasticsearch 服务各拥有一个漏洞&#xff0c;具体结果如下&#xff1a; 1、ZooKeeper 未授权访问【…...

Docker Compose一键部署Spring Boot + Vue项目

目录 前提条件 概述 Compose简介 Compose文件 Compose环境 Compose命令 帮助命令 关键命令 Compose部署项目 初始化环境 查看代码文件 sql数据准备 nginx配置文件准备 创建 compose.yaml 一键启动compose多个容器 浏览器访问虚拟机ip:80(可省略默认的80端口) …...

【maven】idea执行了maven的install命令给本地安装项目依赖包 安装后删除

目录 事件起因环境和工具操作过程解决办法1、找到对应的目录下的文件&#xff0c;手动去删除&#xff0c;比如我的依赖库的路径是D:\qc_code\apache-maven-3.8.2\repository 我只需要找到这个目录下对应的依赖包进行手动删除即可&#xff08;不推荐&#xff0c;强行删除文件夹文…...

Android中的Handle底层原理

先上结论: 一个小小的Handle,就能看出你对Android底层源码理解的深浅。 老规矩,如果你能看懂我上面画的这张草图,这篇文章你可以直接跳过! 开干! 1、先对Handle在Android系统中是如何发挥作用的做一个小结: 1)每一个线程,包括UI主线程,有一个ThreadLocalMap变量:…...

最简单方式SSH连接局域网中另一台电脑的WSL2

1、首先确认一下WSL2中的SSH服务是否开启&#xff1a; 先安装更新一下&#xff0c;再安装一下ssh服务模块&#xff0c;这里很多人都每安装过。 sudo apt update sudo apt upgrade sudo apt install openssh-server 2、配置SSH服务器&#xff1a; 打开配置文件&#xff0c;这…...

热点聚焦:AI在医疗保健领域的深度渗透与变革

内容概要 随着人工智能技术的不断进步&#xff0c;我们正在见证一个充满奇迹的转变&#xff0c;尤其是在医疗保健领域。这种转变不仅仅涉及到提高效率&#xff0c;更在于重新定义我们对疾病诊断和治疗方案的理解。通过智能算法&#xff0c;AI能够在早期识别潜在的健康问题&…...

模板方法模式:定义算法框架的设计模式

1. 引言 在软件开发中&#xff0c;算法的实现通常是可变的&#xff0c;但其基本步骤往往是相对固定的。模板方法模式&#xff08;Template Method Pattern&#xff09;正是为了解决这一问题而设计的&#xff0c;它通过定义一个操作中的算法框架&#xff0c;将一些步骤的实现延…...

[Redis] Redis事务

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…...

编译原理第一次实验报告

源代码及附件&#xff1a;编译原理实验一源程序及附件资源-CSDN文库实验题目 实验要求 实验设计 前两部分指出了实验的宏观把控&#xff0c;为了具体实施实验&#xff0c;我们需要预先为实验做出如下设计&#xff1a; 本次实验我选取了C语言的一个子集进行设计词法分析器&…...

uniapp的video视频属性打包app后层级过高

问题&#xff1a;在使用uniapp开发APP时&#xff0c;使用video标签显示视频发现H5可以正常展示&#xff0c;但是打包到APP后&#xff0c;它的层级过高&#xff0c;把底部导航都盖住了。 官网说明&#xff1a;uni-app官网 官网给了cover-view组件或plus.nativeObj.view、subNVue…...

问:Redis为什么这么快?

Redis&#xff0c;全称Remote Dictionary Server&#xff0c;是一个开源的高性能键值对数据库。它以其卓越的性能、丰富的数据结构和灵活的使用方式&#xff0c;在现代互联网应用中扮演着重要角色。本文将探讨Redis之所以快的原因&#xff0c;包括其数据结构、内存管理、IO多路…...

环信鸿蒙IM SDK实现附件消息发送与下载

环信HarmonyOS IM SDK 正式版已经发布&#xff0c;该版本全面覆盖即时通讯&#xff08;IM&#xff09;的核心功能&#xff0c;为用户提供了完整的IM全功能体验&#xff0c;同时支持从Android APK到 NEXT 的数据迁移&#xff0c;更好地满足企业在不同业务场景下的适配需求。 点…...

后进先出(LIFO)详解

LIFO 是 Last In, First Out 的缩写&#xff0c;中文译为后进先出。这是一种数据结构的工作原则&#xff0c;类似于一摞盘子或一叠书本&#xff1a; 最后放进去的元素最先出来 -想象往筒状容器里放盘子&#xff1a; &#xff08;1&#xff09;你放进的最后一个盘子&#xff08…...

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...

Python Ovito统计金刚石结构数量

大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

Xela矩阵三轴触觉传感器的工作原理解析与应用场景

Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知&#xff0c;帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量&#xff0c;能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度&#xff0c;还为机器人、医疗设备和制造业的智…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道

文/法律实务观察组 在债务重组领域&#xff0c;专业机构的核心价值不仅在于减轻债务数字&#xff0c;更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明&#xff0c;合法债务优化需同步实现三重平衡&#xff1a; 法律刚性&#xff08;债…...

uni-app学习笔记三十五--扩展组件的安装和使用

由于内置组件不能满足日常开发需要&#xff0c;uniapp官方也提供了众多的扩展组件供我们使用。由于不是内置组件&#xff0c;需要安装才能使用。 一、安装扩展插件 安装方法&#xff1a; 1.访问uniapp官方文档组件部分&#xff1a;组件使用的入门教程 | uni-app官网 点击左侧…...

数据挖掘是什么?数据挖掘技术有哪些?

目录 一、数据挖掘是什么 二、常见的数据挖掘技术 1. 关联规则挖掘 2. 分类算法 3. 聚类分析 4. 回归分析 三、数据挖掘的应用领域 1. 商业领域 2. 医疗领域 3. 金融领域 4. 其他领域 四、数据挖掘面临的挑战和未来趋势 1. 面临的挑战 2. 未来趋势 五、总结 数据…...

使用ch340继电器完成随机断电测试

前言 如图所示是市面上常见的OTA压测继电器&#xff0c;通过ch340串口模块完成对继电器的分路控制&#xff0c;这里我编写了一个脚本方便对4路继电器的控制&#xff0c;可以设置开启时间&#xff0c;关闭时间&#xff0c;复位等功能 软件界面 在设备管理器查看串口号后&…...