C++ 线程同步2025年3月17日 | 阅读 15 分钟 在 C++ 中,“线程同步”是指用于同步多个线程执行的任务的**方法和系统**,确保它们能够平稳且在严格控制下运行。在多线程程序中,可能存在多个执行线程,它们可以同时运行,访问共享资源,并导致诸如竞态条件、数据不一致和死锁等问题。 线程同步包括限制对共享资源的访问,确保一次只有一个线程使用某个资源,或者确保线程在继续之前等待特定条件的满足。目标是维护数据完整性并避免当多个线程同时编辑共享数据时可能发生的冲突。 为什么我们需要线程同步?线程同步通常在以下情况下需要:
线程同步示例输出 ![]() 解释 这个 C++ 代码演示了如何使用线程同步机制,允许两个线程同时安全地访问一个共享计数器。incrementGlobalCounter 函数通过互斥锁进行访问保护,一次只有一个线程可以修改 globalCounter。为了模拟并发进程,main 函数创建了两个调用递增函数的线程。 然后,代码会展示两个线程完成任务后的 globalCounter 的最终值。该示例通过使用互斥锁同步来说明如何在多线程环境中避免竞态条件并维护数据完整性。这种方法称为线程同步,并且此代码的实现非常简单。 C++ 中的线程同步技术C++ 提供了以下线程同步技术: 1. 使用互斥锁(std::mutex)进行线程同步在 C++ 中,互斥锁是一种同步工具,有助于管理多个线程之间对共享资源的访问。为了避免数据竞态并确保线程安全,它确保一次只有一个线程可以访问代码的关键部分。线程通过获取和释放互斥锁来同步操作,从而减少冲突并促进有序执行。通过避免竞态条件并允许对共享数据进行受控的并发访问,互斥锁使得多线程程序更加可靠和可预测。 由于线程执行的非确定性序列,当多个线程同时访问和修改共享数据时,可能会发生竞态条件。互斥锁通过一次只允许一个线程访问代码的重要部分来解决此问题,从而防止多个线程同时访问同一资源。 std::mutex 类是 C++ 标准库的一部分,用于基于互斥锁的同步。当一个线程尝试使用 lock() 方法锁定互斥锁时,如果互斥锁之前未被锁定,则操作成功。如果另一个线程已经锁定了该互斥锁,则尝试锁定的线程将被阻塞,直到互斥锁被解锁。由于这种阻塞行为,一次只有一个线程可以执行关键部分。 需要注意的是,如果互斥锁使用不当,可能会导致死锁和争用等问题。当两个或多个线程互相等待对方释放资源时,就会发生死锁,程序会停止。当线程经常争夺同一个互斥锁时,就会发生争用,由于大量的锁定和解锁操作,可能会减慢程序的速度。 现代 C++ 提供了更高级别的抽象,如 std::lock_guard 和 std::unique_lock,它们可以简化互斥锁的管理并降低出错的可能性。这些 RAII(资源获取即初始化)类会在创建和销毁时自动锁定和解锁互斥锁,从而即使在出现异常的情况下也能确保正确的同步。 示例 输出 ![]() 解释 此 C++ 代码示例展示了如何同步线程以及如何为共享资源提供正确的并发访问。程序首先包含输入/输出、线程和互斥锁头文件。声明了一个名为 mtx 的 std::mutex 来控制对共享资源的访问。printNumbers() 方法是多个线程将同时访问的关键组件。 该方法在锁定互斥锁 (mtx.lock()) 后,输出从 0 到 9 的数字序列,每个数字之间用空格隔开。然后使用 mtx.unlock() 解锁互斥锁,使其可供其他线程使用。程序通过创建三个线程(thread1、thread2 和 thread3)来演示多线程,它们同时运行 printNumbers() 方法。这表明互斥锁如何确保一次只有一个线程访问关键区域,从而防止输出重叠。join() 函数在程序结束前等待每个线程完成其执行。 总之,此处展示的代码演示了使用互斥锁来同步线程、防止竞态条件以及在并发访问共享资源时提供可预测和有序结果的核心概念。 2. C++ 中的锁保护 (std::lock_guard)std::lock_guard 函数在 C++ 中是一个强大且实用的 RAII(资源获取即初始化)包装器,它使使用互斥锁进行线程同步变得简单。它提供了一种安全、自动化的方法来处理互斥锁的锁定和解锁,有助于避免常见的线程陷阱,例如忘记解锁互斥锁或在临界区遇到异常。 在构造 std::lock_guard 对象时,其构造函数接受一个互斥锁作为输入并锁定该互斥锁。当 std::lock_guard 对象离开作用域时(例如,当函数结束时),互斥锁会被自动解锁。这确保了即使发生异常,互斥锁也能始终被正确释放。 示例 输出 ![]() 说明 本示例解释了如何使用互斥锁和 std::lock_guard 在多线程环境中提供同步输出。 程序首先包含适当的头文件,并定义一个 outputMutex 互斥锁。创建此互斥锁是为了防止多个线程在访问标准输出 (std::cout) 时交错输出。 printFunction() 函数允许多个线程运行。在十次循环中创建了一个 std::lock_guard 对象,名为 lock。此 lock 自动保护 outputMutex,确保一次只有一个线程可以使用 std::cout。这可以防止线程输出的混乱。 main() 函数启动了两个线程 thread1 和 thread2,它们都运行 printFunction() 函数。由于互斥锁限制了对 std::cout 的访问,这些线程的输出是同步且连贯的。join() 函数确保主程序在退出前等待两个线程完成。 3. 唯一锁 (std::unique_lock)在 C++ 中,std::unique_lock 是一种强大的同步机制,可用于管理互斥锁并提供受控的线程同步。与 std::lock_guard 一样,std::unique_lock 允许安全地协调访问共享资源,但它提供了更大的灵活性。它支持延迟锁定,这意味着它可以在同一作用域内多次锁定和解锁,而 std::lock_guard 则在构造时锁定互斥锁,并在销毁时解锁。 std::unique_lock 类还具有条件变量,允许更复杂的同步模式,例如在继续执行之前等待特定条件的满足。这使其成为需要对互斥锁的锁定、解锁和等待进行细粒度控制的场景的理想选择。此外,std::unique_lock 允许互斥锁的独占(唯一)和共享(多个)所有权,从而在更复杂的情况下实现线程安全资源共享。 在 C++ 中使用 std::unique_lock 进行线程同步的优点包括其增强的灵活性和复杂的功能。与 std::lock_guard 相比,std::unique_lock 支持延迟锁定和解锁,从而可以更动态地控制给定作用域内对互斥锁的访问。这种适应性在处理复杂的同步设置或当互斥锁所有权需要在执行过程中发生变化时特别有用。 示例 输出 ![]() 解释 此代码演示了如何使用 std::unique_lock 和 std::lock 在多线程环境中安全地转移资金,同时避免潜在的死锁并确保对共享资源的同步访问。 Account 结构体代表一个银行账户,其中包含余额和一个互斥锁 (m),以防止对账户余额的未授权访问。此互斥锁确保一次只有一个线程可以访问余额,从而防止竞态条件。 transfer() 函数负责在两个账户之间转移资金。它使用具有 std::defer_lock 选项的 std::unique_lock 对象来创建锁,而不锁定关联的互斥锁。这可以防止任何立即锁定,并允许使用 std::lock() 同时获取锁。即使有多个线程尝试同时锁定互斥锁,这种方法也可以防止死锁。 在 main() 方法中创建了两个线程(thread1 和 thread2)来并发执行 transfer() 函数。线程以相反的方向在两个账户之间转移资金。join() 方法确保主程序在继续执行之前等待两个线程完成其工作。 在两个线程都完成后,将显示账户的最终余额,展示并发转账的效果。互斥锁、std::unique_lock 和 std::lock 的使用确保转账能够正确完成,并且不会发生竞态条件。 4. 递归互斥锁 (std::recursive_mutex)std::recursive_mutex 是 C++ 线程同步中的一种互斥锁,它允许单个线程频繁或重复地锁定,从而防止死锁。 C++ std::recursive_mutex 是处理复杂同步问题的有用工具。为了避免竞态条件并确保多个活动同时执行的一致性和有序性,在多线程编程中进行适当的同步至关重要。 与普通互斥锁(std::mutex)一次只允许一个线程持有锁不同,递归互斥锁增加了一个显著的特性:单个线程可以多次锁定同一个互斥锁而不会导致死锁。 这一决定性特性使其在某个函数可能调用需要使用单个互斥锁进行锁定的其他函数的情况下特别有用,从而防止了在使用传统互斥锁时可能发生的死锁。在恢复灵活性的同时,std::recursive_mutex 还通过确保一次只有一个线程持有互斥锁来遵守线程同步要求。 这种方法的工作原理是跟踪锁定它的线程,并允许该线程再次锁定它,本质上计算该线程锁定了互斥锁的次数。由于使用不当可能导致误用和严重的性能问题,因此仅在必要时使用此种互斥锁至关重要。总的来说,当同一线程中的函数或过程需要访问受互斥锁保护的共享资源时,std::recursive_mutex 是一个有用的工具,可实现更灵活且可维护的多线程程序。 示例 输出 ![]() 解释 在此代码中,在多线程环境中使用了递归互斥锁,特别是 std::recursive_mutex,以实现对共享资源的同步访问。程序构造了 SharedResource 类,该类有一个名为 mtx 的 recursive_mutex,以防止对共享数据成员 sharedData 的并发访问。 5. 读写互斥锁 (std::shared_mutex)读写互斥锁,即 C++ 中的 std::shared_mutex,是一种同步工具,与普通互斥锁相比,它提供了对共享资源的并发访问的更精细控制。它允许多个线程同时从共享资源中读取,同时确保写入操作的独占访问。这在需要并发写入,但读取频繁且可以同时进行的情况下尤其有用。 std::shared_mutex 有两种锁定模式:共享模式和独占模式。
当读者数量多于写者数量时,std::shared_mutex 有助于提高并发性和整体速度。通过允许多个线程同时读取,它可以减少冲突并提高速度。但是,当需要写入时,独占锁可保护数据完整性。 要使用此同步方法,您必须拥有合适的编译器和库,因为 std::shared_mutex 仅在 C++17 及更高版本中可用。 示例 输出 ![]() 解释 此代码提供了一个示例,说明如何在多线程环境中利用互斥锁来确保对共享资源 sharedValue 的同步访问。它旨在展示线程如何同时访问和修改此共享属性,同时仍保持适当的同步以避免数据竞态和不一致的结果。 在程序的最开始,声明了 sharedValue 和 sharedMutex,这是用于防止对该共享变量的非授权访问的互斥锁。定义了 readSharedValue 和 setSharedValue 方法。在获取互斥锁、读取共享值、释放锁并模拟延迟(通过 sleep)后,前者将值存储在 readResult 变量中。后者在获取互斥锁、再次模拟延迟并为 sharedValue 分配新值后,设置共享值。 在 main() 方法中,启动了三个线程 readThread1、readThread2 和 readThread3 来同时读取 sharedValue,并创建了一个线程 setThread 来修改它。通过连接所有线程来确保程序在继续之前等待每个线程完成。 然后,代码显示了 sharedValue 的最终值以及每个线程的读取结果。为了避免数据不一致和竞态场景,互斥锁确保多个线程不能同时访问和修改共享项。 6. 条件变量 (std::condition_variable)std::condition_variable 是 C++ 中同步的基础,它允许线程等待直到满足特定条件后才能继续执行。它与互斥锁交互以控制线程同步和并发执行。当一个线程需要等待特定条件变为 true 才能继续执行时,就会使用条件变量,而互斥锁则用于保护共享资源。 例如,在生产者-消费者场景中,一个线程创建数据,另一个线程消耗数据,std::condition_variable 在构建更复杂的同步模式中至关重要。它有助于避免忙等待,当线程反复检查条件而不放弃控制时,忙等待可能占用大量资源且效率低下。 示例 解释 此代码利用条件变量,展示了主线程和工作线程如何进行通信和同步。主线程向工作线程发送数据准备和处理开始信号。收到此信号后,工作线程处理数据并通知主线程处理完成。 在生成最终处理数据之前,主线程等待此信号。通过这种方法,多线程系统的性能得到了提高,因为工作线程仅在数据准备好时才处理数据,并避免了忙等待。使用互斥锁和条件变量提高了多线程程序的稳定性,它们提供了线程之间同步和协调的交互。 7. 信号量 (std::counting_semaphore)std::counting_semaphore 同步原语首次在 C++20 中引入。它是一种灵活的实用程序,可控制一个允许许可证计数,从而允许一定数量的线程同时使用某个资源。线程在继续之前等待权限被授予;如果获得权限,它们将继续。 这在访问例如工作线程池受到限制的情况下很有用,以优化资源利用率并管理冲突。它是一种更灵活的二元信号量替代品,具有受控的并发访问和对共享资源的更少争用。 8. 原子操作 (std::atomic)原子操作提供了一种在不使用锁的情况下以原子方式对共享变量执行某些操作的机制。它们对于递增计数等简单操作非常有用。 C++ 中线程同步的优点
下一主题C++ 中的数组中的领导者 |
我们请求您订阅我们的新闻通讯以获取最新更新。