线程的几种锁及基本操作

2023-04-07 14:36:12

我们先来看一段代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>         //创建两个线程,分别对两个全变量进行++操作,判断两个变量是否相等,不相等打印

int a = 0;
int b = 0;
// 未初始化 和0初始化的成员放在bbs
pthread_mutex_t mutex;

void* route()
{
    while(1)            //初衷不会打印
    {
        a++;
        b++;
        if(a != b)
        {
            printf("a =%d, b = %d\n", a, b);
            a = 0;
            b = 0;
        }
    }
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, route, NULL);
    pthread_create(&tid2, NULL, route, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

段代码的运行结果优点出乎我们的预料:


我们预计的结构应该是不会打印的,而这里去打印出了我们意想不到的结果。连相等的数据都打印了出来,为什么会出现这样的情况呢?

解释:两个线程互相抢占CPU资源,一个线程对全局变量做了++操作之后,还没来得及比较输出操作,另一个线程抢占CPU,进行比较打印输出。为了避免这样的情况,就需要用到下面介绍的互斥锁。

互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。

1.定义互斥量: pthread_mutex_t mutex;
2.初始化互斥量: pthread_mutex_init(&mutex, NULL); //第二个参数不研究置NULL;          //初始化为 1 (仅做记忆)
3.上锁      pthread_mutex_lock(&mutex);   1->0;    0   等待
4.解锁           pthread_mutex_unlock(&mutex);   置1 返回

5.销毁           pthread_mutex_destroy(&mutex);  

返回值:若成功返回0,若出错返回错误编号。

说明: 互斥锁,在多个线程对共享资源进行访问时,在访问共享资源前对互斥量进行加锁,在访问完再进行解锁,在互斥量加锁后其他的线程将阻塞,直到当前的线程访问完毕并释放锁。如果释放互斥锁时有多个线程阻塞,所有阻塞线程都会变成可运行状态,第一个变成可运行状态的线程可以对互斥量加锁。这样就保证了每次只有一个线程访问共享资源。

至此,我们好像能通过互斥锁解决上面的问题:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int a = 0;
int b = 0;
// 未初始化 和0初始化的成员放在bbs
pthread_mutex_t mutex;

void* route()
{
    while(1)            //初衷不会打印
    {
        pthread_mutex_lock(&mutex);   
        a++;
        b++;
        if(a != b)
        {
            printf("a =%d, b = %d\n", a, b);
            a = 0;
            b = 0;
        }
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tid1, tid2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&tid1, NULL, route, NULL);
    pthread_create(&tid2, NULL, route, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

现有如下场景:线程1和线程2,线程1执行函数A,线程2执行函数B,现只使用一把锁,分别对A,B函数的执行过程加锁和解锁。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>           //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞

pthread_mutex_t mutex;

void* odd(void* arg)
{
  int i = 1;
  for(; ; i+=2)
  {
    pthread_mutex_lock(&mutex);
    printf("%d\n", i);
    pthread_mutex_unlock(&mutex);
  }
}

void* even(void* arg)
{
  int i = 0;
  for(; ; i+=2)
  {
    pthread_mutex_lock(&mutex);
    printf("%d\n", i);
    pthread_mutex_unlock(&mutex);
  }
}


int main()
{
    pthread_t t1, t2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, even, NULL);
    pthread_create(&t2, NULL, odd, NULL);
    //pthread_create(&t3, NULL, even, NULL);
    
    sleep(3);
    pthread_cancel(t2);             //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

一种极限情况是:线程2的取消发生在线程2的解锁之前,那么就会导致因为锁没解开,而线程1无法继续运行。

解决这样的问题我们可以用到下面的宏函数:

宏:              //注册线程回调函数,可用来防止线程取消后没有解锁的问题

void pthread_cleanup_push(void (*routine)(void *),  //回调函数
                                              void *arg); //回调函数的参数
//回调函数执行时机
           1.pthread_exit
           2.pthread_cancel

           3.cleanaup_pop参数不为0,当执行到cleaup_pop时,调用回调函数

void pthread_cleanup_pop(int execute);

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>           //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞

pthread_mutex_t mutex;

void callback(void* arg)      //在cancel中进行解锁
{
  printf("callback\n");
  sleep(1);
  pthread_mutex_unlock(&mutex); 
}

void* odd(void* arg)
{
  int i = 1;
  for(; ; i+=2)
  {
    pthread_cleanup_push(callback, NULL);//因为调用了cancel函数,从而触发了回调函数。
    pthread_mutex_lock(&mutex);
    printf("%d\n", i);
    pthread_mutex_unlock(&mutex);
    pthread_cleanup_pop(0);
  }
}

void* even(void* arg)
{
  int i = 0;
  for(; ; i+=2)
  {
    pthread_mutex_lock(&mutex);
    printf("%d\n", i);
    pthread_mutex_unlock(&mutex);
  }
}


int main()
{
    pthread_t t1, t2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, even, NULL);
    pthread_create(&t2, NULL, odd, NULL);
    //pthread_create(&t3, NULL, even, NULL);
    
    sleep(3);
    pthread_cancel(t2);             //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前
    //pthread_mutex_unlock(&mutex);   有问题,如果执行even的程序有两个,而一个取消线程的函数执行时正好t3函数阻塞,就会导致t3和t1同时在执行even

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

注意:

1.不要销毁一个已经加锁的互斥量,销毁的互斥量确保后面不会再有线程使用。

2.上锁和解锁函数要成对的使用

3.选择合适的锁的粒度(数量)。如果粒度太粗,就会出现很多线程阻塞等待相同锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么太多的锁的开销会使系统的性能受到影响,而且代码会变得相当复杂。

4.加锁要加最小(范围)锁,减少系统负担

使用互斥锁一定要注意避免死锁:《Linux高性能服务器编程》  14.5.3 介绍了两个互斥量因请求顺序产生死锁问题

          如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁。例如,程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

          可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。例如,假设需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量不会产生死锁(当然在其他资源上仍可能出现死锁);类似地,如果所有的线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。

         为了应对死锁,在实际的编程中除除了加上同步互斥量之外,还可以通过以下三原则来避免写出死锁的代码:

1>短:写的代码尽量简洁

2>平:代码中没有复杂的函数调用

3>快:代码的执行速度尽可能快

自旋锁:  应用在实时性要求较高的场合(缺点:CPU浪费较大)

pthread_mutex_spin;

pthread_spin_lock() ; //得不到时,进入忙等待,不断向CPU进行询问请求

pthread_spin_unlock(); 

 pthread_spin_destroy(pthread_spinlock_t *lock);

pthread_spin_init(pthread_spinlock_t *lock, int pshared);

读写锁(共享-独占锁):应用场景---大量的读操作  较少的写操作

注意:读读共享, 读写互斥,写优先级高(同时到达)

1. pthread_rwlock_t rwlock;//定义

2.int pthread_rwlock_init()//初始化

3.pthread_rwlock_rdlock()//pthread_rwlock_wrlock//读锁/写锁

4.pthread_rwlock_unlock() // 解锁

5.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁锁

返回值:成功返回0,出错返回错误编号

说明:不管什么时候要增加一个作业到队列中或者从队列中删除作业,都用写锁,

不管何时搜索队列,首先获取读模式下的锁,允许所有的工作线程并发的搜索队列。在这样的情况下只有线程

搜索队列的频率远远高于增加或删除作业时,使用读写锁才可能改善性能。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>                //创建8个线程,3个写线程,5个读线程

pthread_rwlock_t rwlock;
int counter = 0;

void* readfunc(void* arg)
{
  int id = *(int*)arg;
  free(arg);
  while(1)
  {
    pthread_rwlock_rdlock(&rwlock);
    printf("read thread %d : %d\n", id, counter);
    pthread_rwlock_unlock(&rwlock);
    usleep(100000);
  }
}

void* writefunc(void* arg)
{
  int id = *(int*)arg;
  free(arg);
  while(1)
  {
    int t = counter;
    pthread_rwlock_wrlock(&rwlock);
    printf("write thread %d : t= %d,  %d\n", id, t, ++counter);
    pthread_rwlock_unlock(&rwlock);
    usleep(100000);
  }
}
int main()
{
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock, NULL);
    int i = 0;
    for(i = 0; i < 3; i++)
    {
      int* p =(int*) malloc(sizeof(int));
      *p = i;
      pthread_create(&tid[i], NULL, writefunc, (void*)p);
    }
    for(i = 0; i < 5; i++)
    {
      int* p = (int*)malloc(sizeof(int));
      *p = i;
      pthread_create(&tid[3+i], NULL, readfunc, (void*)p);
    }

    for(i = 0; i < 8; i++)
    {
      pthread_join(tid[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);

    return 0;
}

条件变量:  如果说互斥锁是用于同步线程对共享数据的访问的化,那么条件变量这是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程       

1.定义条件变量  pthread_cond_t cond;
2.初始化        pthread_cond_init(&cond, NULL);
3.等待条件      pthread_cond_wait(&cond, &mutex);
                                 mutex :如果没有在互斥环境,形同虚设
                                 在互斥环境下:wait函数将mutex置1,wait返回,mutex恢复成原来的值
4.修改条件      pthread_cond_signal(&cond);
5.销毁条件      pthread_cond_destroy(&cond);
规范写法:
pthread_mutex_lock();
    while(条件不满足)
    pthread_cond_wait();
//为什么会使用while?
//因为pthread_cond_wait是阻塞函数,可能被信号打断而返回(唤醒),返回后从当前位置向下执行, 被信号打断而返回(唤醒),即为假唤醒,继续阻塞
pthread_mutex_unlock();

pthread_mutex_lock();
pthread_cond_signal(); //信号通知   ----   如果没有线程在等待,信号会被丢弃(不会保存起来)。
pthread_mutex_unlock();
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>             //创建两个线程一个wait print,一个signal sleep()

pthread_cond_t cond;
pthread_mutex_t mutex;

void* f1(void* arg)
{
  while(1)
  {
    pthread_cond_wait(&cond, &mutex);
    printf("running!\n");
  }
}
void* f2(void* arg)
{
  while(1)
  {
    sleep(1);
    pthread_cond_signal(&cond);
  }
}

int main()
{
  pthread_t tid1, tid2;
  pthread_cond_init(&cond, NULL);
  pthread_mutex_init(&mutex, NULL);

  pthread_create(&tid1, NULL, f1, NULL);
  pthread_create(&tid2, NULL, f2, NULL);

  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);

  pthread_cond_destroy(&cond);
  pthread_mutex_destroy(&mutex);
  return 0;
}

                   System V //基于内核持续性

信号量:      POSIX    //基于文件持续性的信号量
1.定义信号量: sem_t sem;
2,初始化信号量:    sem_init(sem_t* sem,
                                                int shared,   //0表示进程内有多少个线程使用
                                                int val);     //信号量初值
3.PV操作        int sem_wait(sem_t* sem);   //sem--;如果小于0,阻塞    P操作
                      int sem_post(sem_t* sem);   //sem++;                 V操作
4.销毁            sem_destroy(sem_t* sem);

信号量实现生产者消费者模型:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
//仓库中装产品编号,没装产品的位置,置为-1,装了的地方置为产品的编号


#define PRO_COUNT 3
#define CON_COUNT 2
#define BUFSIZE 5


sem_t sem_full;     //标识可生产的产品个数
sem_t sem_empty;    //表示可消费的产品个数
pthread_mutex_t mutex;  //互斥量
int num = 0;        //产品编号
int buf[BUFSIZE];   //仓库
int wr_idx;     //写索引
int rd_idx;     //读索引

void* pro(void* arg)
{
  int i = 0;
  int id = *(int*)arg;
  free(arg);
  while(1)
  {
      sem_wait(&sem_full);    //先判断仓库是否满
      pthread_mutex_lock(&mutex); //互斥的访问具体的仓库的空闲位置
      printf("%d生产者开始生产%d\n", id, num);
      for(i = 0; i < BUFSIZE; i++)
      {
        printf("\tbuf[%d]=%d", i, buf[i]);
        if(i == wr_idx)
        {
          printf("<=====");
        }
        printf("\n");
      }
      buf[wr_idx] = num++;      //存放产品
      wr_idx = (wr_idx + 1) % BUFSIZE;
      printf("%d生产者结束生产\n", id);
      pthread_mutex_unlock(&mutex);
      sem_post(&sem_empty);
      sleep(rand()%3);
    }
}

void* con(void* arg)
{
  int i = 0;
    int id = *(int*)arg;
    free(arg);
    while(1)
    {
        sem_wait(&sem_empty);
        pthread_mutex_lock(&mutex);
        
        printf("%d消费者开始消费%d\n", id, num);
        for(i = 0; i < BUFSIZE; i++)
        {
          printf("buf[%d]=%d", i, buf[i]);
          if(i == rd_idx)
          {
            printf("=====>");
          }
          printf("\n");
        }
        int r = buf[rd_idx];
        buf[rd_idx] = -1;
        rd_idx = (rd_idx+1)%BUFSIZE;
        sleep(rand()%4);
        printf("%d\n消费者消费完%d\n", id, r);
        pthread_mutex_unlock(&mutex);
        sem_post(&sem_full);
        sleep(rand()%2);
    }
}

int main()
{
    pthread_t tid[PRO_COUNT+CON_COUNT];
    pthread_mutex_init(&mutex, NULL); //初始化
    sem_init(&sem_empty, 0, 0);
    sem_init(&sem_full, 0, BUFSIZE);
    srand(getpid());

    int i = 0;
    for(i = 0; i < BUFSIZE; i++)      //初始化仓库  -1表示没有品
        buf[i] = -1;

    for(i = 0; i < PRO_COUNT; i++)    //产生生产者
    {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&tid[i], NULL, pro, p);
    }

    for(i = 0; i < CON_COUNT; i++)
    {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&tid[i+CON_COUNT], NULL, con, p);
    }
    
    for(i = 0; i < PRO_COUNT + CON_COUNT; i++)
    {
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&mutex);  //销毁
    sem_destroy(&sem_empty);
    sem_destroy(&sem_full);

    return 0;
}

拓展学习:

乐观锁和悲观锁?

乐观锁:

     在关系数据库管理系统里,乐观并发控制(又名”乐观锁”,Optimistic Concurrency Control,缩写”OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

乐观并发控制的事务包括以下阶段: 
1. 读取:事务将数据读入缓存,这时系统会给事务分派一个时间戳。 
2. 校验:事务执行完毕后,进行提交。这时同步校验所有事务,如果事务所读取的数据在读取之后又被其他事务修改,则产生冲突,事务被中断回滚。 

3. 写入:通过校验阶段后,将更新的数据写入数据库。

优点和不足:

       乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

悲观锁:

    在关系数据库管理系统里,悲观并发控制(又名”悲观锁”,Pessimistic Concurrency Control,缩写”PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

优点和不足:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

系统最多能够创建多少个线程? (一般以实测为准,但根据每次开辟的栈的大小不同,测试结果也会不同)。

一个是直接在命令行查看    cat /proc/sys/kernel/threads-max  我的电脑显示是 7572

另一个是自己计算 用户空间大小3G 即是3072M/8M栈空间  = 380     

第三个写程序:   跑到32754(理论值 32768)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>               //创建线程

void* foo(void* arg)
{
}

int main()
{
    int count = 0;
    pthread_t thread;

    while(1)
    {
        if(pthread_create(&thread, NULL, foo, NULL) != 0)
        return 1;
        count++;
    printf("MAX = %d\n", count);
    }

    return 0;
}

  • 作者:CZF_csdn
  • 原文链接:https://blog.csdn.net/bian_cheng_ru_men/article/details/80210501
    更新时间:2023-04-07 14:36:12