Helpex - Trao đổi & giúp đỡ Đăng nhập
88

Theo thiết kế, std::mutexkhông thể di chuyển và cũng không thể sao chép. Điều này có nghĩa là một lớp Agiữ mutex sẽ không nhận được một hàm tạo di chuyển mặc định.

Làm cách nào để làm cho loại này Acó thể di chuyển được theo cách an toàn cho chủ đề?

88 hữu ích 5 bình luận 13k xem chia sẻ
110

Hãy bắt đầu với một đoạn mã:

class A
{
    using MutexType = std::mutex;
    using ReadLock = std::unique_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

    mutable MutexType mut_;

    std::string field1_;
    std::string field2_;

public:
    ...

Tôi đã đặt một số bí danh kiểu khá gợi ý trong đó mà chúng tôi sẽ không thực sự tận dụng trong C ++ 11, nhưng trở nên hữu ích hơn nhiều trong C ++ 14. Hãy kiên nhẫn, chúng tôi sẽ đến đó.

Câu hỏi của bạn tổng hợp thành:

Làm cách nào để viết hàm tạo chuyển và toán tử gán di chuyển cho lớp này?

Chúng ta sẽ bắt đầu với hàm khởi tạo di chuyển.

Move Constructor

Lưu ý rằng thành viên mutexđã được thực hiện mutable. Nói một cách chính xác thì điều này không cần thiết đối với các thành viên di chuyển, nhưng tôi cho rằng bạn cũng muốn các thành viên sao chép. Nếu không phải như vậy, không cần thiết phải tạo mutex mutable.

Khi thi công Akhông cần khóa this->mut_. Nhưng bạn cần phải khóa mut_đối tượng mà bạn đang xây dựng (di chuyển hoặc sao chép). Điều này có thể được thực hiện như vậy:

    A(A&& a)
    {
        WriteLock rhs_lk(a.mut_);
        field1_ = std::move(a.field1_);
        field2_ = std::move(a.field2_);
    }

Lưu ý rằng chúng tôi phải xây dựng mặc định các thành viên của thisđầu tiên, và sau đó chỉ định giá trị cho chúng sau khi a.mut_bị khóa.

Chuyển bài tập

Toán tử gán di chuyển về cơ bản phức tạp hơn vì bạn không biết liệu một số luồng khác có đang truy cập lhs hoặc rhs của biểu thức gán hay không. Và nói chung, bạn cần đề phòng trường hợp sau:

// Thread 1
x = std::move(y);

// Thread 2
y = std::move(x);

Đây là toán tử gán di chuyển bảo vệ chính xác tình huống trên:

    A& operator=(A&& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            WriteLock rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = std::move(a.field1_);
            field2_ = std::move(a.field2_);
        }
        return *this;
    }

Lưu ý rằng người ta phải sử dụng std::lock(m1, m2)để khóa hai mutex, thay vì chỉ khóa chúng lần lượt. Nếu bạn khóa chúng lần lượt, sau đó khi hai luồng gán hai đối tượng theo thứ tự ngược lại như hình trên, bạn có thể nhận được một deadlock. Vấn đề std::locklà tránh bế tắc đó.

Copy Constructor

Bạn đã không hỏi về các thành viên sao chép, nhưng chúng ta cũng có thể nói về họ ngay bây giờ (nếu không phải bạn, ai đó sẽ cần họ).

    A(const A& a)
    {
        ReadLock  rhs_lk(a.mut_);
        field1_ = a.field1_;
        field2_ = a.field2_;
    }

Hàm tạo bản sao trông giống như hàm tạo di chuyển ngoại trừ ReadLockbí danh được sử dụng thay vì WriteLock. Hiện tại, cả hai bí danh này std::unique_lock<std::mutex>và vì vậy nó không thực sự tạo ra bất kỳ sự khác biệt nào.

Nhưng trong C ++ 14, bạn sẽ có tùy chọn nói thế này:

    using MutexType = std::shared_timed_mutex;
    using ReadLock  = std::shared_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

Đây có thể là một sự tối ưu hóa, nhưng không chắc chắn. Bạn sẽ phải đo để xác định xem nó có đúng như vậy không. Nhưng với sự thay đổi này, người ta có thể sao chép cấu trúc từ cùng một rhs trong nhiều luồng đồng thời. Giải pháp C ++ 11 buộc bạn phải thực hiện tuần tự các luồng như vậy, ngay cả khi rhs không được sửa đổi.

Sao chép bài tập

Để hoàn thiện, đây là toán tử gán bản sao, sẽ khá tự giải thích sau khi đọc về mọi thứ khác:

    A& operator=(const A& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            ReadLock  rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = a.field1_;
            field2_ = a.field2_;
        }
        return *this;
    }

Và vân vân.

Bất kỳ thành viên nào khác hoặc các chức năng miễn phí có Atrạng thái của quyền truy cập cũng sẽ cần được bảo vệ nếu bạn muốn nhiều luồng có thể gọi chúng cùng một lúc. Ví dụ, đây là swap:

    friend void swap(A& x, A& y)
    {
        if (&x != &y)
        {
            WriteLock lhs_lk(x.mut_, std::defer_lock);
            WriteLock rhs_lk(y.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            using std::swap;
            swap(x.field1_, y.field1_);
            swap(x.field2_, y.field2_);
        }
    }

Lưu ý rằng nếu bạn chỉ phụ thuộc vào std::swapviệc thực hiện công việc, việc khóa sẽ ở mức độ chi tiết sai, khóa và mở khóa giữa ba bước di chuyển std::swapsẽ thực hiện bên trong.

Thật vậy, suy nghĩ về nó swapcó thể cung cấp cho bạn thông tin chi tiết về API mà bạn có thể cần cung cấp cho API "an toàn theo luồng" A, nói chung sẽ khác với API "không an toàn theo luồng" do vấn đề "khóa chi tiết".

Cũng cần lưu ý sự cần thiết phải bảo vệ chống lại "tự hoán đổi". "tự hoán đổi" nên không được phép. Nếu không có tính năng tự kiểm tra, người ta sẽ khóa một cách đệ quy cùng một mutex. Điều này cũng có thể được giải quyết mà không cần tự kiểm tra bằng cách sử dụng std::recursive_mutexcho MutexType.

Cập nhật

Trong các bình luận bên dưới, Yakk khá không hài lòng về việc phải tạo mặc định những thứ trong bản sao và di chuyển các hàm tạo (và anh ấy có lý). Nếu bạn cảm thấy đủ mạnh mẽ về vấn đề này, đến mức bạn sẵn sàng dành bộ nhớ cho nó, bạn có thể tránh nó như vậy:

  • Thêm bất kỳ loại khóa nào bạn cần làm thành viên dữ liệu. Các thành viên này phải đến trước dữ liệu đang được bảo vệ:

    mutable MutexType mut_;
    ReadLock  read_lock_;
    WriteLock write_lock_;
    // ... other data members ...
    
  • Và sau đó trong các hàm tạo (ví dụ: hàm tạo bản sao) thực hiện điều này:

    A(const A& a)
        : read_lock_(a.mut_)
        , field1_(a.field1_)
        , field2_(a.field2_)
    {
        read_lock_.unlock();
    }
    

Rất tiếc, Yakk đã xóa bình luận của anh ấy trước khi tôi có cơ hội hoàn thành bản cập nhật này. Nhưng anh ấy xứng đáng được ghi nhận vì đã thúc đẩy vấn đề này và tìm ra giải pháp cho câu trả lời này.

Cập nhật 2

Và dyp đã đưa ra gợi ý hay này:

    A(const A& a)
        : A(a, ReadLock(a.mut_))
    {}
private:
    A(const A& a, ReadLock rhs_lk)
        : field1_(a.field1_)
        , field2_(a.field2_)
    {}
110 hữu ích 5 bình luận chia sẻ
7

Vì dường như không có cách nào tốt đẹp, dễ hiểu và dễ dàng để trả lời câu hỏi này - giải pháp của Anton, tôi nghĩ là đúng nhưng nó chắc chắn gây tranh cãi, trừ khi có câu trả lời tốt hơn, tôi khuyên bạn nên đặt một lớp như vậy lên đống và chăm sóc qua một std::unique_ptr:

auto a = std::make_unique<A>();

Bây giờ nó là một loại có thể di chuyển hoàn toàn và bất kỳ ai có khóa trên mutex bên trong khi di chuyển xảy ra vẫn an toàn, ngay cả khi nó còn đang tranh cãi liệu đây có phải là điều tốt nên làm hay không

Nếu bạn cần sao chép ngữ nghĩa, chỉ cần sử dụng

auto a2 = std::make_shared<A>();
7 hữu ích 0 bình luận chia sẻ
5

Đây là một câu trả lời lộn ngược. Thay vì nhúng "các đối tượng này cần được đồng bộ hóa" làm cơ sở của kiểu, thay vào đó hãy đưa nó vào dưới bất kỳ kiểu nào.

Bạn xử lý một đối tượng đồng bộ rất khác nhau. Một vấn đề lớn là bạn phải lo lắng về deadlock (khóa nhiều đối tượng). Về cơ bản, nó không bao giờ nên là "phiên bản mặc định của một đối tượng": các đối tượng được đồng bộ hóa dành cho các đối tượng sẽ tranh chấp và mục tiêu của bạn phải là giảm thiểu sự tranh chấp giữa các chủ đề, chứ không phải quét nó dưới tấm thảm.

Nhưng đồng bộ hóa các đối tượng vẫn hữu ích. Thay vì kế thừa từ bộ đồng bộ hóa, chúng ta có thể viết một lớp bao bọc một kiểu tùy ý trong đồng bộ hóa. Người dùng phải nhảy qua một vài vòng để thực hiện các thao tác trên đối tượng khi nó đã được đồng bộ hóa, nhưng họ không bị giới hạn ở một số tập hợp giới hạn được mã hóa thủ công trên đối tượng. Họ có thể gộp nhiều thao tác trên đối tượng thành một hoặc có một thao tác trên nhiều đối tượng.

Đây là một trình bao bọc được đồng bộ hóa xung quanh một loại tùy ý T:

template<class T>
struct synchronized {
  template<class F>
  auto read(F&& f) const&->std::result_of_t<F(T const&)> {
    return access(std::forward<F>(f), *this);
  }
  template<class F>
  auto read(F&& f) &&->std::result_of_t<F(T&&)> {
    return access(std::forward<F>(f), std::move(*this));
  }
  template<class F>
  auto write(F&& f)->std::result_of_t<F(T&)> {
    return access(std::forward<F>(f), *this);
  }
  // uses `const` ness of Syncs to determine access:
  template<class F, class... Syncs>
  friend auto access( F&& f, Syncs&&... syncs )->
  std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
  {
    return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
  };
  synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
  synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}  
  // special member functions:
  synchronized( T & o ):t(o) {}
  synchronized( T const& o ):t(o) {}
  synchronized( T && o ):t(std::move(o)) {}
  synchronized( T const&& o ):t(std::move(o)) {}
  synchronized& operator=(T const& o) {
    write([&](T& t){
      t=o;
    });
    return *this;
  }
  synchronized& operator=(T && o) {
    write([&](T& t){
      t=std::move(o);
    });
    return *this;
  }
private:
  template<class X, class S>
  static auto smart_lock(S const& s) {
    return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
  }
  template<class X, class S>
  static auto smart_lock(S& s) {
    return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
  }
  template<class L>
  static void lock(L& lockable) {
      lockable.lock();
  }
  template<class...Ls>
  static void lock(Ls&... lockable) {
      std::lock( lockable... );
  }
  template<size_t...Is, class F, class...Syncs>
  friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
  std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
  {
    auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
    lock( std::get<Is>(locks)... );
    return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
  }

  mutable std::shared_timed_mutex m;
  T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
  return {std::forward<T>(t)};
}

Các tính năng C ++ 14 và C ++ 1z bao gồm.

điều này giả định rằng các consthoạt động là an toàn cho nhiều đầu đọc (đó là những gì stdcác vùng chứa giả định).

Sử dụng trông giống như:

synchronized<int> x = 7;
x.read([&](auto&& v){
  std::cout << v << '\n';
});

cho một intvới quyền truy cập đồng bộ.

Tôi khuyên bạn không nên có synchronized(synchronized const&). Nó hiếm khi cần thiết.

Nếu bạn cần synchronized(synchronized const&), tôi muốn thay thế T t;bằng std::aligned_storage, cho phép xây dựng vị trí thủ công và phá hủy thủ công. Điều đó cho phép quản lý trọn đời thích hợp.

Chặn điều đó, chúng tôi có thể sao chép nguồn T, sau đó đọc từ nó:

synchronized(synchronized const& o):
  t(o.read(
    [](T const&o){return o;})
  )
{}
synchronized(synchronized && o):
  t(std::move(o).read(
    [](T&&o){return std::move(o);})
  )
{}

để phân công:

synchronized& operator=(synchronized const& o) {
  access([](T& lhs, T const& rhs){
    lhs = rhs;
  }, *this, o);
  return *this;
}
synchronized& operator=(synchronized && o) {
  access([](T& lhs, T&& rhs){
    lhs = std::move(rhs);
  }, *this, std::move(o));
  return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
  access([](T& lhs, T& rhs){
    using std::swap;
    swap(lhs, rhs);
  }, *this, o);
}

vị trí và các phiên bản lưu trữ được căn chỉnh lộn xộn hơn một chút. Hầu hết quyền truy cập vào tsẽ được thay thế bằng một hàm thành viên T&t()T const&t()const, ngoại trừ khi xây dựng nơi bạn phải nhảy qua một số vòng.

Bằng cách tạo synchronizedmột trình bao bọc thay vì một phần của lớp, tất cả những gì chúng ta phải đảm bảo là lớp đó tôn trọng nội bộ constlà nhiều người đọc và viết nó theo cách đơn luồng.

Trong những trường hợp hiếm hoi, chúng tôi cần một cá thể được đồng bộ hóa, chúng tôi chuyển qua các vòng như trên.

Xin lỗi vì bất kỳ lỗi chính tả nào ở trên. Có lẽ có một số.

Một lợi ích phụ ở trên là n-ary các phép toán tùy ý trên synchronizedcác đối tượng (cùng loại) hoạt động cùng nhau, mà không cần phải viết mã trước. Thêm khai báo kết bạn và synchronizedcác đối tượng n-ary thuộc nhiều loại có thể hoạt động cùng nhau. Tôi có thể phải chuyển accessra khỏi vai trò là một người bạn nội tuyến để đối phó với những cuộc chiến quá tải trong trường hợp đó.

ví dụ trực tiếp

5 hữu ích 0 bình luận chia sẻ
4

Sử dụng mutexes và C ++ di chuyển ngữ nghĩa là một cách tuyệt vời để chuyển dữ liệu giữa các luồng một cách an toàn và hiệu quả.

Hãy tưởng tượng một chuỗi 'nhà sản xuất' tạo ra nhiều chuỗi chuỗi và cung cấp chúng cho (một hoặc nhiều) người tiêu dùng. Các lô đó có thể được đại diện bởi một đối tượng chứa các đối tượng (có thể lớn) std::vector<std::string>. Chúng tôi hoàn toàn muốn 'di chuyển' trạng thái bên trong của những vectơ đó vào người tiêu dùng của họ mà không có sự trùng lặp không cần thiết.

Bạn chỉ cần nhận ra mutex là một phần của đối tượng chứ không phải là một phần của trạng thái của đối tượng. Đó là, bạn không muốn di chuyển mutex.

Việc khóa nào bạn cần phụ thuộc vào thuật toán của bạn hoặc mức độ tổng quát của các đối tượng và phạm vi sử dụng bạn cho phép.

Nếu bạn chỉ di chuyển từ một đối tượng 'nhà sản xuất' ở trạng thái chia sẻ sang một đối tượng 'tiêu thụ' luồng cục bộ, bạn có thể chỉ khóa đối tượng được di chuyển từ đó.

Nếu đó là một thiết kế tổng quát hơn, bạn sẽ cần phải khóa cả hai. Trong trường hợp này, bạn cần phải xem xét khóa chết.

Nếu đó là một vấn đề tiềm ẩn thì hãy sử dụng std::lock()để có được các khóa trên cả hai mutex theo cách không có bế tắc.

http://en.cppreference.com/w/cpp/thread/lock

Lưu ý cuối cùng, bạn cần đảm bảo rằng bạn hiểu ngữ nghĩa chuyển động. Nhớ lại rằng đối tượng được di chuyển từ được để ở trạng thái hợp lệ nhưng không xác định. Hoàn toàn có thể xảy ra trường hợp một luồng không thực hiện việc di chuyển có lý do hợp lệ để cố gắng truy cập vào đối tượng được di chuyển từ khi nó có thể tìm thấy trạng thái hợp lệ nhưng không xác định đó.

Một lần nữa, nhà sản xuất của tôi chỉ cắt đứt dây và người tiêu dùng đang lấy đi toàn bộ tải trọng. Trong trường hợp đó, mỗi khi nhà sản xuất cố gắng thêm vào vectơ, nó có thể tìm thấy vectơ không trống hoặc rỗng.

Tóm lại, nếu khả năng truy cập đồng thời vào đối tượng được chuyển từ đối tượng thành một bản ghi thì nó có thể là OK. Nếu nó tương đương với một lượt đọc thì hãy nghĩ xem tại sao có thể đọc một trạng thái tùy ý.

4 hữu ích 0 bình luận chia sẻ
3

Trước hết, phải có điều gì đó sai trong thiết kế của bạn nếu bạn muốn di chuyển một đối tượng có chứa mutex.

Nhưng nếu bạn quyết định làm điều đó bằng mọi cách, bạn phải tạo một mutex mới trong hàm tạo move, ví dụ:

// movable
struct B{};

class A {
    B b;
    std::mutex m;
public:
    A(A&& a)
        : b(std::move(a.b))
        // m is default-initialized.
    {
    }
};

Điều này là an toàn cho luồng, bởi vì hàm khởi tạo di chuyển có thể giả định một cách an toàn rằng đối số của nó không được sử dụng ở bất kỳ nơi nào khác, vì vậy việc khóa đối số là không cần thiết.

3 hữu ích 5 bình luận chia sẻ
loading
Không tìm thấy câu trả lời bạn tìm kiếm? Duyệt qua các câu hỏi được gắn thẻ c++ mutex move-constructor , hoặc hỏi câu hỏi của bạn.

Có thể bạn quan tâm

loading