您现在的位置是:首页 >技术教程 >C多线程、锁、同步、信号量网站首页技术教程

C多线程、锁、同步、信号量

即客。 2023-07-07 08:00:02
简介C多线程、锁、同步、信号量

一 线程函数

1.1 创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

thread 是线程变量地址
attr是线程属性,一般为NULL
start_rount 是函数指针
arg 是函数指针指向函数的参数

1.2 线程退出

void pthread_exit(void *retval);

retval可以把退出值带回去,例子见线程回收

1.3 线程回收

int pthread_join(pthread_t thread, void **retval);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>

struct Test
{
    int num;
    int age;
};

void* func(void* arg)
{
    struct Test *temp = (struct Test*)arg;

    for(int i = 0 ;i < 5; i++)
    {
        printf("in_sonthread i = %d
",i);
    }

    temp->age = 12;
    temp->num = 21;

    pthread_exit(temp);
    return NULL;
}



int main()
{
    pthread_t pid;
    struct Test jxk;
     struct Test* jxktemp = &jxk;

    void* ans;

    pthread_create(&pid,NULL,func,&jxk);
    for(int i = 0; i < 5 ;i++)
    {
        printf("in_main,i = %d
",i);
    }

    pthread_join(pid,&ans);
    struct Test* jxk2 = (struct Test*)ans;
    printf("num is %d,age is %d
",jxk2->num,jxk2->age);


    return 0;
}   

在这里插入图片描述

1.4 线程分离:

某些情况下,程序的主线程有自己的其他业务,如果让主线程负责子线程的资源回收,调用pthrad_join()只要子线程不退出,主线程就会一致阻塞,主线程的任务也不能执行了。

线程库提供了线程分离函数 pthread_detach(),调用这个函数后指定的子线程可以和主线程分离,当子线程退出是,其占用的内核资源就被操作系统的其他进程接管并回收了。线程分离后,在主线程中使用pthread_join() 就会收不到子线程资源了。

int pthread_detach(pthread_t thread);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>

struct Test
{
    int num;
    int age;
};

void* func(void* arg)
{
    struct Test *temp = (struct Test*)arg;

    for(int i = 0 ;i < 5; i++)
    {
        printf("in_sonthread i = %d
",i);
    }

    temp->age = 12;
    temp->num = 21;
    
    printf("子线程:%ld
",pthread_self());
    pthread_exit(temp);
    return NULL;
}



int main()
{
    pthread_t pid;
    struct Test jxk;
    pthread_create(&pid,NULL,func,&jxk);
    printf("主线程:%ld
",pthread_self());

    pthread_detach(pid);
    pthread_exit(NULL);

    return 0;
}   

主线程打印了自己的id后便退出了线程,子线程会继续运行
在这里插入图片描述

1.5 其他线程函数

1.5.1 线程取消

线程取消就是在某些特定情况下,在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:

  1. 在线程A中调用线程取消函数 pthread_cancel,指定杀死线程B,这时候线程B是死不了的
  2. 在线程B中进行一个系统调用(从用户区切换到内核区),否则线程B可以一直运行。
int pthread_cancel(pthread_t thread);

1.5.2 线程ID比较

在Linux中,线程ID本质就是一个无符号长整型,因此可以直接使用比较操作符比较两个线程的ID。但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整型,这种情况下比较两个线程的ID必须要使用线程比较函数。

 int pthread_equal(pthread_t t1, pthread_t t2);

二 线程同步

假设有4个线程ABCD,当前一个线程A对内存中的共享资源进行访问的时候,其他线程BCD都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,BCD中的一个才能访问这块内存,剩下的两个需要继续阻塞等待,以此类推,直到所有的线程都完成对这块内存的操作。

线程内对这块内存的访问方式就称之为线程同步。所谓线程同步不是说多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

例子:两个线程数数字

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

int Number;

void* func1(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        int cur = Number;
        cur++;
        usleep(100);
        Number = cur;
        printf("thread A %ld, number is %d
",pthread_self(),Number);
        
    }
    
    return NULL;
}

void* func2(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        int cur = Number;
        cur++;
        Number = cur;
        usleep(50);
        printf("thread B %ld, number is %d
",pthread_self(),Number);
    }

    return NULL;
}




int main()
{
    pthread_t pidA,pidB;
    pthread_create(&pidA,NULL,func1,NULL);
    pthread_create(&pidB,NULL,func2,NULL);
    printf("main thread %ld
",pthread_self());
    sleep(1);
}   

在这里插入图片描述

2.1 互斥锁

2.1.1定义

pthread_mutex_t mutex;

2.1.2 初始化

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
//attr 是锁属性,一般为NULL

restrict 是一个关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址,其他指针都不行。
// p = mutex, p也不能访问mutex的地址

2.1.3 销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

2.1.4 加锁 、 常试锁、解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//不会阻塞,会返回一个错误号

int pthread_mutex_unlock(pthread_mutex_t *mutex);

2.1.5 互斥锁使用

解决上诉数数问题:

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

int Number;
pthread_mutex_t numMutex;

void* func1(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        pthread_mutex_lock(&numMutex);
        int cur = Number;
        cur++;
        Number = cur;
        printf("thread A %ld, number is %d
",pthread_self(),Number);
        pthread_mutex_unlock(&numMutex);
        usleep(100);
        
    }
    
    return NULL;
}

void* func2(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        pthread_mutex_lock(&numMutex);
        int cur = Number;
        cur++;
        Number = cur;
        printf("thread B %ld, number is %d
",pthread_self(),Number);
        pthread_mutex_unlock(&numMutex);
        usleep(50);
    }

    return NULL;
}




int main()
{
    pthread_t pidA,pidB;
    if(pthread_mutex_init(&numMutex,NULL) != 0)
    {
        fprintf(stderr,"init mutex lock failed;
");
        return 0;
    }
    pthread_create(&pidA,NULL,func1,NULL);
    pthread_create(&pidB,NULL,func2,NULL);
    printf("main thread %ld
",pthread_self());
    
    pthread_join(pidA,NULL);
    pthread_join(pidB,NULL);
    pthread_mutex_destroy(&numMutex);
}   

在这里插入图片描述

2.2 死锁

造成死锁的场景:
多个线程访问共享资源时,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程被阻塞,并且进行的阻塞是无法解开的(能解开就不会被阻塞了);

  • 加锁之后忘记解锁
  • 加锁之后,在解锁之前,程序由其他出口跳出当前函数逻辑(异常、满足条件的return等)
  • 重复加锁,造成死锁

2.3 如何避免死锁

  • 避免多次锁定,多检查
  • 对共享资源访问完毕之后,一定要解锁,或者在加锁的时候先使用trylock
  • 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问资源、但在有些情况下做不到),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。
  • 项目程序可以引入一些专门用于死锁检测的模块。

2.4 读写锁

2.4.1 基本信息(特点和记录信息)

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。

**读写锁是一把锁。**类型是 pthrad_rwlock_t,有了类型就可以创建一把互斥锁了。

pthread_rwlock_t rwlock;

这把锁记录了这些信息:

  • 锁的状态:锁定/打开
  • 锁定的是什么操作:读操作/写操作,使用读写锁锁定读操作,需要先解锁才能去锁定写操作,反之亦然
  • 哪个线程把这把锁锁上了。

读写锁的特点:

  • 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
  • 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
  • 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问两个临界区,访问写锁临界区的线程继续运行,访问读锁的临界区线程阻塞,因为写锁的有点急比读锁高。

2.4.2 类型定义 、 初始化函数 和 销毁函数

//类型
pthread_rwlock_t rwlock;

//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
//rwlock,读写锁地址
//attr 读写锁属性,一般为NULL

//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

2.4.3 读锁 和 尝试读函数

//在程序中进行读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;
//如果读写锁已经锁定了读操作,依然可以加锁成功,因为读锁是共享的;
//如果读写锁已经锁定了写操作,调用这个函数会被阻塞。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;
//如果读写锁已经锁定了读操作,依然可以加锁成功,因为读锁是共享的;
//如果读写锁已经锁定了写操作,调用这个函数加锁失败,但是线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败后的处理动作。

2.4.4 写锁

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//调用者函数,如果读写锁是打开的,那么加锁成功;
//如果已经锁定了读操作 或 写操作,调用这个函数线程会阻塞。

2.4.5 解锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

2.4.6 读写锁的使用

例:8个线程同时操作一个全局变量,三个线程不定时写资源,5个线程不定时读资源。

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

int Number;
pthread_rwlock_t rwLock;

void* readNum(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        pthread_rwlock_rdlock(&rwLock);
        printf("Thread read, id = %ld, number = %d
",pthread_self(),Number);
        pthread_rwlock_unlock(&rwLock);
        usleep(rand() % 5);
    }
    
    return NULL;
}

void* writeNum(void* arg)
{
    for(int i = 0; i < 50; i++)
    {
        pthread_rwlock_wrlock(&rwLock);
        int cur = Number;
        cur++;
        Number = cur;
         printf("Thread write, id = %ld, number = %d
",pthread_self(),Number);
        pthread_rwlock_unlock(&rwLock);
        usleep(5);
    }

    return NULL;
}




int main()
{
    pthread_t pidA[5],pidB[3];
    if(pthread_rwlock_init(&rwLock,NULL) != 0)
    {
        fprintf(stderr,"init mutex lock failed;
");
        return 0;
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_create(&pidA[i],NULL,readNum,NULL);
    }

    for(int i = 0; i < 3; i++)
    {
        pthread_create(&pidB[i],NULL,writeNum,NULL);
    }

    //阻塞,资源回收
    for(int i = 0; i < 5; i++)
    {
        pthread_join(pidA[i],NULL);
    }

    for(int i = 0; i < 3; i++)
    {
        pthread_join(pidB[i],NULL);
    }

    pthread_rwlock_destroy(&rwLock);

    printf("main thread %ld
",pthread_self());

    return 0;
}   

在这里插入图片描述

2.5 条件变量

严格意义上说,条件变量的主要作用不是用来处理线程同步,而是进行线程的阻塞。

2.5.1 数据类型、初始化 和 释放

pthread_cond_t cond;

int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);
//cond 条件变量地址
//attr 条件变量属性,一般为NULL   
        
int pthread_cond_destroy(pthread_cond_t *cond);
       

2.5.2 wait 和 timewait

int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);
// 只会阻塞一定的时间长度,时间过了之后就会继续往下执行

time_t mytim = time(NULL);
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = tume(NULL) + 100; //线程阻塞100s

2.5.3 signal 和 broadcast

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

调用以上两个函数的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timewait 阻塞的线程;
区别在于:

  • pthread_cond_signal 是唤醒最少一个被阻塞的线程(总个数不定);
  • pthread_cond_broadcast是唤醒所有被阻塞的线程。

2.6 信号量

信号量用在多线程多任务同步的,一个线程完成了某一动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一资源,而是流程上的概念,比如:有AB两个线程,B线程要等A线程完成某一个任务后在进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或消费者线程的运行,
类型为 sem_t ,对应头文件是 <semaphore.h>

#include<semaphore.h>
sem_t sem;

2.6.1 类型定义、初始化 和 销毁

//类型定义
sem_t sem;

//初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem:信号量变量地址
// pshared:
//      0:线程同步
//      非0:进程同步
//value: 初始化当前信号量拥有的资源(>=0,如果资源数为0,线程就会被阻塞)

//销毁
int sem_destroy(sem_t *sem);

2.6.2 sem_wai 和 sem_trywait

//调用函数嗲用sem中的资源树就会消耗一个,资源数 -1
int sem_wait(sem_t* sem);

当调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源减为0时,资源被耗尽,因此线程也就阻塞了。

//调用函数嗲用sem中的资源树就会消耗一个,资源数 -1
int sem_trywait(sem_t* sem);

当调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源减为0时,资源被耗尽,线程不会阻塞,直接返回错误,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

2.6.3 sem_timedwait

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

struct timespec {
               time_t tv_sec;      /* Seconds */
               long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
           };

2.6.4 sem_getvalue

int sem_getvalue(sem_t *sem, int *sval);


2.6.5 sem_post

int sem_post(sem_t *sem);

2.6.6 使用

例子1:
一个空位,不会出现问题,但是当生产着刚开始给的value大时会出问题,因为访问了公共资源。

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

struct Node
{
    int number;
    struct Node* next;
};

struct Node* head = NULL;

sem_t semp,semc;

void* consumerFunc(void *)
{
    while(1)
    {
        //查看是否可以消费
        sem_wait(&semc);

        //消费
        struct Node* node = head;
        printf("消费者,id : %ld, number:%d
",pthread_self(),node->number);
        head = head->next;
        free(node);

        //消费结束,提示生产者,继续生产
        sem_post(&semp);

        sleep(rand() % 3);

    }


}

void* producerFuc(void *)
{
    while(1)
    {
        //看是否可以生产
        sem_wait(&semp);

        //生产资源
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->number = rand() % 1000;
        newNode->next = head;
        head = newNode;
        printf("生产者,id: %ld,number: %d
",pthread_self(),newNode->number);

        //提示消费者可以消费了
        sem_post(&semc);
        sleep(rand() % 3);
    }

    return NULL;
}

int main()
{
    pthread_t consumer[5],producer[5];

    //初始化信号量,只有一个空位供给生产消费
    sem_init(&semp,0,1);
    sem_init(&semc,0,0);

    //创建线程
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&consumer[i],NULL,producerFuc,NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_create(&producer[i],NULL,consumerFunc,NULL);
    }

    //阻塞回收
    for(int i = 0; i < 5; i++)
    {
        pthread_join(consumer[i],NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(producer[i],NULL);
    }

    //信号量释放
    sem_destroy(&semp);
    sem_destroy(&semc);

    return 0;
}   

加锁解决问题:

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

struct Node
{
    int number;
    struct Node* next;
};

struct Node* head = NULL;

sem_t semp,semc;
pthread_mutex_t mutex;

void* consumerFunc(void *)
{
    while(1)
    {
        //查看是否可以消费
        sem_wait(&semc);
        pthread_mutex_lock(&mutex);
        //加在 sem_wait下,避免死锁

        //消费
        struct Node* node = head;
        printf("消费者,id : %ld, number:%d
",pthread_self(),node->number);
        head = head->next;
        free(node);

        pthread_mutex_unlock(&mutex);
        //消费结束,提示生产者,继续生产
        sem_post(&semp);

        sleep(rand() % 3);

    }


}

void* producerFuc(void *)
{
    while(1)
    {
        //看是否可以生产
        sem_wait(&semp);
        pthread_mutex_lock(&mutex);
        //生产资源
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->number = rand() % 1000;
        newNode->next = head;
        head = newNode;
        printf("生产者,id: %ld,number: %d
",pthread_self(),newNode->number);

        pthread_mutex_unlock(&mutex);
        //提示消费者可以消费了
        sem_post(&semc);
        sleep(rand() % 3);
    }

    return NULL;
}

int main()
{
    pthread_t consumer[5],producer[5];

    //初始化信号量,只有一个空位供给生产消费
    sem_init(&semp,0,1);
    sem_init(&semc,0,0);

    //初始化mutex
    pthread_mutex_init(&mutex,NULL);

    //创建线程
    for(int i = 0; i < 5; i++)
    {
        pthread_create(&consumer[i],NULL,producerFuc,NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_create(&producer[i],NULL,consumerFunc,NULL);
    }

    //阻塞回收
    for(int i = 0; i < 5; i++)
    {
        pthread_join(consumer[i],NULL);
    }

    for(int i = 0; i < 5; i++)
    {
        pthread_join(producer[i],NULL);
    }

    //信号量释放
    sem_destroy(&semp);
    sem_destroy(&semc);

    pthread_mutex_destroy(&mutex);
    return 0;
}   

加锁是加在了sem_wait下,这样可以避免死锁,不然会出现一种情况:
A线程拿到了sem的资源,但是B线程先锁定阻塞在了mutex位置,就会导致最后生产着无法生产,消费者无法消费,死锁了。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。