线程同步互斥

进程或线程同步互斥的控制方法

四种进程或线程同步互斥的控制方法
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
2、互斥量:为协调共同对一个共享资源的单独访问而设计的。
3、信号量:为控制一个具有有限数量用户资源而设计。
4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

临界区(Critical Section)(同一个进程内,实现互斥)

保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

互斥量(Mutex)(可以跨进程,实现互斥)

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。

信号量(Semaphores)(主要是实现同步,可以跨进程)

Semaphores的通俗理解

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。
常用函数

  • int sem_init (sem_t *sem , int pshared, unsigned int value);
    功能:初始化信号量
    sem表示待初始化的信号量
    pshared表示共享属性,Linux中貌似只能设置为0
    value表示信号量的初始值

  • int sem_wait(sem_t *sem);
    功能:申请资源
    信号量的值减一
    sem_wait是阻塞的,wait之后,内部value就会-1;所以如果vaue小于0,那么就有线程正在等待,否则就不等待直接进行下面的工作了。

  • int sem_post(sem_t *sem);
    功能:释放资源
    信号量的值加1
    在主线程或其他地方发送post,让那些工作线程逐个开始工作起来

  • int sem_destroy(sem_t *sem);
    功能:销毁信号量
    此时 sem_wait 就会失败了。所以destroy前 先 sem_post value的绝对值 个信号 就可以了。

事件(Event)(实现同步,可以跨进程)

事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。

ExampleCode

总共有三种类型的下载任务(类型 id 为 1、2、3),每次从键盘读取一种类型的任务进行下载,但是 CPU 最多可以同时执行 2 个下载任务(创建两个线程)。

#include <stdio.h> 
#include <pthread.h> 
#include <semaphore.h> 
#define MAXNUM (2) 
sem_t semDownload; 
pthread_t a_thread, b_thread, c_thread; 
int g_phreadNum = 1; 
 
void func1(void *arg) 
{ 
    // 等待信号量的值 > 0 
    sem_wait(&semDownload); 
    printf("============== Downloading taskType 1 ============== \n"); 
    sleep(5); 
    printf("============== Finished taskType 1 ============== \n"); 
    g_phreadNum--; 
    // 等待线程结束 
    pthread_join(a_thread, NULL); 
} 
 
void func2(void *arg) 
{ 
    sem_wait(&semDownload); 
    printf("============== Downloading taskType 2 ============== \n"); 
    sleep(3); 
    printf("============== Finished taskType 2 ============== \n"); 
    g_phreadNum--; 
    pthread_join(b_thread, NULL); 
} 
 
void func3(void *arg) 
{ 
    sem_wait(&semDownload); 
    printf("============== Downloading taskType 3 ============== \n"); 
    sleep(1); 
    printf("============== Finished taskType 3 ============== \n"); 
    g_phreadNum--; 
    pthread_join(c_thread, NULL); 
} 
 
int main() 
{ 
    // 初始化信号量 
    sem_init(&semDownload, 0, 0); 
    int taskTypeId; 
    while (scanf("%d", &taskTypeId) != EOF) 
    { 
        // 输入 0, 测试程序是否能正常退出 
        if (taskTypeId == 0 && g_phreadNum <= 1) 
        { 
            break; 
        } else if (taskTypeId == 0) 
        { 
            printf("Can not quit, current running thread num is %d\n", g_phreadNum - 1); 
        } 
        printf("your choose Downloading taskType %d\n", taskTypeId); 
        // 线程数超过 2 个则不下载 
        if (g_phreadNum > MAXNUM) 
        { 
            printf("!!! You've reached the max number of threads !!!\n"); 
            continue; 
        } 
        // 用户选择下载 Task 
        switch (taskTypeId) 
        { 
        case 1: 
            // 创建线程 1 
            pthread_create(&a_thread, NULL, func1, NULL); 
            // 信号量 + 1,进而触发 func1 的任务 
            sem_post(&semDownload); 
            // 总线程数 + 1 
            g_phreadNum++; 
            break; 
        case 2: 
            pthread_create(&b_thread, NULL, func2, NULL); 
            sem_post(&semDownload); 
            g_phreadNum++; 
            break; 
        case 3: 
            pthread_create(&c_thread, NULL, func3, NULL); 
            sem_post(&semDownload); 
            g_phreadNum++; 
            break; 
        default: 
            printf("!!! error taskTypeId %d !!!\n", taskTypeId); 
            break; 
        } 
    } 
    // 销毁信号量 
    sem_destroy(&semDownload); 
    return 0; 
} 

一个常见的面试题

编写一个程序,开启3个线程,线程1输出A,线程2输出B,线程3输出C,要求输出结果必须按ABC的顺序显示;如:ABCABC….

典型的线程同步的问题:

线程1进行后线程2才能进行,然后才是线程3,线程3执行后线程1有开始执行。

也就是:

可以看到形成了一个环形,也就可能会因为出现环路等待而形成死锁,解决的办法就是,指定一个进程先执行,而且题目中让我们依次输出ABC,所以我们指定线程1先运行。

代码如下:

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

static sem_t A_B;
static sem_t B_C;
static sem_t C_A;


void *printA(void *arg)
{
    int i = 0;
    for(i = 1;i < 11;i++)
    {
        sem_wait(&C_A);
        printf("第%02d次:A",i);
        sem_post(&A_B);

    }
    return NULL;
}
void  *printB(void *arg)
{
    int i = 0;
    for(i = 1;i < 11;i++)
    {
        sem_wait(&A_B);
        printf("B");
        sem_post(&B_C);

    }
    return NULL;
}
void *printC(void *arg)
{
    int i = 0;
    for(i = 1;i < 11;i++)
    {
        sem_wait(&B_C);
        printf("C\n");
        sem_post(&C_A);

    }
    return NULL;
}
int main()
{
    pthread_t thread_A;
    pthread_t thread_B;
    pthread_t thread_C;
    sem_init(&A_B,0,0);
    sem_init(&B_C,0,0);
    sem_init(&C_A,0,1);
    pthread_create(&thread_A,NULL,printA,NULL);
    pthread_create(&thread_B,NULL,printB,NULL);
    pthread_create(&thread_C,NULL,printC,NULL);
    pthread_join(thread_A,NULL);
    pthread_join(thread_B,NULL);
    pthread_join(thread_C,NULL);
    sem_destroy(&A_B);
    sem_destroy(&B_C);
    sem_destroy(&C_A);
    printf("\n");
    
    return 0;

}

refs

https://www.itdaan.com/blog/2017/09/28/cd170578394f6837ff9d4b2e39f862c3.html
https://www.cnblogs.com/ayanmw/archive/2012/12/10/2811830.html
https://os.51cto.com/art/202011/631454.htm

posted @ 2021-08-18 19:47  summerL  阅读(100)  评论(0)    收藏  举报