多线程编程2--线程的同步和互斥

当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。

如果每个线程内部的变量其他线程都不会访问到,那么就不存在一致性问题;
如果变量是只读的,那么多个线程同时访问它也不存在不一致性问题;
但是,一旦一个变量是可写,当一个线程对它进行修改的时候,其他有可能对它进行读取或者写入操作从而导致数据不一致的问题。此时就需要同步机制来保证。

APUE上给出一个例子:

这里写图片描述

由于递增操作不是原子性的,因此不可避免的会出现上述数据不一致的问题。


1.互斥量

我们可以通过pthread库提供的互斥接口来保护数据,确保在同一时间只有一个线程访问这个数据。互斥量(mutex)本质上是一把锁,当我们访问共享资源对互斥量加锁,访问资源对互斥量解锁。对互斥量加锁后,任何试图向对该互斥量加锁的线程都会阻塞直到该锁被释放。

考虑多个线程访问一个数据,每个线程在访问数据之前都会先访问该互斥量,如果该互斥量已经加锁则阻塞,不然就对该互斥量加锁再去访问实际的数据。当有多线程因为互斥量加锁被阻塞时,一旦锁释放了,这些阻塞的线程都会运行起来,此时,第一个线程再去加锁互斥量访问数据,其他线程只能再次等待。

这里写图片描述

这里写图片描述

下面是一个关于引用计数的实例,其中count记为引用对象的个数,要求是对于count++,count–必须只有一个线程能访问,且只有当count == 0才释放资源。

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

typedef struct Counter
{
    int count;
    pthread_mutex_t mutex;
}Counter;

void Init(Counter* pc);
void Inc(Counter* pc);
void Dec(Counter* pc);

int main()
{

    Counter* pc = (Counter*)malloc(sizeof(Counter));
    if (pc == NULL)
        exit(-1);
    Init(pc);
    //...
    //...

    return 0;
}

void Init(Counter* pc)
{
    pc->count = 1;
    pthread_mutex_init(&pc->mutex, NULL);
}

void Inc(Counter* pc)
{
    pthread_mutex_lock(&pc->mutex);
    pc->count++;
    pthread_mutex_unlock(&pc->mutex);
}

void Dec(Counter* pc)
{
    pthread_mutex_lock(&pc->mutex);
    pc->count--;
    if (pc->count == 0)
    {
        pthread_mutex_unlock(&pc->mutex);
        pthread_mutex_destroy(&pc->mutex);
        free(pc);
    }
    else
    {
        pthread_mutex_unlock(&pc->mutex);
    }
}

关于死锁

如果一个线程试图对同一个互斥量加两次锁则会造成死锁。当然还有其他不明显的方式可能导致死锁,考虑以下的情况,线程A占有第一个互斥量,线程B占有第二个互斥量,线程A试图去占有第二个互斥量而出于阻塞,线程B试图去占有第一个互斥量时也处于阻塞,这样子一来,A在等待B释放锁,B也在等待A释放锁,相互阻塞,结果是谁也释放不了锁,造成死锁。

这里写图片描述

这里写图片描述

死锁一般要满足四个条件:

  1. 互斥
  2. 不可抢占
  3. 占有并等待
  4. 循环等待

上述解除死锁的方法实际上就是破坏这几个其中的一个,比如用trylock函数如果不能获得锁就释放锁,这就是破坏第三个条件,而按照锁的先后次序去得到锁则是破坏了第四个条件。

具体可参见死锁,银行家算法

2.条件变量

条件变量是线程可用的另一种同步机制,条件变量和互斥量一起使用,允许线程以无竞争的方式等待特定条件的发生。

这里写图片描述

这里写图片描述

基于上述函数,我们写一个生产者消费者的例子,生产者在链表表头插入,消费者在链表头删除,保证链表头有数据消费者才删除,不然消费者阻塞。

代码如下:

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

#define TIMES 5


typedef struct node
{
    int data;
    struct Node* next;
}node, *node_p, **node_pp;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;


node_p head = NULL;

node_p allocate_node(int x);

int empty(node_p head);

void init_list(node_pp head);

void push_front(node_p head, int x);

void pop_front(node_p head, int* value);

void free_node(node_p node);

void destroy_list(node_p head);

void show_list(node_p head);


void* thread1(void* args) //push_front
{
    int x = 0;
    srand(time(NULL));

    while (1)
    {
        x = rand() % 10 + 1;
        pthread_mutex_lock(&mutex);
        push_front(head, x);
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&condition);
        printf("push_front : %d\n", x);    
        sleep(1);
    }
}

void* thread2(void* args) //pop_front
{
    int x = 0;
    while (1)
    {
        pthread_mutex_lock(&mutex);
        while (empty(head))
        {
            pthread_cond_wait(&condition, &mutex);
        }
        pop_front(head, &x);
        pthread_mutex_unlock(&mutex);
        if (x != 0)
            printf("pop_front : %d\n", x);    
    }
}


int main()
{
    init_list(&head);

    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);


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

    destroy_list(head);
    return 0;
}




node_p allocate_node(int x)
{
    node_p ret = malloc(sizeof(node));
    if (ret == NULL)
    {
        perror("malloc failure");
        exit(-1);
    }

    ret->data = x;
    ret->next = NULL;
    return ret;
}

int empty(node_p head)
{
    return head->next == NULL;
}

void init_list(node_pp head)
{
    *head = allocate_node(0);
}

void push_front(node_p head, int x)
{
    node_p tmp = allocate_node(x);

    tmp->next = head->next;
    head->next = tmp;
}

void pop_front(node_p head, int* value)
{
    if (empty(head))
    {
        printf("list is empty\n");
        return;
    }
    node_p del = head->next;
    head->next = del->next;

    *value = del->data;
    free_node(del);
}

void free_node(node_p n)
{
    if (n != NULL)
    {
        free(n);
        n = NULL;
    }
}

void show_list(node_p head)
{
    head = head->next;
    while (head != NULL)
    {
        printf("%d  ", head->data);
        head = head->next;
    }
    printf("\n");
}

void destroy_list(node_p head)
{
    int data = 0;
    while (!empty(head))
    {
        pop_front(head, &data);
    }

    free_node(head);
}

3.信号量

mutex变量是二元信号量,非0即1,初始化mutex = 1表示只有一个可用资源,加锁获得该资源,将mutex置为0,表示没有资源;释放锁,将mutex = 1表示又有了一个资源。

信号量则可以表示有多个资源,一般sem_t数据类型来表示,具体会用到以下函数:

这里写图片描述

这里写图片描述

下面我们以一个例子来看下具体这些函数是如何使用的?

我们基于环形队列实现生产者消费者模型:
//该模型需要满足以下条件:
//1.互斥访问,任意时刻只能有一个线程访问临界资源(buffer)
//2.条件同步,buffer满时,生产者必须等待消费者;buffer空时,消费者必须等待生产者

我们设定缓冲区大小为SIZE个,用buf_sem表示缓冲区的剩余容量,data _sem表示缓冲区的元素个数,满足data _sem + buf _sem == SIZE 每次生产者生成一个,则buf减小一个,对应的data增大一个;同理消费者消费一个,buf增大一个,data减小一个。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#include<stdlib.h>
#include<time.h>

#define SIZE 32

int buffer[SIZE];//数组用来模拟环形队列

sem_t buf_sem;
sem_t data_sem;

void* consumer(void* arg)
{
    int index = 0;
    while (1)
    {   
        sem_wait(&data_sem);
        int data = buffer[index];
        printf("consumer consumes data : %d\n", data);        
        sem_post(&buf_sem);
        index++;
        index %= SIZE;
        sleep(1);
    }
}

void* producer(void* arg)
{
    srand(time(NULL));
    int index = 0;
    while (1)
    {
        sem_wait(&buf_sem);
        int data = rand() % 100;
        buffer[index] = data;
        printf("producer produces data : %d\n", data);
        sem_post(&data_sem);
        index++;
        index %= SIZE;
    }
}



int main()
{
    sem_init(&buf_sem, 0, SIZE);//buf_sem初始化为buf的大小
    sem_init(&data_sem , 0, 0);//data_sem初始化为数据的个数,开始为0

    pthread_t c, p;

    //Create threads
    pthread_create(&c, NULL, consumer, NULL);
    pthread_create(&p, NULL, producer, NULL);


    pthread_join(c, NULL);
    pthread_join(p, NULL);

    sem_destroy(&buf_sem);
    sem_destroy(&data_sem);
    return 0;
}

注意到sem_ init (&buf_sem, 0, SIZE); 第二参数我们设为0,这是因为我们实现的线程之间的同步,具体我们参见手册:

int sem_init(sem_t *sem, int pshared, unsigned int value);

The pshared argument indicates whether this semaphore is to be shared between the threads of a process, or between processes.

       If  pshared  has the value 0, then the semaphore is shared between the threads of a process, and should be located at some address that is visible to all threads (e.g., a global variable, or a variable allocated dynamically on the heap).

       If pshared is nonzero, then the semaphore is shared between processes, and should be located in a region of shared memory (see shm_open(3), mmap(2), and  shmget(2)).   (Since  a child created by fork(2) inherits its parent's memory mappings, it can also access the semaphore.)  Any process that can access the shared memory region can operate on the semaphore using sem_post(3), sem_wait(3), and so on.

4.读写锁

读写锁和互斥量有点类似,但是它允许更高的并行性。在很多情况下,有些公共数据其实修改的机会很少,他们读的机会很多,也就是读多少写,我们知道多个线程读取数据时并不会导致数据的不一致性,这时候如果在读时候加锁是会降低我们程序的效率。

因此,针对这种多读少写的情况,我们引入了读写锁。读写锁将共享资源的访问者分为读者和写者,它有以下特点:

  1. 允许多个读者读取数据(读者最多是实际的CPU数)
  2. 只允许一个写者写入数据
  3. 保证任意时刻读者和写者不能共存

读写锁也叫共享-独占锁,共享性是针对读模式的,独占性是针对写模式的。

这里写图片描述

这里写图片描述

这里写图片描述

为了演示如何使用这些函数,我们定义一个全局变量data,保证三个线程读,三个线程写,看看利用读写锁是什么样的结果?

/*************************************************************************
    > File Name: lock.c
    > Author: xuyang
    > Mail: [email protected] 
    > Created Time: 2017-06-14 22:03:05
 ************************************************************************/

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

#define READERS 2
#define WRITERS 2

int data = 0;

pthread_rwlock_t rwlock;


void* do_read(void* arg)
{
    pthread_detach(pthread_self());
    while (1)
    {
        if (pthread_rwlock_rdlock(&rwlock) != 0)
        {
            printf("writer is writing, reader waiting\n");
        }
        else
        {
            printf("reader %d : data read is %d\n", (int)arg, data);
            pthread_rwlock_unlock(&rwlock);
        }
        sleep(1);
    }

}

void* do_write(void* arg)
{
    pthread_detach(pthread_self());
    while (1)
    {
        if (pthread_rwlock_wrlock(&rwlock) != 0)
        {
            printf("reader is reading , writer waiting\n");
        }
        else
        {
            data++;
            printf("writer %d : data write is %d\n", (int)arg, data);
            pthread_rwlock_unlock(&rwlock);
        }
        sleep(5);
    }
}


int main()
{

    pthread_rwlock_init(&rwlock, NULL);

    pthread_t pid;
    int i = 0;
    for (; i < READERS; i++)
    {
        pthread_create(&pid, NULL, do_read, (void*)i);
    }

    int j = 0;
    for (; j < WRITERS; i++)
    {
        pthread_create(&pid, NULL, do_write, (void*)j);
    }
    return 0;
}
发布了96 篇原创文章 · 获赞 119 · 访问量 17万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章