在方法的一个实例上运行的线程

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?

是的。但是有什么意义呢?

如果线程将所有时间都花在等待其他线程上,您从多线程中获得了什么?

产生更多线程的三个(有时是重叠的)原因:

  1. 同时使用多个内核:整体吞吐量增加。
  2. 在另一个线程正在等待其他东西(通常 I/O 来自文件、数据库或网络)时完成工作:整体吞吐量增加。
  3. 在完成工作时响应用户交互:整体吞吐量降低,但用户感觉更快,因为它们是单独响应的。

此处最后一项不适用。

如果您的 "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;
  }
}

现在我们的优势是:

  1. 我们所有的锁定都是低级别的(但请注意,Interlocked 操作在面对争用时仍会减慢速度,尽管不如 lock 慢)。
  2. 我们的大部分锁定都不受其他锁定的影响。具体来说,在 SetIfFalse 中可以允许检查单独的区域,而根本不会相互妨碍。

但这既不是灵丹妙药(这种方法在争论中仍然会受到影响,并且也会带来成本)也不容易推广到其他情况(将 SetIfFalse 更改为可以做更多事情的东西)检查并更改单个值并不容易)。即使在有很多内核的机器上,这仍然很可能比单线程方法慢。

另一种可能性是根本没有 SetIfFalse 线程安全,但要确保每个线程彼此分区,以便它们永远不会达到相同的值 并且在这种多线程访问的情况下结构是安全的(当线程只命中不同的索引时,超过机器字大小的固定元素数组是线程安全的,必须可变结构,其中可以Add and/or Remove 不是).

总而言之,关于如何使用 lock 防止线程导致错误,您的想法是正确的,当某些东西适合多线程时,98% 的时间都是使用这种方法因为它涉及等待其他东西的线程。你的例子虽然命中锁定太多而无法从多核中获益,但创建这样做的代码并不简单。