【操作系统】Linux编程 - 多线程的创建和使用 II (临界区 、互斥量、信号量的使用)

        本文共计4505字,预计阅读时间8分钟

目录

临界区的概念 

模拟多线程任务

线程同步

互斥量

互斥量锁住和解锁

信号量


临界区的概念 

        之前的实例中我们只尝试创建了1个线程来处理任务,接下来让我们来尝试创建多个线程。

        不过,还是得先拓展一个概念——“临界区”

        临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore

        多个线程同时执行时很容易产生问题,这个问题集中在对共用资源的访问上。

        根据函数在执行时是否会导致在临界区发生问题,可将函数的类型分为两类:

  • 线程安全函数 (Thread-safe function)
  • 非线程安全函数 (Thread-unsafe function)

        线程安全函数在被多个线程同时调用时不会引发问题,而非线程安全函数在被多个线程调用时则会发生问题。

拓展:

        无论是Linux还是Windwos,我们都无需去区分线程安全函数和非线程安全函数,因为这在设计非线程安全函数的同时,开发者们也设计了具有相同功能的线程安全函数。

        线程安全函数的名称一般是在函数添加后缀_r ,但在编程中如果我们全以这种方式来书写函数表达式,那么将会变得十分麻烦,为此我们可以通过在声明头文件前定义_REENTRANT宏。

        如果追求更加快捷的代码编写体验,可以在编译键入参数时加入- D_REENTRANT,而不在编写代码时去引用_REENTRANT宏。

模拟多线程任务

        接下来,让我们模拟出一个场景把这个问题体现出来,下列为示例代码:

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

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    return 0;
}

void *thread_inc(void *arg)
{
    for (int i = 0; i < 100000; i++)
        num += 1;
    return NULL;
}

void *thread_des(void *arg)
{
    int i;
    for (int i = 0; i < 100000; i++)
        num -= 1;
    return NULL;
}

运行结果:

        很明显结果并不是实际想要的“0”值 ,并且该输出值是随时变化的。

        那么是什么原因导致这样不符合实际的值的出现呢?

        在这里列举一种情形:

        当线程A发起对变量λ=98的访问时,线程B也发起了访问,那么此时线程A、B都拿到了λ=98的数值。线程对该数值进行 +1 计算后,得出了99,并向该资源变量发起更新请求,但此时线程B也做同样的操作,并且是以之前同样拿到的数值λ=98为基础,那么最终的结果便是A算出了λ=99,B算的也是λ=99,最后更新的数值也是99,而实际应是100。

        总结来讲,造成这类问题的原因在于相同时间内对同一资源的访问、处理出现了“时差”,导致了最终结果与实际偏离。

        明白了原因,这个问题就很好解决了,那就是要把正在同时访问的资源读、写权限做一个限制,将线程同步起来。

线程同步

        对于线程的同步,需要依靠“互斥量”和“信号量”这2种概念定义。

互斥量

        互斥量用以限制多个线程同时访问,主要解决线程同步访问的问题,是一种“锁”的机制。

        而互斥量在pthread.h库中也有专门的函数,用以创建和销毁,让我们来看看他的函数结构:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex , const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);

//成功时返回 0 ,失败时返回其他值。

/* 参数定义
 
    mutex: 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
    attr:  传递即将创建的互斥量属性,没有需要指定的属性时可以传递NULL。

        另外,如果不需要配置特定的互斥量属性,可以通过使用PTHREAD_MUTEX_INITIALIZER宏来进行初始化,示例如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

        不过,最好还是使用pthread_mutex_init函数来初始化,因为宏在调试时很难定位报错点,同时pthread_mutex_init对互斥量属性的设置也更直观可控一点。

互斥量锁住和解锁

        上面所说到的两个函数只用于创建和销毁,最关键的还是上锁解锁这两个操作函数,他们的结构如下:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

// 成功时返回 0 ,失败时返回其他值 。

        进入临界区前需调用的函数是pthread_mutex_lock,若调用该函数时发现有其他线程已进入临界区,那么此时pthread_mutex_lock函数不会返回值,除非直到里面的线程调用pthreaed_mutex_unlock函数退出临界区后。

        一般临界区的结构设计如下:

pthread_mutex_lock(&mutex);
//临界区的开始
//..........
//..........
//临界区的结束
pthread_mutex_unlock(&mutex);

        特别注意,pthread_mutex_unlock()pthread_mutex_lock()一般是成对的关系,如果线程退出临界区后没有对锁进行释放,那么其他等待进入临界区的线程将无法摆脱阻塞态,最终成为“死锁”状态。

        接下来让我们尝试一下用互斥量来解决之前出现的问题吧

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

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;
pthread_mutex_t mutex;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    pthread_mutex_init(&mutex, NULL);

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    pthread_mutex_destroy(&mutex);
    return 0;
}

void *thread_inc(void *arg)
{
    pthread_mutex_lock(&mutex);
    // ↓临界区代码执行块
    for (int i = 0; i < 100000; i++)
        num += 1;
    // ↑临界区代码执行块
    pthread_mutex_unlock(&mutex);
    return NULL;
}
void *thread_des(void *arg)
{
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < 100000; i++)
    {
        // ↓临界区代码执行块
        num -= 1;
        // ↑临界区代码执行块
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

运行结果:

        结果终于正确了~

        需要特别注意的是,大家在设计锁的区域时一定要仔细考虑边界,确认出恰好需要“锁住”的那一个代码执行点和恰好可以结束的“释放”点,这样可以避免频繁调用“锁”和“解锁”操作,进而提高操作系统对于代码的执行效率。

信号量

        信号量与互斥量相似,也是一种实现线程同步的方法。一般信号量用二进制0和1来表示,因此也称这种信号量为“二进制信号量”。

        下面是信号量的创建及销毁方法:

#include <semaphore.h>
int sem_init(sem_t * sem , int pshared, unsigned int value);
int sem_destroy(sem_ t * sem);

//成功时返回0,失败时返回其他值

/* 参数含义
    
    sem: 创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
    pshared: 传递0时,创建只允许1个进程内部使用的信号量。传递其他值时,创建可由多个进程共享的信号量。
    value: 指定新创建的信号量初始值。
*/

        与互斥量一样,有“锁”和“解锁”函数

#include <semaphore.h>
int sem_post(sem_ t * sem);
int sem_wait(sem_t * sem);

//成功时返回0,失败时返回其他值。

/* 参数含义

    sem: 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait信号量减1。 

*/

        调用sem_init函数时,操作系统会创建信号量对象,并初始化好信号量值。在调用sem_post函数时该值+1,调用sem_wait函数时该值-1

        当线程调用sem_wait函数使信号量的值为0时,该线程将进入阻塞状态,若此时有其他线程调用sem_post函数,那么之前阻塞的线程将可以脱离阻塞态并进入到临界区。

        信号量的临界区结构一般如下(假设信号量初始值为1):

sem_wait(&sem); //进入到临界区后信号量为0
// 临界区的开始
// ..........
// ..........
// 临界区的结束
sem_post(&sem); // 信号量变为1

        信号量一般用以解决线程任务中具有强顺序的同步问题

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
生产者-消费者模型是一个经典的并发编程问题,可以使用线程和信号量、PV操作来实现。下面是一个基于C语言的生产者-消费者模型的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 10 int buffer[BUFFER_SIZE]; int buffer_index; pthread_mutex_t buffer_mutex; sem_t full_sem, empty_sem; void insert(int item) { buffer[buffer_index++] = item; } int remove_item() { return buffer[--buffer_index]; } void *producer(void *param) { int item; while (1) { item = rand() % 100; // 产生随机数 sem_wait(&empty_sem); pthread_mutex_lock(&buffer_mutex); insert(item); pthread_mutex_unlock(&buffer_mutex); sem_post(&full_sem); printf("Producer produced %d\n", item); } } void *consumer(void *param) { int item; while (1) { sem_wait(&full_sem); pthread_mutex_lock(&buffer_mutex); item = remove_item(); pthread_mutex_unlock(&buffer_mutex); sem_post(&empty_sem); printf("Consumer consumed %d\n", item); } } int main(int argc, char *argv[]) { pthread_t producer_thread, consumer_thread; pthread_mutex_init(&buffer_mutex, NULL); sem_init(&full_sem, 0, 0); sem_init(&empty_sem, 0, BUFFER_SIZE); pthread_create(&producer_thread, NULL, producer, NULL); pthread_create(&consumer_thread, NULL, consumer, NULL); pthread_join(producer_thread, NULL); pthread_join(consumer_thread, NULL); pthread_mutex_destroy(&buffer_mutex); sem_destroy(&empty_sem); sem_destroy(&full_sem); return 0; } ``` 在上面的代码中,我们定义了一个大小为10的缓冲区(数组),然后使用一个整数`buffer_index`来表示缓冲区中已有的元素个数。我们还定义了一个互斥锁`buffer_mutex`和两个信号量`full_sem`和`empty_sem`,其中`full_sem`表示缓冲区中已有的元素个数,`empty_sem`表示缓冲区还能容纳的元素个数。 生产者线程在每次生产一个元素后,将它插入到缓冲区中,并发送一个`full_sem`信号量,表示缓冲区中已有的元素个数加1。消费者线程在每次消费一个元素后,将它从缓冲区中移除,并发送一个`empty_sem`信号量,表示缓冲区还能容纳的元素个数加1。 需要注意的是,我们在使用互斥锁和信号量时,要注意它们的加锁和解锁的顺序,否则可能会出现死锁等问题。在上面的代码中,我们先使用`sem_wait`函数来等待信号量,然后再使用`pthread_mutex_lock`函数来加锁互斥锁;同样地,在解锁时,也要先使用`pthread_mutex_unlock`函数来解锁互斥锁,然后再使用`sem_post`函数来发送信号量

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干吃咖啡豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值