Linux多线程间同步

11 篇文章 1 订阅

竞争与同步

当多个线程同时访问其所共享的进程资源时,需要相互协调,以防止出现数据不一致、不完整的问题。这就需要线程同步。

一、信号量

信号量是一个计数器,用于控制访问有限共享资源的线程数。
头文件:semaphore.h

 int sem_init (sem_t* sem, int pshared,unsigned int value);
  • 功能:创建信号量
  • sem - 信号量ID,输出。
  • pshared - 一般取0,表示调用进程的信号量。
      非0表示该信号量可以共享内存的方式,
      为多个进程所共享(Linux暂不支持)。
  • value - 信号量初值。
 int sem_wait (sem_t* sem);
  • 功能:信号量减1,不够减即阻塞
 int sem_trywait (sem_t* sem);
  • 功能: 信号量减1,不够减则直接返回-1,errno为EAGAIN
 int sem_timedwait (sem_t* sem,const struct timespec* abs_timeout);
 struct timespec {
     time_t tv_sec; // Seconds
     long tv_nsec; // Nanoseconds [0 - 999999999]
 };
  • 功能:信号量减1,不够减则阻塞,直到abs_timeout超时才返回-1,errno设置为ETIMEDOUT
 int sem_post (sem_t* sem);
  • 功能:信号量加1
 int sem_destroy (sem_t* sem);
  • 功能:销毁信号量
    在操作系统中,信号量sem是一个整数,在sem大于等于0时代表可供并发进程使用的资源实体数,但sem小于0时则表示正在等待使用临界区的进程数。

练习:图书馆有5本《书》,创建10个线程,每个线程去借阅这本书的阅读时间(0~10)然后还书。

二、互斥量

  • 互斥量从本质上来说是一把锁,在访问共享内存前对互斥量进行设置(加锁),在访问完成后释放互斥量(解锁)。对互斥量进行加锁之后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。当有一个以上的线程阻塞时,则当互斥量释放时线程会互相竞争,先竞争到的线程可以对互斥量加锁,而其他线程要再次阻塞等待。
 int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
  • 功能:初始化互斥量
  • 也可以这样初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 int pthread_mutex_lock (pthread_mutex_t* mutex);
  • 功能:加锁
 int pthread_mutex_unlock (pthread_mutex_t* mutex);
  • 功能:解锁
int pthread_mutex_destroy (pthread_mutex_t* mutex);
  • 功能:销毁互斥量

  • 互斥量使用:

    • 互斥量被初始化为非锁定状态;
    • 线程1调用pthread_mutex_lock函数,立即返回,互斥量呈锁定状态;
    • 线程2调用pthread_mutex_lock函数,阻塞等待;
    • 线程1调用pthread_mutex_unlock函数,互斥量呈非锁定状态;
    • 线程2被唤醒,从pthread_mutex_lock函数中返回,互斥量呈锁定状态;
2.1、死锁
  • 如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
  • 程序中使用一个以上的互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,因为两个线程都在互相请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就陷入死锁。
    锁A 锁B
    线程A 线程B
    加锁A 加锁B
    延时   延时//保证A加锁A成功,B加锁B成功
    加锁B 加锁A //已死锁
    解锁A 解锁B
    解锁B 解锁A
  • 死锁的四个必要条件:
    • 互斥性:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
    • 不可剥夺性:不能剥夺别人已经占用的资源
    • 请求与保持:本身已经占有资源(一种或多种),同时还有资源未得到满足,正在等待其他程序释放该资源
    • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源
练习:使用互斥量实现一个死锁程序,思考如何避免死锁。

答:

  • 不要连续的加锁;
  • 仔细控制互斥量加锁的顺序来避免死锁的发生。
2.2活跃度失败问题

在使用锁保证线程安全时可能会出现活跃度失败的情况主要包括 饥饿、丢失信号、和活锁、死锁 等。【多线程除了死锁之外遇到最多的就是活跃度问题了】

  • 饥饿 :指线程需要访问的资源被永久拒绝 ,以至于不能再继续进行。解决饥饿问题需要平衡线程对资源的竞争,如线程的优先级、任务的权重、执行的周期等。

  • 活锁 :指线程虽然没有被阻塞,但由于某种条件不满足,一直尝试重试却始终失败。解决活锁问题需要对重试机制 引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试,这会大大减少碰撞的可能性。

三、自旋锁

  • 特点:获取不到锁时不会休眠,而是一直轮询请求锁,因此会导致CPU占用比较高
  • 自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部 分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存 器中关闭/打开中断标志位,不需要自旋锁)
  • 在单核cpu下不起作用:被自旋锁保护的临界区代码执行时不能进行挂起状态。会造成死锁
  • 自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的 线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。
//初始化
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// pshared可取以下属性:
// PTHREAD_PROCESS_PRIVATE 
// PTHREAD_PROCESS_SHARED 
//销毁
int pthread_spin_destroy(pthread_spinlock_t *lock); 

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock); 
int pthread_spin_unlock(pthread_spinlock_t *lock);

四、读写锁

概念:
读写锁实际是一种特殊的自旋锁,这组锁它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋 锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源, 最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

  • 因为读写锁保持期间也是抢占失效的。如果读写锁当前没有读者,也没有写者,那么 写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁 没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读 写锁。

  • 特性: 一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁

    • 当读写锁是写加锁(独占)状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程 都会被阻塞
    • 当读写锁在读加锁(共享)状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权。但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁,并且读 写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁 请求长期阻塞.
//初始化和销毁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//成功则返回0,出错则返回错误编号. 同互斥量以上,在释放读写锁占用的内存之前,需要先通过 pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源. 

//读和写: 
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 
//成功则返回0,出错则返回错误编号.这3个函数分别实现获取读锁,获取写锁和释放锁的操作. 

//获取锁的两个函数是阻塞操作,同样,非阻塞的函数为: 
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//成功则返回0,出错则返回错误编号.非阻塞的获取锁操作,如果可以获取则返回0,否则返回错误的EBUSY

五、条件变量

条件变量可以让调用线程在满足特定条件的情况下暂停。

 int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr);
  • 功能:初始化条件变量
  • 初始化也可以:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex);
  • 功能:使调用线程睡入条件变量cond,同时释放互斥锁mutex
 int pthread_cond_timedwait (pthread_cond_t* cond, pthread_mutex_t* mutex,const struct timespec* abstime);
 struct timespec {
     time_t tv_sec; // Seconds
     long tv_nsec; // Nanoseconds [0 - 999999999]
 };
  • 功能:使调用线程睡入条件变量cond,同时释放互斥锁mutex,并在时间到了之后即使没有被唤醒,也醒过来。
 int pthread_cond_signal (pthread_cond_t* cond);
  • 功能:从条件变量cond中唤出一个线程,令其重新获得原先的互斥锁。
  • 注意:被唤出的线程此刻将从pthread_cond_wait函数中返回,但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。
 int pthread_cond_broadcast (pthread_cond_t* cond);
  • 功能:从条件变量cond中唤出所有线程
 int pthread_cond_destroy (pthread_cond_t* cond);
  • 功能: 销毁条件变量
5.1、生产者与消费者模型

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。

  • 问题1:在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。

  • 问题2:同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
    只有把问题1和问题2协调好,才能最大限度的提高效率。
    在这里插入图片描述

  • 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

  • 在这个模型中,最关键就是内存缓冲区为空的时候消费者必须等待,而内存缓冲区满的时候,生产者必须等待。其他时候可以是个动态平衡。

    实现:生产消费者模式。

5.2、哲学家就餐问题

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁。
做以下两件事情之一:吃饭,或者思考。
吃东西的时候,他们就停止思考,思考的时候也停止吃东西。
餐桌中间有一大碗意大利面,每两个哲学家之间有一只筷子。因为用一只筷子很难吃到意大利面,所以哲学家必须用两只筷子吃东西。他们只能使用自己左右手边的那两只筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)。

即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只筷子超过五分钟后就放下自己手里的那一只筷子,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的筷子,那么这些哲学家就会等待五分钟,同时放下手中的筷子,再等五分钟,又同时拿起这些筷子。

  • 解决方案:只有左右两边的哲学家都在思考,才去吃面,吃完后通信左右两边我吃完了。
  • 注意:吃面时间和思考时间应该随机。

六、线程屏障

  • 屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有合作线程都到达某一点,然后从该点继续执行。挺适用于多线程排序然后最后归并的场景
pthread_barrier_t ; //屏障数据类型
//初始化
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t * restrict attr, unsigned int count);
//销毁
int pthread_barrier_destory(pthread_barrier_t * barrier);
//同步等待
int pthread_barrier_wait(pthread_barrier_t *barrier)

七、CAS (Compare And Set / Compare And Swap)

在计算机科学中,比较与交换(CAS)是多线程中用来实现同步的原子指令。它将内存位置的内容与给定值进行比较,只有当它们相同时,才将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的

  • CAS是一组原语指令,用来实现多进/线程下的变量同步。
  • CAS原语有三个参数,内存地址,期望值,新值。如果内存地址的值==期望值,表示该值未修改,此时可以修改成新值。否则表示修改失败,返回false,由用户决定后续操作
bool CAS(T* addr, T expected, T newValue) 
{ 
      if( *addr == expected ) 
     { 
          *addr =  newValue; 
           return true; 
     } 
     return false; 
 }
  • 优点:
    • 在高并发的情况下,它比有锁的程序拥有更好的性能;
    • 它天生就是死锁免疫的。
  • 缺点:
    • .CPU开销较大在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力
    • CAS只能保证一个共享变量的原子操作
    • ABA问题:如果变量初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
  • 因此冲突如果过于频繁的场景不建议使用CAS原语进行处理(CAS也是乐观锁的机制,乐观锁不擅长冲突频繁的场景,这时候可以选择悲观锁的机制)
  • GCC下的CAS完整的原子操作可参看
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)

type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

 template < class T >
 bool  atomic_compare_exchange_weak( std::atomic<T>* obj,T* expected, T desired );
       
 template< class T >
 bool atomic_compare_exchange_weak( volatile std::atomic<T>* obj,T* expected, T desired );

附录

图书馆:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;

void* rentbook(void* arg)
{
     int time = *(int*)arg;
     printf("线程:%lu 进入图书馆了...\n",pthread_self());
     sem_wait(&sem);
     printf("线程:%lu 借了一本书,还剩 %d 本书\n",pthread_self(),sem);
     sleep(time);
     sem_post(&sem);
     printf("线程 %lu 还书了,图书馆剩余书还有 %d\n",pthread_self(),sem);
}

int main(int argc, char const *argv[])
{
     pthread_t pth_id[10];
     int time[10] = {};
     int ret = sem_init(&sem,0,5);
     for (int i = 0; i < 10; ++i)
     {
          time[i] = rand()%10;//获取(0~10)的随机数
          pthread_create(&pth_id[i],NULL,rentbook,&time[i]);
     }
     for (int i = 0; i < 10; ++i)
      {
          pthread_join(pth_id[i],NULL);
      }
 return 0;
}

返回信号量

死锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

unsigned int count = 0;

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void* lock_pthread(void* arg)
{
 pthread_mutex_lock(&lock1);
 sleep(1);
 pthread_mutex_lock(&lock2);
 for (int i = 0; i < 100; ++i)
 {
  count++;
  printf("pthread:%d\n",count);
 }
 pthread_mutex_unlock(&lock1);
 pthread_mutex_unlock(&lock2);
}

int main(int argc, char const *argv[])
{
 pthread_t pth_id;
 pthread_create(&pth_id,NULL,lock_pthread,NULL);


 pthread_mutex_lock(&lock2);
 sleep(1);
 pthread_mutex_lock(&lock1);
 for (int i = 0; i < 100; ++i)
 {
  count++;
  printf("main:%d\n",count);
 }
 pthread_mutex_unlock(&lock2);
 pthread_mutex_unlock(&lock1);

 pthread_join(pth_id,NULL);
 return 0;
}

返回死锁

生产消费者模式
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define MAX 20

char storage[MAX] = {};
int count = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;

void show_storage(char* role,char* op,char prod)
{
     printf("%s:",role);
     for(int i=0; i<count; i++)
     {
          printf("%c",storage[i]);
     }
     printf("%s%c\n",op,prod);
}

void* pro_run(void* arg)
{
     char* who = "生产者";
     while(1)
     {
          pthread_mutex_lock(&mutex);
          while(count >= MAX)
          {
               printf("%s:满仓\n",who);
               pthread_cond_wait(&full,&mutex);
          }
          char prod = 'A'+rand()%26;
          storage[count++] = prod;

          show_storage(who,"->",prod);

          pthread_cond_signal(&empty);
          pthread_mutex_unlock(&mutex);
          usleep(rand()%100*1000);
     }
}

void* con_run(void* arg)
{
     char* who = "消费者";
     while(1)
     {
          pthread_mutex_lock(&mutex);
          while(count <= 0)
          {
               printf("%s:空仓\n",who);
               pthread_cond_wait(&empty,&mutex);
          }
      char prod = storage[count--];
      show_storage(who,"<-",prod);
  
      pthread_cond_signal(&full);
      pthread_mutex_unlock(&mutex);
  
      usleep(rand()%100*1000);
     }
}

int main()
{
     pthread_t tid1,tid2;
     pthread_create(&tid1,NULL,pro_run,NULL);
     pthread_create(&tid2,NULL,con_run,NULL);
     getchar();
}

返回生产消费

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值