【问题标题】:C# Multithreading with slots带插槽的 C# 多线程
【发布时间】:2023-04-05 09:40:01
【问题描述】:

我有这个检查代理服务器的功能,目前它只检查一些线程并等待所有线程完成,直到下一组开始。是否可以在一个线程从允许的最大值完成后立即启动一个新线程?

for (int i = 0; i < listProxies.Count(); i+=nThreadsNum)
{                              
    for (nCurrentThread = 0; nCurrentThread < nThreadsNum; nCurrentThread++)
    {
        if (nCurrentThread < nThreadsNum)
        {
           string strProxyIP = listProxies[i + nCurrentThread].sIPAddress;
           int nPort = listProxies[i + nCurrentThread].nPort;
                    tasks.Add(Task.Factory.StartNew<ProxyAddress>(() => CheckProxyServer(strProxyIP, nPort, nCurrentThread)));
        }
     }                

     Task.WaitAll(tasks.ToArray());

     foreach (var tsk in tasks)
     {
        ProxyAddress result = tsk.Result;
        UpdateProxyDBRecord(result.sIPAddress, result.bOnlineStatus);
     }

     tasks.Clear();                
}

【问题讨论】:

  • this 是您要找的吗?
  • 也许您正在寻找 ThreadPool:docs.microsoft.com/en-us/dotnet/api/… 或 Parallel.ForEach:docs.microsoft.com/en-us/dotnet/api/…
  • @EJoshuaS 不是真的,因为延续任务/链继续其父级的结果,我不需要将结果从一个线程传递到另一个
  • 您没有正确使用任务。 “检查代理”受 I/O 限制。您可能不需要超过 1 个线程来同时驱动所有线程。您不需要多线程或使用任务并行库(用于 CPU 绑定操作)接受的答案。不要执行Task.Result,阅读async best practices 并选择BionicCode's answer 中的一个更好的选项。

标签: c# .net multithreading


【解决方案1】:

这看起来要简单得多:

int numberProcessed = 0;
Parallel.ForEach(listProxies,
  new ParallelOptions { MaxDegreeOfParallelism = nThreadsNum },
  (p)=> {
    var result = CheckProxyServer(p.sIPAddress, s.nPort, Thread.CurrentThread.ManagedThreadId);
    UpdateProxyDBRecord(result.sIPAddress, result.bOnlineStatus);
    Interlocked.Increment(numberProcessed);
});

带插槽:

var obj = new Object();
var slots = new List<int>();
Parallel.ForEach(listProxies,
  new ParallelOptions { MaxDegreeOfParallelism = nThreadsNum },
  (p)=> {
    int threadId = Thread.CurrentThread.ManagedThreadId;
    int slot = slots.IndexOf(threadId);
    if (slot == -1)
    {
      lock(obj)
      {
        slots.Add(threadId);
      }
      slot = slots.IndexOf(threadId);
    }
    var result = CheckProxyServer(p.sIPAddress, s.nPort, slot);
    UpdateProxyDBRecord(result.sIPAddress, result.bOnlineStatus);
});

我在那里采取了一些捷径来保证线程安全。您不必进行正常的检查-锁定-检查舞蹈,因为永远不会有两个线程尝试将相同的 threadid 添加到列表中,因此第二次检查将始终失败并且不需要。其次,出于同样的原因,我认为您也不需要锁定外部 IndexOf。这使得这是一个非常高效的并发例程,无论可枚举项中有多少项,它都很少锁定(它应该只锁定 nThreadsNum 次)。

【讨论】:

【解决方案2】:

另一种解决方案是使用SemaphoreSlim 或使用BlockinCollection&lt;T&gt; 的生产者-消费者模式。两种解决方案都支持取消。

SemaphoreSlim

private async Task CheckProxyServerAsync(IEnumerable<object> proxies)
{
  var tasks = new List<Task>();
  int currentThreadNumber = 0;
  int maxNumberOfThreads = 8;

  using (semaphore = new SemaphoreSlim(maxNumberOfThreads, maxNumberOfThreads))
  {
    foreach (var proxy in proxies)
    {
      // Asynchronously wait until thread is available if thread limit reached
      await semaphore.WaitAsync();

      string proxyIP = proxy.IPAddress;
      int port = proxy.Port;
      tasks.Add(Task.Run(() => CheckProxyServer(proxyIP, port, Interlocked.Increment(ref currentThreadNumber)))
        .ContinueWith(
          (task) =>
          {
            ProxyAddress result = task.Result;

            // Method call must be thread-safe!
            UpdateProxyDbRecord(result.IPAddress, result.OnlineStatus);

            Interlocked.Decrement(ref currentThreadNumber);

            // Allow to start next thread if thread limit was reached
            semaphore.Release();
          },
          TaskContinuationOptions.OnlyOnRanToCompletion));
    }

    // Asynchronously wait until all tasks are completed
    // to prevent premature disposal of semaphore
    await Task.WhenAll(tasks);
  }
}

生产者-消费者模式

// Uses a fixed number of same threads
private async Task CheckProxyServerAsync(IEnumerable<ProxyInfo> proxies)
{
  var pipe = new BlockingCollection<ProxyInfo>();
  int maxNumberOfThreads = 8;
  var tasks = new List<Task>();

  // Create all threads (count == maxNumberOfThreads)
  for (int currentThreadNumber = 0; currentThreadNumber < maxNumberOfThreads; currentThreadNumber++)
  {
    tasks.Add(
      Task.Run(() => ConsumeProxyInfo(pipe, currentThreadNumber)));
  }

  proxies.ToList().ForEach(pipe.Add);
  pipe.CompleteAdding();

  await Task.WhenAll(tasks);
}

private void ConsumeProxyInfo(BlockingCollection<ProxyInfo> proxiesPipe, int currentThreadNumber)
{
  while (!proxiesPipe.IsCompleted)
  {
    if (proxiesPipe.TryTake(out ProxyInfo proxy))
    {
      int port = proxy.Port;
      string proxyIP = proxy.IPAddress;
      ProxyAddress result = CheckProxyServer(proxyIP, port, currentThreadNumber); 

      // Method call must be thread-safe!
      UpdateProxyDbRecord(result.IPAddress, result.OnlineStatus);
    }
  }
}

【讨论】:

  • 这是一个很好的解决方案,并且在修复 nCurrentThread 后可以很好地使用 SemaphoreSlim。
  • @RobertMcKee 什么需要修复,你能告诉我吗?我认为nCurrentThread 只是线程计数器,对吗?我的意思是总线程数或相对于可用线程数(是否也需要减少)?
  • 他把它用作线程索引,所以它的范围应该是从 0 到最大线程数,所以每个当前运行的线程在这个范围内都会有一个唯一的值。
  • @RobertMcKee 谢谢你的提示。我改了。
【解决方案3】:

如果我正确理解了您的问题,那么使用await Task.WhenAny 做这件事实际上相当简单。基本上,您保留所有正在运行的任务的集合。一旦达到一定数量的正在运行的任务,您就等待一个或多个任务完成,然后从您的集合中删除已完成的任务并继续添加更多任务。

下面是我的意思的一个例子:

        var tasks = new List<Task>();

        for (int i = 0; i < 20; i++)
        {
            // I want my list of tasks to contain at most 5 tasks at once
            if (tasks.Count == 5)
            {
                // Wait for at least one of the tasks to complete
                await Task.WhenAny(tasks.ToArray());

                // Remove all of the completed tasks from the list
                tasks = tasks.Where(t => !t.IsCompleted).ToList();
            }

            // Add some task to the list
            tasks.Add(Task.Factory.StartNew(async delegate ()
                {
                    await Task.Delay(1000);
                }));
        }

【讨论】:

  • @MarioM 在从列表中删除任务之前,您可以对已完成的任务执行 LINQ 查询并获取其结果。
  • 是否可以有一个插槽ID?所以每个线程都知道线程的编号是多少?所以我可以在网格中显示每个线程的进度状态
  • @MarioM 你能使用列表中的索引吗?
  • 不,因为索引上升到 5000,而我只有 32 个线程
  • 我也无法使用异步委托任务将参数添加到函数中。Add(Task.Factory.StartNew(async delegate () => CheckProxyServer(strProxyIP, nPort, nCurrentThread)));
【解决方案4】:

我建议稍微改变你的方法。不要启动和停止线程,而是将您的代理服务器数据放入一个并发队列中,每个代理服务器一个项目。然后创建一个固定数量的线程(或异步任务)来处理队列。在我看来,这更有可能提供流畅的性能(您不会一遍又一遍地启动和停止线程,这会产生开销)并且更容易编码。

一个简单的例子:

class ProxyChecker
{
    private ConcurrentQueue<ProxyInfo> _masterQueue = new ConcurrentQueue<ProxyInfo>();

    public ProxyChecker(IEnumerable<ProxyInfo> listProxies)
    {
        foreach (var proxy in listProxies)
        {
            _masterQueue.Enqueue(proxy);
        }
    }

    public async Task RunChecks(int maximumConcurrency)
    {
        var count = Math.Max(maximumConcurrency, _masterQueue.Count);
        var tasks = Enumerable.Range(0, count).Select( i => WorkerTask() ).ToList();
        await Task.WhenAll(tasks);
    }

    private async Task WorkerTask()
    {
        ProxyInfo proxyInfo;
        while ( _masterList.TryDequeue(out proxyInfo))
        {
            DoTheTest(proxyInfo.IP, proxyInfo.Port)
        }
    }
} 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多