linux环境下的多线程编程(二)之线程同步

目录

线程同步的概念

 同步方式

一,互斥锁

1.互斥锁函数

 1.1加锁与解锁函数

 2.互斥锁使用

 二,读写锁

1.读写锁函数

  1.1读写锁的加锁与解锁函数

 2.读写锁的使用

三,条件变量

 1 条件变量函数

2 生产者和消费者(通常条件变量都是配合生产者和消费者模型进行工作)

四,信号量 

1 信号量函数

 2.信号量使用

 (1)总资源数为 1时

(2) 总资源数大于 1时

 


线程同步的概念

假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

  •  同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。

下面会依次详细介绍这四种线程同步的方法。

一,互斥锁

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。

1.互斥锁函数

  • 在 Linux 中互斥锁的类型为 pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:pthread_mutex_t  mutex;
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源            
int pthread_mutex_destroy(pthread_mutex_t *mutex);

  • 参数 
  • mutex: 互斥锁变量的地址
  • attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL

 1.1加锁与解锁函数

// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);

 2.互斥锁使用

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 50
// 全局变量
int number;

//创建一把互斥锁
pthread_mutex_t mutex;

// 线程处理函数
void* funcA_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

void* funcB_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
        usleep(5);
    }

    return NULL;
}

int main(int argc, const char* argv[])
{
    pthread_t p1, p2;
    pthread_mutex_init(&mutex, NULL);

    // 创建两个子线程
    pthread_create(&p1, NULL, funcA_num, NULL);
    pthread_create(&p2, NULL, funcB_num, NULL);

    // 阻塞,资源回收
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

 二,读写锁

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。

 读写锁是一把锁,锁的类型为 pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了:pthread_rwlock_t rwlock;

1.读写锁函数

#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数:
  • rwlock: 读写锁的地址,传出参数
  • attr: 读写锁属性,一般使用默认属性,指定为 NULL 

  1.1读写锁的加锁与解锁函数

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。

// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。

// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

 2.读写锁的使用

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 50
// 全局变量
int number;

//创建一把 读写锁
pthread_rwlock_t rwlock;

// 线程处理函数
void* write_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("Thread_write, id = %lu, number = %d\n", pthread_self(), number);
        pthread_rwlock_unlock(&rwlock);
    }

    return NULL;
}

void* read_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("Thread_read, id = %lu, number = %d\n", pthread_self(), number);
        pthread_rwlock_unlock(&rwlock);
        usleep(rand()%5);
    }

    return NULL;
}

int main(int argc, const char* argv[])
{
    pthread_t p1[3], p2[5];
    pthread_rwlock_init(&rwlock, NULL);

    //8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
    for(int i = 0; i < 3; i++)
    {
        pthread_create(&p1[i], NULL, write_num, NULL);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&p2[i], NULL, read_num, NULL);
    }

    // 阻塞,资源回收
    for(int i = 0; i < 3; i++)
    {
        pthread_join(p1[i], NULL);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_join(p2[i], NULL);
    }
    
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

三,条件变量

 1 条件变量函数

严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:

假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了:pthread_cond_t cond;

条件变量操作函数函数原型如下:

#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);
  • 参数:
  • cond: 条件变量的地址
  • attr: 条件变量属性,一般使用默认属性,指定为 NULL
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

2 生产者和消费者(通常条件变量都是配合生产者和消费者模型进行工作)

/* 使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

pthread_cond_t cond;    //定义条件变量, 控制消费者线程
pthread_mutex_t mutex;  //互斥锁变量

//链表的节点
struct node{
    int date;
    struct node* next;
};

//指向头结点的指针
struct node* head = NULL;

void* producer(void* arg);
void* consumer(void* arg);

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

    pthread_t t1[5], t2[5];
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&t1[i], NULL, producer, NULL);
        pthread_create(&t2[i], NULL, consumer, NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(t1[i], NULL);
        pthread_join(t2[i], NULL);
    }

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

void* producer(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        //创建一个链表的新节点
        struct node* newNode = (struct node*)malloc(sizeof(struct node));
        // 节点初始化
        newNode->date = rand()%100 + 1;
        newNode->next = head;
        head = newNode;
        printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);
        pthread_mutex_unlock(&mutex);

         // 生产了任务, 通知消费者消费
        pthread_cond_broadcast(&cond);
        sleep(rand()%3);
    }
    return NULL;
}

void* consumer(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        while(head == NULL)
        {
             pthread_cond_wait(&cond, &mutex);
        }
        struct node* head_node = head;
        printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
        head = head->next;
        free(head_node);
        pthread_mutex_unlock(&mutex);
        sleep(rand()%3);
    }
    return NULL;
}

四,信号量 

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为 <semaphore.h>

1 信号量函数

#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数            
int sem_destroy(sem_t *sem);
  •  参数:
  • sem:信号量变量地址
  • pshared:
  • 0:线程同步
  • 非 0:进程同步
  • value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了
// 参数 sem 就是 sem_init() 的第一个参数  
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);

 当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。

 2.信号量使用

 (1)总资源数为 1时

如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了,因此不使用互斥锁也可以实现线程同步。

/* 使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h> //信号量的头文件

//生产者线程信号量
sem_t psem;

//消费者线程信号量
sem_t csem;

// pthread_mutex_t mutex;  //互斥锁变量

//链表的节点
struct node{
    int date;
    struct node* next;
};

//指向头结点的指针
struct node* head = NULL;

void* producer(void* arg);
void* consumer(void* arg);

int main()
{
    // 初始化信号量
    // 生产者和消费者拥有的资源的总和为1
    sem_init(&psem, 0, 1);  // 生成者线程一共有1个资源
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个资源

    pthread_t t1[5], t2[5];
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&t1[i], NULL, producer, NULL);
        pthread_create(&t2[i], NULL, consumer, NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(t1[i], NULL);
        pthread_join(t2[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}

void* producer(void* arg)
{
    while(1)
    {
        // 生产者资源减一
        sem_wait(&psem);

        //创建一个链表的新节点
        struct node* newNode = (struct node*)malloc(sizeof(struct node));
        // 节点初始化
        newNode->date = rand()%100 + 1;
        newNode->next = head;
        head = newNode;
        printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);

        // 通知消费者消费, 给消费者的资源加一
        sem_post(&csem);
        sleep(rand()%3);
    }
    return NULL;
}

void* consumer(void* arg)
{
    while(1)
    {
        //消费者的资源减一
        sem_wait(&csem);

        struct node* head_node = head;
        printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
        head = head->next;
        free(head_node);

        // 通知生产者生成, 给生产者的资源加一
        sem_post(&psem);
        sleep(rand()%3);
    }
    return NULL;
}

(2) 总资源数大于 1时

如果生产者和消费者线程使用的信号量对应的总资源数为大于 1,这种场景下出现的情况就比较多了:

  • 多个生产者线程同时生产
  • 多个消费者同时消费
  • 生产者线程和消费者线程同时生产和消费

以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,处理代码如下:

/* 使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h> //信号量的头文件

//生产者线程信号量
sem_t psem;

//消费者线程信号量
sem_t csem;

pthread_mutex_t mutex;  //互斥锁变量

//链表的节点
struct node{
    int date;
    struct node* next;
};

//指向头结点的指针
struct node* head = NULL;

void* producer(void* arg);
void* consumer(void* arg);

int main()
{

     // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 初始化信号量
    // 生产者和消费者拥有的资源的总和为5
    sem_init(&psem, 0, 5);  // 生成者线程一共有5个资源
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个资源

    pthread_t t1[5], t2[5];
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&t1[i], NULL, producer, NULL);
        pthread_create(&t2[i], NULL, consumer, NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(t1[i], NULL);
        pthread_join(t2[i], NULL);
    }

    pthread_mutex_destroy(&mutex);
    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}

void* producer(void* arg)
{
    while(1)
    {
        // 生产者资源减一
        sem_wait(&psem);

        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);

        //创建一个链表的新节点
        struct node* newNode = (struct node*)malloc(sizeof(struct node));
        // 节点初始化
        newNode->date = rand()%100 + 1;
        newNode->next = head;
        head = newNode;
        printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);

        pthread_mutex_unlock(&mutex);

        // 通知消费者消费, 给消费者的资源加一
        sem_post(&csem);
        sleep(rand()%3);
    }
    return NULL;
}

void* consumer(void* arg)
{
    while(1)
    {
        //消费者的资源减一
        sem_wait(&csem);

        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);

        struct node* head_node = head;
        printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
        head = head->next;
        free(head_node);

        pthread_mutex_unlock(&mutex);

        // 通知生产者生成, 给生产者的资源加一
        sem_post(&psem);
        sleep(rand()%3);
    }
    return NULL;
}

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值