线程同步与互斥

临界区

  • 临界区是指必须以互斥的方式执行的代码段,也就是说临界区范围内只能有一个活动的线程。例如:修改共享变量的过程中其他的执行线程可能会访问共享变量,那么修改共享变量的代码就被看成是临界区的一部分。
  • 临界区管理指用安全、公平和对称的方式来执行临界区代码的方法。
  • 临界区管理的基本原则
    ①如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。
    ②任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。
    ③进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
    ④如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。

互斥与同步

  • 互斥
    1.互斥,是指在不同进程/线程之间的若干程序片断,当某个进程/线程运行其中一个程序片段时,其它进程/线程就不能运行它 们之中的某个程序片段,只能等到该进程/线程运行完这个程序片段后才可以运行。
    2.互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 同步
    1.同步,是在不同进程/线程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。
    2.同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问,具有顺序性。
    3.可以说同步是更为复杂的互斥

多线程的同步与互斥

  • 由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的同步与互斥问题。
  • POSIX中两种线程同步与互斥机制,分别为互斥锁和信号量。这两个同步与互斥机制可以互相通过调用对方来实现,也就是相互可以实现对方的功能,但互斥锁更适合用于同时可用的资源是惟一的情况;信号量更适合用于同时可用的资源为多个的情况。

互斥锁线程控制

  • 互斥锁是用一种简单的加锁方法来控制对共享资源的原子操作。
  • 互斥锁只有两种状态:上锁和解锁。
  • 在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。
  • 这把互斥锁保证让每个线程对共享资源按顺序进行原子操作。
  • 互斥锁机制主要包括下面的基本函数。
互斥锁初始化:pthread_mutex_init()
互斥锁上锁:pthread_mutex_lock()
互斥锁判断上锁:pthread_mutex_trylock()
互斥锁解锁:pthread_mutex_unlock()
消除互斥锁:pthread_mutex_destroy()


函数pthread_mutex_trylock是pthread_mutex_lock的非阻塞版本,表示尝试加锁,如果该互斥锁已经锁定,则返回一个不为0的错误值,如果该互斥锁没有锁定,则返回0,表示尝试加锁成功

  • 互斥锁可以分为快速互斥锁、递归互斥锁和检错互斥锁。
    1.快速锁是指调用线程会阻塞直至拥有互斥锁的线程解锁为止。
    2.递归互斥锁能够成功地返回,并且增加调用线程在互斥上加锁的次数。
    3.检错互斥锁则为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息。
    默认属性为快速互斥锁。
  • 这三种锁的区别主要在于其他未占有互斥锁的线程在希望得到互斥锁时是否需要阻塞等待。
  • 互斥锁代码实战
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>//调用linux线程函数必须包含此头文件

#define THREAD_NUMBER       3
#define REPEAT_NUMBER       3
#define DELAY_TIME_LEVELS   3.0

pthread_mutex_t mutex;
void * thrd_func(void *arg)//子线程入口函数
{
    int thrd_num = (int)arg; //主线程对每个线程自定义了一个编号,放在参数arg中
    int delay_time = 0;
    int count = 0;
    pthread_mutex_lock(&mutex);//获取互斥锁
    printf("Thread %d is starting\n", thrd_num);
    
    for (count = 0; count < REPEAT_NUMBER; count++)
    {
        
         //rand()为随机数产生函数,其随机数的最大值为RAND_MAX(系统定义),
        //(int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX))将会得到一个0——9之间的随机数,注意//强制类型转换int不能省!!
        delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX)) + 1;
        sleep(delay_time);//睡眠
        printf("\tThread %d: job %d delay = %d\n", thrd_num, count, delay_time);
    }

    printf("Thread %d finished\n", thrd_num);
    pthread_mutex_unlock(&mutex);//释放锁
    pthread_exit(NULL);//线程退出,这里也可以不要此语句,让线程“自然死亡”!
}

int main(void)//主线程入口
{
    pthread_t thread[THREAD_NUMBER];//声明此数组用来保存子线程号
    int no = 0, res;
    void * thrd_ret;
    
    srand(time(NULL));//用当前时间作为一个随机数种子,否则随机数序列每次都一样!
    pthread_mutex_init(&mutex,NULL);
    for (no = 0; no < THREAD_NUMBER; no++)
    {
         //no的值将传递给线程入口函数
        res = pthread_create(&thread[no], NULL, thrd_func, (void*)no);//创建子线程
       // sleep(1);
        if (res != 0)
        {
            printf("Create thread %d failed\n", no);
            exit(res);
        }
        else
        {
            printf("Create thread %d success\n", no);
        }
    }
    
    printf("Create treads success\n Waiting for threads to finish...\n");
    
    for (no = 0; no < THREAD_NUMBER; no++)
    {
        //按创建顺序逐个等待子线程结束
        res = pthread_join(thread[no], &thrd_ret);
        if (!res)
        {
            printf("Thread %d joined\n", no);
        }
        else
        {
            printf("Thread %d join failed\n", no);
        }
        
        
    }
    
    pthread_mutex_destroy(&mutex);
    
    return 0;        
}

信号量线程控制

POSIX信号量是操作系统中所用到的PV原子操作,它广泛用于进程或线程间的同步与互斥。
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

  • PV原子操作是对非负整数信号量sem的操作
    • P操作
      判断sem是否大于0
      如果是就执行sem=sem-1;访问资源
      否则就阻塞线程,直到sem大于0为止
    • V操作
      判断在该信号量队列中是否有被阻塞的线程
      如果有就唤醒排在第一的阻塞线程,sem=sem+1
      否则就只执行sem=sem+1;


      信号量的互斥

      信号量的同步
  • 信号量操作函数
sem_init()//用于创建一个信号量,并初始化它的值。
sem_wait()//和sem_trywait()都相当于P操作,在信号量大于零时它们都能将信号量的值减一,两者的区别在于若信号量等于零时,sem_wait()将会阻塞进程,而sem_trywait()//则会立即返回。
sem_post()//相当于V操作,它将信号量的值加一同时发出信号来唤醒等待的进程。
sem_getvalue()//用于得到信号量的值。
sem_destroy()//用于删除信号量。 

  • 信号量代码实战
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>//调用linux线程函数必须包含此头文件
#include<semaphore.h>

#define THREAD_NUMBER       3
#define REPEAT_NUMBER       3
#define DELAY_TIME_LEVELS   3.0

sem_t sem[THREAD_NUMBER];
void * thrd_func(void *arg)//子线程入口函数
{
    int thrd_num = (int)arg; //主线程对每个线程自定义了一个编号,放在参数arg中
    int delay_time = 0;
    int count = 0;
    int thrd_num1;//下个可以获取job执行资源的线程
    printf("Thread %d is starting\n", thrd_num);
    
    for (count = 0; count < REPEAT_NUMBER; count++)
    {
        
         //rand()为随机数产生函数,其随机数的最大值为RAND_MAX(系统定义),
        //(int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX))将会得到一个0——9之间的随机数,注意//强制类型转换int不能省!!
        sem_wait(&sem[thrd_num]);//等待job可执行资源
        delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX)) + 1;
        sleep(delay_time);//睡眠
        printf("\tThread %d: job %d delay = %d\n", thrd_num, count, delay_time);
        if(thrd_num==2)
        {
            thrd_num1=0;
            sem_post(&sem[thrd_num1]);//释放下个线程job可执行资源
        }
        else
        {
            thrd_num1=thrd_num+1;
            sem_post(&sem[thrd_num1]);//释放下个线程job可执行资源
        }
            
    }

    printf("Thread %d finished\n", thrd_num);
    pthread_exit(NULL);//线程退出,这里也可以不要此语句,让线程“自然死亡”!
}

int main(void)//主线程入口
{
    pthread_t thread[THREAD_NUMBER];//声明此数组用来保存子线程号
    int no = 0, res;
    void * thrd_ret;
    
    srand(time(NULL));//用当前时间作为一个随机数种子,否则随机数序列每次都一样!
    for (no = 0; no < THREAD_NUMBER; no++)
    {
        sem_init(&sem[no],0,0);//为每个线程创建一个信号量,并初始值为零,即使三个线程初始任务可执行资源都为0
         //no的值将传递给线程入口函数
        res = pthread_create(&thread[no], NULL, thrd_func, (void*)no);//创建子线程
       // sleep(1);
        if (res != 0)
        {
            printf("Create thread %d failed\n", no);
            exit(res);
        }
        else
        {
            printf("Create thread %d success\n", no);
        }
        
    }
    
    printf("Create treads success\n Waiting for threads to finish...\n");
    sem_init(&sem[0],0,1);//使线程0的信号量为1,即有资源,使其先开始执行
    for (no = 0; no < THREAD_NUMBER; no++)
    {
        //按创建顺序逐个等待子线程结束
        res = pthread_join(thread[no], &thrd_ret);
        if (!res)
        {
            printf("Thread %d joined\n", no);
        }
        else
        {
            printf("Thread %d join failed\n", no);
        }   
    }
    //删除信号量
    for (no = 0; no < THREAD_NUMBER; no++)
    {
      sem_destroy(&sem[no]);    
    }
    
    return 0;        
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 多线程三个特征:原子性、可见性以及有序性. 同步锁 /并发锁/ 读写锁,显示锁, ReentrantLock与Co...
    架构师springboot阅读 1,970评论 0 5
  • 1、多线程并行和并发的区别 并行:多个处理器或者多核处理器同时执行多个不同的任务。 并发:一个处理器处理多个任务。...
    北山学者阅读 5,384评论 0 5
  • Linux--线程编程 多线程编程-互斥锁 线程同步与互斥 互斥锁 信号量 条件变量 互斥锁 互斥锁的基本使用...
    吃苹果的猫C阅读 649评论 1 6
  • 01概述 线程同步,是协调步调,按预定先后次序执行,解决与时间相关的错误。 线程不同步,产生的现象就是数据混乱: ...
    程序员姜戈阅读 124评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,603评论 28 53