【unix高级编程系列】线程

news2024/12/28 18:07:07

引言

我们知道unix进程中可以有多个线程,进程中的线程可以访问该进程的所有组成部分。并且CPU的调度单元就是线程。这就面临一个问题:当进程中的临界资源需要在多个线程中共享时,如何解决一致性问题?

本文将从线程的概念、线程的使用方式、unix提供哪些方式解决一致性问题进行介绍,加深对线程的理解。

线程概念

线程的优点:

  • 简化代码结构。比如在业务上为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。
  • 提高程序的吞吐量以及响应时间。
  • 对进程的共享资源访问更加的方便。

线程的资源:

每个线程除了共享进程的所有组成部分,也包含线程执行所必须信息:线程ID、一组寄存器、栈、调度优先级和策略、信号屏蔽字、error变量以及线程私有数据

线程的使用

线程ID

每一个进程有一个进程ID,每个线程也有一个线程ID。我们可以通过pthread_self获取线程ID。

#include <pthread.h>
pthread_t pthread_self(void);
    // 返回值:调用线程的线程ID

打印线程的ID,在程序调试阶段有时是非常有用的。

线程创建

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
                    const pthread_attr_t *restrict attr,
                    void *(*start_rtn)(void*),
                    void *restrict arg);
// 返回值:若成功返回0 ,不成功,返回错误编码
  • tidp。当线程创建成功后,tidp 会被设置为新创建子线程的线程ID。(《UNIX环境高级编程 第3版》 似乎描述错误了。
  • attr参数用于设置线程的属性。比如:设置线程的栈大小(默认8MB)线程的调度策略及调度参数和优先级等。
  • start_rtn是新创建线程的运行开始地址。
  • arg 是传给子线程的参数。如果需要向子线程传递两个以上的线程,需要将这些参数放到一个结构体中,然后将这个结构体地址传入(最好是堆内容,由子线程管理,释放)。

注:线程创建时并不能保证哪个线程会先执行:是新创建的线程,还是调用线程。

如下列示例就存在隐患:

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

void* my_thread(void * param)
{
    int num = *param; 
    printf("param = %d\n",num);
    return NULL;
}

int demo()
{
    pthread_t tidp;
    int num = 5;
    if(pthread_create(&tidp,NULL,my_thread,&num) != 0)
    {
        printf("create pthread failed");
    }
    return 0;
}

分析:

  1. demo函数创建子线程成功后,子线程中的入参param 设置为demo函数局部变量 num的地址。
  2. 此时CPU优先调度 demodemo 函数执行完成,进行了栈回收。此时num的地址空间可能就会被修改。
  3. CPU再次调用到子线程my_thread。此时访问num地址的内容,就与预期不符。

简单的修改方式:传入num的值。

线程终止

在进程控制章节,我们了解到在代码的任何地方调用exit_Exit_exit,那么整个进程就会终止。那么是否可以在不停止进程的情况下,停止对应的进程呢。unix提供了三种方式:

  1. 线程可以简单地从启动例程中返回,返回值是线程的退出码。
  2. 线程可以被同一进程中的其他线程取消。
  3. 线程调用pthread_exit

这里着重介绍一下第二、三种方式:

#include <pthread.h>
int pthread_canncel(pthread_t tid);
// 返回值:若成功给,返回0;否则,返回错误编号

进程可以通过pthread_cancel接口向指定同进程中的线程发起退出请求。但是它并不等待线程终止。而线程可以选择忽略此请求或控制如何被取消。

#include <pthread.h>
void pthrad_exit(void *rval_ptr);

int pthread_join(pthread_t thread, void **rval_ptr);

线程可以通过pthread_exit接口退出线程,其中rval_ptr是退出码,其它进程可以通过pthread_join捕获退出码,但是调用线程在指定线程没有退出前,会一直处于阻塞状态

线程清理处理程序

在进程环境章节,我们介绍到exit函数在进程退出时,会先执行终止处理程序(类似C++中的析构函数),再清理标准I/Oatexit提供了注册该处理程序的能力。类似的,线程也可以注册退出时调用的函数。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void*), void *arg);
void pthread_cleanup_pop(int execute);

注:线程清理处理程序只有两种情况下触发:

  1. 在调用pthread_exit主动退出时;
  2. 响应其他线程的取消请求时。

即:线程正常从启动例程中return 退出是不会触发 线程清理处理程序

线程分离

在进程环境章节,我们了解到子进程退出时,会在内存中保留退出状态,等待父进程通过waitpid获取,否则会一直存在,成为僵尸进程,造成资源浪费。类似的,线程退出时,也会将终止状态保存着,等待其他进程调用pthread_jion进行回收,否则同样也会造成资源浪费。

但是调用pthread_jion可能会造成调用线程一直阻塞,与我们业务设计不符。若我们对线程退出状态不关心的话,可以将其进行线程分离。若线程已经被分离,线程的底层存储资源在线程终止时立即被回收。

#include <pthread.h>
int pthread_detach(pthread_t tid);

一致性问题探讨

当多个线程共享同一块内存时,就需要考虑数据一致性问题。多线程访问共享内存的场景可以分为以下几个场景。

  1. 共享变量(比如全局变量),仅由一个线程访问,其他线程不会读取和修改。这种场景就不存在问题
  2. 多线程对共享变量只存在读取操作,不会修改。这种场景不存在问题
  3. 当多线程访问一个共享变量,并且其中有一个以上的线程可以修改变量。则存在一致性问题

一致性问题存在的根因:修改全局变量的操作往往不是原子操作,存在多个存储器访问周期。当其它线程读取时,可能在其修改周期内访问,则会造成读取异常值。举个例子:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
long num = 0x00000000;
void* my_thread(void * param)
{
    printf("num = 0x%0lx\n",num);
    return NULL;
}

int main()
{
    pthread_t tidp;

    if(pthread_create(&tidp,NULL,my_thread,NULL) != 0)
    {
        printf("create pthread failed");
    }
    num = 0xffffffff;

    pthread_join(tidp,NULL);

    return 0;
}

分析:

  1. 主进程修改num 变量,可能存在需要两个存储器周期(num正好分配在两个物理页中)。
    a. 将第一个页中的num低32bit 设置为0xffff
    b. 将第二物理页中的num 高32bit设置为0xffff
  2. 正如上节讨论的,CPU对线程的调用顺序是随机的,因此子线程在访问num变量时,可能是主线程刚刚更新一个物理页中的数据。此时子线程得到的值就是0x0000ffff。这是就出现了异常,num的业务含义可能只有0和0xffffffff。但是此时子线程获取到0x0000ffff,则会造成程序异常。

注:若修改操作是原子操作,就不存在竞争问题。比如C++中的原子变量,就可以避免多线程访问的一致性问题

C语言并没有原子变量,但是unix也提供了多种方式,在多线程访问共享变量时,如何保持同步。比如互斥量、读写锁、条件变量、自旋锁、屏障。

互斥量

互斥量使用pthread_mutex_t数据类型表示。常见接口如下:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量可以通过上述接口进行初始化。也可以静态初始化,设置为常量PTHREAD_MUTEX_INITIALIZER

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中,若不希望线程被阻塞,可以使用pthread_mutex_trylock,互斥量若未被锁住,则返回0,并锁住互斥量;若互斥量已经被锁住,则返回EBUSY

注:若同一个线程,连续对互斥量加锁两次以上,线程自身则会陷入死锁。并且其它线程也无法再次获取到互斥量,导致整个业务进入死锁状态

#include <pthread.h>
#include <time.h>
int pthread_mutex_timelock(pthread_mutex_t *mutex,
                            const struct timespec *restrict tsptr);

pthread_mutex_timelock尝试获取互斥量时,若互斥量已经被锁住,则进行阻塞。直到其它线程将互斥量释放,获取到互斥量。或达到超时,返回ETIMEDOUT(超时指愿意等待的绝对时间,即在时间X之前可以阻塞等待,而不是等待Y秒)这就存在一个问题,若系统的时间变更了,则会出现意料之外的情况。

读写锁

读写锁和互斥量类似,不过读写锁在一些场景下,提供了更高的并行性。那是因为读写锁的特性决定的,读写锁有三种状态:

  1. 读模式加锁状态。当处于该状态时,所有试图以读模式对它进行加锁的线程,都可以得到访问全。但是任何以写模式加锁的线程都会被阻塞。
  2. 写模式加锁状态。当处于该状态时,所有试图对这个锁加锁的线程都会被阻塞。
  3. 不加锁状态。任何加锁请求都可以满足。

注:针对第一种状态,若当前已经处于读模式加锁状态,下一个线程写模式获取锁,会被阻塞。并且后续以读模式获取锁的线程也会被阻塞。其目的是防止读模式锁长期占用

由于读写锁的特性,非常适合共享变量读取次数远远大于修改的场景。

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

读写锁在使用之前必须初始化,在释放底层内存之前,必须要销毁。

#include <pthread.h>
#include <time.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    // 读模式获取锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    // 写模式获取锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    // 释放锁

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);    // 读模式获取锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);    // 写模式获取锁

int pthread_mutex_timerdlock(pthread_rwlock_t *restrict rwlock,
                            const struct timespec *restrict tsptr);
int pthread_mutex_timewrlock(pthread_rwlock_t *restrict rwlock,
                            const struct timespec *restrict tsptr);

条件变量

条件变量是线程可用的另一种同步机制,条件变量本身需要使用互斥量保护。因此两者需要一同使用。

pthread_cond_t 数据类型表示条件变量,它可以用两种方式进行初始化。

  1. 常量PTHREAD_COND_INITAIALIZER赋值给静态分配的条件变量
  2. 动态分配,再使用pthread_cond_init初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
                        const pthread_condattr_t * restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
                        pthread_mutet_t *restrict mutex);

int pthread_cond_timewait(pthread_cond_t *restrict cond,
                        pthread_mutet_t *restrict mutex,
                        const struct timespec *restrict tsptr);

这里的互斥量是用于对条件的保护。调用者需要将锁住的互斥量传给函数,函数然后回自动把调用线程放到等待条件的线程列表上,对互斥量解锁,等待条件变量满足。将这个流程分步骤理解如下:

  1. 获取互斥量
  2. 将条件变量放到等待条件的线程列表上
  3. 解锁互斥量。其它线程可以获取互斥量
  4. 线程阻塞,等待条件满足
  5. 当条件满足时,线程会再次尝试获取互斥量
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
//伪代码如下:
    pthread_mutext_lock(&qlock);
    pthread_cond_wait(&qready,&qlock);

    /* 临界资源处理*/

    pthread_mutext_unlock(&qlock);

通知条件已满足,有两个接口。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t  *cond);
int pthread_cond_broadcast(pthread_cond_t  *cond);

自旋锁

自旋锁与互斥量类似,但是它不是通过休眠使线程阻塞,而是在获取锁之前一致处于忙等待阻塞状态。

在CPU性能优化——“瑞士军刀“章节中,我们了解到上下文切换的概念。一旦线程阻塞进入休眠,再次运行到此线程时,需要将该线程的上下文恢复,这个切换的过程是比较耗时的。

而自旋锁的特性,决定了:若明确等待锁的时间小于上下文切换的损耗,则在性能上获得提升。因此自旋锁的使用场景有:

  • 短时间锁定。当预计线程持有锁的时间非常短时,使用自旋锁可能更有效。因为自旋锁避免了线程切换的开销,在等待锁释放的过程中,线程仍然在运行。
  • 多核处理器:在多核处理器上,如果锁被持有的时间很短,让等待的线程在另一个核心上自旋,可能比将其挂起和稍后重新调度更高效。
  • 低延迟要求:在需要低延迟响应的环境中,自旋锁可以减少线程因等待锁而被挂起的时间,从而降低响应时间。
  • 内核态同步:在操作系统内核中,自旋锁经常用于同步对共享资源的访问,因为内核通常不能承受线程切换带来的开销。
  • 无锁数据结构:在实现无锁(lock-free)或无等待(wait-free)数据结构时,自旋锁可以作为辅助工具,帮助确保在修改数据结构时的一致性。
  • 高性能计算:在高性能计算(HPC)应用中,为了减少同步开销,可能会使用自旋锁来同步对共享资源的访问。
  • 频繁访问的共享资源:当共享资源被频繁访问,且每次访问的时间都很短时,自旋锁可以减少线程切换的次数,提高效率。
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int psshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

屏障

屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程达到某点,然后从该点继续执行。

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t * restrict barrier,
                        const pthread_barrierattr_t *restrict attr,
                        unsigned int count);

int pthread_barrier_destroy(pthread_barrier_t *barrier);

其中count参数指定,在允许所有线程继续运行前,必须达到屏障的线程数目。

#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);

线程在调用pthread_barrier_wait接口时,会进行屏障计数,若未满足条件,则会进入休眠状态。若该线程是最后一个调用pthread_barrier_wait接口的线程,所有线程都会被唤醒。

总结

本文主要介绍了Unix环境下多线程编程的概念、使用方式以及如何解决一致性问题。

线程概念:

  • 线程是进程内的一个执行流,具有自己的线程ID、寄存器、栈等资源,但与同进程的其他线程共享进程资源。
  • 线程的优点包括简化代码结构、提高程序吞吐量和响应时间,以及对共享资源的便捷访问。

线程的使用:

  • 线程的创建、终止、清理处理程序、分离等操作方法。
  • 线程ID的获取和使用,以及线程创建时可能出现的隐患和解决方法。

一致性问题探讨:

  • 当多个线程共享内存时,可能存在一致性问题,特别是在多个线程对共享变量进行读写操作时。
  • 一致性问题的根源在于修改操作的原子性不足,可能导致读取到中间状态的数据。

同步机制:

  • 互斥量(Mutex):用于保证同一时间只有一个线程访问共享资源。
  • 读写锁(RWLock):适用于读多写少的场景,提供更高的并行性。
  • 条件变量(Cond):与互斥量结合使用,用于线程间的条件等待和通知。
  • 自旋锁(Spinlock):适用于短时间锁定场景,减少线程切换开销。
  • 屏障(Barrier):用于协调多个线程的并行工作,使它们在某个点上同步。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2113123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Bootstrap 字体图标无法显示问题,<i>标签字体图标无法显示问题

bootstrap fileInput 以及 Bootstrap 字体图标无法显示问题。 今天在用 bootstrap fileInput 插件的时候发现图标无法显示&#xff0c;如下&#xff1a; 查看DOM&#xff0c;发现那些图标是<i>标签做的&#xff1a; 网上的方案 方案1 网上很多人说是我们打乱了boots…

dfs序+线段树,CF 838B - Diverging Directions

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 838B - Diverging Directions 二、解题报告 1、思路分析 注意这是一棵有…

正点原子STM32F103+ESP8266+DS18B20+DHT11连接阿里云

文章目录 MQTT协议1. 基础知识2. 报文形式3. 连接报文4. 心跳报文5. 订阅报文5.1. 订阅主题报文SUBSCRIBE5.2. 订阅确认SUBACK5.3. 取消订阅UNSUBSCRIBE5.4. 取消订阅确认UNSUBACK 6. 发布报文6.1. 发布消息PUBLISH6.2. 发布确认PUBACK 7. 阿里云账号创建8. 网络调试助手接入阿…

机器人助力上下料搬运,加速仓库转运自动化

近年来&#xff0c;国内制造业领域掀起了一股智能化改造的浪潮&#xff0c;众多工厂纷纷采纳富唯智能提供的先进物流解决方案&#xff0c;这一举措显著优化了生产流程&#xff0c;实现了生产效率的飞跃式增长。得益于这些成功案例&#xff0c;某信息技术服务企业在工厂智能物流…

如何打造高校实验室教学管理系统?Java SpringBoot助力,MySQL存储优化,2025届必备设计指南

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

每日一题——第八十一题

打印如下图案: #include<stdio.h> int main() {int i, j;char ch A;for (i 1; i < 5; i, ch){for (j 0; j < 5 - i; j){printf(" ");//控制空格输出}for (j 1; j < 2 * i; j)//条件j < 2 * i{printf("%c", ch);//控制字符输出}prin…

❤《实战纪录片 1 》原生开发小程序中遇到的问题和解决方案

《实战纪录片 1 》原生开发小程序中遇到的问题和解决方案 文章目录 《实战纪录片 1 》原生开发小程序中遇到的问题和解决方案1、问题一&#xff1a;原生开发中 request请求中返回 的数据无法 使用this传递给 data{}中怎么办&#xff1f;2、刚登录后如何将token信息保存&#xf…

[网鼎杯 2020 青龙组]bang-快坚持不下去的第五天

ps:安卓的解壳&#xff0c;也就是frida-dexdump一把嗦&#xff0c;其他wp上说用真机成功的几率大一些&#xff0c;我就用的模拟器 详细的看这一篇博客&#xff1a; 安卓模拟器Frida环境搭建 &#xff08;mumuadbfrida&#xff09; 在python中安装frida时多一步 pip install…

【Dash】feffery_antd_componenet 中的 AntdSpace

一、feffery_antd_componenet 中的 AntdSpace feffery_antd_components&#xff08;简称fac&#xff09;中的AntdSpace组件是一个基于Ant Design的Dash第三方组件&#xff0c;它用于在水平或垂直方向上放置多个元素&#xff0c;并提供元素之间的间距。以下是AntdSpace组件的一…

一文说清什么是AI原生(AI Native)应用以及特点

引言&#xff1a;智能新纪元 如今&#xff0c;走在街头&#xff0c;哪儿不被智能科技包围&#xff1f;智能音箱、自动驾驶汽车、聊天机器人......这些都在用不同的方式提升我们的生活体验。然而&#xff0c;究竟什么才能称得上“AI原生应用”呢&#xff1f; 什么是AI原生&…

C++:STL简介

✨ Blog’s 主页: 白乐天_&#xff3e;&#xff56;&#xff3e; &#x1f308; 个人Motto&#xff1a;他强任他强&#xff0c;清风拂山岗&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 一、STL引入 STL&#xff08;Standard template libaray-标准模板库…

最新厦门新能安社招入职Verify测评笔试攻略敬请查收SHL

作为全球知名锂电池供应商——ATL与CATL的合资公司&#xff0c;新能安专注高端锂电池的研究与创新&#xff0c;聚焦储能系统、微型车、智能产品驱动电池等三大领域的研发、生产、销售与服务。 a.测评内容包含演绎推理数字推理两部分&#xff0c;大约用时40分钟左右; b.正式测评…

node.js实现阿里云短信发送

效果图 实现 一、准备工作 1、官网直达网址&#xff1a; 阿里云 - 短信服务 2、按照首页提示依次完成相应资质认证和短信模板审核&#xff1b; 3、获取你的accessKeySecret和accessKeyId&#xff1b; 方法如下&#xff1a; 获取AccessKey-阿里云帮助中心 4、获取SignNa…

测试你们认为最好的AI工具,是不是好用得自己试试!

大家好&#xff0c;我是凡人&#xff0c;在 OpenAI 春季发布会后&#xff0c; GPT-4o 一时风光无量&#xff0c;一个同事不信邪&#xff0c;非要用 GPT-4o 版本对 OpenAI 官网上的例子尝试生成&#xff0c;本来还是嘲笑他的心态&#xff0c;但他还真的发现了点有意思的事情。 …

多线程篇(阻塞队列- BlockingQueue)(持续更新迭代)

目录 一、了解什么是阻塞队列之前&#xff0c;需要先知道队列 1. Queue&#xff08;接口&#xff09; 二、阻塞队列 1. 前言 2. 什么是阻塞队列 3. Java里面常见的阻塞队列 三、BlockingQueue&#xff08;接口&#xff09; 1. 前言 2. 简介 3. 特性 3.1. 队列类型 …

实时地图+瞬移,黑神话地图工具来了

工具下载: https://pan.quark.cn/s/12b9cef46bf0 瞬移功能使用说明&#xff1a; 1、必须在一修大师客户端: 使用猫修APP扫码登陆后使用&#xff1b; 2、打开《黑神话》游戏&#xff1b; 3、点击修改器页面右上角“开始使用”按钮&#xff1b; 4、点击你想要瞬移的点位图标…

使用Azure Devops Pipeline将Docker应用部署到你的Raspberry Pi上

文章目录 1. 添加树莓派到 Agent Pool1.1 添加pool1.2 添加agent 2. 将树莓派添加到 Deployment Pool2.1 添加pool2.2 添加target 3. 添加编译流水线3.1 添加编译命令3.2 配置触发器 4. 添加发布流水线4.1 添加命令行4.2 配置artifact和触发器 5. 完成 1. 添加树莓派到 Agent P…

spring项目使用邮箱验证码校验

本项目采用免费QQ邮箱验证码方式进行登录安全的校验。 前期工作 申请邮箱安全授权码 打开QQ邮箱官网点击设置 进入设置页面后点击账户按钮 进入账户后一直往下拉页面找到POP3服务栏&#xff0c;然后点击管理服务&#xff08;如果没有开启服务需要先开启服务&#xff0c;按照…

vivado 时钟交互报告

步骤6&#xff1a;时钟交互报告 在创建约束之后或期间&#xff0c;必须验证约束是否完整且安全。 Vivado Design Suite默认情况下将所有时钟一起计时&#xff0c;除非您通过定义另有指定 时钟组或其他定时异常。set_clock_groups命令指定 异步或独占时钟域&#xff0c;并禁用它…

GIT | git提交注释自动添加信息头

GIT | git提交注释自动添加信息头 时间&#xff1a;2024年9月6日10:20:11 文章目录 GIT | git提交注释自动添加信息头1.操作2.commit-msg文件 1.操作 2.commit-msg文件 #!/bin/sh # # An example hook script to check the commit log message. # Called by "git commit&q…