网站建设中通知,做刀模线网站,南昌有做网站的吗,试论述外贸网站建设应注意的问题1 多线程编程基础
线程是操作系统能够进行运算调度的最小单位#xff0c;它被包含在进程之中#xff0c;每个进程至少有一个线程#xff0c;即主线程。线程依赖于进程#xff0c;不能单独存在。线程的主要作用是实现并发执行#xff0c;即多个线程可以同时执行不同的任务…1 多线程编程基础
线程是操作系统能够进行运算调度的最小单位它被包含在进程之中每个进程至少有一个线程即主线程。线程依赖于进程不能单独存在。线程的主要作用是实现并发执行即多个线程可以同时执行不同的任务。 多线程编程能够充分利用多核处理器或多处理器系统的优势通过同时执行多个线程来提高程序的执行性能。在具有多个核心的现代计算机系统中多线程编程可以确保每个核心都能得到充分利用从而实现并行处理显著提高程序的执行速度。另外对于交互式应用程序如用户界面或网络服务多线程编程可以显著提高系统的响应性。通过将耗时的操作如文件读写、网络请求等放在单独的线程中执行可以避免阻塞主线程保持用户界面的流畅和响应。这对于提供良好的用户体验至关重要。
1.1 C 中的多线程编程
在 C 中多线程编程指的是编写能够在同一时间或同一时间段内执行多个执行线程的程序。每个线程代表一个独立的执行路径它们共享程序的地址空间但拥有自己独立的栈空间。这意味着多个线程可以并发地执行不同的任务从而提高程序的执行效率。 C 中的多线程编程涉及以下几个关键概念 线程 Thread 线程是程序执行的最小单元。每个线程都有自己的指令指针、栈和局部变量。在多线程环境中多个线程可以并发执行共享程序的内存空间堆和静态存储区但每个线程有自己的栈空间。 进程 Process 进程是操作系统分配资源的基本单位它包含了一个程序的执行实例。一个进程可以包含一个或多个线程至少包含一个主线程。 线程生命周期线程的生命周期包括创建、就绪、运行、阻塞和终止等状态。线程可以通过系统调用、用户请求或程序控制等方式进入不同的状态。 线程同步由于多个线程可能同时访问共享资源因此需要机制来同步线程的执行避免数据冲突和不一致。线程同步包括互斥锁 Mutexes 、条件变量 Condition Variables 、信号量 Semaphores等。 线程通信线程间可能需要交换数据或信息以协调它们的工作。线程通信可以通过共享内存、消息队列、管道、套接字等方式实现。 线程安全性线程安全性指的是在多线程环境中代码能够正确地处理多个线程同时访问共享数据的情况而不会导致数据不一致或其他错误。 在 C11 之前跨平台的多线程编程通常需要使用第三方库或平台特定的 API 。由于 C 标准库在 C11 之前没有提供对多线程的原生支持因此开发者需要依赖特定平台的线程库或者跨平台的线程库。 以下是一些在 C11 之前常用的跨平台多线程编程方法 1使用POSIX线程 Pthreads Pthreads 是一个在类 Unix 系统如 Linux 和 macOS 上广泛使用的多线程编程接口。 Pthreads 提供了一套丰富的 API 来创建和管理线程以及进行线程同步。然而 Pthreads 是类 Unix 系统特有的因此在 Windows 平台上无法使用。 2使用 Windows 线程 Win32 threads Windows 平台提供了自己的线程 API 即 Win32 线程。 Win32 线程 API 允许开发者在 Windows 操作系统上创建和管理线程。然而与 Pthreads 一样 Win32 线程也是平台特有的无法在 Unix 或类 Unix 系统上使用。 C11 标准引入了对多线程的原生支持包括 thread 、mutex 、 condition_variable 等头文件这些头文件提供了创建和管理线程、同步线程以及进行线程间通信的工具。此外C标准库还提供了线程安全的容器如 std::vector 、 std::list 等这些容器可以在多线程环境中安全地使用。
1.2 线程与进程的区别
线程与进程在操作系统中各自扮演不同的角色并具有显著的区别。以下是线程与进程的主要区别 1资源分配与调度 进程进程是资源分配的基本单位它拥有独立的地址空间、数据栈和其他系统资源。当创建一个新进程时操作系统会为其分配必要的资源并确保它与其他进程隔离。进程间的切换涉及较多资源的管理因此效率相对较低。 线程线程是CPU调度的基本单位它共享进程的资源如内存空间、打开的文件等但拥有独立的执行栈和程序计数器。线程切换时只需保存和恢复少量寄存器内容因此切换开销小效率高。 2执行方式 进程进程是独立的执行实体拥有自己的地址空间和系统资源。一个进程崩溃不会影响其他进程的执行。 线程线程是进程内的一条执行路径多个线程共享进程的资源。同一个进程内的线程间通信较为容易因为它们可以直接访问共享内存空间。然而一个线程的错误可能导致整个进程的崩溃。 3并发性 进程由于进程拥有独立的地址空间多个进程可以同时执行实现真正的并发。 线程线程之间共享进程的资源因此多个线程可以同时执行但它们实际上是在同一个地址空间内并发执行。 4独立性 进程进程之间相互独立一个进程的状态不会影响其他进程。 线程线程是进程的一部分它们共享进程的资源因此线程之间的独立性相对较低。 5系统开销 进程由于进程拥有独立的资源创建和销毁进程涉及较多资源的管理因此开销较大。 线程线程创建和销毁的开销相对较小因为它们共享进程的资源。 总的来说进程和线程在资源分配、调度、执行方式、并发性、独立性和系统开销等方面存在显著的区别。在选择使用进程还是线程时需要根据具体的应用场景和需求进行权衡。
1.3 多线程编程的优势与挑战
多线程编程的优势主要体现在以下几个方面 1提高性能 多线程编程能够充分利用多核处理器或多处理器系统的优势实现并行处理从而提高程序的执行性能。通过将任务分解为多个线程并同时执行可以显著提高程序的运行速度。 2增强响应性 对于交互式应用程序如用户界面或网络服务多线程编程可以显著提高系统的响应性。通过将耗时的操作放在单独的线程中执行可以避免阻塞主线程保持用户界面的流畅和响应。这对于提供良好的用户体验至关重要。 3简化设计 多线程编程可以简化某些复杂问题的设计。通过将大问题分解为多个小问题并使用多个线程分别处理这些小问题可以使程序结构更加清晰便于理解和维护。 4资源利用率 多线程编程可以提高系统的资源利用率。多个线程可以共享计算机的资源如CPU、内存、硬盘等从而更有效地利用系统资源。 然而多线程编程也面临一些挑战 1线程同步与数据竞争 多个线程同时访问共享资源时需要采取适当的同步措施来避免数据竞争和不一致性问题。线程同步可能涉及互斥锁、条件变量等机制这些机制的使用需要谨慎否则可能导致死锁或性能下降。 2复杂性增加 多线程编程增加了程序的复杂性。线程之间的交互和同步需要仔细设计以避免出现竞态条件、死锁等问题。此外线程的管理和调试也比单线程程序更加复杂。 3性能开销 虽然多线程编程可以提高性能但线程的创建、销毁和上下文切换都需要一定的开销。如果线程数量过多或频繁切换可能会导致性能下降。 4可靠性问题 多线程编程可能导致一些可靠性问题。例如一个线程的异常或错误可能导致整个进程的崩溃影响其他线程的执行。 因此在编写多线程程序时需要权衡其优势与挑战并采取适当的措施来确保程序的正确性和性能。
1.4 C11 中的多线程支持
C11 标准引入了对多线程的原生支持为开发者提供了更加便捷和高效的方式来编写多线程程序。在 C11 中引入了以下几个关键组件来支持多线程编程 1thread 头文件 这个头文件包含了std::thread类用于创建和管理线程。开发者可以使用 std::thread 对象来表示一个线程并通过调用其成员函数来执行线程任务。 2atomic 头文件 这个头文件提供了原子操作的支持包括 std::atomic 类和一套 C 风格的原子类型与原子操作函数。原子操作是一种在多线程环境中安全执行的操作即在执行过程中不会被其他线程打断从而保证了数据的一致性和正确性。 3mutex 头文件 这个头文件提供了互斥量mutex的支持用于同步线程间的访问共享资源。互斥量是一种常用的同步机制可以确保同一时间只有一个线程可以访问共享资源从而避免数据竞争和不一致性问题。 4condition_variable 头文件 这个头文件提供了条件变量的支持用于线程间的条件同步。条件变量允许一个或多个线程等待某个条件成立当条件满足时等待的线程可以被唤醒并继续执行。 5future 头文件 这个头文件提供了异步任务的支持包括 std::future 和 std::promise 等类。这些类允许开发者启动一个异步任务并在需要时获取其结果。这对于实现异步编程和并发计算非常有用。 通过使用这些头文件和类 C11 使得多线程编程更加简单和直观。开发者可以更加容易地创建和管理线程实现线程间的同步和通信从而编写出高效且可靠的多线程程序。需要注意的是虽然 C11 提供了多线程支持但编写多线程程序仍然需要谨慎处理线程同步和数据竞争等问题以确保程序的正确性和性能。
2 线程的创建与管理
在 C11 中线程的创建与管理主要通过 std::thread 类来实现。使用 C11 创建与管理线程的主要流程如下 1创建线程 可以使用 std::thread 的构造函数创建一个新线程并传递给它一个可调用对象例如函数、函数指针、成员函数指针、 Lambda 表达式等。这个可调用对象将在新线程中执行。 2管理线程 线程被创建后可以使用std::thread类的成员函数来管理 join() : 阻塞当前线程直到被调用的线程完成执行。 detach() : 将线程标记为分离状态允许它在后台运行并且不需要显式调用 join() 。分离状态的线程在结束时会自动释放其资源。 get_id() : 获取线程的唯一标识符。 hardware_concurrency() : 返回可用于执行线程的硬件并发性级别通常对应于CPU的核数。 3线程状态 线程可以有以下几种状态 joinable : 线程可以被 join 。这是线程刚被创建时的默认状态。 detached : 线程是分离的即它将在后台运行并且在结束时自动释放资源。 joined : 线程已经被 join 并且不再处于活动状态。
2.1 使用 std::thread 创建线程
在 C11 中使用 std::thread 创建线程主要有两种方式 1通过全局可调用对象创建线程 可以将任何全局可调用对象如函数、 Lambda 表达式、 bind 表达式、函数对象等传递给 std::thread 的构造函数来创建线程。这是 C11 引入的更现代和灵活的方式。如下为样例代码
#include iostream
#include thread
#include string
#include functionalvoid threadFunc(std::string str)
{// 线程执行的代码 printf(%s\n, str.c_str());
}class FuncClass
{
public:void operator()(std::string str){// 线程执行的代码 printf(%s\n, str.c_str());}
};int main()
{// 通过函数指针创建线程std::thread t1(threadFunc,function pointer);t1.join();// 通过 Lambda 表达式创建线程std::thread t2([]{// 线程执行的代码 printf(Lambda expression\n);});t2.join();// 通过 bind 表达式创建线程std::functionvoid(std::string) func std::bind(threadFunc, std::placeholders::_1);std::thread t3(func, bind expression);t3.join();// 通过函数对象创建线程FuncClass funcObj;std::thread t4(funcObj, function object);t4.join();return 0;
}上面代码的输出为
function pointer
Lambda expression
bind expression
function object在上面代码中分别使用函数指针、 Lambda 表达式、 bind 表达式、函数对象这四种方式通过 std::thread 创建了线程随后调用 t.join() 阻塞 main 线程直到新线程 t 执行完毕。这是一种同步机制确保主线程等待新线程完成后再继续执行。如果不希望主线程等待可以使用 t.detach() 来将新线程设置为分离状态这样新线程将在后台运行并且当它的任务完成后会自动释放资源。 2通过成员函数指针和对象实例创建线程 如果想要在新线程上调用类的成员函数则需要传递一个成员函数指针和一个类的实例给 std::thread 。在这个过程中可以使用 std::bind 或者 Lambda 表达式来绑定成员函数和对象实例。如下为样例代码
#include iostream
#include thread
#include stringclass MyClass
{
public:void threadFunc(std::string str){// 线程执行的代码 printf(%s\n, str.c_str());}
};int main()
{MyClass obj;// 使用成员函数指针和对象实例创建线程 std::thread t1(MyClass::threadFunc, obj, member function pointers and object instances);t1.join();// 使用lambda表达式和this指针创建线程 std::thread t2([obj]() {obj.threadFunc(Lambda expressions and this pointers);});t2.join();return 0;
}上面代码的输出为
member function pointers and object instances
Lambda expressions and this pointers2.2 std::thread 线程的启动与终止
std::thread 对象的启动和终止是通过其构造函数和成员函数来管理的。 启动线程 要启动一个新线程需要创建一个 std::thread 对象并在其构造函数中提供要在新线程上执行的函数或可调用对象。然后新线程会立即启动并开始执行提供的函数。如下为样例代码
#include iostream
#include thread
#include stringvoid threadFunc()
{// 线程执行的代码 printf(hello thread\n);
}int main()
{std::thread t(threadFunc);t.join();return 0;
}上面代码的输出为
hello thread在上面代码中std::thread t(threadFunc); 语句创建并启动了一个新线程该线程执行 threadFunc 函数。 终止线程 在 C11 中线程可以通过两种方式终止 1隐式终止当线程函数执行完成后线程会自动终止。在上面的例子中 threadFunc 函数执行完毕后对应的线程就会自然终止。 2显式终止通过调用线程对象的detach或join成员函数来显式地管理线程的终止 join 调用线程对象的 join 成员函数会阻塞当前线程通常是主线程直到被调用的线程执行完毕。这是一种同步机制确保主线程等待新线程完成后再继续执行。 detach 调用线程对象的 detach 成员函数会将线程设置为分离状态。这意味着一旦线程函数执行完成线程对象会自动释放其资源而无需显式调用 join 。设置为分离状态的线程在其完成时会自动终止。如下为样例代码
#include iostream
#include thread
#include stringvoid threadFunc()
{// 线程执行的代码 std::this_thread::sleep_for(std::chrono::milliseconds(10));printf(hello thread\n);
}int main()
{std::thread t(threadFunc);// 分离线程线程完成时自动释放资源 t.detach();// 主线程继续执行不再等待myThread线程 printf(continue main thread\n);// 避免子线程没有结束整个程序即退出std::this_thread::sleep_for(std::chrono::milliseconds(100));return 0;
}上面代码的输出为
continue main thread
hello thread在上面代码中由于调用了detach主线程不会等待 t 线程完成而是继续执行。当 threadFunc 函数执行完毕后 t 线程会自动终止并且其资源会被自动释放。 注意事项 1一个线程对象只能被 join() 或 detach() 一次。尝试对一个已经 join() 或 detach() 过的线程对象再次调用这些函数会导致未定义行为。 2如果线程对象在其生命周期结束前既没有被 join() 也没有被 detach() 程序会在终止时抛出 std::terminate() 异常。为了避免这种情况通常建议在线程对象销毁前确保它被 join() 或 detach() 。 3在调用 join() 或 detach() 后线程对象就不再代表一个可执行的线程它变成了一个空线程对象可以再次被赋值新的线程。 4在分离状态下运行的线程不应该访问任何需要在线程结束时保持有效的资源因为线程对象可能会在线程函数返回后立即被销毁。
2.3 std::thread 线程的 ID
注意std::thread 不提供获取当前线程的系统 ID 例如在 POSIX 系统上的线程 ID 或 Windows 上的线程句柄的方法。如果需要获取系统级别的线程 ID 则要使用平台特定的 API。 在 C 中std::thread 类型的对象代表一个线程并且每个线程都有一个与之关联的线程 ID 该 ID 的类型是 std::thread::id 。线程 ID 是线程的唯一标识符可以在程序内部用于区分不同的线程。 std::thread::id 类型提供了两个主要的成员函数来获取线程 ID get_id() 这是 std::thread 类的成员函数用于获取与线程对象关联的线程ID。如果线程对象没有与任何线程关联例如如果它是默认构造的或已经通过调用 join() 或 detach() 终止了则 get_id() 返回一个默认构造的 std::thread::id 对象值为 0 通常表示没有线程。 std::this_thread::get_id() 这是一个自由函数用于获取当前执行线程的线程 ID 。这个函数在任何线程中都可以调用包括主线程和由 std::thread 对象表示的任何线程。 如下是使用 get_id() 和 std::this_thread::get_id() 的样例代码
#include iostream
#include thread void threadFunc()
{// 获取当前线程的ID std::thread::id threadId std::this_thread::get_id();std::cout current thread id : threadId std::endl;
}int main()
{// 创建并启动一个新线程 std::thread t(threadFunc);// 获取主线程的 ID std::thread::id mainThreadId std::this_thread::get_id();// 等待子线程完成 t.join();std::cout main thread id : mainThreadId std::endl;return 0;
}上面代码的输出为
current thread id : 7572
main thread id : 12492注意虽然使用 std::cout 将 std::thread::id 输出的结果是一个整数使用 std::thread::id 定义的流插入运算符来输出线程 ID 输出的格式可能会因平台而异但实际上将 std::thread::id 直接转换为 int 是不可行的因为 std::thread::id 是一个平台相关的类型它可能不是基于整数的也没有提供直接的转换机制到整数。此外std::thread::id 的内部表示和大小在不同平台和编译器实现之间可能会有所不同。std::thread::id 的设计目的是提供一个唯一的标识符而不是一个可以安全转换为字符串的类型因此最安全的做法是直接输出或使用 std::thread::id 对象本身。 在开发过程中可以使用 std::thread::id 的比较操作符 、 ! 来检查两个线程 ID 是否相同或不同。此外可以通过比较 std::thread::id 对象与默认构造的 std::thread::id 对象来检查一个线程是否有效。
3 线程同步与互斥
线程同步与互斥是处理并发编程中线程之间交互和共享资源时的两个重要概念。它们有助于确保数据的一致性和防止竞态条件。 线程同步 线程同步是指协调多个线程的执行顺序以确保它们之间的交互按照预期的方式进行。线程同步通常使用同步原语来实现如互斥锁 mutexes 、条件变量 condition variables 、信号量 semaphores 等。 线程同步的目的是 1保护共享资源确保同时只有一个线程可以访问或修改共享资源以防止数据不一致或损坏。 2控制执行顺序确保线程按照预定的顺序执行例如先执行某个线程然后再执行另一个线程。 3避免死锁通过合理的同步机制确保线程在等待资源时不会陷入死锁状态。 线程互斥 互斥是指确保同一时刻只有一个线程可以访问某个共享资源或执行某段代码。这通常通过互斥锁 mutex 来实现互斥锁是一种同步原语用于保护共享资源不被多个线程同时访问。 互斥的的目的是 1保护数据一致性确保共享资源在多个线程之间的访问不会导致数据不一致或损坏。 2防止竞态条件竞态条件是指多个线程在没有同步的情况下访问共享资源导致结果取决于线程的执行顺序。互斥可以确保只有一个线程在任何时候访问共享资源从而消除竞态条件。
3.1 std::mutex 互斥锁
在C中 std::mutex 是一个类它定义在 头文件中用于实现互斥锁 mutual exclusion 。互斥锁是一种同步机制用于保护共享资源防止多个线程同时访问和修改这些资源从而避免数据竞争和不一致。
3.1.1 std::mutex 的定义与初始化
要定义和初始化一个 std::mutex 对象只需要声明一个该类型的变量即可。如下为样例代码
#include mutex // 包含互斥锁的头文件 // 定义全局的互斥锁对象
std::mutex g_mutex;int main() {// 定义并初始化一个局部的互斥锁对象 std::mutex localMutex;// 使用互斥锁 g_mutex.lock(); // 锁定全局互斥锁 // 执行需要互斥访问的代码 g_mutex.unlock(); // 解锁全局互斥锁 localMutex.lock(); // 锁定局部互斥锁 // 执行需要互斥访问的代码 localMutex.unlock(); // 解锁局部互斥锁 return 0;
}在上面带啊吗中 g_mutex 是一个全局的 std::mutex 对象它在程序开始执行时就被创建并初始化。 localMutex 是一个在 main 函数内部定义的局部 std::mutex 对象它在声明时就被创建并初始化。 一般是不需要显式调用 std::mutex 的构造函数来初始化互斥锁因为编译器会自动调用默认构造函数来初始化对象。如果需要更复杂的初始化可以使用构造函数参数来提供。但通常直接使用 std::mutex mutexName ;这种简单的声明和初始化方式就足够了。 注意使用互斥锁时应当谨慎地锁定和解锁以避免死锁或资源争用。此外尽可能的使用 std::lock_guard 或 std::unique_lock 等 RAII 风格的锁包装器来自动管理锁的生命周期这样可以减少错误并提高代码的安全性。
3.1.2 std::mutex 的锁定和解锁
std::mutex 类提供了 lock() , unlock() 和 try_lock() 这 3 个成员函数来管理互斥锁的锁定和解锁。 lock() 方法 lock() 方法用于锁定互斥锁。如果互斥锁已经被另一个线程锁定那么调用 lock() 的线程将会被阻塞直到互斥锁变得可用为止。这是一种阻塞操作意味着线程会等待直到能够获取锁。如下为样例代码
std::mutex mtx; // 锁定互斥锁
mtx.lock(); unlock() 方法 unlock()函数用于解锁互斥锁使得其他线程可以锁定它。在调用 unlock() 之前必须先成功调用 lock() 来锁定互斥锁否则会抛出异常在没有捕获的情况下会导致程序崩溃。如下为样例代码
std::mutex mtx; // 锁定互斥锁
mtx.lock(); // 执行需要互斥访问的代码 // 解锁互斥锁
mtx.unlock();try_lock() 方法 try_lock() 函数用于尝试锁定互斥锁如果锁已经被其他线程持有那么它不会阻塞当前线程而是立即返回一个表示是否成功获取锁的值。通常这个函数返回一个布尔值 true 表示成功获取锁 false 表示未能获取锁。如下为样例代码
#include iostream
#include thread
#include mutex
#include chrono std::mutex g_mutex; // 全局互斥锁 void printBlockStr(int num, std::string str)
{// 尝试锁定互斥锁 if (g_mutex.try_lock()){try {for (int i 0; i num; i){printf(%s,str.c_str());}}catch (...) {g_mutex.unlock(); // 如果在打印过程中发生异常确保解锁互斥锁 throw;}g_mutex.unlock(); // 解锁互斥锁 }else{printf(g_mutex is locked, cannot print now.);}
}int main() {std::thread t1(printBlockStr, 10, *);std::thread t2(printBlockStr, 10, ~);t1.join();t2.join();return 0;
}上面代码的输出为
****g_mutex is locked, cannot print now.******
// 上面输出中的字符串 g_mutex is locked, cannot print now. 在星号中间是由于 t2 线程在 t1 打印过程中执行了语句输出。在上面代码中有两个线程 t1 和 t2 它们都尝试使用 try_lock() 来锁定同一个互斥锁 g_mutex 。如果 g_mutex 是可用的线程将打印出相应数量的字符并在完成后解锁互斥锁。如果 g_mutex 已经被另一个线程锁定线程将输出一条消息表明它无法打印。 需要注意的是 try_lock() 不提供任何超时机制。如果互斥锁被锁定它会立即返回 false 。如果需要一个带超时的尝试锁定功能则要使用 std::condition_variable 或者 std::future 与 std::async 来实现。 此外在 printBlockStr 函数中我们使用了try块来确保在打印过程中如果发生任何异常互斥锁仍然能够被正确解锁。这是一种良好的编程实践可以避免因异常导致的互斥锁死锁。
3.1.3 std::lock_guard 的使用
std::lock_guard 是 C11 标准库中引入的一个类模板用于管理互斥锁 std::mutex 的生命周期。它遵循 RAIIResource Acquisition Is Initialization 原则即在构造时获取资源在这种情况下是互斥锁并在析构时释放资源。通过使用 std::lock_guard 开发者可以确保互斥锁在适当的时候被锁定和解锁从而避免手动管理锁时可能出现的错误和复杂性。 std::lock_guard 的优势包括 1自动管理锁的生命周期 std::lock_guard 在构造时自动锁定互斥锁并在析构时自动解锁。这意味着即使在异常或提前返回的情况下锁也能被正确地释放从而避免死锁或资源泄漏。 2简化锁的管理使用 std::lock_guard 不需要显式调用 lock() 和 unlock() 方法。这减少了出错的机会并使代码更加简洁。 3局部作用域控制 std::lock_guard 的作用域通常限制在其声明的代码块内。这意味着锁的保护范围清晰可见易于理解和维护。 4防止重复锁定由于 std::lock_guard 在构造时锁定互斥锁并且在析构时解锁因此它不允许同一互斥锁被多次锁定。这有助于避免潜在的竞态条件。 std::lock_guard 的用法非常简单如下为样例代码
#include iostream
#include thread
#include mutex
#include chrono std::mutex g_mutex; // 全局互斥锁 void printBlockStr(int num, std::string str)
{// 构造 std::lock_guard 对象自动锁定 g_mutex std::lock_guardstd::mutex lock(g_mutex);for (int i 0; i num; i){printf(%s, str.c_str());}printf(\n);
}int main() {std::thread t1(printBlockStr, 10, *);std::thread t2(printBlockStr, 10, ~);t1.join();t2.join();return 0;
}上面代码的输出为
**********
~~~~~~~~~~在上面代码中std::lock_guardstd::mutex lock(g_mutex); 创建了一个 std::lock_guard 对象并在构造时自动锁定了 g_mutex 互斥锁。 std::lock_guard 对象的生命周期与它的作用域绑定因此当 lock 对象离开其作用域在这个例子中是 printBlockStr 函数的末尾时它的析构函数会被调用从而自动解锁 g_mutex 。 std::lock_guard的使用确保了即使在 printBlockStr 函数中出现异常或提前返回的情况下 g_mutex 也会被正确地解锁避免了死锁和资源泄漏的问题。 注意std::lock_guard 是不可复制的这意味着不能复制一个已经锁定了互斥锁的 std::lock_guard 对象到另一个对象这有助于防止因复制而导致的潜在问题。
3.1.4 std::unique_lock 的使用
std::unique_lock 是一个互斥锁包装器它允许开发者以独占所有权的方式 unique ownership 管理互斥锁。这意味着在 std::unique_lock 对象的生命周期内没有其他 std::unique_lock 对象可以同时拥有同一个互斥锁的所有权。 std::unique_lock 也支持延迟锁定、手动解锁以及与其他同步原语如条件变量一起使用。 std::unique_lock 提供了一种更灵活的方式来管理互斥锁 std::mutex 的锁定和解锁。与上面的 std::lock_guard 相比 std::unique_lock 提供了更多的控制选项和功能。 std::unique_lock 的优势包括 1灵活性 std::unique_lock 比 std::lock_guard 更加灵活。它允许延迟锁定即在构造时不立即锁定互斥锁手动解锁通过调用 unlock() 方法以及重新锁定通过调用 lock() 方法。此外 std::unique_lock 还可以与条件变量一起使用以实现更复杂的同步模式。 2可移动性 std::unique_lock 对象可以被移动 move 这意味着它可以被用作函数的返回值也可以被存储在 STL 容器中。这使得 std::unique_lock 在编写更复杂的线程代码时更加有用。 3更好的错误处理 std::unique_lock 提供了更好的错误处理能力。如果尝试锁定一个已经被其他线程锁定的互斥锁 std::unique_lock 的构造函数将返回一个错误如果使用了 std::try_to_lock 策略。这使得开发者可以在代码中处理这种错误情况。 4支持多种锁定策略std::unique_lock 支持多种锁定策略包括独占锁和共享锁。默认情况下它使用独占锁但也可以通过指定 std::defer_lock 或 std::adopt_lock 来改变锁定行为。 总的来说 std::unique_lock 提供了比 std::lock_guard 更多的控制和灵活性适用于需要更复杂同步操作的场景。然而对于简单的锁定需求 std::lock_guard 通常更加简洁和易于使用。 创建 std::unique_lock 对象 std::unique_lock 的构造函数接受一个 std::mutex 对象作为参数并在构造时锁定该互斥锁。
std::unique_lockstd::mutex lock(g_mutex);锁定和解锁互斥锁 类似于前面介绍的 std::lock_guard 当 std::unique_lock 对象被创建时它会锁定互斥锁。在 std::unique_lock 对象的生命周期结束时例如离开作用域时它会自动解锁互斥锁。这种自动管理锁的机制有助于避免忘记解锁而导致的问题。
{ std::unique_lockstd::mutex lock(g_mutex); // 在这里执行需要互斥访问的代码 // ...
} // lock 对象离开作用域自动解锁 g_mutex延迟锁定和手动解锁 std::unique_lock 支持延迟锁定和手动解锁。可以使用 std::defer_lock 作为 std::unique_lock 的第二个参数来延迟锁定互斥锁并在需要时调用 lock() 方法来手动锁定它。同样可以使用 unlock() 方法来手动解锁互斥锁。
std::unique_lockstd::mutex lock(g_mutex, std::defer_lock); // 在此处执行一些不需要互斥锁的操作 // 手动锁定互斥锁
lock.lock(); // 在此处执行需要互斥锁的操作 // 当 unique_lock 对象离开作用域时它会自动解锁互斥锁 尝试锁定 std::unique_lock 还提供了尝试锁定的功能。可以使用 try_lock() 方法来尝试锁定互斥锁如果互斥锁已经被其他线程锁定则 try_lock() 方法将返回 false 否则返回 true 。这允许在尝试锁定失败时执行备选代码路径。
std::unique_lockstd::mutex lock(g_mutex, std::try_to_lock);
if (lock.owns_lock())
{ // 成功锁定互斥锁执行需要互斥访问的代码 // ...
} else { // 锁定失败执行备选代码路径 // ...
}配合条件变量使用 std::unique_lock 还经常与 std::condition_variable 一起使用以实现线程之间的同步。可以使用 std::unique_lock 来锁定互斥锁并在等待条件变量时释放锁以便其他线程可以修改共享资源。当条件满足时可以再次锁定互斥锁并继续执行。
#include iostream
#include thread
#include mutex std::mutex g_mutex; // 全局互斥锁
std::condition_variable g_cv;
bool g_ready false;void threadFunc()
{std::unique_lockstd::mutex lock(g_mutex);g_cv.wait(lock, [] { return g_ready; }); // 等待条件变量释放锁 // 条件满足继续执行 // ...
}int main()
{std::thread t(threadFunc);{std::unique_lockstd::mutex lock(g_mutex);// 修改共享资源 g_ready true;} // lock对象离开作用域自动解锁 g_mutex g_cv.notify_one(); // 通知等待的线程条件已满足 t.join();return 0;
}上面代码展示了如何使用 std::unique_lock 和 std::condition_variable 来实现线程之间的同步。工作线程在等待条件变量时释放了互斥锁以便主线程可以修改共享资源。当主线程修改完共享资源并通知工作线程时工作线程再次锁定互斥锁并继续执行。
3.2 std::condition_variable 条件变量
std::condition_variable 是 C11 引入的一个类用于在并发编程中同步线程。它通常与互斥量 std::mutex 一起使用允许一个或多个线程等待某个条件成立而其他线程可以在该条件成立时通知等待的线程。
3.2.1 std::condition_variable 的基本使用
如下是一个简单的 std::condition_variable 使用示例
#include iostream
#include thread
#include mutex
#include condition_variable std::mutex g_mutex; // 全局互斥锁
std::condition_variable g_cv;
bool g_ready false;void printID(int id)
{std::unique_lockstd::mutex lock(g_mutex);while (!g_ready) // 如果条件不满足则等待 {g_cv.wait(lock); // 当前线程被阻塞直到被通知 }// 执行线程任务 printf(thread %d\n, id);
}int main() {std::thread threads[5];// 创造 5 个线程for (int i 0; i 5; i){threads[i] std::thread(printID, i);}g_ready true;g_cv.notify_all();//唤醒全部线程for (auto th : threads){th.join();}return 0;
}上面代码的输出为
thread 3
thread 2
thread 1
thread 0
thread 4在上面代码中创建了 5 个线程这些线程都试图打印自己的 ID。但是在它们开始打印之前它们会等待 ready 变量变为 true。 语句 g_cv.notify_all(); 负责唤醒所有等待的线程。
3.2.2 std::condition_variable::wait_for
std::condition_variable::wait_for 是一个成员函数它允许线程等待一个特定的时间段或者直到被其他线程唤醒或者直到一个特定的条件满足通过提供的谓词来检查。这个函数返回一个表示实际等待时间的 std::chrono::duration 对象。 wait_for 的行为如下 1解锁互斥量允许其他线程锁定它。
2阻塞当前线程直到以下条件之一满足
谓词等待条件返回 true如果提供了的话。超过了指定的时间段。其他线程调用了 notify_one() 或 notify_all() 方法。
3重新锁定互斥量。
4如果谓词等待条件被提供且返回 false或者超时了而没有收到通知则返回表示实际等待时间的 std::chrono::duration 对象可能小于 预定的等待时间段因为等待可以被提前唤醒。 如下为样例代码
#include iostream
#include thread
#include mutex
#include condition_variable std::mutex g_mutex; // 全局互斥锁
std::condition_variable g_cv;
bool g_ready false;void threadFunc()
{std::unique_lockstd::mutex lock(g_mutex);auto timeout std::chrono::seconds(2);// 使用 wait_for 等待同时检查 ready 的状态或超时 bool res g_cv.wait_for(lock, timeout, [] { return g_ready; });// 等待结果 std::string strRes res ? true : false;printf(wait_for result : %s\n,strRes.c_str());// 执行线程任务 printf(do thread task.\n);
}int main() {std::thread t(threadFunc);t.join();return 0;
}上面代码的输出为
wait_for result : false
do thread task.注意上面代码的输出说明即使 g_ready 始终为 false 超过预定的等待时间后既然可以执行后续的线程任务。如果需要在没有满足谓词等待条件为 true 则可以做循环等待如下为对应的代码修改
while(!g_cv.wait_for(lock, timeout, [] { return g_ready; }))3.4 std::future 和 std::async
在 C11 中 std::future 和 std::async 是与线程和异步编程相关的两个重要组件。 std::future 是一个模板类它提供了一种从异步操作获取结果的方式。而 std::async 是一个函数模板它启动一个异步任务并返回一个 std::future 对象该对象表示该任务的结果。 std::future std::future 对象是一个占位符它存储了某种类型的值该值在将来的某个时间点变得可用。通常这个值是由一个异步任务如线程计算得到的。你可以使用 std::future::get() 成员函数来获取这个值。如果值还没有变得可用 get() 函数会阻塞直到值变得可用为止。 std::async std::async 函数是一个启动异步任务的便捷方式。它接受一个可调用对象如函数、函数指针或 Lambda 表达式作为参数并立即返回一个 std::future 对象。这个 std::future 对象表示异步任务的结果。 如下为样例代码
#include iostream
#include future
#include chrono int computeAdd(int val1, int val2)
{std::this_thread::sleep_for(std::chrono::microseconds(100));return val1 val2;
}int main() {// 使用std::async启动一个异步任务 std::futureint result(std::async(std::launch::async, computeAdd, 1, 2));// 在异步任务计算期间主线程可以做其他工作 printf(do some other work\n);// 当需要异步任务的结果时使用 std::future::get() 来获取它 int value result.get(); // 这会阻塞直到异步任务完成 printf(result is %d\n, value);return 0;
}上面代码的输出为
do some other work
result is 3在上面中 computeAdd 函数被异步执行并返回 1 2 的结果。主线程在异步任务执行期间可以继续执行其他工作。当需要异步任务的结果时它调用 result.get() 来获取它。这会阻塞主线程直到异步任务完成并返回结果。
4 线程休眠
C11 标准库中的两个函数 std::this_thread::sleep_for 和 std::this_thread::sleep_until 用于使当前线程暂停执行一段时间。这两个函数是std::this_thread命名空间的一部分用于控制当前线程的行为。 std::this_thread::sleep_for 这个函数使当前线程休眠至少指定的时间段。它接受一个std::chrono::duration对象作为参数该对象表示要休眠的时间长度。
std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠1秒
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 休眠100毫秒std::this_thread::sleep_until 这个函数使当前线程休眠直到指定的时间点。它接受一个 std::chrono::time_point 对象作为参数该对象表示线程应该休眠直到的时间点。
auto now std::chrono::system_clock::now();
std::this_thread::sleep_until(now std::chrono::seconds(1)); // 休眠直到从现在开始的1秒后与 sleep_for 不同 sleep_until 允许指定一个绝对的时间点而不是一个相对的时间段。这使得 sleep_until 在需要多次休眠或在特定时间唤醒的场景中更为有用。 在实际使用中应该根据具体需求选择 sleep_for 或 sleep_until 。如果只需要让线程休眠一个固定的时间段那么 sleep_for 通常更简单且更直观。但是如果需要在特定的时间点唤醒线程或者需要累积多个休眠时间段来达到一个特定的唤醒时间那么 sleep_until 可能更适合。 如下为样例代码
#include iostream
#include thread
#include chrono int main()
{ // 使用 sleep_for 休眠1秒 std::this_thread::sleep_for(std::chrono::seconds(1)); // 使用 sleep_until 休眠直到从现在开始的2秒后 auto now std::chrono::system_clock::now(); std::this_thread::sleep_until(now std::chrono::seconds(2)); return 0;
}