并发编程

并发程序:使用应用级并发

  1. 进程驱动

    • 将每个逻辑控制流视为进程。
    • 内核自动管理流,进程管理开销较大。
    • 流必须通过IPC进行通信。
    • 难以共享资源,但同时也避免了可能带来的共享问题

    例如服务器的父进程接受不同客户端的请求,然后创建1个子进程为客户提供服务。
    这里写图片描述

  2. 事件驱动

    • 将流视为状态机:
      • 状态
      • 输入事件
      • 转移(根据当前状态和输入事件转移到下一个状态)

    服务器通过I/O多路复用响应不同的I/O事件。select函数首先挂起进程(阻塞),在有I/O事件发生后,将控制返回给应用程序。
    例如在读集合中保存描述符的类型,在有I/O事件发生后,如果导致有描述符准备好可读,退出select函数。程序通过宏指令确定描述符的类型,进行相应的操作。

    • 手动控制多个逻辑流
    • 所有的逻辑流共享同一个地址空间
    • 编码复杂,无法提供精细度较高的并行
    • 无法充分利用多核处理器
  3. 线程驱动
    线程:运行在进程上下文的逻辑流。

    1. 每个线程都有线程上下文(线程 ID,栈,栈指针,PC,条件码,GP 寄存器),共享剩下的进程上下文,切换更快。
    2. 一个进程可以有多个线程,即作为单独可执行的部分从进程中抽离出来。
      1. 位于同一进程的线程组成对等线程池
      2. 主线程是第1个运行的线程
    3. 属于基于进程和基于事件的混合体:

      • 内核自动管理多个线程
      • 多个线程运行在进程上下文,共享地址空间
    4. 每个线程独立线程 id,保存局部变量的栈(其他线程可以修改)但是会共享所有的代码、数据以及内核上下文。
    5. 共享变量(实例被多个线程引用)可能会造成同步错误,难以调试和测试。
      • 全局变量:定义在函数之外。每个线程引用同一实例。
      • 本地自动变量:函数内部的非static变量。每个线程有自己的实例。
      • 本地静态变量:函数内部的非static变量。每个线程引用同一实例。
volatile long cnt=0;//共享变量
sem_t mulex;//互斥锁

void *thread(void *vargp);

int main()
{
   pthread_t tid1,tid2;
   Sem_init(&mutex,0,1);//初始化信息量/*进程1*/
   Pthread_creat(&tid1,NULL,thread,&niters);
   /*进程2*/
   Pthread_creat(&tid2,NULL,thread,&niters);
   …
} 

void *thread(void *vargp)
{
   long i,niters=*((long *)vargp);

   for(i=0;i<niters;i++)
   {
      P(&mutex);
      cnt++;
      V(&mutex);
   }


   return NULL;
}

Pthreads:C语言处理线程的标准接口

  • 线程例程:封装线程代码和本地数据
  • pthread_creat函数:利用线程例程创建线程,生成线程ID。返回后与主线程同时运行。
  • pthread_join函数:等待指定线程终止,并回收内存资源
  • pthread_exit函数:终止线程。当主线程调用时会等待其他对等线程终止后再终止自己和所在进程。
    注意当线程调用Linux的exit()函数时,将会终止进程及其所有线程。
  • pthread_detach函数:将指定的线程分离(不能被其他线程回收或杀死,内存资源由系统自动释放)

将线程例程的循环分成5段:

  1. H:第1次循环前进行条件判断
  2. L:将cnt加载到%rdx
  3. U:更新%rdx
  4. S:将%rdx的值存回
  5. T:更新循环变量并进行条件判断

其中i位于栈中,程序运行时无法确定CPU执行指令的顺序是否会产生正确结果。如图12-18(a)所示:CPU执行线程1的例程时将更新后的值返回存储后再执行线程2的例程;但是(b)执行线程1的例程时,在更新cnt后并没有存储就执行线程2的例程,此时cnt依然为0.
根据CP702的进程图:
这里写图片描述

  1. 横坐标为线程1的运行时间;纵坐标为线程2的运行时间。
  2. 由于程序不会反向运行,且同一时间只能有1条指令运行。所以箭头只能向上(执行线程2的指令)或者向右移动(执行线程1的指令)。
  3. 将操作cnt的指令划为不安全区。一旦进入说明某个线程没有完成对共享变量的操作。
  4. 多线程的程序必须保证任何可行的轨迹线都正常工作。

信号量:非负整数的全局变量

  • 互斥
    将共享变量和互斥锁(二元信息量:初始化为1)联系起来。
    P(互斥锁加锁)和V(互斥锁解锁)操作保证了程序的信号量不变性(信号量在初始化后保持非负),确保了程序对临界区的互斥访问。在进程图中加入P(S)和V(S)。
    这里写图片描述
  • P(S)
//P操作的包装函数
int sem_wait(sem_t *s);

企图进入不安全区的线程需要先运行 P。当 S 为非负时减1并返回,线程可以获准进入不安全区;当信号量 S=0时,线程会被挂起,直到信号量S不为负值时。

  • V(S)
//V操作的包装函数
int sem_post(sem_t *s);

结束离开临界区块的线程将会运行 V,将信号量 S 的值加1。选择1个被P阻塞的线程将其重启,获准进入不安全区。

  • 调度对共享资源的访问
    线程用信号量通知另一线程,程序状态的某个条件为真。
  • 生产者-消费者问题
    这里写图片描述
    • 生产者将项目存储到缓存区,并通知消费者
    • 消费整从缓存区中移除项目,并通知生产者
      在此模型中,不仅需要对缓冲区的互斥访问;还考虑到生产者/消费者在缓冲区为满/空时必须等待。
      为实现一个有 n 个元素 缓冲区,需要一个 mutex 和两个用来计数的 semaphore:
typedef struct {
    …
    sem_t mutex; // 保证对 buffer 的互斥访问
    sem_t slots; // 统计 buffer 中可用的空槽数目
    sem_t items; // 统计 buffer 中可用的项目数目
} sbuf_t;
//为缓冲区分配堆内存
void sbuf_init(sbuf_t *sp, int n);
//为缓冲区分配堆内存
void sbuf_deinit(sbuf_t *sp);
//等待空槽位,对互斥锁加锁,添加项目,对互斥锁解锁,发送消息
void sbuf_insert(sbuf_t *sp, int item);
//等待项目,对互斥锁加锁,取出项目,对互斥锁解锁,发送消息
int sbuf_remove(sbuf_t *sp);
  • 读者-写者问题
    并发的线程访问共享对象
    • 读者:只读对象,可以共享
    • 写者:修改对象,独占对对象的访问
      例如电影票预订系统允许多个用户查看座位分配;但预订座位的用户必须独占对数据库的访问。
      1. 读者优先:只有没有读者的情况下,写者才能工作
int readcnt;//共享变量
sem_t mutex;//保护共享变量
sem_t w;//信号量

void writer()
{
   while(1){
        P(&w);//保证只有1个写者访问V(&w);
   }
}

void reader()
{
   while(1){
        P(&mutex);
        readcnt++;
        //第1个读者进入时
        if(readcnt==1)  P(&w);
        V(&mutex);
        …
        P(&mutex);
        readcnt--;
        //最后1个读者离开时
        if(readcnt==1)  V(&w);
        V(&mutex);
   }
}

2 写者优先:只有没有写者的情况下,读者才能工作
可能导致starvation(线程无限期的阻塞)

总结:

  • 相同点:有自己的逻辑控制流,可以并行,都需要进行上下文切换。
  • 不同点:线程共享代码和数据(进程通常不会),线程开销比较小(创建和回收)

    这里写图片描述

线程安全函数:
被多个并发线程调用时会始终产生正确的结果。

可重入函数:
被多线程调用时不会引用共享数据。由于不需要同步,所以效率更高。

  • 显式可重入:函数参数为传值传递,不引用共享变量
  • 隐式可重入:函数参数包含指向非共享数据的指针

线程不安全的函数:

  • 不保护共享变量的函数

    • 解决办法:使用 P 和 V semaphore 操作
    • 问题:同步操作会影响性能
  • 在多次调用间保存状态的函数

    • 解决办法:把状态当做传入参数
  • 返回指向静态变量的指针的函数
    • 解决办法1:重写函数,传地址用以保存
    • 解决办法2:上锁,并且进行复制
  • 调用线程不安全函数的函数

    竞争:
    线程2到达y点之前,线程1必须到达x点。

#define N 4

void *thread(void *vargp);

int main()
{
  pthread_t tid[N];
  int i;

  for(i=0;i<N;++i)
      Pthread_creat(&tid[i],NULL,thread,&i);
  …
}

主线程在创建对等线程时传递了本地变量i的指针。参考之前的进程图,只有在线程2执行完thread函数后,线程1执行++i程序才会得到正确的结果。
所以必须用另一变量存储i的值,并将其作为参数传入。

死锁
线程被无限期阻塞
由于2个线程使用P和V的顺序不当,箭头只能向上/右移动,所以箭头在进入死锁区后,必定将进入s/t的禁止区。
这里写图片描述
互斥锁加锁顺序:

  1. 每个线程以一种顺序获得互斥锁
  2. 以相反顺序释放
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值