首页 > 系统相关 >【嵌入式Linux】<总览> 多线程(更新中)

【嵌入式Linux】<总览> 多线程(更新中)

时间:2024-07-01 21:28:18浏览次数:22  
标签:多线程 int Linux NULL 线程 pthread mutex sem 总览

文章目录

前言

一、多线程

1. 概述

2. 创建线程

3. 线程退出

4. 线程回收

5. 线程分离

6. 线程取消

7. 线程的ID比较

二、线程同步

1. 概述

2. 互斥锁

3. 死锁

4. 读写锁

5. 条件变量

6. 信号量

三、线程池


前言

记录学习多线程的知识重点与难点,若涉及版权问题请联系本人删除!


一、多线程

1. 概述

  • 线程是轻量级的进程,一个进程可以涵盖多个线程。
  • 线程是操作系统调度的最小单位,进程是资源分配的最小单位。
  • 多个线程有不同的“地位”:进程的虚拟地址空间的生命周期默认和主线程一样,与创建的子线程无关。当主线程执行完毕,虚拟地址空间就会被释放。
  • 每个线程都有唯一的线程ID(无符号的长整型数),类型为pthread_t。调用pthread_self函数就能得到当前线程的ID。
#include <pthread.h>

pthread_t pthread_self(void);

//使用pthread库,链接时需要加上选项-lpthread来指定动态库

进程与线程的区别:

  • 进程有独立的地址空间,线程共用同一个地址空间。每个线程都有自己的栈区和寄存器,多个线程共享代码段、堆区、全局数据区和文件描述符表。
  • 线程的上下文切换比进程快得多。

上下文切换:进程/线程分时复用CPU时间片,在切换前会保存当前状态,下次执行该任务时加载保存的状态。

控制多个线程个数:

  • 针对文件IO:线程个数=2*CPU核心数。
  • 针对复杂算法:线程个数=CPU核心数。

2. 创建线程

调用pthread_create函数:

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

【3】参数说明:

  • thread:传出的参数,保存创建的线程ID。
  • attr:线程的属性,一般为NULL表示默认的属性。
  • start_routine:函数指针,线程的执行函数。
  • arg:作为实参传递到start_routine指向的函数内部。

【4】返回值:成功返回0,失败返回错误码。

程序实例:创建子线程,并在主线程内打印子线程和主线程的tid。同时,在子线程执行函数中打印信息。

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

void* doing(void *arg)
{
    printf("我是子线程, ID: %ld\n", pthread_self());
    return NULL;
}

int main(int argc, char ** argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (!pthread_create(&tid, NULL, doing, NULL)) {
        printf("子线程创建成功,其ID为:%ld\n", tid);
    }

    /* 主线程才会执行 */
    printf("主线程的ID:%ld\n", pthread_self());
    while (1) {
        sleep(1);
    }
    return 0;
}

3. 线程退出

调用pthread_exit函数,调用后线程就退出了。只有当所有线程都退出了,虚拟地址空间才会释放。

【1】头文件:#include <pthread.h>

【2】函数原型:void pthread_exit(void *retval);

【3】参数说明:retval表示子线程退出后返回主线程的数据。不需要时使用NULL。

程序实例:创建子线程,在子线程的执行函数中睡眠3秒后打印信息,主进程创建完子线程后调用pthread_exit函数退出。

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

/* 子线程执行函数 */
void* doing(void *arg)
{
        sleep(3);//延迟三秒,确定主线程是否会退出
        for (int i = 0; i < 9; ++i) {
                printf("我是子线程!\n");
                if (i == 3) {
                        pthread_exit(NULL);
                }
        }
        return NULL;
}

int main(int argc, char **argv)
{
        /* 创建子线程 */
        pthread_t tid;
        if (!pthread_create(&tid, NULL, doing, NULL)) {
                printf("子线程创建成功,ID: %ld\n", tid);
        }

        /* 主线程:调用pthread_exit不会释放虚拟地址空间 */
        pthread_exit(NULL);
        return 0;
}

4. 线程回收

主线程调用pthread_join函数来阻塞式回收子线程。若子线程还在运行,那么该函数就会阻塞。该函数只能回收一个子线程,若想回收多个可以考虑采用循环。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_join(pthread_t thread, void **retval);

【3】参数说明:

  • thread:要回收的子线程ID。
  • retval:接收子线程通过pthread_exit传出的数据。

【4】返回值:回收成功返回0,失败返回错误号。

【5】注意:

  • 若子线程返回的数据位于子线程的栈区中,那么当子线程退出后其栈区就会被释放,主线程获取的数据就是无效的。
  • 由于多个线程共用堆区和全局数据区,可以将子线程的数据保存于全局变量中。
  • 由于主线程要回收子线程,一般都是最后退出。因此,可以将主线程的栈区变量传入子线程,在子线程中进行修改。

程序实例:借助主线程的栈区变量,将其传入子线程中,子线程将数据保存至该变量中,最后通过pthread_exit函数返回主线程,主线程调用pthread_join函数回收子线程并获取子线程返回的数据。

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

void* doing(void *arg)
{
    int *i = (int *)arg;
    *i = 666666;
    printf("子线程将参数修改为: %d\n", *i);
    pthread_exit(i);
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    int variable = 0;//主线程栈区变量
    pthread_create(&tid, NULL, doing, &variable);

    /* 主线程获取子线程退出时的数据 */
    void *ret = NULL;
    pthread_join(tid, &ret);//回收子线程
    printf("主线程获取子线程数据: %d\n", *(int*)ret);
    return 0;
}

5. 线程分离

如果总是让主线程来回收子线程,那么可能会出现子线程一直运行,而主线程阻塞在pthread_join函数,无法执行其他任务。因此,可以调用pthread_detach函数将子线程分离。当子线程退出时,其内核资源就会被系统其他进程接管并回收。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_detach(pthread_t thread);

【3】参数说明:thread是要分离的线程ID。

【4】返回值:成功返回0,失败返回错误号。

程序实例:主线程与子线程分离,主线程执行完任务后就退出。

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

void* doing(void *arg)
{
    for (int i = 0; i < 9; ++i) {
        printf("Hello, Can! %d\n", i);
        if (i == 4) {
            pthread_exit(NULL);
        }
    }
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程  */
    pthread_t tid;
    int retCreate = pthread_create(&tid, NULL, doing, NULL);
    if (retCreate != 0) {
        perror("pthread_create error!");
        return -1;
    }

    /* 主线程与子线程分离 */
    int retDetach = pthread_detach(tid);
    if (retDetach != 0) {
        perror("pthread_detach error!");
        pthread_join(tid, NULL);
    }

    /* 主线程执行其他任务 */
    for (int i = 0; i < 5; i++) {
        printf("我是主线程, %d\n", i);
    }
    pthread_exit(NULL);

    return 0;
}

6. 线程取消

线程取消就是一个线程杀死另一个线程。线程A杀死线程B需要两个条件:①线程A调用pthread_cancel函数;②线程B进行一次系统调用,从用户态切换回内核态。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_cancel(pthread_t thread);

【3】参数说明:thread是要杀死的线程ID。

【4】返回值:成功返回0,失败返回错误号。

程序实例:主线程杀死子线程。当子线程执行pthread_self函数时就会被杀死,因为该函数间接调用了系统调用函数。

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

void* doing(void *arg)
{
    printf("子线程: 我要调用pthread_self了!\n");
    printf("子线程: %ld\n", pthread_self());
    sleep(2);//确保杀死子线程时间足够
    printf("子线程: 我还活着吗?\n");

    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (pthread_create(&tid, NULL, doing, NULL) != 0) {
        perror("pthread_create error!");
        return -1;
    }

    /* 主线程杀死子线程 */
    if (pthread_cancel(tid) != 0) {
        perror("pthread_cancel error!");
    }

    /* 主线程退出  */
    pthread_exit(NULL);
    return 0;
}

7. 线程的ID比较

调用pthread_equal函数来比较两个线程的ID是否相等。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_equal(pthread_t t1, pthread_t t2);

【3】参数说明:t1和t2就是两个线程的ID。

【4】返回值:相同返回非0值,不同返回0.

程序实例:主线程创建子线程,然后判断主线程和子线程的ID是否相同。

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

void* doing(void *arg)
{
    printf("我是子线程\n");
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (pthread_create(&tid, NULL, doing, NULL) != 0) {
        perror("create error!");
        return -1;
    }

    /* 比较线程ID是否相等*/
    pthread_t tidMain = pthread_self();
    if (pthread_equal(tidMain, tid) > 0) {
        printf("主线程ID: %ld, 子线程ID: %ld, 二者相等\n", tidMain, tid);
    } else {
        printf("主线程ID: %ld, 子线程ID: %ld, 二者不相等\n", tidMain, tid);
    }

    return 0;
}


二、线程同步

1. 概述

  • 当有多个线程访问同一个共享资源(临界资源)时,且不允许同时访问,那么就需要线程同步。
  • 常见的线程同步方式:互斥锁、读写锁、条件变量、信号量。

2. 互斥锁

互斥锁的方式可以简单概括为:锁定操作临界资源的代码片段,锁定后每次只能由一个线程来进行操作。这样能够解决多个线程同时访问临界资源造成的数据混乱,但是降低了执行效率(因为并行操作变成了串行操作)。

【1】互斥锁类型:pthread_mutex_t。创建一个该类型的变量就得到一把互斥锁。该变量中保存了锁的状态(打开还是锁定),若为锁定则保存了加锁的线程ID。锁定时其他线程将会阻塞,直到这个互斥锁被打开。

以下函数的返回值:成功返回0,失败返回错误号。

【2】初始化互斥锁:

//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr);
//mutex: 互斥锁地址
//attr: 互斥锁属性,一般为NULL默认属性

【3】释放互斥锁资源:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

【4】加锁(阻塞):

int pthread_mutex_lock(pthread_mutex_t *mutex);
//若锁是打开的,那么加锁成功,锁会记录线程ID。
//若锁是锁定的,那么加锁失败,线程阻塞在此,直到上一个线程解锁。

【5】加锁(非阻塞):

int pthread_mutex_trylock(pthread_mutex_t *mutex);
//若加锁失败,则不会阻塞,而是直接返回错误号。

【6】解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
//解锁还需加锁人,哪个线程加的锁就得哪个线程来解锁。

程序实例:创建两个子线程对全局变量number进行+1操作。若不使用互斥锁,就会造成数据混乱。使用了互斥锁,运行正常。

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

pthread_mutex_t mutex;  //全局的互斥锁
int number = 0;         //全局变量

//线程t1执行函数
void* funA(void *arg)
{
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("线程1的ID: %ld, number: %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

//线程t2执行函数
void* funB(void *arg)
{
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        printf("线程2的ID: %ld, number: %d\n", pthread_self(), number);
        usleep(5);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

//主函数
int main(int argc, char **argv)
{
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建两个子线程 */
    pthread_t t1, t2;
    pthread_create(&t1, NULL, funA, NULL);
    pthread_create(&t2, NULL, funB, NULL);

    /* 阻塞回收两个子线程*/
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    return 0;
}

3. 死锁

  • 互斥锁若使用不当,就会造成死锁。一旦多线程造成死锁,就会使得所有线程处于阻塞状态,且无法解除。
  • 常见的死锁场景:①加锁后忘记解锁;②重复加锁;③存在多个共享资源,随意加锁。
  • 避免死锁:①多检查代码;②对共享资源访问完毕后解锁,或者在加锁时使用trylock非阻塞;③引入一些专门用于死锁检测的模块;④如果程序中有多把锁, 可以控制对锁的访问顺序。另外,在加其它锁之前先释放拥有的互斥锁。

4. 读写锁

读写锁可以视为互斥锁的升级版,可以指定锁定的是读操作还是写操作,且同一时间内只能锁定其中一个操作。读写锁的使用方式与互斥锁相同。

【1】读写锁类型:pthread_rwlock_t。创建一个该类型的变量就得到一把读写锁。读写锁中保存了以下信息:①锁的状态(打开还是锁定);②锁的是哪个操作(读/写);③哪个线程锁定了这把锁。

【2】读写锁特点:

  • 使用读写锁的读锁锁定了临界区,那么读操作是并行的。
  • 使用读写锁的写锁锁定了临界区,那么写操作是串行的。
  • 使用读写锁的读锁和写锁分别锁定了两个临界区,那么访问写锁的线程优先进入。因为写锁的优先级高于读锁。

以下函数的返回值:成功返回0,失败返回错误号。

【3】初始化读写锁:

//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
//rwlock: 读写锁地址
//attr: 读写锁属性,一般为NULL默认属性

【3】释放读写锁资源:

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

【4】锁定读操作(阻塞):

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会阻塞。

【5】锁定读操作(非阻塞):

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会返回错误号,而不会阻塞。

【6】锁定写操作(阻塞):

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么就会阻塞。

【7】锁定写操作(非阻塞):

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么返回错误号,而不会阻塞。

【8】解锁:

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//不管锁定的是读锁还是写锁,都能解锁。

程序实例:创建3个子线程进行写操作,创建5个子线程进行读操作。它们都针对一个全局变量。


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

int number = 0;             //全局变量(临界资源)
pthread_rwlock_t rwlocker;  //全局的读写锁

/* 写线程执行函数 */
void* writeNum(void *arg)
{
    while (1) {
        pthread_rwlock_wrlock(&rwlocker);
        int cur = number;
        cur++;
        number = cur;
        printf("写操作完毕!number = %d, tid: %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlocker);
        usleep(rand() % 100);//让子线程交替写
    }
    return NULL;
}

/* 读线程执行函数 */
void* readNum(void *arg)
{
    while (1) {
        pthread_rwlock_rdlock(&rwlocker);
        printf("读操作完毕!number = %d, tid: %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlocker);
        usleep(rand() % 100);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化读写锁
    pthread_rwlock_init(&rwlocker, NULL);

    //创建8个线程,3个为写线程,5个为读线程
    pthread_t wtid[3];
    pthread_t rtid[5];
    for (int i = 0; i < 3; i++) {
        pthread_create(&wtid[i], NULL, writeNum, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&rtid[i], NULL, readNum, NULL);
    }

    //主线程回收8个线程
    for (int i = 0; i < 3; i++) {
        pthread_join(wtid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(rtid[i], NULL);
    }

    //释放读写锁
    pthread_rwlock_destroy(&rwlocker);
    return 0;
}

5. 条件变量

条件变量的作用是进行线程的阻塞,而不是线程同步。当满足某个特定条件时才会阻塞线程。一般用于生产者-消费者模型,且和互斥锁相互配合。

【1】条件变量类型:pthread_cond_t。被条件变量阻塞的线程的信息会被记录到该类型的变量中,以便在解除阻塞时使用。

【2】初始化条件变量:

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

【3】释放条件变量资源:

int pthread_cond_destroy(pthread_cond_t *cond);

【4】阻塞线程:

// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

//mutex是互斥锁,用于线程同步。
//当阻塞线程时,若线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁
//当线程解除阻塞时,函数内部会帮助这个线程再次将这个mutex锁上,继续向下访问临界区

【5】阻塞线程(时间到解除):

// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

// 以下结构体表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};

//使用示例
time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

【6】解除阻塞(至少一个线程):

int pthread_cond_signal(pthread_cond_t *cond);

【7】解除阻塞(全部线程):

int pthread_cond_broadcast(pthread_cond_t *cond);

生产者-消费者模型:

①若干个生产者线程:生产商品放入任务队列中,若任务队列满则阻塞,可以使用一个生产者条件变量来控制是否阻塞。

②若干个消费者线程:消费者从任务队列中拿走商品,若任务队列空则阻塞,可以使用一个消费者条件变量来控制是否阻塞。

③任务队列:可以是数组、链表、stl容器等等。

程序实例:使用条件变量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。

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

/* 单链表定义 */
typedef struct LNode {
    int number;
    struct LNode *next;
} LNode;

/* 全局变量及锁 */
pthread_mutex_t mutex;  //互斥锁
pthread_cond_t cond;    //条件变量
LNode *head = NULL;     //临界资源,单链表

/* 生产者执行函数 */
void* produce(void *arg)
{
    while (1) {
        //生产商品,往链表头部添加节点
        pthread_mutex_lock(&mutex);
        LNode *tmp = (LNode*)malloc(sizeof(LNode));//创建新节点
        tmp->number = rand() % 100;
        tmp->next = head;
        head = tmp;
        printf("生产完毕! 新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        pthread_mutex_unlock(&mutex);
        //通知消费者拿走商品
        pthread_cond_broadcast(&cond);
        sleep(rand() % 3);//生产慢一点
    }
    return NULL;
}

/* 消费者执行函数 */
void* consume(void *arg)
{
    while (1) {
        pthread_mutex_lock(&mutex);
        //无商品则等待
        //这里不能用if,可能出现段错误,如下场景:
        //假设消费者线程1进入后阻塞,然后切换到生产者线程,解除其阻塞
        //然后切换到消费者线程2拿走了商品,此时链表又是空。
        //接着,切换回消费者线程1,由于if之前已经判定过了,这里直接进行后续操作,
        //从而出现段错误。因此通过while循环判断,当阻塞解除后也会再次判断。
        while (head == NULL) {
            pthread_cond_wait(&cond, &mutex);
        }
        //拿走商品,删除链表头部节点
        LNode *tmp = head;
        printf("消费完毕! 节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        head = head->next;
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    //创建5个生产者线程和5个消费者线程
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; i++) {
        pthread_create(&ptid[i], NULL, produce, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&ctid[i], NULL, consume, NULL);
    }

    //主线程回收10个线程
    for (int i = 0; i < 5; i++) {
        pthread_join(ptid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(ctid[i], NULL);
    }

    //释放互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

6. 信号量

信号量(信号灯)用在多线程的多任务同步中,一个线程完成了某个任务就通过信号量告诉其它线程,其它线程再进行相关操作。信号量也是用于阻塞线程,要保证线程安全,需要信号量与互斥锁一起使用。

【1】信号量类型:sem_t。需要添加头文件<semaphore.h>.

【2】初始化信号量:

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

【3】释放信号量资源:

int sem_destroy(sem_t *sem);
//sem: 信号量地址

【4】消耗资源函数(阻塞):

//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会阻塞
int sem_wait(sem_t *sem);

【5】消耗资源函数(非阻塞):

//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会返回错误号,而不会阻塞
int sem_trywait(sem_t *sem);

【6】时间到解除阻塞:

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//sem: 信号量地址
//abs_timeout: 与pthread_cond_timedwait中的参数一样

【7】增加资源函数:

//给sem中的资源数+1
//若资源数从0加到1,那么阻塞的线程就会解除阻塞
int sem_post(sem_t *sem);

【8】查看资源数:

//查看信号量sem中的整形数值, 这个值会被写到sval指针对应的内存中
int sem_getvalue(sem_t *sem, int *sval);

程序实例:使用信号量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。同时,限定了平台容纳商品的最大容量为6个。我们可以设置两个信号量,分别来代表生产者线程生产的商品数量、平台中现有的商品数量(用于给消费者线程拿走)。

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

/* 单链表节点声明 */
typedef struct LNode{
    int number;
    struct LNode *next;
} LNode;

/* 全局变量 */
sem_t psem; //生产者生产商品的信号量
sem_t csem; //消费者拿走时的信号量
pthread_mutex_t mutex;  //互斥锁
LNode *head = NULL;     //单链表(临界区资源)

/* 生产者线程执行函数 */
void* produce(void *arg)
{
    while (1) {
        //生产者信号量-1
        sem_wait(&psem);
        //生产者生产商品
        pthread_mutex_lock(&mutex);
        LNode *tmp = (LNode*)malloc(sizeof(LNode));
        tmp->number = rand() % 100;
        tmp->next = head;
        head = tmp;
        printf("生产完毕!新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        pthread_mutex_unlock(&mutex);
        //消费者信号量+1
        sem_post(&csem);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 消费者线程执行函数 */
void* consume(void *arg)
{
    while (1) {
        //消费者信号量-1
        sem_wait(&csem);
        //消费者拿走商品
        pthread_mutex_lock(&mutex);
        LNode *tmp = head;
        head = head->next;
        printf("消费完毕!节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        //生产者信号量+1
        sem_post(&psem);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化信号量和互斥锁
    sem_init(&psem, 0, 6);//平台最多容纳的商品数
    sem_init(&csem, 0, 0);//平台最初没有商品
    pthread_mutex_init(&mutex, NULL);

    //创建5个生产者线程,5个消费者线程
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; i++) {
        pthread_create(&ptid[i], NULL, produce, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&ctid[i], NULL, consume, NULL);
    }

    //释放线程资源
    for (int i = 0; i < 5; i++) {
        pthread_join(ptid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(ctid[i], NULL);
    }

    //释放其它资源
    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);
    return 0;
}

三、线程池

标签:多线程,int,Linux,NULL,线程,pthread,mutex,sem,总览
From: https://blog.csdn.net/weixin_46249470/article/details/140076830

相关文章

  • linux高级编程(线程)(1)
    虚拟地址:线程:        概念:线程是轻量级进程,一般是一个进程中的多个任务。        进程是系统中最小的资源分配单位。(竞争计算机资源的最小单位)(进程能分配硬件资源,线程不行)线程是系统中最小的执行单位。   特征:   1、共享资源(除了栈区都共享)-->......
  • Linux基本技巧
    linux查看cpu占用率的方法查看单核CPU占用率,终端上输入“top”。查看多核CPU占用率,终端上输入“top”,进入界面再输入“1”。Linuxtop命令里面%CPU和cpu(s)的差别Cpu(s):所有用户进程占用整个cpu的平均值,由于每个核心占用的百分比不同,所以按平均值来算比较有参考意义。%CPU......
  • Java进阶学习|Day4.Java多线程,线程池
    文章目录了解多线程CPU进程(Process)线程多线程开发多线程优点实现方式继承Thread类实现Runnable接口实现Callable接口线程状态转换线程状态线程调度调整线程优先级线程睡眠线程等待线程让步线程加入线程唤醒线程同步线程同步方式多线程间通信线程池了解线程池定义......
  • linux yum的安装与管理
    (学生填写)(一)yum配置本地源在使用yum安装之前是必须配置yum源的,在不能联网的情况下,可使用DVD光盘或ISO文件做本地yum源.建立光盘文件存放目录(/mnt/dvd)和创建光盘挂载点,如图1所示。 图1挂载光盘成功2.利用挂载光盘命令,已将光盘正常挂载好,如图2......
  • linux网络配置与管理
    1.Setup配置(centos7之后使用nmtui)首先,查看网卡的信息,是否有IP地址。如图所示:图1查看网卡信息进入set设置,在终端输入“setup”,点击“enter”键。如图所示:图2进入setup设置点击“enter”后,就进入了setup配置界面了,选择“网络配置”。如图所示:图3......
  • Linux 中 uid、gid、euid、egid、groups 之间的关系
    导航1权限匹配流程2五种身份变化3有效用户/组4特权对Shell脚本无效5Sudo与SUID/SGID的优先级6SUID、SGID、Sticky各自的功能Linux最优秀的地方之一,就在于他的多人多工环境。而为了让各个使用者具有较保密的档案资料,因此档案的权限管理就变的很重要了。Linu......
  • linux命令汇总
    top(命令)显示系统中所有动态进程top-pPID显示指定进程编号的进程信息top-d10每隔10秒显示进程变化top-n3更新3次进程变化后结束进程top-i显示正在运行,正在执行的进程top命令下字段含义PID进程编号USER进程......
  • linux使用tftp命令上传文件
    tftp-g-rup.rar192.168.1.249是使用TFTP(TrivialFileTransferProtocol)从指定的服务器(192.168.1.249)下载文件(up.rar)的命令。tftp:是TFTP命令行客户端的命令名称。-g:表示使用TFTP客户端的"get"模式,用于从服务器获取文件。-rup.rar:指定要下载的文件名称为"u......
  • 不用虚拟机在Windows上安装Linux子系统(win11)
    打开终端输入以下命令查看是否支持安装systeminfo最底下是4个yes代表支持 在开始菜单输入如下搜索 打开拉到最底下,勾选这两个选项 按照提示重启电脑 打开终端输入以下命令会自动安装最新的Ubuntu发行版wsl--install可以通过如下命令查看其他版本wsl--list......
  • Linux容器篇-Docker镜像的使用
    文章目录前言一、列出镜像列表二、获取一个新镜像三、查找镜像四、拉取镜像五、删除镜像六、创建镜像1、更新镜像2、构建镜像七、设置镜像标签总结前言当运行容器时,使用的镜像如果在本地中不存在,docker就会自动从docker镜像仓库中下载,默认是从DockerHub公共......