• 嵌入式Linux入门—Linux多线程编程、互斥量、信号量、条件变量


    目录

    1. 认识线程

    1.1 线程的概念

    1.2 线程号tid

    1.3 创建线程pthread_create()

    1.4 线程的退出与回收

    1.4.1 pthread_exit 主动退出线程

    1.4.2 pthread_cancel 线程被动退出

    1.4.3 pthread_join 线程资源回收(阻塞方式)

    1.4.4 pthread_tryjoin_np 线程资源回收(非阻塞)

    2. 互斥量

    2.1 初始化互斥量pthread_mutex_init

    2.2 互斥量加锁/解锁

    2.3 互斥量加锁(非阻塞方式)

    2.4 互斥量销毁

    3. 信号量

    3.1 初始化信号量sem_init

    3.2 信号量申请与释放sem_wait/sem_post

    3.3 信号量申请(非阻塞方式)sem_trywait

    3.4 信号量销毁sem_destory

    4. 条件变量

    4.1 创建和销毁条件变量

    4.2 等待条件变量pthread_cond_wait

    4.3 通知条件变量pthread_cond_signal


    嵌入式Linux入门完整教程:嵌入式Linux教程—裸机、应用、驱动完整教程目录

    操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。同一时间,操作系统可以运行多个进程,每个进程还可以同时执行多个任务,同一进程中,执行的每个任务都被视为一个线程。

    1. 认识线程

    一个进程中可以包含多个线程,所有线程共享进程拥有的资源。当然,每个线程也可以拥有自己的私有资源。

    1.1 线程的概念

    线程,是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量,每个线程都可以去访问它,与进程共享“ 4G”内存空间,使得系统资源消耗减少。

    1.2 线程号tid

    tid,其本质是一个pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对
    于线程号而言,其仅仅在其所属的进程上下文中才有意义。

    也就是说在不同进程里的两个线程tid是可能相等的。

    可以用函数pthread_self(),获取当前线程的线程号:

    1. #include
    2. #include
    3. int main()
    4. {
    5. pthread_t tid = pthread_self();//获取主线程的 tid 号
    6. printf("tid = %lu\n",(unsigned long)tid);
    7. return 0;
    8. }

    注意: 因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库,使用 gcc编译应 gcc xxx.c -lpthread 方可编译多线程程序

    1.3 创建线程pthread_create()

    函数原型:

    1. #include
    2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routi
    3. ne) (void *), void *arg);

    该函数一共四个参数:

    第一个参数为 pthread_t 指针,用来保存新建线程的线程号

    第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;

    第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*,形参为 void*。

    第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充。void*型万能指针代表什么都能传。

    1. #include
    2. #include
    3. void *thread_printf_tid(void *arg)
    4. {
    5. pthread_t tid;
    6. tid=pthread_self();
    7. printf("sub_thread_tid=id:%lu\n",tid);
    8. }
    9. int main(int argc,char **argv)
    10. {
    11. pthread_t tid;
    12. tid=pthread_self();
    13. printf("tid_main:%lu\n",tid);
    14. pthread_create(&tid,NULL,thread_printf_tid,NULL);
    15. sleep(1);
    16. return 0;
    17. }

    结果:

    当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。

    因线程执行顺序随机,上面的程序不加 sleep 可能导致主线程先执行,导致进程结束,无法执行到子线程。

    1.4 线程的退出与回收

    线程的退出情况有三种

    1.进程结束,进程中所有的线程也会随之结束。

    2.通过函数 pthread_exit 来主动的退出线程。

    3.被其他线程调用 pthread_cancel 来被动退出。

    当线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np来回收线程的资源,并且获得线程结束后需要返回的数据

    1.4.1 pthread_exit 主动退出线程

    pthread_exit 函数原型如下:

    1. #include
    2. void pthread_exit(void *retval);

    pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。

    1.4.2 pthread_cancel 线程被动退出

    其他线程使用该函数让另一个线程退出,pthread_cancel 函数原型如下:

    1. #include
    2. int pthread_cancel(pthread_t thread);

    该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0。

    1.4.3 pthread_join 线程资源回收(阻塞方式)

    pthread_join 函数原型如下:

    1. #include
    2. int pthread_join(pthread_t thread, void **retval);

    该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据.线程资源回收(非阻塞方式)。

    1.4.4 pthread_tryjoin_np 线程资源回收(非阻塞)

    函数原型如下:

    1. #define _GNU_SOURCE
    2. #include
    3. int pthread_tryjoin_np(pthread_t thread, void **retval);

    该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回 0,其余参数与 pthread_join 一致。

    注意:通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先后顺序自由的进行资源的回收。

    2. 互斥量

    当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼此“矛盾”现象。

    例如线程 1 企图想让变量自增,而线程 2 企图想要变量自减,两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程 1 得到执行权后将变量自加,当线程 2 得到执行权后将变量自减,变量似乎永远在某个范围内浮动,无法到达期望数值。

    为了解决上述对临界资源的竞争问题, pthread 线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。

    2.1 初始化互斥量pthread_mutex_init

    函数原型如下:

    1. int pthread_mutex_init(phtread_mutex_t *mutex,
    2. const pthread_mutexattr_t *restrict attr);

    该函数初始化一个互斥量,第一个参数是互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。

    还可以可以调用宏来快速初始化,代码如下:

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

    2.2 互斥量加锁/解锁

    函数原型如下:

    1. #include
    2. int pthread_mutex_lock(pthread_mutex_t *mutex);
    3. int pthread_mutex_unlock(pthread_mutex_t *mutex);

    成功:返回 0

    lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的pthread_mutex_t 互斥量指针。 成功后会返回 0。

    当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。

    特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!

    2.3 互斥量加锁(非阻塞方式)

    函数原型如下:

    1. #include
    2. int pthread_mutex_trylock(pthread_mutex_t *mutex);

    该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加锁函数一致。

    2.4 互斥量销毁

    函数原型如下:

    1. #include
    2. int pthread_mutex_destory(pthread_mutex_t *mutex);

    成功:返回 0

    该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0。

    3. 信号量

    我们在1.3中提到:线程执行顺序随机,信号量就是用来使得线程执行具有一定顺序。

    互斥量用来防止多个线程同时访问某个临界资源。信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就可以给线程 A 发信号。

    3.1 初始化信号量sem_init

    函数原型如下:

    int sem_init(sem_t *sem,int pshared,unsigned int value);

    第一个参数传入 sem_t 类型指针;
     第二个参数传入 0 代表线程控制,否则为进程控制;
     第三个参数表示信号量的初始值, 0 代表阻塞, 1 代表运行。
    待初始化结束信号量后,若执行成功会返回 0。

    3.2 信号量申请与释放sem_wait/sem_post

    函数原型如下:

    1. #include
    2. int sem_wait(sem_t *sem);
    3. int sem_post(sem_t *sem);

    成功:返回 0
    sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“ sem-1”的操作。所谓的“ sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回 0。

    sem_post 函数会释放指定信号量的资源,执行“ sem+1”操作。

    通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成对线程执行顺序的控制。

    3.3 信号量申请(非阻塞方式)sem_trywait

    函数原型如下:

    1. #include
    2. int sem_trywait(sem_t *sem);

    成功:返回 0

    此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致,唯一区别在于此函数为非阻塞。

    3.4 信号量销毁sem_destory

    函数原型如下:

    1. #include
    2. int sem_destory(sem_t *sem);

    成功:返回 0

    该函数为信号量销毁函数,执行过后可将信号量进行销毁。
     

    4. 条件变量

    条件变量是线程的一种同步机制,条件变量与互斥量一起使用,适用于多个线程之间存在某种依赖关系,只有满足某种条件时,某个线程才可以使用的情景。

    与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。

    条件变量使我们可以睡眠等待某种条件出现,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

    条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

    有一个IO请求队列,入队线程不断的往队列里面push_back请求,出队线程不断的从队列里面pop_front请求。入队线程在push_back的时候要独占队列,出队线程在pop_front的时候也要独占队列。如果在某一时刻,入队线程抢到了互斥量,但是发现队列是满的,自己不断的轮询查询队列是否非满状态这样很消耗CPU资源,也导致无意义的占用锁资源。出队线程由于拿不到互斥量,一直阻塞。这样会导致程序僵死或者时间消耗变大。换句话说就是当线程拿到锁之后,如果发现不满足自己的执行条件就应该立即释放锁,并阻塞在当前位置,等待满足自己的执行条件时,通过某种途径来唤醒它继续运行。这时我们引入条件变量来解决这种场景。

    4.1 创建和销毁条件变量

    函数原型如下:

    1. #include
    2. // 初始化条件变量
    3. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    4. int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_at
    5. tr 通常为 NULL
    6. // 销毁条件变量
    7. int pthread_cond_destroy(pthread_cond_t *cond);

    这些函数成功时都返回0。

    4.2 等待条件变量pthread_cond_wait

    函数原型如下:

    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    这需要结合互斥量一起使用,示例代码如下:

    1. pthread_mutex_lock(&g_tMutex);
    2. // 如果条件不满足则,会 unlock g_tMutex
    3. // 条件满足后被唤醒,会 lock g_tMutex
    4. pthread_cond_wait(&g_tConVar, &g_tMutex);
    5. /* 操作临界资源 */
    6. pthread_mutex_unlock(&g_tMutex);

    4.3 通知条件变量pthread_cond_signal

    函数原型如下:

    int pthread_cond_signal(pthread_cond_t *cond);

    pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程,示例代码如下:

    pthread_cond_signal(&g_tConVar);


     

  • 相关阅读:
    MyBatis-plus:查询操作、分页查询
    商城网站被攻击了,网站打开的速度很慢该怎么办,接入CDN有没有用处呢,CDN能不能有效的防止被攻击的情况
    LTSPICE仿真那些事
    Code-Audit(代码审计)习题记录4-5
    [机缘参悟-72]:深度思考-人生自省的四重境界:不觉、自觉、觉他、圆满
    jvm优化(二)JVM 内存大小设置
    如何阅读外文文献?文献阅读七步法
    CDISC SDTM IG 3.3 版本相比于 3.2版本的变化 (下)
    Leetcode1128. 等价多米诺骨牌对的数量
    结合均线分析k线图的基本知识
  • 原文地址:https://blog.csdn.net/freestep96/article/details/126821594