在方法的一个实例上运行的线程
Threads operating on the one instance of a method
我正在创建一个应用程序,其中有 50x50 的地图。在这张地图上,我可以添加点,它们是 class "dot" 的新实例。每个点都有自己的线程,每个连接到特定点的线程都在class的方法"explore"上运行,在这个方法中还有另一个方法"check_place(x,y)"负责检查如果地图上的某个地方已经被发现。如果不是,则应递增 class "num_discovered" 的静态变量。该方法的单个实例 "check_place(x,y)" 应由应用程序中启动的每个线程实时访问。
构造函数:
public dot(Form1 F)
{
/...
thread = new System.Threading.Thread(new System.Threading.ThreadStart(explore)); //wątek wykonujący metodę explore klasy robot
thread.Start();
}
check_place(x,y) 方法:
static void check_place(int x, int y)
{
lock (ob)
{
if (discovered[x, y] == false)
{
discovered[x, y] = true;
num_discovered += 1;
}
}
}
在探索方法中,我像这样调用方法 "check_place(x,y)":
dot.check_place(x, y);
是否足以实现单次只有一个点可以检查地点是否已经被发现的情况?
你在这方面的表现可能会很糟糕 - 我建议在此处使用 Task.Run 以在你需要 运行 在多个线程上并行探索方法时提高效率。
就锁定和线程安全而言,如果 check_place 中的锁定是您在已发现变量中设置布尔值并设置 num_discovered 变量的唯一位置,则现有代码将起作用.如果您从代码中的其他地方开始设置它们,您也需要在那里使用锁。
此外,在读取这些变量时,您应该使用相同的锁对象将这些值读入其他锁内的局部变量,以维护这里的线程安全。
我还有其他建议,但这是您在这里需要的两个最基本的东西。
Is it enough to achieve a situation where in the single time only one dot can check if place was already discovered?
是的。但是有什么意义呢?
如果线程将所有时间都花在等待其他线程上,您从多线程中获得了什么?
产生更多线程的三个(有时是重叠的)原因:
- 同时使用多个内核:整体吞吐量增加。
- 在另一个线程正在等待其他东西(通常 I/O 来自文件、数据库或网络)时完成工作:整体吞吐量增加。
- 在完成工作时响应用户交互:整体吞吐量降低,但用户感觉更快,因为它们是单独响应的。
此处最后一项不适用。
如果您的 "checking" 涉及 I/O 那么第二个可能适用,并且此策略可能有意义。
第一个很适用,但是因为所有线程的大部分时间都在等待其他线程,所以您不会获得吞吐量的改进。
确实,因为设置线程和在线程之间切换涉及开销,所以这段代码会比让一个线程做所有事情要慢:如果一次只有一个线程可以工作,那么就只有一个线程!
所以你在这里使用锁是正确的,因为它可以防止损坏和错误,但毫无意义,因为它会使一切变得太慢。
如何处理:
如果您的实际情况涉及 I/O 或线程实际上将大部分时间花在彼此之间的其他原因,那么您所拥有的就很好。
否则你有两个选择。
简单:只需使用一个线程。
硬:有更好的锁定。
获得更精细锁定的一种方法是进行双重检查:
static void check_place(int x, int y)
{
if (!discovered[x, y])
lock (ob)
if (!discovered[x, y])
{
discovered[x, y] = true;
num_discovered += 1;
}
}
现在至少一些线程会跳过某些 discovered[x, y]
是 true
的情况,而不会阻止其他线程。
当线程要在锁定期结束时获得结果时,这很有用。不过它在这里仍然不够好,因为如果它再次争夺锁,它只会快速移动到一个案例。
如果我们对 discovered
的查找本身是线程安全的,并且线程安全是细粒度的,那么我们可以取得一些进展:
static void check_place(int x, int y)
{
if (discovered.SetIfFalse(x, y))
Interlocked.Increment(ref num_discovered)
}
到目前为止,我们只是解决了问题;我们如何在不使用单个锁并导致相同问题的情况下使 SetIfFalse
线程安全?
有几种方法。我们可以使用条带锁或低锁并发集合。
看来你有一个50×50的固定大小的结构,在这种情况下这并不难:
private class DotMap
{
//ints because we can't use interlocked with bools
private int[][] _map = new int[50][];
public DotMap()
{
for(var i = 0; i != 50; ++i)
_map[i] = new int[50];
}
public bool SetIfFalse(int x, int y)
{
return Interlocked.CompareExchange(ref _map[x][y], 1, 0) == 0;
}
}
现在我们的优势是:
- 我们所有的锁定都是低级别的(但请注意,
Interlocked
操作在面对争用时仍会减慢速度,尽管不如 lock
慢)。
- 我们的大部分锁定都不受其他锁定的影响。具体来说,在
SetIfFalse
中可以允许检查单独的区域,而根本不会相互妨碍。
但这既不是灵丹妙药(这种方法在争论中仍然会受到影响,并且也会带来成本)也不容易推广到其他情况(将 SetIfFalse
更改为可以做更多事情的东西)检查并更改单个值并不容易)。即使在有很多内核的机器上,这仍然很可能比单线程方法慢。
另一种可能性是根本没有 SetIfFalse
线程安全,但要确保每个线程彼此分区,以便它们永远不会达到相同的值 并且在这种多线程访问的情况下结构是安全的(当线程只命中不同的索引时,超过机器字大小的固定元素数组是线程安全的,必须可变结构,其中可以Add
and/or Remove
不是).
总而言之,关于如何使用 lock
防止线程导致错误,您的想法是正确的,当某些东西适合多线程时,98% 的时间都是使用这种方法因为它涉及等待其他东西的线程。你的例子虽然命中锁定太多而无法从多核中获益,但创建这样做的代码并不简单。
我正在创建一个应用程序,其中有 50x50 的地图。在这张地图上,我可以添加点,它们是 class "dot" 的新实例。每个点都有自己的线程,每个连接到特定点的线程都在class的方法"explore"上运行,在这个方法中还有另一个方法"check_place(x,y)"负责检查如果地图上的某个地方已经被发现。如果不是,则应递增 class "num_discovered" 的静态变量。该方法的单个实例 "check_place(x,y)" 应由应用程序中启动的每个线程实时访问。
构造函数:
public dot(Form1 F)
{
/...
thread = new System.Threading.Thread(new System.Threading.ThreadStart(explore)); //wątek wykonujący metodę explore klasy robot
thread.Start();
}
check_place(x,y) 方法:
static void check_place(int x, int y)
{
lock (ob)
{
if (discovered[x, y] == false)
{
discovered[x, y] = true;
num_discovered += 1;
}
}
}
在探索方法中,我像这样调用方法 "check_place(x,y)":
dot.check_place(x, y);
是否足以实现单次只有一个点可以检查地点是否已经被发现的情况?
你在这方面的表现可能会很糟糕 - 我建议在此处使用 Task.Run 以在你需要 运行 在多个线程上并行探索方法时提高效率。
就锁定和线程安全而言,如果 check_place 中的锁定是您在已发现变量中设置布尔值并设置 num_discovered 变量的唯一位置,则现有代码将起作用.如果您从代码中的其他地方开始设置它们,您也需要在那里使用锁。
此外,在读取这些变量时,您应该使用相同的锁对象将这些值读入其他锁内的局部变量,以维护这里的线程安全。
我还有其他建议,但这是您在这里需要的两个最基本的东西。
Is it enough to achieve a situation where in the single time only one dot can check if place was already discovered?
是的。但是有什么意义呢?
如果线程将所有时间都花在等待其他线程上,您从多线程中获得了什么?
产生更多线程的三个(有时是重叠的)原因:
- 同时使用多个内核:整体吞吐量增加。
- 在另一个线程正在等待其他东西(通常 I/O 来自文件、数据库或网络)时完成工作:整体吞吐量增加。
- 在完成工作时响应用户交互:整体吞吐量降低,但用户感觉更快,因为它们是单独响应的。
此处最后一项不适用。
如果您的 "checking" 涉及 I/O 那么第二个可能适用,并且此策略可能有意义。
第一个很适用,但是因为所有线程的大部分时间都在等待其他线程,所以您不会获得吞吐量的改进。
确实,因为设置线程和在线程之间切换涉及开销,所以这段代码会比让一个线程做所有事情要慢:如果一次只有一个线程可以工作,那么就只有一个线程!
所以你在这里使用锁是正确的,因为它可以防止损坏和错误,但毫无意义,因为它会使一切变得太慢。
如何处理:
如果您的实际情况涉及 I/O 或线程实际上将大部分时间花在彼此之间的其他原因,那么您所拥有的就很好。
否则你有两个选择。
简单:只需使用一个线程。 硬:有更好的锁定。
获得更精细锁定的一种方法是进行双重检查:
static void check_place(int x, int y)
{
if (!discovered[x, y])
lock (ob)
if (!discovered[x, y])
{
discovered[x, y] = true;
num_discovered += 1;
}
}
现在至少一些线程会跳过某些 discovered[x, y]
是 true
的情况,而不会阻止其他线程。
当线程要在锁定期结束时获得结果时,这很有用。不过它在这里仍然不够好,因为如果它再次争夺锁,它只会快速移动到一个案例。
如果我们对 discovered
的查找本身是线程安全的,并且线程安全是细粒度的,那么我们可以取得一些进展:
static void check_place(int x, int y)
{
if (discovered.SetIfFalse(x, y))
Interlocked.Increment(ref num_discovered)
}
到目前为止,我们只是解决了问题;我们如何在不使用单个锁并导致相同问题的情况下使 SetIfFalse
线程安全?
有几种方法。我们可以使用条带锁或低锁并发集合。
看来你有一个50×50的固定大小的结构,在这种情况下这并不难:
private class DotMap
{
//ints because we can't use interlocked with bools
private int[][] _map = new int[50][];
public DotMap()
{
for(var i = 0; i != 50; ++i)
_map[i] = new int[50];
}
public bool SetIfFalse(int x, int y)
{
return Interlocked.CompareExchange(ref _map[x][y], 1, 0) == 0;
}
}
现在我们的优势是:
- 我们所有的锁定都是低级别的(但请注意,
Interlocked
操作在面对争用时仍会减慢速度,尽管不如lock
慢)。 - 我们的大部分锁定都不受其他锁定的影响。具体来说,在
SetIfFalse
中可以允许检查单独的区域,而根本不会相互妨碍。
但这既不是灵丹妙药(这种方法在争论中仍然会受到影响,并且也会带来成本)也不容易推广到其他情况(将 SetIfFalse
更改为可以做更多事情的东西)检查并更改单个值并不容易)。即使在有很多内核的机器上,这仍然很可能比单线程方法慢。
另一种可能性是根本没有 SetIfFalse
线程安全,但要确保每个线程彼此分区,以便它们永远不会达到相同的值 并且在这种多线程访问的情况下结构是安全的(当线程只命中不同的索引时,超过机器字大小的固定元素数组是线程安全的,必须可变结构,其中可以Add
and/or Remove
不是).
总而言之,关于如何使用 lock
防止线程导致错误,您的想法是正确的,当某些东西适合多线程时,98% 的时间都是使用这种方法因为它涉及等待其他东西的线程。你的例子虽然命中锁定太多而无法从多核中获益,但创建这样做的代码并不简单。