C# 在方法的一个实例上操作的线程

C# 在方法的一个实例上操作的线程,c#,multithreading,mutex,C#,Multithreading,Mutex,我正在创建一个应用程序,其中我有50x50地图。在这张地图上我可以添加点,它们是类“点”的新实例。每个点都有自己的线程,每个与特定点连接的线程都在类的“explore”方法上运行,在这个方法中还有另一个方法“check_place(x,y)”,它负责检查地图上的某个地方是否已经被发现。如果不是,则类“num_discovered”的静态变量应该递增。应用程序中启动的每个线程都应该实时访问“check_place(x,y)”方法的单个实例 建造商: public dot(Form1 F) {

我正在创建一个应用程序,其中我有50x50地图。在这张地图上我可以添加点,它们是类“点”的新实例。每个点都有自己的线程,每个与特定点连接的线程都在类的“explore”方法上运行,在这个方法中还有另一个方法“check_place(x,y)”,它负责检查地图上的某个地方是否已经被发现。如果不是,则类“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();
}
检查位置(x,y)方法:

在explore方法中,我调用方法“check_place(x,y)”,如下所示:

dot.check_place(x, y);

如果一次只有一个点可以检查位置是否已被发现,这是否足以实现这种情况?

您在这方面的性能可能非常差-我建议使用Task.Run here,以便在需要在多个线程上并行运行explore方法时提高效率

就锁定和线程安全而言,如果在check_place中锁定是您在discovered变量中设置bools并设置num_discovered变量的唯一位置,那么现有代码将起作用。如果您从代码中的其他地方开始设置它们,那么您也需要在那里使用锁

此外,在读取这些变量时,您应该使用相同的锁对象将这些值读入其他锁内的局部变量,以维护线程安全


我还有其他建议,但这是您在这里需要的两个最基本的东西。

您在这方面的性能可能非常差-我建议使用Task.Run here,以便在需要在多个线程上并行运行explore方法时提高效率

就锁定和线程安全而言,如果在check_place中锁定是您在discovered变量中设置bools并设置num_discovered变量的唯一位置,那么现有代码将起作用。如果您从代码中的其他地方开始设置它们,那么您也需要在那里使用锁

此外,在读取这些变量时,您应该使用相同的锁对象将这些值读入其他锁内的局部变量,以维护线程安全

我还有其他建议,但这是你在这里需要的两个最基本的东西

这是否足以实现一种情况,即在单一时间内只有一个点可以检查该位置是否已被发现

对。但这有什么意义呢

如果线程将所有时间都花在等待其他线程上,那么多线程有什么好处呢

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

  • 同时使用多个核心:总体吞吐量增加
  • 在另一个线程等待其他线程(通常是来自文件、数据库或网络的I/O)时完成工作:总体吞吐量增加
  • 在工作完成时响应用户交互:总体吞吐量降低,但用户感觉更快,因为他们分别对交互做出响应
  • 这里最后一个不适用

    如果您的“检查”涉及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
    ,而不占用其他线程

    当线程将在锁定周期结束时获得结果时,这非常有用。不过,这还不够好,因为如果它再次争夺锁,它将很快进入一个案例

    如果我们对
    的查找发现
    本身是线程安全的,并且线程安全是细粒度的,那么我们可以取得一些进展:

    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;
      }
    }
    
    现在我们的优势是:

  • 我们所有的锁定级别都要低得多(但请注意,
    联锁的
    操作在遇到争用时仍然会减慢,尽管不如
    锁定
  • 我们的大部分锁定都是以其他锁定方式实现的。具体地说,在
    SetIfFalse
    中,可以允许对单独的区域进行检查,而无需相互检查
  • 但这既不是万灵药(此类方法在面临争议时仍会受到影响,也会带来自身的成本),也不容易推广到其他情况(将
    SetIfFalse
    更改为只检查和更改单个值并不容易)。即使在拥有大量内核的机器上,这也很可能比单线程方法慢

    阿诺特
    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;
      }
    }