【操作系统】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

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

猜你喜欢

转载自blog.csdn.net/weixin_42839065/article/details/131532784