欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux多线程互斥与同步

程序员文章站 2022-05-05 10:58:43
...

有些变量被多个线程共享,这样的变量被称为共享变量,在程序中,可以通过共享变量来完成线程间的交互。但是多个线程在用共享变量进行交互时,常常会出现某些问题。来看下面代码:

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

int ticket = 1000;

void *sellticket(void *arg)
{
    char *id = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket: %d\n", id, ticket);
            ticket--;
        }
        else
            break;
    }
    return NULL;
}

int main(void)
{
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, NULL, sellticket, "thread 1");
    pthread_create(&tid2, NULL, sellticket, "thread 2");
    pthread_create(&tid3, NULL, sellticket, "thread 3");
    pthread_create(&tid4, NULL, sellticket, "thread 4");

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

    return 0;
}

这是一个模拟买票系统的代码,主线程创建了四个新线程用于“买票”,因为有全局变量ticket这个共享变量,因此这是一个线程不安全的代码,那么会出什么问题呢?看运行结果:
Linux多线程互斥与同步
很明显,这里出问题了。按照预期,所有的票号都为正整数,但是买到的票号居然有0和负数,显然系统多卖出三张票。那么是出了什么问题呢?我们来看看线程函数:

void *sellticket(void *arg)
{
    char *id = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket: %d\n", id, ticket);
            ticket--;
        }
        else
            break;
    }
    return NULL;
}

在线程函数中,每个线程都会多次访问共享变量ticket。其中临界区:

        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket: %d\n", id, ticket);
            ticket--;
        }
        else
            break;

假设当ticket等于1时,所有线程都没在临界区内,当某一个线程进入临界区,只要这个进程还没有执行ticket–; 操作,其他的线程都有可能进入临界区,这样就发生了错误,即在最后本该只有一个线程能买到票,结果四个线程都进入了临界区,都买到票了。那么这个问题该怎么解决呢?——只要加一把锁就行了。如果能保证同一时刻只有一个线程能访问临界区,即当临界区已经有线程进入时,其他需要访问临界区的线程就只能等待,待临界区内的线程访问结束后等待的线程有一个能进入,这样问题就解决了,这就是线程的互斥。Linux中实现这把锁的机制叫做互斥量。
互斥量接口:
初始化互斥量:

1、静态方法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2、动态方法
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量。
attr:设置属性,若设置默认属性则用NULL。
返回值:成功返回0,失败返回错误编码。

销毁互斥量:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:要销毁的互斥量。
返回值:成功返回0,失败返回错误编码。

销毁互斥量时,要注意的问题:
1、使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁。
2、不要销毁一个已经加锁的互斥量。
3、已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
互斥量加锁和解锁:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:信号量。
返回值:成功返回0,失败返回错误号。

在调用pthread_mutex_lock时,可能会出现的情况:
1、互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
2、发起函数调⽤用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤用会陷⼊入阻塞,等待互斥量解锁。
如果不希望被阻塞,可以用另一个函数:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值:若成功返回0,失败返回错误码。

如果调用pthread_mutex_trylock时互斥量处于未锁状态,就将互斥量锁住,否则失败返回EBUSY。
现在用互斥量来修改前面的代码,其大致结构为:
Linux多线程互斥与同步
代码:

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

int ticket = 1000;
pthread_mutex_t mutex;

void *sellticket(void *arg)
{
    char *id = (char*)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s buy ticket: %d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return NULL;
}

int main(void)
{
    pthread_t tid1, tid2, tid3, tid4;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&tid1, NULL, sellticket, "thread 1");
    pthread_create(&tid2, NULL, sellticket, "thread 2");
    pthread_create(&tid3, NULL, sellticket, "thread 3");
    pthread_create(&tid4, NULL, sellticket, "thread 4");

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    pthread_join(tid4, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

这里仅仅对临界区加了一把锁,来看看运行结果:
Linux多线程互斥与同步
这样就实现了线程的互斥。
但是多线程只有互斥是不够的,比如执行上面的代码可能会出现饥饿问题:
Linux多线程互斥与同步
如果同一个线程对同一个互斥量加锁两次,那么在第二次加锁时就会造成死锁问题,此时访问同一临界区的所有线程都会进入阻塞状态。此时就需要引入同步机制。
条件变量是线程可用的一种同步机制。条件变量给多个线程一个会合的场所条件变量和互斥量一起使用时,允许线程以特定的方式等待特定的条件发生。条件本身是由互斥量保护的,线程在改变条件状态之前首先必须先锁住互斥量,其他线程在获得互斥量前不会觉察到这种改变,因为必须锁定互斥量以后才能计算条件。
条件变量函数:
初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

如果条件变量是静态分配的,可以用常量PTHREAD_COND_INITIALIZER进行初始化。
销毁条件变量:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,对条件进行保护
timeout:指定等待时间,它是通过timespec结构指定。时间值用秒数或者分秒数来表示。
struct  timespec
{
    time_t tv_sec;
    long tv_nsec;
}
返回值:成功返回0,错误返回错误编号。

调用者把锁住的互斥量传给函数,函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。当pthread_cond_wait返回时,互斥量再次被锁住。
唤醒等待:

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,否则返回错误码。

pthread_cond_signal用于唤醒等待此条件的某个线程,pthread_cond_broadcast用于唤醒等待该条件的所有线程。
简单的线程交互代码:

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

pthread_cond_t cond;
pthread_mutex_t mutex;

void *thread_run1(void *arg)
{
    while(1)
    {
        pthread_cond_wait(&cond, &mutex);
        printf("thread1 run!\n");
    }
}
void *thread_run2(void *arg)
{
    while(1)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

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

    pthread_create(&tid1, NULL, thread_run1, NULL);
    pthread_create(&tid2, NULL, thread_run2, NULL);

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

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

线程1只有在线程2发出信号满足条件后才能执行打印操作,否则进入等待状态。运行结果:
Linux多线程互斥与同步
这样就完成了线程的同步。