多线程编程精髓(三)

本篇主要讲Linux环境下的多线程同步内核对象。

(1)linux线程同步之互斥体:linux互斥体的用法与windows的临界区对象类似,使用数据结构 pthread_mutex_t表示互斥体对象(定义于pthread.h头文件中),初始化方式有两种:

              1.使用 PTHREAD_MUTEX_INITIALIZER 直接给互斥体变量赋值,如:pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;

              2.使用pthread_mutex_init函数初始化,如果互斥量是动态分配的或者需要给互斥量设置属性,函数签名:

                  int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);  

                //参数 mutex 即我们需要初始化的 mutex 对象的指针,参数 attr 是需要设置的互斥体属性,设置NULL表示默认属性PTHREAD_MUTEX_NORMAL(普通锁),此外还有

PTHREAD_MUTEX_ERRORCHECK(检错锁),PTHREAD_MUTEX_RECURSIVE(嵌套锁),普通锁则是线程独占,有线程占用的情况下其他线程调用上锁函数阻塞,检错锁是已

经上锁的线程对互斥体对象重复加锁,上锁返回EDEADLK,允许同一个线程对其持有的互斥体重复加锁,每加锁一次互斥体对象的锁引用就会新增一次,解锁会减少一次,当计数为0时

其他线程才能获得该锁。

               互斥体对象销毁:int pthread_mutex_destroy(pthread_mutex_t* mutex); 成功则返回0 ,注意:使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量无须销毁,要去销毁一

个已经加锁或正在被条件变量使用的互斥体对象。

               对于互斥体的加锁和解锁操作一般使用以下三个函数:

                int   pthread_mutex_lock(pthread_mutex_t* mutex);

                int   pthread_mutex_trylock(pthread_mutex_t* mutex);

                int   pthread_mutex_unlock(pthread_mutex_t* mutex);

             使用 pthread_mutexattr_settype/pthread_mutexattr_gettype 设置或获取想要的属性类型:

                int  pthread_mutexattr_settype(pthread_mutexattr_t* attr,int type);

                int  pthread_mutexattr_gettype(const pthread_mutexattr_t* restrictattr,int* restricttype);

如下使用例子:

                   #include<pthread.h>

                  #include<stdio.h>

                  #include<errno.h> 

                  int  main(){

                       pthread_mutex_t mymutex;

                       pthread_mutex_init(&mymutex, NULL);

                       int ret = pthread_mutex_lock(&mymutex);

                       ret = pthread_mutex_destroy(&mymutex);

                         if (ret != 0)

                         {

                                      if (errno == EBUSY)

                                                 printf("EBUSY\n");

                                    printf("Failed to destroy mutex.\n");

                            }

                        ret = pthread_mutex_unlock(&mymutex);

                        ret = pthread_mutex_destroy(&mymutex);

                         if (ret == 0)

                        {

                                    printf("Succeed to destroy mutex.\n");

                        }

                        return 0;

                 }

(2)linux线程同步之信号量:与windows的Semaphore 对象使用原理一样,linux的信号量也可以资源多份,可同时被多个线程访问,头文件semaphore.h,常用的一组API函数:

        int  sem_init(sem_t* sem,int pshared,unsigned int value); //初始化信号量,参数 sem 传入初始化信号量地址;参数 pshared表示该信号量是否可以被初始化该信号量的进程 fork 出

来的子进程共享,取值为 0 (不可以共享)、1(可以共享);参数 value 用于设置信号量初始状态下资源的数量;初始化成功返回0,失败返回-1;

        int  sem_destroy(sem_t* sem); //销毁信号量

        int  sem_post(sem_t* sem);  //将信号量的资源计数加一,并解锁sem_wait而阻塞的线程

        int  sem_wait(sem_t* sem);  // 如果当前信号量资源计数为 0,函数会阻塞调用线程;直到信号量对象的资源计数大于 0 时被唤醒,唤醒后将资源计数递减 1,然后立即返回

        int  sem_trywait(sem_t* sem); //sem_wait函数的非阻塞版,当前信号量对象的资源计数等于 0,函数会立即返回不会阻塞调用线程,返回值是 ﹣1,错误码 errno 被设置成 EAGAIN

        int  sem_timedwait(sem_t* sem,conststructtimespec* abs_timeout); // 带有等待时间的版本,等待时间在第二个参数 abs_timeout 中设置,不能设置为NULL,否则会奔溃

       注意:1.sem_wait、sem_trywait、sem_timedwait 函数将资源计数递减一时会同时锁定信号量对象,因此当资源计数为 1 时,如果有多个线程调用 sem_wait 等函数等待该信号量

时,只会有一个线程被唤醒。当 sem_wait 函数返回时,会释放对该信号量的锁。

                 2.sem_wait、sem_trywait、sem_timedwait 函数调用成功后返回值均为 0,调用失败返回 ﹣1,可以通过错误码 errno 获得失败原因。

                 3.sem_wait、sem_trywait、sem_timedwait 可以被 Linux 信号中断,被信号中断后,函数立即返回,返回值是 ﹣1,错误码 errno 为 EINTR。

(3)linux线程同步之条件变量:为了让条件变量和互斥对象两者为原子操作,则两者需要同时结合使用,否则会出现CPU的调度导致线程获得互斥对象但是却错过了条件变量唤醒的信

号导致线程阻塞。条件变量常用API函数如下:

        int  pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr); //初始化函数 也可使用 pthread_cond_t cond = PTHREAD_COND_INITIALIZER代替;

        int  pthread_cond_destroy(pthread_cond_t* cond);  //销毁函数

        int  pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex); //条件变量不满足,线程阻塞

        int  pthread_cond_timedwait(pthread_cond_t* restrictcond,pthread_mutex_t* restrictmutex,const struct timespec* restrict abstime); //等待的非租塞函数,指定时间返回

        int  pthread_cond_signal(pthread_cond_t* cond); // 一次唤醒一个线程,具体哪个线程视情况而定,返回值非0为失败

        int  pthread_cond_broadcast(pthread_cond_t* cond);  //广播唤醒,一次唤醒多个线程 ,返回值非0为失败

      注意:1.当 pthread_cond_wait 函数阻塞时,它会释放其绑定的互斥体,并阻塞线程,因此在调用该函数前应该对互斥体有个加锁操作,当收到条件信号时, pthread_cond_wait 

会返回并对其绑定的互斥体进行加锁,因此在其下面一定有个对互斥体进行解锁的操作。

                2.条件变量的虚假唤醒:操作系统可能会在一些情况下唤醒条件变量,即使没有其他线程向条件变量发送信号,等待此条件变量的线程也有可能会醒来。pthread_cond_wait 

是 futex 系统调用,属于阻塞型的系统调用,当系统调用被信号中断的时候,会返回 ﹣1,并且把 errno 错误码置为 EINTR。很多这种系统调用为了防止被信号中断都会重启系统调用

(即再次调用一次这个函数)。

              3.如果一个条件变量信号条件产生时(调用 pthread_cond_signal 或pthread_cond_broadcast),没有相关的线程调用 pthread_cond_wait 捕获该信号,那么该信号条件就

会永久性地丢失了,再次调用 pthread_cond_wait 会导致永久性的阻塞,因此,一定要确保等待的线程在产生条件变量信号的线程发送条件信号之前调用 pthread_cond_wait。 

(4) linux线程同步之读写锁:读写锁在 Linux 系统中使用类型 pthread_rwlock_t 表示, 

            读锁用于共享模式:

           如果当前读写锁已经被某线程以读模式占有了,其他线程调用pthread_rwlock_rdlock(请求读锁)会立刻获得读锁;

           如果当前读写锁已经被某线程以读模式占有了,其他线程调用pthread_rwlock_wrlock(请求写锁)会陷入阻塞。

           写锁用的是独占模式:

          如果当前读写锁被某线程以写模式占有,无论调用pthread_rwlock_rdlock还是pthread_rwlock_wrlock都会陷入阻塞,即写模式下不允许任何读锁请求通过,也不允许任何写锁

请求通过,读锁请求和写锁请求都要陷入阻塞,直到线程释放写锁。

           读写锁初始化和销毁的API:

           int  pthread_rwlock_init(pthread_rwlock_t* rwlock,const pthread_rwlockattr_t* attr); //参数 rwlock 即需要初始化和销毁的读写锁对象的地址,参数 attr 用于设置读写锁的属性,设置

NULL表示默认属性,返回非0 为失败,当不需要动态创建或者设置非默认属性的读写锁对象,可使用 pthread_rwlock_t myrwlock = PTHREAD_RWLOCK_INITIALIZER; 初始化

           int  pthread_rwlock_destroy(pthread_rwlock_t* rwlock); //销毁读写锁

          请求读锁和写锁API数:

           int  pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);

           int  pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);

           int  pthread_rwlock_timedrdlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);

           int  pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);

           int  pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

           int  pthread_rwlock_timedwrlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);

          无论是读锁还是写锁,锁的释放都是一个接口:

          int  pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

          读写锁的属性类型是 pthread_rwlockattr_t ,glibc 引入了如下接口来查询和改变读写锁的类型:

          int  pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr,intpref);

          int  pthread_rwlockattr_getkind_np(constpthread_rwlockattr_t* attr,int* pref);

          pthread_rwlockattr_setkind_np 的第二个参数 pref 即设置读写锁的类型,其取值有如下几种:

        enum{

        PTHREAD_RWLOCK_PREFER_READER_NP,   //读者优先(即同时请求读锁和写锁时,请求读锁的线程优先获得锁) 

        PTHREAD_RWLOCK_PREFER_WRITER_NP, //不要被名字所迷惑,也是读者优先

       PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,    //写者优先(即同时请求读锁和写锁时,请求写锁的线程优先获得锁)            

        PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP // 默认,读者优先

        };

       初始化和销毁 pthread_rwlockattr_t 对象:

       int  pthread_rwlockattr_init(pthread_rwlockattr_t* attr);

       in  tpthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);

       初始化一个写者优先的读写锁的例子:

       pthread_rwlockattr_t attr;

       pthread_rwlockattr_init(&attr);

       pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);pthread_rwlock_t rwlock;

       pthread_rwlock_init(&rwlock, &attr);

(5)C++11/14/17线程资源同步对象:C/C++提供的直接使用操作系统的功能函数虽然强大,也存在功能限制,同样的代码不能通知兼容linux和windows,因此C++11以及后续更新封装

同步辅助类std::mutex、std::condition_variable、std::lock_guard、std::unique_lock,极大的方便了跨平台开发。

         常用的比如 std::mutex (C++11互斥对象)、std::shared_mutex(C++17共享的互斥量),均提供了加锁(lock)、尝试加锁(trylock)和解锁(unlock)的方法,为了避免死锁,例

如std::mutex.lock() 和 std::mutex::unlock() 方法需要成对使用,为了防止函数出口过多导致加锁后没有解锁导致死锁,推荐 RAII 技术封装加锁和解锁的两个接口,同时C++11也提供如

下封装:

         互斥量管理                                    版本                          作用

        lock_guard                                    C++11                  基于作用域的互斥量管理

        unique_lock                                  C++11                   更加灵活的互斥量管理

        shared_lock                                 C++14                    共享互斥量的管理

        scope_lock                                   C++17                    多互斥量避免死锁的管理

        注意:1.比如  void func(){

                            std::lock_guard<std::mutex> guard(mymutex);

                              //在这里放被保护的资源操作

                           } 

         mymutex 的类型是 std::mutex,在 guard 对象的构造函数中,会自动调用 mymutex.lock() 方法加锁,当该函数出了作用域后,调用 guard 对象时析构函数时会自动调用  

mymutex.unlock() 方法解锁 ,因此 mymutex 生命周期必须长于函数 func 的作用域。

                  2.重复加锁可能会造成程序奔溃,如果一个 std::mutex 对象已经调用了 lock() 方法,再次调用时,其行为是未定义的,“行为未定义”即在不同平台上可能会有不同的行为。

        std::condition_variable表示条件变量,与linux环境下的原生条件变量一样,提供了等待条件变量满足的 wait 系列方法(wait、wait_for、wait_until 方法),发送条件信号使用 

notify 方法(notify_one 和 notify_all 方法),当然使用 std::condition_variable 对象时需要绑定一个 std::unique_lock 或 std::lock_guard 对象,但是C++ 11 中 std::condition_variable 不

再需要显式调用方法初始化和销毁。

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

推荐阅读更多精彩内容

  • 多线程系列文章源码头文件内容: #include #include #include 作为程序员,就是要减少重复劳...
    batbattle阅读 917评论 0 1
  • Q:为什么出现多线程? A:为了实现同时干多件事的需求(并发),同时进行着下载和页面UI刷新。对于处理器,为每个线...
    幸福相依阅读 1,576评论 0 2
  • Linux 多线程 - 线程异步与同步机制 I. 同步机制 线程间的同步机制主要包括三个: 互斥锁:以排他的方式,...
    不爱吃饭的牛牛阅读 1,393评论 0 1
  • 线程基础 线程是进程的一个执行单元,执行一段程序片段,线程共享全局变量;线程的查看可以使用命令或者文件来进行查看;...
    秋风弄影阅读 736评论 0 0
  • 摘要 线程概念,线程与进程的区别与联系学会线程控制,线程创建,线程终止,线程等待了解线程分离与线程安全学会线程同步...
    狼之足迹阅读 461评论 2 3