【Linux】线程与同步互斥相关知识详细梳理

news/2025/1/19 13:39:24/

目录

1. 线程概念

2. 线程优势

3. 线程劣势

4. 线程控制

4.1 POSIX线程库

4.2 线程操作

        

5. 线程互斥 

        5.1 互斥相关概念

        5.2 互斥量mutex

5.3 互斥量实现原理 

6. 线程同步

        6.1 同步概念与竞态条件        

        6.2 条件变量

6.3 条件变量使用规范及细节


1. 线程概念

        什么是线程:

        在⼀个程序里的⼀个执行路线就叫做线程(thread)。

        更准确的定义是:线程是“⼀个进程内部的控制序列”。⼀切进程至少都有⼀个执行线程。

        线程在进程内部运行,本质是在进程地址空间内运行。
        在Linux系统中,在CPU眼中,管理线程的方式也比传统的进程更加轻量化。(同样使用PCB管理,但更加轻量化)
        透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
        总的来说,进程是线程的集合,一个进程可以含有多个线程,线程是粒度最小的执行流。

2. 线程优势

        1.创建⼀个新线程的代价要比创建⼀个新进程小得多。


        2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多:
               最主要的区别是线程切换时,虚拟内存空间是不变的,但是进程切换时要加载新的内存空间。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种将寄存器中的内容切换出的过程伴随着显著的性能损耗。
               另外⼀个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚
拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题。


        3.线程占用的资源要比进程少很能充分利用多处理器的可并行数量。


        4.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务


        5.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。


        6.I/O密集型应用,为了提搞性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3. 线程劣势

        1.性能损失
        ⼀个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同⼀个处理器(无阻塞的计算密集型线程会频繁申请调度,占用大量执行时间)。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

        2.健壮性降低
        编写多线程需要更全面更深入的考虑,在⼀个多线程程序里,因时间分配上的细微偏差或者
因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
        3.缺乏访问控制
        进程是访问控制的基本粒度,在⼀个线程中调用某些OS函数会对整个进程造成影响。
        4.编程难度提高

        编写与调试⼀个多线程程序比单线程程序困难得多

4. 线程异常

        单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

        线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
 

4. 线程控制

        说完了概念,下面来实操一下把。

4.1 POSIX线程库

        与线程有关的函数构成了⼀个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
        要使用这些函数库,要通过引入头文件 <pthread.h>。
        链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

4.2 线程操作

        

pthread_create

  • 用于创建一个新线程。
  • 原型
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
    

  • 参数
    • thread:指向线程标识符的指针,线程创建成功后会保存线程 ID。
    • attr:线程属性(通常设置为 NULL,表示默认属性)。
    • start_routine:线程执行的函数(入口函数)。
    • arg:传递给 start_routine 的参数。
  • 返回值:成功返回 0,失败返回错误代码。

(pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错
误代码通过返回值返回,pthreads同样也提供了线程内的errno变量,以⽀持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小) 

        示例:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>void *rout(void *arg)
{int i;for (;;){printf("I'am thread 1\n");sleep(1);}
}
int main(void)
{pthread_t tid;int ret;if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0){fprintf(stderr, "pthread_create : %s\n", strerror(ret));exit(EXIT_FAILURE);}int i;for (;;){printf("I'am main thread\n");sleep(1);}
}

pthread_exit

  • 使当前线程终止执行,并返回一个值给其他线程。
  • 原型
void pthread_exit(void *retval);
  • 参数retval:线程的退出状态。 

pthread_join

  • 等待指定线程终止,并回收线程资源。
  • 原型
    int pthread_join(pthread_t thread, void **retval);
    
  • 参数
    • thread:要等待的线程 ID。
    • retval:指向线程退出状态的指针。

        (若不想得到退出状态,传入NULL/nullptr即可)

        示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{printf("thread 1 returning ... \n");int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}
void *thread2(void *arg)
{printf("thread 2 exiting ...\n");int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}
void *thread3(void *arg)
{while (1){ //printf("thread 3 is running ...\n");sleep(1);}return NULL;
}
int main(void)
{pthread_t tid;void *ret;// thread 1 returnpthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);free(ret);// thread 2 exitpthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);free(ret);// thread 3 cancel by otherpthread_create(&tid, NULL, thread3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n",tid);elseprintf("thread return, thread id %X, return code:NULL\n", tid);
}

运行结果: 

pthread_detach

  • 将线程与主线程分离,使主线程不再等待该线程终止,可以自动清理线程资源。
  • 原型
    int pthread_detach(pthread_t thread);
    
  • 参数
    • thread:要分离的线程 ID。

        默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
        如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

        分离的线程无法join:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{pthread_detach(pthread_self());//自己分离自己printf("%s\n", (char *)arg);return NULL;
}
int main(void)
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0){printf("create thread error\n");return 1;}int ret = 0;sleep(1); // 等一下,不然线程可能来不及分离if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");ret = 0;}else{printf("pthread wait failed\n");ret = 1;}return ret;
}

5. 线程互斥 

        5.1 互斥相关概念

        临界资源:多线程执行流共享的资源就叫做临界资源。
        临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
        互斥:任何时刻,互斥保证有且只有⼀个执行流进⼊临界区,访问临界资源,通常对临界资源起保护作用。
        原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

        5.2 互斥量mutex

        ⼤部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
        但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
        多个线程并发的操作共享变量,会带来⼀些问题。

        示例: 

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;void *route(void *arg)
{char *id = (char *)arg;while (1){//临界区 ticket是临界资源if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

 

可以看到,正常情况下票数为零时,就会停止减少,但是这时却减到了负数。这是因为线程对于共享资源的操作顺序是不确定的,可能在检查(ticket>0)刚刚进入自减逻辑时,ticket就被别的线程更改,此时就在ticket不满足if条件的情况下继续执行了自减操作,导致了ticket变为负数。

        要解决这种问题,就需要互斥量mutex,也就是锁。

pthread_mutex_init

  • 初始化互斥锁(mutex)。
  • 原型
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    
  • 参数
    • mutex:指向互斥锁的指针。
    • attr:互斥锁属性(通常设置为 NULL)。

        也可以直接定义全局锁,静态分配。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

pthread_mutex_lock

  • 锁定互斥锁,确保线程在访问共享资源时不会发生竞争。
  • 原型
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    

pthread_mutex_unlock

  • 解锁互斥锁,允许其他线程访问被锁定的资源。
  • 原型:
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

pthread_mutex_destroy 

  • 销毁互斥锁,释放资源。
  • 原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);

       使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex; //定义一个全局的锁
void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&mutex); //加锁if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex); //解锁}else{pthread_mutex_unlock(&mutex);break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL); //初始化锁pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex); //释放锁
}

        顾名思义,给临界区加锁可以保证一次只有一个线程拿到锁,临界区里最多只会有一个执行流,这样临界资源就不会被错误操作,同时,在执行流出临界区后,一定要释放锁,不然其他线程就会一直等待,出现死锁问题。 (若持有锁的线程申请自己的锁,也会死锁)

        使用互斥量锁时一定要注意图示规范:

        

5.3 互斥量实现原理 

         下面是互斥量的逻辑:

         一次只有一个执行流可以拿到mutex的“1”,因此达到加锁的效果。其中movb已经xchgb都是原子性的操作。

6. 线程同步

        6.1 同步概念与竞态条件
        

        同步:在保证数据安全的前提下,让线程能够

int pthread_cond_signal(pthread_cond_t *cond);

按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
        竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

        6.2 条件变量

pthread_cond_init

  • 初始化条件变量。
  • 原型
    int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    

    或定义全局的条件变量,静态分配。

    pthread_cond_t PTHREAD_COND_INITIALIZER

pthread_cond_wait

  • 使线程等待条件变量,直到条件成立。
  • 原型
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    

pthread_cond_signal

  • 唤醒等待条件变量的一个线程。
  • 原型
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast 

  • 唤醒所有等待条件变量的线程。
  • 原型
    int pthread_cond_broadcast(pthread_cond_t *cond);
    

    示例:

    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    #include <pthread.h>
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    void *active(void *arg)
    {std::string name = static_cast<const char *>(arg);while (true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex);std::cout << name << " 活动..." << std::endl;pthread_mutex_unlock(&mutex);}
    }
    int main(void)
    {pthread_t t1, t2;pthread_create(&t1, NULL, active, (void *)"thread-1");pthread_create(&t2, NULL, active, (void *)"thread-2");sleep(3); // 可有可⽆,这⾥确保两个线程已经在运⾏while (true){// 对⽐测试// pthread_cond_signal(&cond); // 唤醒⼀个线程pthread_cond_broadcast(&cond); // 唤醒所有线程sleep(1);}pthread_join(t1, NULL);pthread_join(t2, NULL);
    }

        利用条件变量的特性就可以基于阻塞队列实现一个简易的生产消费者模型,后续我会再出一个文章来叙述详细实现。

6.3 条件变量使用规范及细节

        聪明的你一定已经发现,phtread_cond_wait()函数中的一个参数为mutex,这是因为线程在条件上进行等待时,要先释放相应的锁资源,不然先lock了再wait,那不就死锁了吗,同时要注意,线程在被唤醒时,会再次申请锁资源,申请成功后才会继续运行。

        条件等待示例:

pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(cond, mutex);
修改条件pthread_mutex_unlock(&mutex);

        说明:条件判断一定要为while循环判断,不然可能线程被唤醒前满足条件,而唤醒时不满足条件,而导致线程错误的执行逻辑,这中情况也叫做“伪唤醒”,while循环则可保证线程出循环时一定满足正确条件,从而防止“伪唤醒”

        给条件发送信号代码:

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

 


 

 


http://www.ppmy.cn/news/1564424.html

相关文章

Linux《Linux简介与环境的搭建》

在学习了C或者是C语言的基础知识之后就可以开始Linux的学习了&#xff0c;现在Linux无论是在服务器领域还是在桌面领域都被广泛的使用&#xff0c;所以Linxu也是我们学习编程的重要环节&#xff0c;在此接下来我们将会花大量的时间在Linxu的学习上。在学习Linux初期你可以会像初…

【设计模式-结构型】代理模式

一、什么是代理模式 在港片中&#xff0c;经常能看到一些酷炫的大哥被警察抓了&#xff0c;警察会试图从他们口中套出一些关键信息。但这些大哥们通常会非常冷静地回应&#xff1a;“我有权保持沉默&#xff0c;我要找我的律师。” 这个律师就像是大哥的“法律盾牌”&#xff…

iOS - 关联对象的实现

根据源码总结一下关联对象(Associated Objects)的实现&#xff1a; 1. 关联对象的基本结构 // 对象的 isa 结构中包含关联对象标记 union isa_t {struct {uintptr_t nonpointer : 1; // 是否使用优化的 isauintptr_t has_assoc : 1; // 是否有关联对象// ...其他位…

掌握 React 高阶组件与高阶函数:构建可复用组件的新境界

一、引言 在 React 开发中&#xff0c;代码复用性和逻辑分离是提高开发效率和维护性的重要手段。高阶组件&#xff08;Higher-Order Component, HOC&#xff09;和高阶函数&#xff08;Higher-Order Function, HOF&#xff09;是实现这一目标的两种强大工具。本文将详细介绍这…

隧道IP广播与紧急电话系统:提升隧道安全的关键技术

隧道IP广播与紧急电话系统&#xff1a;提升隧道安全的关键技术 随着现代城市交通的迅猛发展&#xff0c;隧道作为重要的交通基础设施&#xff0c;其安全性与应急处理能力显得尤为重要。隧道IP广播与紧急电话系统作为保障隧道安全的关键技术&#xff0c;正发挥着越来越重要的作…

HarmonyOS使用Grid网格实现计算器功能实现

使用Grid网格处理&#xff0c;实现了计算器的加减乘除功能 Entry Component struct GridPage {State str: string ""; //暂存区State num: string "0"; //输入区State flagNum: boolean false; //标识build() {Column() {Grid() {GridItem() {Text(this…

WPS excel使用宏编辑器合并 Sheet工作表

使用excel自带的工具合并Sheet表&#xff0c;我们会发现需要开通WPS会员才能使用合并功能&#xff1b; 那么WPS excel如何使用宏编辑器进行合并 Sheet表呢&#xff1f; 1、首先我们要看excel后缀是 .xlsx 还是 .xls &#xff1b;如果是.xlsx 那么 我们需要修改为 .xls 注…

在 Azure 100 学生订阅中新建 Ubuntu VPS 并部署 Mastodon 服务器

今天想和大家分享一下如何在 Azure 的 100 学生订阅中&#xff0c;创建一台 Ubuntu VPS&#xff0c;并通过 Docker 部署 Mastodon 服务器。Mastodon 是一个开源的社交网络平台&#xff0c;允许用户创建自己的实例&#xff0c;类似于 Twitter&#xff0c;但更加去中心化。Docker…