私有变量 类 与其他 类 之间的线程安全
Thread safety among classes with other classes for private variables
我正在编写一个游戏引擎(为了好玩),并发有很多线程 运行。我有一个 class 将另一个 class 的实例作为私有变量保存,而后者又将另一个 class 的实例作为私有变量保存。我的问题是,我应该努力确保线程安全 class 中的哪一个?
我是否让它们都成为线程安全的,并且让它们中的每一个都使用互斥锁来保护它们的数据,我是否只让其中一个成为线程安全的,并且假设任何使用我的代码的人都必须明白,如果你正在使用底层 classes 它们本质上不是线程安全的。
示例:
class A {
private:
B b;
}
class B {
private:
C c;
}
class C {
// data
}
我知道我需要每个 class 的数据以避免因数据竞争而损坏,但是我想避免在每个 class 的每个方法上都抛出大量互斥锁。我不确定正确的约定是什么。
你几乎肯定不想尝试让 every class 线程安全,因为这样做最终会非常低效(有很多不必要的锁定和解锁互斥量没有任何好处)并且也容易出现死锁(您必须一次锁定的互斥量越多,您就越有可能让不同的线程以不同的顺序锁定互斥量序列,这是一个入口条件死锁,因此你的程序冻结在你身上)。
如果弄清楚哪个线程需要访问哪个数据结构,您想做什么。在设计数据结构时,您想尝试以线程之间共享的数据量尽可能少的方式设计它们——如果您可以将其减少到零,那么您就不需要进行任何序列化根本! (您可能无法做到这一点,但如果您进行 CSP/message-passing 设计,您可以非常接近,因为您唯一需要锁定的互斥量是保护消息传递队列的互斥量)
还请记住,您的互斥量不仅用于 "protect the data",还允许线程进行一系列更改,从可能访问该数据的其他线程的角度来看,这些更改似乎是原子的.也就是说,如果您的线程 #1 需要对对象 A、B 和 C 进行更改,并且所有这三个对象都有自己的互斥体,线程 #1 在修改对象之前锁定并在之后解锁,您仍然可以存在竞争条件,因为线程 #2 可能 "see" 更新已完成一半(即线程 #2 可能会在您更新 A 之后但在更新 B 和 C 之前检查对象)。因此,您通常需要将互斥量提高到一个级别,使它们涵盖您可能需要一次性更改的所有对象——在 ABC 示例中,这意味着您可能希望拥有一个用于序列化访问的互斥量到 A、B 和 C。
一种方法是从整个程序的单个全局互斥体开始——任何时候任何线程需要读取或写入任何 其他线程可以访问的数据结构,即它锁定(并在之后解锁)的互斥锁。这种设计可能不会非常有效(因为线程可能会花费大量时间等待互斥体),但绝对不会遇到死锁问题。然后,一旦你完成了这项工作,你就可以看看那个单一的互斥量是否真的是你明显的性能瓶颈——如果不是,你就完成了,发布你的程序:) OTOH 如果它是瓶颈,你可以然后分析您的哪些数据结构在逻辑上彼此独立,并将您的全局互斥锁分成两个互斥锁——一个用于序列化对数据结构子集 A 的访问,另一个用于序列化对子集 B 的访问。(请注意,子集不'需要大小相等——子集 B 可能只包含一个对性能至关重要的特定数据结构)根据需要重复,直到您对性能感到满意,或者您的程序开始变得太复杂或错误(在这种情况下你可能想再次拨回互斥粒度以恢复你的理智)。
我正在编写一个游戏引擎(为了好玩),并发有很多线程 运行。我有一个 class 将另一个 class 的实例作为私有变量保存,而后者又将另一个 class 的实例作为私有变量保存。我的问题是,我应该努力确保线程安全 class 中的哪一个?
我是否让它们都成为线程安全的,并且让它们中的每一个都使用互斥锁来保护它们的数据,我是否只让其中一个成为线程安全的,并且假设任何使用我的代码的人都必须明白,如果你正在使用底层 classes 它们本质上不是线程安全的。
示例:
class A {
private:
B b;
}
class B {
private:
C c;
}
class C {
// data
}
我知道我需要每个 class 的数据以避免因数据竞争而损坏,但是我想避免在每个 class 的每个方法上都抛出大量互斥锁。我不确定正确的约定是什么。
你几乎肯定不想尝试让 every class 线程安全,因为这样做最终会非常低效(有很多不必要的锁定和解锁互斥量没有任何好处)并且也容易出现死锁(您必须一次锁定的互斥量越多,您就越有可能让不同的线程以不同的顺序锁定互斥量序列,这是一个入口条件死锁,因此你的程序冻结在你身上)。
如果弄清楚哪个线程需要访问哪个数据结构,您想做什么。在设计数据结构时,您想尝试以线程之间共享的数据量尽可能少的方式设计它们——如果您可以将其减少到零,那么您就不需要进行任何序列化根本! (您可能无法做到这一点,但如果您进行 CSP/message-passing 设计,您可以非常接近,因为您唯一需要锁定的互斥量是保护消息传递队列的互斥量)
还请记住,您的互斥量不仅用于 "protect the data",还允许线程进行一系列更改,从可能访问该数据的其他线程的角度来看,这些更改似乎是原子的.也就是说,如果您的线程 #1 需要对对象 A、B 和 C 进行更改,并且所有这三个对象都有自己的互斥体,线程 #1 在修改对象之前锁定并在之后解锁,您仍然可以存在竞争条件,因为线程 #2 可能 "see" 更新已完成一半(即线程 #2 可能会在您更新 A 之后但在更新 B 和 C 之前检查对象)。因此,您通常需要将互斥量提高到一个级别,使它们涵盖您可能需要一次性更改的所有对象——在 ABC 示例中,这意味着您可能希望拥有一个用于序列化访问的互斥量到 A、B 和 C。
一种方法是从整个程序的单个全局互斥体开始——任何时候任何线程需要读取或写入任何 其他线程可以访问的数据结构,即它锁定(并在之后解锁)的互斥锁。这种设计可能不会非常有效(因为线程可能会花费大量时间等待互斥体),但绝对不会遇到死锁问题。然后,一旦你完成了这项工作,你就可以看看那个单一的互斥量是否真的是你明显的性能瓶颈——如果不是,你就完成了,发布你的程序:) OTOH 如果它是瓶颈,你可以然后分析您的哪些数据结构在逻辑上彼此独立,并将您的全局互斥锁分成两个互斥锁——一个用于序列化对数据结构子集 A 的访问,另一个用于序列化对子集 B 的访问。(请注意,子集不'需要大小相等——子集 B 可能只包含一个对性能至关重要的特定数据结构)根据需要重复,直到您对性能感到满意,或者您的程序开始变得太复杂或错误(在这种情况下你可能想再次拨回互斥粒度以恢复你的理智)。