c++ mutex库

参考cplusplus
参考cppreference

1.mutex

  • 用于保护临界区(critical section)代码的访问。

1.1 mutex

  • 特定mutex上所有的lock和unlock顺序要一致
  • 非成员lock函数允许一次lock多个mutex,可以避免多线程mutex的lock/unlock顺序不同造成的死锁。
// mutex::lock/unlock
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx;           // mutex for critical section

void print_thread_id (int id) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  std::cout << "thread #" << id << '\n';
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
  • try_lock
    尝试加锁,但不阻塞。如果是同一个线程重复加锁,也会死锁。

1.2 timed_mutex

  • 跟mutex一致,只不过增加了try_lock_for和try_lock_util
  • try_lock_for
    尝试加锁,最多阻塞rel_time这么长的时间。
template <class Rep, class Period>
  bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
// timed_mutex::try_lock_for example
#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex

std::timed_mutex mtx;

void fireworks () {
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << "*\n";
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks);

  for (auto& th : threads) th.join();

  return 0;
}
结果如下:
------------------------------------*
----------------------------------------*
-----------------------------------*
------------------------------*
-------------------------*
--------------------*
---------------*
----------*
-----*
*
  • try_lock_until
    尝试加锁,阻塞最多到abs_time。
template <class Clock, class Duration>
  bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);

1.3 recursive_mutex

  • 同一个线程可以重复加锁

1.4 recursive_timed_mutex

  • 支持try_lock_for和try_lock_until
  • 同一个线程可以重复加锁

1.5 shared_mutex (头文件<shared_mutex>)

  • 本质是读写锁
    可以同时被多个读者拥有,但是只能被一个写者拥有的锁。
  • 独占式
    lock try_lock unlock
  • 共享式
    lock_shared try_lock_shared unlock_shared
#include <iostream>
#include <mutex>  // For std::unique_lock
#include <shared_mutex>
#include <thread>
 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
 
  // Multiple threads/readers can read the counter's value at the same time.
  unsigned int get() const {
    std::shared_lock<std::shared_mutex> lock(mutex_);
    return value_;
  }
 
  // Only one thread/writer can increment/write the counter's value.
  void increment() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_++;
  }
 
  // Only one thread/writer can reset/write the counter's value.
  void reset() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_ = 0;
  }
 
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
 
int main() {
  ThreadSafeCounter counter;
 
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
 
      // Note: Writing to std::cout actually needs to be synchronized as well
      // by another std::mutex. This has been omitted to keep the example small.
    }
  };
 
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
 
  thread1.join();
  thread2.join();
}

1.6 shared_timed_mutex

  • 提供时间设置

2.lock——Generic mutex management

  • 管理mutex

2.1 lock_guard

  • 构造函数
    创建的对象管理m,并且调用m.lock()锁住mutex。
//locking (1)   
explicit lock_guard (mutex_type& m);
//adopting (2)  
lock_guard (mutex_type& m, adopt_lock_t tag);
  • C++11的标准库中提供了std::lock_guard类模板做mutex的RAII。
    采用”资源分配时初始化”(RAII——Resource Acquisition Is Initialization)方法来加锁、解锁,这避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题。
  • std::lock_guard类的构造函数禁用拷贝构造,且禁用移动构造。std::lock_guard类除了构造函数和析构函数外没有其它成员函数。
  • 在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。
// lock_guard example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error

std::mutex mtx;

void print_even (int x) {
  if (x%2==0) std::cout << x << " is even\n";
  else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
  try {
    // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
    std::lock_guard<std::mutex> lck (mtx);
    print_even(id);
  }
  catch (std::logic_error&) {
    std::cout << "[exception caught]\n";
  }
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}

2.2 scoped_lock

  • scoped_lock加锁多个mutexes时,不会出现死锁,并且是RAII.
    这个跟std::lock有什么区别?
  • 只有构造函数和析构函数
  • 构造函数如下
explicit scoped_lock( MutexTypes&... m );
scoped_lock( std::adopt_lock_t, MutexTypes&... m );
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
 
struct Employee {
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;
    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for( const auto& partner : lunch_partners )
            ret += partner + " ";
        return ret;
    }
};
 
void send_mail(Employee &, Employee &)
{
    // simulate a time-consuming messaging operation
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }
 
    {
        // use std::scoped_lock to acquire two locks without worrying about 
        // other calls to assign_lunch_partner deadlocking us
        // and it also provides a convenient RAII-style mechanism
 
        std::scoped_lock lock(e1.m, e2.m);
 
        // Equivalent code 1 (using std::lock and std::lock_guard)
        // std::lock(e1.m, e2.m);
        // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
 
        // Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
        // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
        // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        // std::lock(lk1, lk2);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }
 
    send_mail(e1, e2);
    send_mail(e2, e1);
}
 
int main()
{
    Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
 
    // assign in parallel threads because mailing users about lunch assignments
    // takes a long time
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
 
    for (auto &thread : threads) thread.join();
    std::cout << alice.output() << '\n'  << bob.output() << '\n'
              << christina.output() << '\n' << dave.output() << '\n';
}

2.3 unique_lock

  • std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
  • std::unique_lock还支持同时锁定多个mutex,这避免了多道加锁时的资源”死锁”问题。在使用std::condition_variable时需要使用std::unique_lock而不应该使用std::lock_guard。
  • 其lock try_lock 等操作都是调用mutex对象的相应操作
  • 构造函数如下:
//default (1)
unique_lock() noexcept;

//locking (2)   
//调用 m.lock()   
explicit unique_lock (mutex_type& m);

//try-locking (3)   
//调用 m.try_lock()   
unique_lock (mutex_type& m, try_to_lock_t tag);

//deferred (4)  
// m 当前应该没有被锁
unique_lock (mutex_type& m, defer_lock_t tag) noexcept;

//adopting (5)  
// m 当前应该是被锁的
unique_lock (mutex_type& m, adopt_lock_t tag);

//locking for (6)   
// 调用 m.try_lock_for(rel_time)

template <class Rep, class Period>
unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);

//locking until (7) 
// 调用 m.try_lock_until(abs_time)
template <class Clock, class Duration>
unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);

//copy [deleted] (8)    
unique_lock (const unique_lock&) = delete;

//move (9)  
unique_lock (unique_lock&& x);
// unique_lock constructor example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock
                          // std::adopt_lock, std::defer_lock
std::mutex foo,bar;

void task_a () {
  std::lock (foo,bar);         // simultaneous lock (prevents deadlock)
  std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
  std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
  std::cout << "task a\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b () {
  // foo.lock(); bar.lock(); // replaced by:
  std::unique_lock<std::mutex> lck1, lck2;
  lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
  lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
  std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)
  std::cout << "task b\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}


int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

2.4 shared_lock

  • shared_lock就是读锁,被锁后仍允许其他线程执行同样被shared_lock的代码。这是一般做读操作时的需要。
  • unique_lock就是写锁。被锁后不允许其他线程执行被shared_lock或unique_lock的代码。在写操作时,一般用这个,可以同时限制unique_lock的写和share_lock的读。
  • lock try_lock try_lock_for try_lock_until调用的都是shared版本:如mutex()->lock_shared()
std::shared_lock::mutex如下:
mutex_type* mutex() const noexcept;
  • 构造函数如下
shared_lock() noexcept;

shared_lock( shared_lock&& other ) noexcept;

//调用 m.lock_shared()
explicit shared_lock( mutex_type& m );

//调用 m.lock_shared()
shared_lock( mutex_type& m, std::defer_lock_t t ) noexcept;

//调用 m.try_lock_shared()
shared_lock( mutex_type& m, std::try_to_lock_t t );

shared_lock( mutex_type& m, std::adopt_lock_t t );

//调用 m.try_lock_shared_until(timeout_duration)
template< class Rep, class Period >
shared_lock( mutex_type& m, 
             const std::chrono::duration<Rep,Period>& timeout_duration );

// 调用 m.try_lock_shared_for(timeout_time)
template< class Clock, class Duration >
shared_lock( mutex_type& m, 
             const std::chrono::time_point<Clock,Duration>& timeout_time );
#include <shared_mutex>
#include <iostream>
#include <thread>
#include <chrono>
 
std::shared_timed_mutex m;
int i = 10;
 
void read()
{
   // both the threads get access to the integer i
   std::shared_lock<std::shared_timed_mutex> slk(m);
   std::cout << "read i as " << i << "...\n"; // this is not synchronized
   std::this_thread::sleep_for(std::chrono::milliseconds(10));
   std::cout << "woke up...\n";
}
 
int main()
{
   std::thread r1(read);
   std::thread r2(read);
 
   r1.join();
   r2.join();
   return 0;
}

2.5 defer_lock_t / try_to_lock_t /adopt_lock_t

  • std::defer_lock_t 、 std::try_to_lock_t和 std::adopt_lock_t 是用于为 std::lock_guard、 std::scoped_lock、 std::unique_lock 和 std::shared_lock指定锁定策略的空结构体标签类型。
// 不用获取mutex的所有权
struct defer_lock_t { explicit defer_lock_t() = default; };

// 尝试获取mutex的所有权,不阻塞
struct try_to_lock_t { explicit try_to_lock_t() = default; };

// 假设调用线程已经获得mutex的所有权
struct adopt_lock_t { explicit adopt_lock_t() = default; };

2.6 defer_lock / try_to_lock / adopt_lock

  • std::defer_lock 、 std::try_to_lock和 std::adopt_lock分别是空结构体标签类型 std::defer_lock_t 、 std::try_to_lock_t和 std::adopt_lock_t的实例。
    它们用于为 std::lock_guard 、 std::unique_lock及 std::shared_lock指定锁定策略。
inline constexpr std::defer_lock_t defer_lock {};
inline constexpr std::try_to_lock_t try_to_lock {};
inline constexpr std::adopt_lock_t adopt_lock {};
#include <mutex>
#include <thread>
 
struct bank_account {
    explicit bank_account(int balance) : balance(balance) {}
    int balance;
    std::mutex m;
};
 
void transfer(bank_account &from, bank_account &to, int amount)
{
    // lock both mutexes without deadlock
    std::lock(from.m, to.m);
    // make sure both already-locked mutexes are unlocked at the end of scope
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
 
// equivalent approach:
//    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
//    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//    std::lock(lock1, lock2);
 
    from.balance -= amount;
    to.balance += amount;
}
 
int main()
{
    bank_account my_account(100);
    bank_account your_account(50);
 
    std::thread t1(transfer, std::ref(my_account), std::ref(your_account), 10);
    std::thread t2(transfer, std::ref(your_account), std::ref(my_account), 5);
 
    t1.join();
    t2.join();
}

3.Generic locking algorithms

  • 同时对多个mutex进行加锁

3.1 try_lock

  • 尝试对所有mutex进行加锁(非阻塞)
  • 如果全部加锁成功,返回-1
    如果有一个加锁失败,则将加锁成功的mutex解锁,并返回加锁失败的那个锁的序号(从0开始)
template <class Mutex1, class Mutex2, class... Mutexes>
  int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::try_lock

std::mutex foo,bar;

void task_a () {
  foo.lock();
  std::cout << "task a\n";
  bar.lock();
  // ...
  foo.unlock();
  bar.unlock();
}

void task_b () {
  int x = try_lock(bar,foo);
  if (x==-1) {
    std::cout << "task b\n";
    // ...
    bar.unlock();
    foo.unlock();
  }
  else {
    std::cout << "[task b failed: mutex " << (x?"foo":"bar") << " locked]\n";
  }
}

int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

3.2 lock

  • 锁住所有的mutex,可能会阻塞
    调用mutex对象的lock try_lock
    如果不能锁住所有mutex(因为其中有一个抛出异常),则先unlock加锁成功的mutex。
template <class Mutex1, class Mutex2, class... Mutexes>
  void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
  • 原来多个锁会造成死锁,换成lock后可正常运行
// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock

std::mutex foo,bar;

void task_a () {
  // foo.lock(); bar.lock(); // replaced by:
  std::lock (foo,bar);
  std::cout << "task a\n";
  foo.unlock();
  bar.unlock();
}

void task_b () {
  // bar.lock(); foo.lock(); // replaced by:
  std::lock (bar,foo);
  std::cout << "task b\n";
  bar.unlock();
  foo.unlock();
}

int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

4.Call once

  • 直接阻止指定函数的并发执行。

4.1 once_flag

  • 作为call_one的参数
    Using the same object on different calls to call_once in different threads causes a single execution if called concurrently.
struct once_flag {
  constexpr once_flag() noexcept;
  once_flag (const once_flag&) = delete;
  once_flag& operator= (const once_flag&) = delete;
};

4.2 call_once

  • 相同的flag只有一个函数正在执行,fn使用args进行调用。
  • 所有相同的flag的调用只有一个是活动的执行(active execution),其他是passive executions。被动执行等到活动的执行返回后才返回,所有调用的可见副作用同步到这些函数。
    一旦一个ative execution返回,当前和以后有相同标识的call_once都会直接返回。
  • 如果当前的活动执行抛出异常后结束,从passive里面选择一个作为新的活动执行实体。
template <class Fn, class... Args>
  void call_once (once_flag& flag, Fn&& fn, Args&&... args);
// call_once example
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flag

int winner;
void set_winner (int x) { winner = x; }
std::once_flag winner_flag;

void wait_1000ms (int id) {
  // count to 1000, waiting 1ms between increments:
  for (int i=0; i<1000; ++i)
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  // claim to be the winner (only the first such call is executed):
  std::call_once (winner_flag,set_winner,id);
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(wait_1000ms,i+1);

  std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

  for (auto& th : threads) th.join();
  std::cout << "winner thread: " << winner << '\n';

  return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容