Linux系统编程——多任务的同步与互斥

同步与互斥

  现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务

这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

  互斥:是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。


  同步:是指散布在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

举例:内存中有100字节, 线程 T1 欲填入全 1,线程 T2 欲全填入 0.但如果 T1 在执行了 50各字节后失去了 CPU, 此时 T2执行,会将 T1 写过的内容覆盖。当 T1 再次获得 CPU 时,继续从失去 CPU 的位置开始向后写入 1,当执行结束后,内存中的数据就是混乱的。

  产生的这种现象叫做:与时间有关的错误,为了避免这种数据混乱,线程需要同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能

  同步的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步,例如 信号

数据混乱

数据混乱的原因有:

  • 资源共享(独享资源不会混乱)
  • 调度随机(对数据的访问出现竞争,随机执行)
  • 线程间缺乏必要的同步机制

  以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。

  所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

互斥锁

为什么需要互斥锁?

  在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,> 公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,> > 如果不做任何处理的话,打印出来的东西肯定是错乱的。

这里一份小 demo 可以作为测试,模拟一下上述情况

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

void *tun(void *arg){
    while(1){
        printf("hello ");
        sleep(rand() % 3);  //模拟长时间操作共享数据,导致 CPU 易主,即打印机同时打印了其他东西
        printf("world\n");
        sleep(rand() % 3); 
    }   

    pthread_exit(0);
}

int main(){
    pthread_t tid;
    srand(time(NULL));

    pthread_create(&tid, NULL, tun, NULL);

    while(1){
        printf("HELLO ");
        sleep(rand()%3);
        printf("WORLD\n");
        sleep(rand()%3);
    }   

    pthread_join(tid, NULL);
    return 0;
}

  实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。

  Linux 中提供了一把互斥锁 mutex(也称为互斥量)。每个线程在对共享资源操作前都尝试先加锁,成功加锁后才能操作,操作结束后解锁,以便其他线程加锁。在加锁的过程前,资源还是共享的,线程还是竞争的,但是通过 就将资源的访问变成互斥操作,而后与时间有关的错误也不会产生了。

  注意:同一时刻,只能由一个线程持有该锁
当 A 线程对某个全局变量进行加锁访问,B 在访问前尝试加锁,拿不到锁,B 线程阻塞, C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

  所以,互斥锁实质上是操作系统提供的一把 “建议锁”(又称作“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,没有强制限定。

  因此,即使有了互斥锁,如果线程不按规则来访问数据,依然会造成数据混乱

相关操作

互斥锁的基本操作流程如下:

  1. 在访问共享资源后临界区域前,对互斥锁进行加锁
  2. 在访问完成后释放互斥锁导上的锁
  3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放

互斥锁的数据类型为:pthread_mutex_t,本质上是一个结构体,为了简化理解,应用时可以忽略其实现细节,简单当成整数看待,取值只有 1 和 0 两种

image.png

头文件: #include <pthread.h>
返回值:

  • 成功:返回 0
  • 失败:返回 errno

初始化互斥锁

函数原型: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

作用: 初始化一个互斥锁。

参数:

  • mutex:互斥锁地址
  • attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL

可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:
pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。

此外,成功申请的锁是默认打开的。

加锁

函数原型: int pthread_mutex_lock(pthread_mutex_t *mutex);

作用: 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁

参数:

  • mutex:互斥锁地址

int pthread_mutex_trylock(pthread_mutex_t *mutex); 尝试加锁,调用该函数时,若互斥锁未加锁,则上锁,返回 0;若互斥锁已加锁,则函数直接返回失败,即 EBUSY,不会阻塞。

解锁

函数原型: int pthread_mutex_unlock(pthread_mutex_t * mutex);

作用: 对指定的互斥锁解锁

参数:

  • mutex:互斥锁地址

销毁互斥锁

函数原型: int pthread_mutex_destroy(pthread_mutex_t *mutex);

作用: 销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源

参数:

  • mutex:互斥锁地址

通过上述函数,完成,上述错乱的 demo

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

pthread_mutex_t mutex; //创建锁

void *tun(void *arg){
    while(1){
        pthread_mutex_lock(&mutex); //加锁
        printf("hello ");
        sleep(rand() % 3); 
        printf("world\n");
        pthread_mutex_unlock(&mutex); //在对共享数据操作结束后立即解锁,保证锁的粒度尽可能的小
        sleep(rand() % 3); 
    }   

    pthread_exit(0);
}

int main(){
    pthread_t tid;
    srand(time(NULL));
    
    pthread_mutex_init(&mutex, NULL); //初始化锁
    pthread_create(&tid, NULL, tun, NULL);

    while(1){
        pthread_mutex_lock(&mutex);
        printf("HELLO ");
        sleep(rand()%3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex);
        sleep(rand()%3);
    }   

    pthread_join(tid, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

此时代码已经可以按照期望的逻辑进行打印。

结论:在访问共享数据前加锁,访问结束后立即解锁,锁的 “粒度” 越小越好

死锁

是使用锁不恰当所产生的现象,通常会出现以下两种情况:

  1. 线程试图对同一个互斥锁 A 加锁两次
  2. 线程 T1 拥有 A 锁,请求获得 B 锁,线程 T2 拥有 B 锁请求获得 A 锁
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

pthread_mutex_t mutex;

int main(){
    pthread_t tid;
    srand(time(NULL));
    
    pthread_mutex_init(&mutex, NULL);

    pthread_mutex_lock(&mutex);  //第一次加锁
    printf("------------First lock\n ");
    
    pthread_mutex_lock(&mutex); //重复加锁,会阻塞在这里
    printf("------------Second lock\n ");
    pthread_mutex_unlock(&mutex);

    pthread_mutex_destroy(&mutex);

    return 0;
}
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

pthread_mutex_t mutexA, mutexB; //创建两把锁
int var1 = 0, var2 = 0;

void *tunA(void *arg){
        pthread_mutex_lock(&mutexA); //A 线程获得 A 锁
        printf("A thread get mutexA\n");
        int t = var1;
        sleep(5); //挂起,让 B 线程 获得 B 锁
        printf("Thread A want get mutexB\n");
        pthread_mutex_lock(&mutexB); // A 线程获取 B 锁, 阻塞

        printf("Thread A id = %lu, var = %d, ++var = %d", pthread_self(), t, ++var1);
        pthread_mutex_unlock(&mutexA); //解锁
        usleep(10);

    pthread_exit(0);
}

void *tunB(void *arg){

        pthread_mutex_lock(&mutexB); // B 线程获得 B 锁
        printf("B thread get mutexB\n");
        int t = var2;
        sleep(5);  //挂起, 让 A 线程 获得 A 锁
        printf("Thread B want get mutexA\n");
        pthread_mutex_lock(&mutexA); // B 线程获取 A 锁, 阻塞

        printf("Thread B id = %lu, var = %d, ++var = %d", pthread_self(), t, ++var2);
        pthread_mutex_unlock(&mutexB);
        usleep(10);
    pthread_exit(0);
}

int main(){
    pthread_t tid, cid;
    srand(time(NULL));
    
    pthread_mutex_init(&mutexA, NULL);
    pthread_mutex_init(&mutexB, NULL);

    pthread_create(&tid, NULL, tunA, NULL);
    pthread_create(&cid, NULL, tunB, NULL);


    pthread_join(tid, NULL);
    pthread_join(cid, NULL);
    pthread_mutex_destroy(&mutexA);
    pthread_mutex_destroy(&mutexB);

    return 0;
}

补充

  1. 本文代母为了突出代码逻辑均未进行返回值检查,在之际编写中均应添加