C#创建与处理器一样多的类实例

时间:2009-06-19 15:50:58

标签: c# multithreading

我有一个GUI C#应用程序,只有一个按钮Start / Stop。

最初,这个GUI创建了一个查询数据库的类的单个实例,如果有结果则执行某些操作,并且一次从数据库中获取一个“任务”。

然后我被要求尝试利用8个核心系统中的一些系统的所有计算能力。使用我认为的处理器数量,我可以创建我的类的实例数量并运行它们,并且非常接近使用公平的计算能力。

Environment.ProccessorCount;

使用此值,在GUI表单中,我一直尝试循环一次循环ProccessorCount并启动一个在类中调用“doWork”类型方法的新线程。然后休眠1秒钟(以确保初始查询通过),然后进入循环的下一部分。

我一直遇到问题,因为它似乎要等到循环完成才能启动查询导致某种类型的冲突(从MySQL数据库中获取相同的值)。

在主窗体中,一旦启动“worker”,它就会将按钮文本更改为STOP,如果再次按下该按钮,它应该在每个“worker”上执行“stopWork”方法。

我想要完成的是否有意义?有没有更好的方法(不涉及重组工人阶级)?

9 个答案:

答案 0 :(得分:2)

重构您的设计,以便在后台运行一个线程,检查数据库是否有工作要做。

当找到要做的工作时,为每个工作项生成一个新线程。

不要忘记使用同步工具(如信号量和互斥量)作为密钥有限的资源。微调同步是值得的。

您还可以尝试使用最大数量的工作线程 - 我的猜测是它会超过您当前的处理器数量。

答案 1 :(得分:2)

虽然关于多线程开发的最佳实践的详尽答案有点超出我在这里所写的内容,但有几点:

  1. 除非绝对必要,否则请勿使用Sleep()等待某些事情继续。如果您还有另一个需要等待完成的代码流程,则可以Join()该线程或使用ManualResetEventAutoResetEvent。 MSDN上有很多关于它们用法的信息。花一些时间来阅读它。
  2. 你无法保证你的线程每个都运行在自己的核心上。虽然操作系统线程调度程序完全可能会这样做,但请注意它并不能保证。

答案 2 :(得分:1)

我认为增加处理器使用的最简单方法是在ThreadPool(通过调用ThreadPool.QueueUserWorkItem)的线程上简单地生成worker方法。如果在循环中执行此操作,运行时将从线程池中获取线程并并行运行工作线程。

ThreadPool.QueueUserWorkItem(state => DoWork());

答案 3 :(得分:1)

永远不要使用Sleep进行线程同步。

您的问题没有提供足够的详细信息,但您可能希望使用ManualResetEvent让工作人员等待初始查询。

答案 4 :(得分:1)

是的,你想要做的事情是有意义的。

让8个工作人员从队列中消耗任务是有意义的。如果需要访问共享状态,则应该正确地同步线程。根据您对问题的描述,听起来您遇到线程同步问题。

您应该记住,您只能从GUI线程更新GUI。这也可能是你问题的根源。

如果没有更多信息或代码示例,真的无法分辨出问题究竟是什么。

答案 5 :(得分:0)

我怀疑你有这样的问题:你需要将循环变量(任务)的副本复制到currenttask中,否则所有线程实际上都共享同一个变量。

<main thread>
var tasks = db.GetTasks();
foreach(var task in tasks) {
   var currenttask = task; 
   ThreadPool.QueueUserWorkItem(state => DoTask(currenttask));
   // or, new Thread(() => DoTask(currentTask)).Start()
   // ThreadPool.QueueUserWorkItem(state => DoTask(task)); this doesn't work!
}

请注意,主线程上不应该使用Thread.Sleep()来等待工作线程完成。如果使用线程池,则可以继续对工作项进行排队,如果要等待执行任务完成,则应使用AutoResetEvent之类的东西来等待线程完成。

答案 6 :(得分:0)

您似乎遇到了多线程编程的常见问题。它被称为Race Condition,你做得很好,可以在进行太多之前对这个和其他多线程问题进行一些研究。快速搞乱所有数据非常容易。

缺点是您必须确保在单个事务的范围内执行对数据库的所有命令(例如:获取可用任务)。

我不知道MySQL足够好给出一个完整的答案,但T-SQL的一个非常基本的例子可能如下所示:

BEGIN TRAN 
DECLARE @taskid int 
SELECT @taskid=taskid FROM tasks WHERE assigned = false
UPDATE tasks SET assigned=true WHERE taskid = @taskID 
SELECT * from tasks where taskid = @taskid 
COMMIT TRAN

MySQL 5及以上版本有support for transactions too

答案 7 :(得分:0)

你也可以锁定“从DB获取任务”代码,这样一次只有一个线程会查询数据库 - 但显然这会稍微降低性能。

你正在做的一些代码(也许是一些SQL,这真的取决于它)将是一个巨大的帮助。

但是假设您从数据库中获取任务,并且这些任务需要一些时间在C#中,您可能需要这样的内容:

object myLock;

void StartWorking()
{
    myLock = new object(); // only new it once, could be done in your constructor too.
    for (int i = 0; i < Environment.Processorcount; i++)
    {
        ThreadPool.QueueUserWorkItem(null => DoWork());
    }
}

void DoWork(object state)
{
    object task;
    lock(myLock)
    {
        task = GetTaskFromDB();
    }

    PerformTask(task);
}

答案 8 :(得分:0)

上面发布了一些好主意。我们遇到的一件事是,我们不仅需要一个支持多处理器的应用程序,还需要一个支持多服务器的应用程序。根据您的应用程序,我们使用一个队列,通过一个公共Web服务器锁定一个锁(导致其他人被阻止),同时我们得到下一个要处理的东西。

在我们的例子中,我们处理大量数据,我们保持单一,我们锁定一个对象,获取下一个未处理项的id,将其标记为正在处理,解锁对象,将记录ID设为处理回调用服务器上的主线程,然后进行处理。这对我们来说似乎很有效,因为锁定,获取,更新和释放所需的时间非常短,而且当阻塞确实发生时,我们在等待reasource时从未遇到死锁情况(因为我们使用的是lock(对象) ){}和一个很好的紧凑的try catch来确保我们在内部优雅地处理错误。

如其他地方所述,所有这些都在主线程中处理。给定要处理的信息,我们将其推送到一个新线程(对我们来说,检索100mb的数据并按每次调用处理它)。这种方法使我们能够扩展到单个服务器之外。在过去我们不得不通过高端硬件解决问题,现在我们可以抛出几个更便宜但功能更强的服务器。我们还可以在低利用率的整个虚拟化场中实现这一目标。

另一方面,我没有提及,我们也在我们的存储过程中使用锁定互斥锁,所以如果两个服务器上的两个应用程序同时调用它,它会被优雅地处理。所以上面的概念适用于我们的应用程序和数据库。我们的客户端后端是MySql 5.1系列,只需几行即可完成。

我认为人们在开发过程中会忘记的其中一件事是你想要相对快速地进出锁。如果你想返回大块的数据,我个人不会在锁本身中这样做,除非你真的不得不这样做。否则,如果每个人都在等待获取数据,那么你就无法真正做多次多线程化。

好的,找到了我的MySql代码,可以满足您的需求。

    DELIMITER //

    CREATE PROCEDURE getnextid(
        I_service_entity_id INT(11)
      , OUT O_tag VARCHAR(36)
    )

    BEGIN
      DECLARE L_tag VARCHAR(36) DEFAULT '00000000-0000-0000-0000-000000000000';
      DECLARE L_locked INT DEFAULT 0;

      DECLARE C_next CURSOR FOR
        SELECT tag FROM workitems
        WHERE status in (0)
          AND processable_date <= DATE_ADD(NOW(), INTERVAL 5 MINUTE)
        ;

      DECLARE EXIT HANDLER FOR NOT FOUND
      BEGIN
        SET L_tag := '00000000-0000-0000-0000-000000000000';
        DO RELEASE_LOCK('myuniquelockis');
      END;

      SELECT COALESCE(GET_LOCK('myuniquelockis',20), 0) INTO L_locked;
      IF L_locked > 0 THEN
        OPEN C_next;
        FETCH C_next INTO I_tag;
        IF I_tag <> '00000000-0000-0000-0000-000000000000' THEN
          UPDATE workitems SET
              status = 1
            , service_entity_id = I_service_entity_id
            , date_locked = NOW()
          WHERE tag = I_tag;

        END IF;
        CLOSE C_next;
        DO RELEASE_LOCK('myuniquelockis');
      ELSE
        SET I_tag := L_tag;
      END IF;
    END
    //

    DELIMITER ;

在我们的例子中,我们将GUID作为out参数返回给C#。您可以使用SELECT L_tag替换最后的SET;并完成它并松开OUT参数,但我们从另一个包装器中调用它......

希望这有帮助。