【muduo】线程同步精要

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daaikuaichuan/article/details/85331865

  线程同步的四项原则, 按重要性排列:

  • 首要原则是尽量最低限度地共享对象, 减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露如果暴露就优先考虑immutable对象(const);实在不行才暴露可修改的对象,并用同步措施来充分保护它。

  • 其次, 使用高级的并发编程构件, 如任务队列(TaskQueue), 生产者消费者队列(Producer-Consumer Queue), 倒计时(CountDownLatch)等。

  • 最后不得已才使用底层同步原语(primitives)时, 只用非递归的互斥器和条件变量, 慎用读写锁, 不要用信号量。

  • 除了使用atomic整数之外, 不要自己编写lock-free代码, 也不要用"内核级"同步原语; 不能凭空猜测’那种做法性能会更好’, 比如spin lock(自旋锁) vs mutex(互斥量);

一、互斥量(mutex)

  mutex是最常用的同步原语。mutex可以保护一个临界区,使得任何时刻最多仅有一个线程在临界区内活动。

1、互斥量的使用原则

  • RAII封装mutex的生存及其相关操作。

  • 使用Guard管理mutex锁,并利用生存期控制Guard,基于此结构(Scoped Locking)可以使debug很方便。

  • 构造Guard时,防止加锁顺序不同导致死锁

  • 使用非递归的mutex

  • 跨进程时使用tcp sockets

  • 尽量使用高层同步方法

2、只使用非递归的mutex

  mutex分为递归(recursive)和非递归(non-recursive)两种, 这是POSIX的叫法, 另外的名字是可重入(reentrant)和非可重入。这两种mutex对线程间(inter-thread)的同步基本没有区别, 他们的唯一区别就是: 同一线程可以重复对recursive mutex加锁, 但是不能重复对non-recursive mutex加锁

  recursive mutex(可重入的互斥量)可能会隐藏代码里的一些问题:典型情况是, 以为拿到一把锁就可以修改对象了, 没想到外层代码已经拿到了锁, 正在修改(或读取)同一个对象。

3、死锁

  坚持使用Scoped Locking(作用域加锁), 很容易在出现死锁的时候定位bug。

二、条件变量(condition variable)

  互斥器(mutex), 是加锁原语, 用来排他性地访问共享数据, 它不是等待原语;如果需要等待某个条件成立, 应该使用条件变量(condition variable);条件变量是一个或多线程等待某个布尔表达式为真, 即等待别的线程唤醒它

【wait 端】:

  • 与mutex一起使用;

  • 在mutex上锁时才能调用;

  • 把判断bool条件放到while循环中(避免虚假唤醒(spurious wakeup))。

【signal/broadcast端】:

  • 不一定要在mutex已上锁时调用signal;

  • 在signal之前一般要修改bool表达式;

  • 修改bool表达式时通常要mutex保护;

  • 注意区分signal与broadcast。

// Wait 端一个代码示例
muduo::MutexLock mutex;
muduo::Condition cond(mutex);
std::deque queue;
int deque()
{
    //与mutex一起使用,上锁时才能调用
    MutexLockGuard lock(mutex);
    // "queue.empty()" 是这里的bool表达式
    while(queue.empty())
    {
        cond.wait();//并放到while循环中
        //唤醒后可能仍不满足条件所以用while
    }
    assert(!queue.empty());
    int top=queue.front();
    queue.pop_front();
    return top;
}
void enqueue(int x)
{
    MutexLockGuard lock(mutex);//修改表达式时通常要mutex保护
    queue.push_back(x);
    //此处已经将bool表达式修改完毕,可以发送signal
    cond.notify();//发送signal,理论上此处调用时不需要mutex上锁,可以移出临界区之外
    //notify只通知一个,且随机决定;notifyAll通知所有。
}

  condition variable 是十分底层的同步原语,可以用它实现高层的一些同步措施。如倒计时(CountDownLatch), 线程池(ThreadPool),队列等。mutex和condition variable不能互相替代,它们共同构成了多线程编程的全部必备同步原语。

三、读写锁、信号量

  读写锁(Readers-Writer lock, 简写为rwlock)是个看上去很美的抽象, 它明确区分了read和write两种行为。但不见得比mutex更高效。

四、sleep(3)不能保证同步

  一般用于构建一些测试情况时使用,sleep本身只负责延时,一般只能出现在测试代码中。否则程序的设计一般是有问题的。

五、总结

  线程同步的四项原则:

  • 最低限度共享对象,减少需要同步的场合。

  • 使用高级的并发编程构件。

  • 若不得已使用底层同步原语,只是用 non-recursive mutex 和 condition variable,其他如读写锁、信号量最好不要用。使用时利用RAII和Scoped Locking保证结构清晰。

  • 除了使用atomic整数意外不去自己编写lock-free代码,也不要用内核级同步原语。

  一般依照这些原则就可以应付多线程开发的各种场合了。"让一个正确的程序变快"远比"让一个快的程序变正确"容易得多。所以首先保证程序的清晰、简单和正确性,再去考虑性能优化。

猜你喜欢

转载自blog.csdn.net/daaikuaichuan/article/details/85331865