Chapter 2-01

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

第2章 线程同步精要

?并发编程有两种基本模型,一种是message passing,另一种是shared memory。在分布式系统中,运行在多台机器上的多个进程的并行编程只有一种实用模型:message passing。在单机上,我们也可以用message passing作为多个进程的并发模型。这样整个分布式系统架构的一致性很强,扩容(scale out)起来也较容易。

?在多线程编程中,message passing更容易保证程序的正确性。不过在用C/C++编写多线程程序时,我们需要了解底层shared memory模型下的同步原语,以备不时之需。本章多次引用《Real World Concurrency》http://queue.acm.org/detail.cfm?id=1454462简称[RWC]。

?多线程编程教程:

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

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

2.其次是使用高级的并发编程构件,如TaskQueue、Producer-Consumer Queue、CountDownLatch等等。

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

4.除了使用atomic整数之外,不自己编写lock-free代码3,也不要用“内核级”同步原语4 5。不凭空猜测“哪种做法性能会更好”,比如spin lock vs. mutex。

2.1 互斥器(mutex)

?互斥器(mutex)保护了临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动。单独使用mutex时,主要为了保护共享数据。原则是:

1.用RAII手法封装mutex的创建、销毁、加锁、解锁这四个操作。即保证锁的生效期间等于一个作用域(scope),不会因异常而忘记解锁。

2.只用非递归的mutex(即不可重入的mutex)。

3.不手工调用lock()和unlock()函数,一切交给栈上的Guard对象的构造和析构函数负责。Guard对象的生命期正好等于临界区。这样我们保证始终在同一个函数同一个scope里对某个mutex加锁和解锁。避免在foo()里加锁,然后跑到bar()里解锁;也避免在不同的语句分支中分别加锁、解锁。这种做法被称为Scoped Locking7。

4.在每次构造Guard对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁(deadlock)。由于Guard对象是栈上对象,看函数调用栈就能分析用锁的情况,非常便利。

5.不使用跨进程的mutex,进程间通信只用TCP sockets。

6.加锁、解锁在同一个线程,线程a不能去unlock线程b已经锁住的mutex(RAII自动保证)。

7.别忘了解锁(RAII自动保证)。

8.不重复解锁(RAII自动保证)。

9.必要的时候可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错。

2.1.1 只使用非递归的mutex

?mutex分为递归(recursive)和非递归(non-recursive)两种,这是POSIX的叫法,另外的名字是可重入(reentrant)与非可重入。这两种mutex作为线程间的同步工具时没有区别,唯一区别在于:同一个线程可以重复对recursive mutex加锁,但是不能重复对non-recursive mutex加锁。

?首选非递归mutex,不是为了性能,而是为了体现设计意图。non-recursive和recursive的性能差别不大,因为前者少用一个计数器,略快一点。在同一个线程里多次对non-recursive mutex加锁会立刻导致死锁,这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。

?recursive mutex使用方便,因为不用考虑一个线程会自己把自己给锁死了。但可能会隐藏代码里的一些问题。典型情况是你以为拿到一个锁就能修改对象了,没想到外层代码已经拿到了锁,正在修改(或读取)同一个对象。例子(recipes/thread/test/NonRecursiveMutex_test.cc)。

MutexLock mutex;
std::vector<Foo> foos;
void post(const Foo& f)
{
    MutexLockGuard lock(mutex);
    foos.push_back(f);
}

void traverse()
{
    MutexLockGuard lock(mutex);
    for (std::vector<Foo>::const_iterator it=foos.begin(); it !=foos.end(); ++it)
    {
        it->doit();
    }
}

?post()加锁,然后修改foos对象;traverse()加锁,然后遍历foos向量。这些都是正确的。考虑Foo::doit()间接调用了post():

1.mutex是非递归的,于是死锁了。

2.mutex是递归的,由于push_back()可能(但不总是)导致vector迭代器失效,程序偶尔会crash。

?这时候能体现non-recursive的优越性:把程序的逻辑错误暴露出来。死锁容易debug,把各个线程的调用栈打出来8,只要每个函数不是特别长,容易看出来是怎么死的,见§2.1.2的例子9。或者可以用PTHREAD_MUTEX_ERRORCHEC找到错误(前提是MutexLock带debug选项)。

?如果确实需要在遍历的时候修改vector,有两种做法。

1.把修改推后,记住循环中试图添加或删除哪些元素,等循环结束了再依记录修改foos;

2.用copy-on-write,见§2.8的例子。

?如果一个函数既可能在已加锁的情况下调用,又可能在未加锁的情况下调用,那么就拆成两个函数:

1.跟原来的函数同名,函数加锁,转而调用第2个函数。

2.给函数名加上后缀WithLockHold,不加锁,把原来的函数体搬过来。就像这样:

void post(const Foo& f)
{
    MutexLockGuard lock(mutex);
    postWithLockHold(f); // 不用担心开销,编译器会自动内联
}

void postWithLockHold(const Foo& f)
{
    foos.push_back(f);
}

?这有可能出现两个问题:

(a)误用了加锁版本,死锁了。

(b)误用了不加锁版本,数据损坏了。

?对于(a),仿造§2.1.2的办法能比较容易地排错。

?对于(b),如果Pthreads提供 isLockedByThisThread()就好办,可以写成:

void postWithLockHold(const Foo& f)
{
assert(mutex.isLockedByThisThread());
// muduo::MutexLock提供了这个成员函数
// ...
}

?WithLockHold这个显眼的后缀也让程序中的误用容易暴露出来。

?Pthreads的权威专家,《Programming with POSIX Threads》的作者David Butenhof也排斥使用recursive mutex。他说10:

First, implementation of efficient and reliable threaded coder evolves around one simple and basic principle: follow your design. That implies, of course, that you have a design, and that you understand it.

A correct and well understood design does not require re-cursive mutexes.(后略)

?Linux的Pthreads mutex采用futex(2)实现11,不必每次加锁、解锁都陷入系统调用,效率不错。

2.1.2 死锁

?如果坚持只使用Scoped Locking,那么在出现死锁的时候很容易定位。考虑下面这个线程自己与自己死锁的例子(recipes/thread/test/SelfDeadLock.cc)。

class Request
{
public:
    void process() // __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        // ...
        print(); // L8 原本没有这行,某人为了调试程序不小心添加了。
    }

    void print() const // __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        // ...
    }

private:
    mutable muduo::MutexLock mutex_;
};

int main()
{
    Request req;
    req.process();
}

?添加L8之后,程序立刻出现了死锁。

?调试定位这种死锁很容易,只要把函数调用栈打印出来,结合源码一看,立刻就会发现第6帧Request::process()和第5帧Request::print()先后对同一个mutex上锁,引发了死锁。(必要的时候可以加上attribute来防止函数inline展开

$ gdb ./self_deadlock core
(gdb) bt
#0 __lll_lock_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:136
#1 _L_lock_953 () from /lib/libpthread.so.0
#2 __pthread_mutex_lock (mutex=0x7fffecf57bf0) at pthread_mutex_lock.c:61
#3 muduo::MutexLock::lock () at test/../Mutex.h:49
#4 MutexLockGuard () at test/../Mutex.h:75
#5 Request::print () at test/SelfDeadLock.cc:14
#6 Request::process () at test/SelfDeadLock.cc:9
#7 main () at test/SelfDeadLock.cc:24

?修复这个错误很容易,从Request::print()抽取出Request::printWithLockHold(),并让Request::print()和Request::process()都调用它即可。

?一个两个线程死锁的例子(recipes/thread/test/MutualDeadLock.cc)。

?有一个Inventory(清单)class,记录当前的Request对象。Inventory的add()和 remove()成员函数都是线程安全的,它使用了mutex来保护共享数据requests_。

class Inventory
{
public:
    void add(Request* req)
    {
        muduo::MutexLockGuard lock(mutex_);
        requests_.insert(req);
    }

    void remove(Request* req) __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        requests_.erase(req);
    }

    void printAll() const;

private:
    mutable muduo::MutexLock mutex_;
    std::set<Request*> requests_;
};
Inventory g_inventory; // 为了简单起见,这里使用了全局对象。

?Request class在处理(process)请求的时候,往g_inventory中添加自己。在析构的时候,从g_inventory中移除自己。

class Request
{
public:
    void process() // __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        g_inventory.add(this);
        // ...
    }

    ~Request() __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        sleep(1); // 为了容易复现死锁,这里用了延时
        g_inventory.remove(this);
    }

    void print() const __attribute__ ((noinline))
    {
        muduo::MutexLockGuard lock(mutex_);
        // ...
    }

private:
    mutable muduo::MutexLock mutex_;
};

?Inventory class能打印全部已知的Request对象。Inventory::printAll()的逻辑单独看没问题,但有可能引发死锁。

void Inventory::printAll() const
{
    muduo::MutexLockGuard lock(mutex_);
    sleep(1); // 为了容易复现死锁,这里用了延时
    for(    std::set<Request*>::const_iterator it=requests_.begin();
it !=requests_.end(); ++it)
    {
        (*it)->print();
    }
    printf("Inventory::printAll() unlocked\n");
}

?下面这个程序运行起来发生了死锁:

void threadFunc()
{
    Request* req = new Request;
    req->process();
    delete req;
}
int main()
{
    Thread thread(threadFunc);
    thread.start();
    usleep(500 * 1000);  // 为了让另一个线程等在前面第14行的sleep()上。
    g_inventory.printAll();
    thread.join();
}

?通过gdb查看两个线程的函数调用栈,我们发现两个线程都等在mutex上(__lll_lock_wait),估计是发生了死锁。因为一个程序中的线程一般只会等在condition variable上,或者等在epoll_wait上(§3.5.3)。

$ gdb ./mutual_deadlock core
(gdb) thread apply all bt
Thread 1 (Thread 31229): # 这是 main() 线程
#0 __lll_lock_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:136
#1 _L_lock_953 () from /lib/libpthread.so.0
#2 __pthread_mutex_lock (mutex=0xecd150) at pthread_mutex_lock.c:61
#3 muduo::MutexLock::lock (this=0xecd150) at test/../Mutex.h:49
#4 MutexLockGuard (this=0xecd150) at test/../Mutex.h:75
#5 Request::print (this=0xecd150) at test/MutualDeadLock.cc:51
#6 Inventory::printAll (this=0x605aa0) at test/MutualDeadLock.cc:67
#7 0x0000000000403368 in main () at test/MutualDeadLock.cc:84
Thread 2 (Thread 31230): # 这是 threadFunc() 线程
#0 __lll_lock_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:136
#1 _L_lock_953 () from /lib/libpthread.so.0
#2 __pthread_mutex_lock (mutex=0x605aa0) at pthread_mutex_lock.c:61
#3 muduo::MutexLock::lock (this=0x605aa0, req=0x80) at test/../Mutex.h:49
#4 MutexLockGuard (this=0x605aa0, req=0x80) at test/../Mutex.h:75
#5 Inventory::remove (this=0x605aa0, req=0x80) at test/MutualDeadLock.cc:19
#6 ~Request (this=0xecd150, ...) at test/MutualDeadLock.cc:46
#7 threadFunc () at test/MutualDeadLock.cc:76
#8 boost::function0<void>::operator() (this=0x7fff21c10310) at  /usr/include/boost/function/function_template.hpp:1013
#9 muduo::Thread::runInThread (this=0x7fff21c10310) at Thread.cc:113
#10 muduo::Thread::startThread (obj=0x605aa0) at Thread.cc:105
#11 start_thread (arg=<value optimized out>) at pthread_create.c:300
#12 clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:112

?注意到main()线程是先调用Inventory::printAll(#6)再调用Request::print(#5),而threadFunc()线程是先调用Request::~Request(#6)再调用Inventory::remove(#5)。这两个调用序列对两个mutex的加锁顺序正好相反,造成了死锁。

?Inventory class的mutex的临界区由灰底表示,Request class的mutex的临界区由斜纹表示。一旦main()线程中的printAll()在另一个线程的~Request()和remove()之间开始执行,死锁不可避免。

?思考:如果printAll()晚于remove()执行,还会出现死锁吗?

?练习:修改程序,让~Request()在printAll()和print()之间开始执行,复现另一种可能的死锁时序。这里也出现了第1章所说的对象析构的race condition,即一个线程正在析构对象,另一个线程却在调用它的成员函数。

?解决死锁的办法很简单,要么把print()移出printAll()的临界区,这可以用§2.8介绍的办法;要么把remove()移出~Request()的临界区,比如交换§2.1.2中L13和L15两行代码的位置。当然这没有解决对象析构的race condition,留做练习。

?思考:Inventory::printAll→Request::print有无可能与Request::process→Inventory::add发生死锁?

?死锁会让程序行为失常,其他一些锁使用不当则会影响性能,例如潘爱民老师写的《Lock Convoys Explained》15详细解释了一种性能衰退的现象。除此之外,编写高性能多线程程序至少还要知道false sharing和CPU cache效应,可看脚注中的这几篇文章16 17 18 19。

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

时间: 2024-10-29 10:45:56

Chapter 2-01的相关文章

我喜欢减肥我们来减肥吧

http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313278016/2015.01.28.html http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313282016/2015.01.28.html http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313289016/2015.01.28.html http://www.ebay.com/cln/usli

百度回家看沙发沙发是减肥了卡斯加积分卡拉是减肥

http://www.ebay.com/cln/hpryu-caw8ke/cars/158056866019/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/158445650015/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/158445674015/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/1584456790

巢哑偕倥乇椭煞谙暗逞帕俸

IEEE Spectrum 杂志发布了一年一度的编程语言排行榜,这也是他们发布的第四届编程语言 Top 榜. 据介绍,IEEE Spectrum 的排序是来自 10 个重要线上数据源的综合,例如 Stack Overflow.Twitter.Reddit.IEEE Xplore.GitHub.CareerBuilder 等,对 48 种语言进行排行. 与其他排行榜不同的是,IEEE Spectrum 可以让读者自己选择参数组合时的权重,得到不同的排序结果.考虑到典型的 Spectrum 读者需求

我国第三代移动通信研究开发进展-尤肖虎200106

众所周知,数据科学是这几年才火起来的概念,而应运而生的数据科学家(data scientist)明显缺乏清晰的录取标准和工作内容.此次课程以<星际争霸II>回放文件分析为例,集中在IBM Cloud相关数据分析服务的应用.面对星际游戏爱好者希望提升技能的要求,我们使用IBM Data Science Experience中的jJupyter Notebooks来实现数据的可视化以及对数据进行深度分析,并最终存储到IBM Cloudant中.这是个介绍+动手实践的教程,参会者不仅将和讲师一起在线

pl/sql学习1——标量变量psahnh6S

为类型.不能用于表列的数据类型.范围为的子类型.自然数.为的子类型.具有约束为单精度浮点数.为变量赋值时.后面要加为双精度浮点数.为变量赋值时.后面要加.为数字总位数.为小数位数是的子类型.最大精度位是的子类型.最大精度位单精度浮点型是的子类型.最大精度位双精度浮点型定义精度为位的实数..定义为位的整数.变长字符串.最长测试变量数据!.定长字符串.最长测试变长二进制字符串物理存储的为类型...固定长度.个字节使用定义数据类型那个最小值:最大值:最小值:最大值:最小值:最大值:最小值:最大值:最小

chapter 01

hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.

[Learning You a Haskell for Great Goods!] chapter 01 starting out

Installation under CentOS/Fedora # yum install ghc Version [[email protected] haskell]# ghc -v Glasgow Haskell Compiler, Version 7.0.4, for Haskell 98, stage 2 booted by GHC version 7.0.4 Change prompt echo :set prompt "ghci> " > ~/.ghci C

Chapter 01:创建和销毁对象

<一>考虑用静态工厂方法代替构造器 下面是Boolean类的一个简单示例: public final class Boolean implements java.io.Serializable, Comparable<Boolean> { public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static

Notes : &lt;Hands-on ML with Sklearn &amp; TF&gt; Chapter 7

.caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordere

[CSS Mastery]Chapter 1: Setting the Foundations

Chapter 1: Setting the Foundations The human race is a naturally inquisitive species. We just love tinkering with things. When I recently bought a new iMac, I had it to bits within seconds, before I’d even read the instruction manual. We enjoy workin