簡體   English   中英

C#創建的類實例與處理器一樣多

[英]C# creating as many instances of a class as there are processors

我有一個具有單個按鈕“開始/停止”的GUI C#應用程序。

最初,此GUI創建的是一個類的單個實例,該類查詢數據庫並在有結果的情況下執行一些操作,並一次從數據庫中獲取單個“任務”。

然后,我被要求嘗試在8個核心系統中的某些上利用所有計算能力。 使用我認為的處理器數量,我可以創建該類實例的數量,並全部運行它們,並且幾乎可以使用相當數量的計算能力。

Environment.ProccessorCount;

在GUI表單中,使用此值,我試圖遍歷循環ProccessorCount並啟動一個新線程,該線程在類中調用“ doWork”類型方法。 然后睡眠1秒鍾(以確保初始查詢通過),然后繼續進行循環的下一部分。

我一直對此有問題,因為它似乎要等到循環完成才能開始查詢,從而導致某種沖突(從MySQL數據庫獲取相同的值)。

在主窗體中,一旦啟動“工作人員”,它將按鈕文本更改為“停止”,如果再次單擊該按鈕,則應在每個“工作人員”上執行“ stopWork”方法。

我要完成的工作是否有意義? 有沒有更好的方法可以做到這一點(不涉及重組worker類)?

重組您的設計,以便您有一個線程在后台運行,以檢查數據庫是否有工作要做。

當發現有工作要做時,為每個工作項目產生一個新線程。

不要忘記為關鍵的有限資源使用同步工具,例如信號量和互斥量。 微調同步值得您花費時間。

您還可以嘗試使用最大工作線程數-我的猜測是,這將比您當前的處理器數少。

盡管對多線程開發最佳實踐的詳盡解答超出了我在此處可以寫的內容,但有兩點:

  1. 除非絕對必要,否則不要使用Sleep()等待繼續。 如果您需要等待完成的另一個代碼過程,則可以將該線程Join()或使用ManualResetEventAutoResetEvent MSDN上有很多有關其用法的信息。 花一些時間閱讀它。
  2. 您不能真正保證每個線程都可以在各自的內核上運行。 盡管很有可能OS線程調度程序會執行此操作,但是請注意,這不能保證。

我認為增加處理器使用率的最簡單方法是簡單地從ThreadPool生成線程上的worker方法(通過調用ThreadPool.QueueUserWorkItem )。 如果在循環中執行此操作,則運行時將從線程池中拾取線程並並行運行工作線程。

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

切勿將Sleep用於線程同步。

您的問題沒有提供足夠的詳細信息,但是您可能想使用ManualResetEvent來使工作程序等待初始查詢。

是的,您要嘗試執行的操作很有意義。

安排8個工作人員有意義,每個工作人員都從隊列中消耗任務。 如果線程需要訪問共享狀態,則應注意正確同步線程。 從對問題的描述中,聽起來好像您遇到了線程同步問題。

您應該記住,只能從GUI線程更新GUI。 那也可能是問題的根源。

沒有更多的信息或代碼示例,實際上沒有辦法說出問題到底出在哪里。

我懷疑您有這樣的問題:您需要將循環變量(任務)的副本復制到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之類的東西來等待線程完成。

您似乎在多線程編程中遇到一個常見問題。 這被稱為“ 競態條件” ,您最好在進行過多研究之前,先對此和其他多線程問題進行一些研究。 快速弄亂所有數據非常容易。

簡而言之,您必須確保對數據庫的所有命令(例如:獲取可用任務)都在單個事務范圍內執行。

我不太了解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及更高版本也支持事務

您還可以圍繞“從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);
}

上面有一些好主意。 我們遇到的一件事是,我們不僅需要支持多處理器的應用程序,而且還需要支持多服務器的應用程序。 根據您的應用程序,我們使用一個隊列,該隊列通過一個通用的Web服務器被包裹在一個鎖中(導致其他服務器被阻止),同時我們還要處理下一個事件。

在我們的例子中,我們正在處理大量數據,我們要保持單一狀態,鎖定一個對象,獲取下一個未處理項目的ID,將其標記為已處理,將對象解鎖,將要處理的記錄ID交還給調用服務器上的主線程,然后對其進行處理。 因為鎖定,獲取,更新和釋放所花費的時間很小,而且確實發生了阻塞,但在等待資源時我們絕不會遇到死鎖的情況(因為我們正在使用lock(object } {}並嘗試在內部進行精確嘗試,以確保我們在內部優雅地處理錯誤。

如在其他地方提到的,所有這些都在主線程中處理。 給定要處理的信息,我們將其推送到新線程(對於我們來說,這將檢索100mb的數據並在每次調用時對其進行處理)。 這種方法使我們可以擴展到單個服務器之外。 過去,我們不得不通過高端硬件解決問題,而現在,我們可以投入一些更便宜但仍非常強大的服務器。 我們還可以在低利用率期間在我們的虛擬化場中完成此任務。

我沒有提到的另一件事是,我們還在存儲的proc中也使用了鎖定互斥鎖,因此,如果兩個服務器上的兩個應用程序同時調用它,則可以正常處理。 因此,以上概念適用於我們的應用程序和數據庫。 我們的客戶后端是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參數,但是我們從另一個包裝器中調用此方法...

希望這可以幫助。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM