2015-10-08 14 views
8

Trong ví dụ sau (một trò chơi "lý tưởng") có hai chủ đề. Các chủ đề chính mà cập nhật dữ liệu và RenderThread mà "ám" nó vào màn hình. Tôi cần hai thứ đó để được đồng bộ. Tôi không thể đủ khả năng để chạy một số bản cập nhật mà không cần chạy một render cho mỗi một trong số họ.Đồng bộ hóa các chủ đề rất nhanh

Tôi sử dụng condition_variable để đồng bộ hóa hai thứ đó, vì vậy, lý tưởng là chuỗi nhanh hơn sẽ dành chút thời gian chờ chậm hơn. Tuy nhiên các biến điều kiện dường như không thực hiện công việc nếu một trong các chủ đề hoàn thành một lần lặp trong một khoảng thời gian rất nhỏ. Có vẻ như để nhanh chóng phản ứng lại khóa của mutex trước wait trong chủ đề khác có thể thu được nó. Mặc dù notify_one được gọi

#include <iostream> 
#include <thread> 
#include <chrono> 
#include <atomic> 
#include <functional> 
#include <mutex> 
#include <condition_variable> 

using namespace std; 

bool isMultiThreaded = true; 

struct RenderThread 
{ 
    RenderThread() 
    { 
     end = false; 
     drawing = false; 
     readyToDraw = false; 
    } 

    void Run() 
    { 
     while (!end) 
     { 
      DoJob(); 
     } 
    } 

    void DoJob() 
    { 
     unique_lock<mutex> lk(renderReadyMutex); 
     renderReady.wait(lk, [this](){ return readyToDraw; }); 
     drawing = true; 

     // RENDER DATA 
     this_thread::sleep_for(chrono::milliseconds(15)); // simulated render time 
     cout << "frame " << count << ": " << frame << endl; 
     ++count; 

     drawing = false; 
     readyToDraw = false; 

     lk.unlock(); 
     renderReady.notify_one(); 
    } 

    atomic<bool> end; 

    mutex renderReadyMutex; 
    condition_variable renderReady; 
    //mutex frame_mutex; 
    int frame = -10; 
    int count = 0; 

    bool readyToDraw; 
    bool drawing; 
}; 

struct UpdateThread 
{ 
    UpdateThread(RenderThread& rt) 
     : m_rt(rt) 
    {} 

    void Run() 
    { 
     this_thread::sleep_for(chrono::milliseconds(500)); 

     for (int i = 0; i < 20; ++i) 
     { 
      // DO GAME UPDATE 

      // when this is uncommented everything is fine 
      // this_thread::sleep_for(chrono::milliseconds(10)); // simulated update time 

      // PREPARE RENDER THREAD 
      unique_lock<mutex> lk(m_rt.renderReadyMutex); 
      m_rt.renderReady.wait(lk, [this](){ return !m_rt.drawing; }); 

      m_rt.readyToDraw = true; 

      // SUPPLY RENDER THREAD WITH DATA TO RENDER 
      m_rt.frame = i; 

      lk.unlock(); 
      m_rt.renderReady.notify_one(); 

      if (!isMultiThreaded) 
       m_rt.DoJob(); 
     }   

     m_rt.end = true; 
    } 

    RenderThread& m_rt; 
}; 

int main() 
{ 
    auto start = chrono::high_resolution_clock::now(); 

    RenderThread rt; 
    UpdateThread u(rt); 

    thread* rendering = nullptr; 
    if (isMultiThreaded) 
     rendering = new thread(bind(&RenderThread::Run, &rt)); 

    u.Run(); 

    if (rendering) 
     rendering->join(); 

    auto duration = chrono::high_resolution_clock::now() - start; 
    cout << "Duration: " << double(chrono::duration_cast<chrono::microseconds>(duration).count())/1000 << endl; 


    return 0; 
} 

Here is the source of this small example code, và như bạn có thể thấy ngay cả trên ideone ấy chạy ra là frame 0: 19 (điều này có nghĩa là làm cho chủ đề đã hoàn thành một sự lặp lại duy nhất, trong khi thread cập nhật đã hoàn thành tất cả 20 của mình).

Nếu chúng tôi bỏ ghi chú dòng 75 (tức là mô phỏng một thời gian cho vòng lặp cập nhật) mọi thứ đều chạy tốt. Mỗi lần lặp lại cập nhật đều có kết xuất lặp lại. Có một cách để thực sự thực sự đồng bộ hóa các chủ đề đó, ngay cả khi một trong số chúng hoàn thành một lần lặp chỉ trong một nano giây, nhưng cũng không có một hình phạt hiệu suất nếu cả hai mất một số tiền hợp lý của mili giây để hoàn thành?

+0

Điều này giống như một người tiêu dùng sản xuất truyền thống Hãy xem http://vichargrave.com/multithreaded-work-queue-in-c/ – CreativeMind

Trả lời

3

Nếu tôi hiểu đúng, bạn muốn 2 đề để làm việc luân phiên: updater chờ đợi cho đến khi kết thúc renderer trước khi lặp đi lặp lại một lần nữa, và trình kết xuất chờ cho đến khi hoàn thành cập nhật trước để lặp lại. Một phần của tính toán có thể song song, nhưng số lần lặp lại sẽ giống nhau giữa cả hai.

Bạn cần 2 ổ khóa:

  • một cho việc cập nhật
  • một cho render

Updater:

wait (renderingLk) 
update 
signal(updaterLk) 

Renderer:

wait (updaterLk) 
render 
signal(renderingLk) 

EDITED:

Ngay cả nếu nó trông đơn giản, có một số vấn đề cần giải quyết:

Cho phép một phần của các tính toán được thực hiện song song: Như trong đoạn mã trên, cập nhật và đưa ra sẽ không song song nhưng theo thứ tự, do đó, không có lợi ích để có nhiều chủ đề. Để một giải pháp thực sự, một số tính toán phải được thực hiện trước khi chờ đợi, và chỉ có bản sao của các giá trị mới cần phải được giữa chờ đợi và tín hiệu. Tương tự cho rendering: tất cả các render cần phải được thực hiện sau khi tín hiệu, và chỉ nhận được giá trị giữa chờ đợi và tín hiệu.

Việc triển khai cũng cần phải quan tâm đến trạng thái ban đầu: vì vậy không hiển thị được thực hiện trước lần cập nhật đầu tiên.

Việc chấm dứt cả hai luồng: vì vậy sẽ không có ai bị khóa hoặc lặp vô hạn sau khi kết thúc khác.

+0

Thật vậy ... Đây thực sự là giải pháp tốt nhất và đơn giản nhất. Làm thế nào ngớ ngẩn của tôi :) Xin lỗi vì điều đó. Tôi đoán tôi đã quá phân tâm rằng các chủ đề cập nhật quá nhanh và tôi không nghĩ về giải pháp cơ bản đó ... –

+0

Ý tưởng rất đơn giản, nhưng việc triển khai không quá nhiều. Tôi không biết làm thế nào bạn quen thuộc với các loại vấn đề, tôi sẽ chỉnh sửa các giải pháp để làm cho một số ghi chú để quan tâm. –

+1

Vì mã đồng bộ hóa luồng được bảo vệ bởi các mutex trong ví dụ của tôi, nó thực sự là vấn đề của một dòng đơn. 'm_rt.drawing = true' ở dòng 85. Đúng vậy. –

2

Tôi nghĩ rằng một mutex (một mình) không phải là công cụ phù hợp cho công việc. Bạn có thể muốn xem xét sử dụng một semaphore (hoặc một cái gì đó tương tự) để thay thế. Những gì bạn mô tả âm thanh rất giống như một producer/consumer problem, tức là, một quá trình được phép chạy một lần mỗi quá trình khác đã kết thúc một tác vụ. Do đó, bạn cũng có thể xem xét các mẫu nhà sản xuất/người tiêu dùng. Ví dụ loạt bài này có thể giúp bạn một số ý tưởng:

Có một std::mutex được kết hợp với một std::condition_variable để bắt chước hành vi của một semaphore. Một cách tiếp cận xuất hiện khá hợp lý. Có thể bạn sẽ không đếm lên và xuống mà chuyển đổi đúng và sai biến với cần vẽ lại ngữ nghĩa.

Để tham khảo:

+0

Bạn không bao giờ nên sử dụng 'yield()' trong mã được đồng bộ chính xác –

+0

@JonathanWakely Điều đó có lẽ đúng. Tôi đã xóa ghi chú. – moooeeeep

1

Kỹ thuật thường được sử dụng trong đồ họa máy tính là sử dụng bộ đệm đôi. Thay vì có trình kết xuất và nhà sản xuất hoạt động trên cùng một dữ liệu trong bộ nhớ, mỗi bộ lọc có bộ đệm riêng. Điều này được thực hiện bằng cách sử dụng hai bộ đệm độc lập và chuyển đổi chúng khi cần. Nhà sản xuất cập nhật một bộ đệm, và khi nó được thực hiện, nó chuyển bộ đệm và điền vào bộ đệm thứ hai với dữ liệu tiếp theo. Bây giờ, trong khi nhà sản xuất đang xử lý bộ đệm thứ hai, trình kết xuất hoạt động với bộ đệm đầu tiên và hiển thị nó.

Bạn có thể sử dụng kỹ thuật này bằng cách cho phép trình kết xuất khóa hoạt động hoán đổi sao cho nhà sản xuất có thể phải chờ cho đến khi kết xuất xong.

1

Điều này là do bạn sử dụng biến số drawing riêng biệt chỉ được đặt khi chuỗi hiển thị phản hồi mutex sau wait, có thể là quá muộn. Vấn đề sẽ biến mất khi biến drawing là loại bỏ và kiểm tra cho wait trong thread cập nhật được thay thế bằng ! m_rt.readyToDraw (mà đã được thiết lập bởi các chủ đề cập nhật và do đó không nhạy cảm với cuộc đua logic.

Modified code and results

Đó cho biết, vì các chủ đề không làm việc song song, tôi không thực sự có được điểm có hai chủ đề.Nếu bạn không nên chọn để thực hiện bộ đệm đôi (hoặc thậm chí gấp ba) sau đó

Các vấn đề liên quan