【探索Linux】—— 强大的命令行工具 P.21(多线程 | 线程同步 | 条件变量 | 线程安全)

在这里插入图片描述

阅读导航

  • 引言
  • 一、线程同步
    • 1. 竞态条件的概念
    • 2. 线程同步的概念
  • 二、条件变量
    • 1. 条件变量函数
      • ⭕使用前提
      • (1)初始化条件变量
      • (2)等待条件满足
      • (3)唤醒等待
        • pthread_cond_broadcast()
        • pthread_cond_signal()
      • (4)销毁条件变量
    • 2. 条件变量使用规范
      • (1)条件变量的使用流程
      • (2)条件变量的使用注意事项
    • 3. 使用条件变量的示例
  • 三、线程安全
    • 1. 概念
    • 2. 常见的线程不安全的情况
    • 3. 常见的线程安全的情况
    • 4. 可重入与线程安全的关系(八股文)
      • (1)可重入与线程安全的联系
      • (2)可重入与线程安全的区别
  • 温馨提示

引言

在上一篇文章中,我们详细探讨了多线程编程的基础概念,包括线程互斥、互斥锁以及死锁和资源饥饿等问题。我们了解到,在多线程环境下,为了防止数据竞争和保证程序的正确性,需要采用一定的同步机制来协调线程之间的执行顺序。本篇文章将继续深入探讨多线程编程中的另一组关键概念:线程同步、条件变量和线程安全

在这篇文章中,我们将具体介绍线程同步的技术和模式,探讨条件变量的工作原理以及如何在实际编程中正确使用它们来避免竞态条件和提高程序效率。同时,我们还将分析线程安全的概念,并通过示例展示如何编写线程安全的代码,以确保多线程程序的可靠性和稳定性。随着对这些概念的深入理解,我们将能够更加熟练地掌握多线程编程,打造出更加健壮和高效的软件系统。

一、线程同步

1. 竞态条件的概念

竞态条件(Race Condition)是并发编程中的一个重要概念,它指的是程序的输出或行为依赖于事件或线程的时序。在多线程环境中,如果多个线程共享某些数据,并且它们试图同时读写这些数据而没有适当的同步机制来协调这些操作,就可能出现竞态条件

简单来说,当两个或更多的线程访问共享数据,并且至少有一个线程在修改这些数据时,如果线程之间的执行顺序会影响最终的结果,那么就存在竞态条件。由于线程调度通常由操作系统进行,而且具有一定的随机性,因此竞态条件可能导致程序行为不可预测,有时候甚至非常难以复现和调试

竞态条件的一个典型例子是“检查后行动”(check-then-act)操作,其中线程检查某个条件(如资源是否可用),然后基于这个条件采取行动。如果在检查和行动之间的时间窗口内,另一个线程改变了条件(如抢占了资源),那么第一个线程的行动可能基于错误的假设。

另一个常见的竞态条件是“读-改-写”(read-modify-write)操作,这涉及到读取一个变量的值,对其进行修改,然后写回新值。如果两个线程同时执行这样的操作,而且它们的读取和写入操作是交织在一起的,那么最终写回的值可能只反映了其中一个线程的修改,而另一个线程的修改则丢失了。

为了避免竞态条件,我们需要使用线程同步机制,如互斥锁、信号量、条件变量等,来确保在任何时刻只有一个线程能够访问临界区的代码。通过这种方式,可以序列化对共享资源的访问,从而避免不确定的时序和数据冲突,保证程序的正确性和稳定性

2. 线程同步的概念

线程同步是指在多线程环境中,控制不同线程之间的执行顺序,确保它们能够有序地共享资源和协调工作的一系列机制和方法。当多个线程访问共享资源时,如果没有适当的同步,就可能发生竞态条件(Race Condition),导致数据不一致、程序错误甚至崩溃。

为了防止这些问题,线程同步提供了一种方式,使得在任何时刻只有一个线程可以访问到临界区(Critical Section)。临界区是指那些访问共享资源的代码段,这些资源可能是内存、文件或者其他外部状态。通过线程同步,我们可以确保每次只有一个线程可以操作临界区内的共享资源,从而避免非预期的交互和数据冲突

二、条件变量

条件变量是一种同步原语,它用于线程间的通信,使得一个线程能够在某个特定条件不满足时挂起(等待),直到另一个线程更新了这个条件并通知等待的线程。条件变量通常与互斥锁(mutex)一起使用,以避免竞态条件,并确保数据的一致性。

1. 条件变量函数

⭕使用前提

在Linux环境下,使用条件变量相关的函数需要包含<pthread.h>头文件:

#include <pthread.h>

<pthread.h> 头文件中定义了所有与POSIX线程相关的数据类型、函数原型和宏。这包括了条件变量的操作函数、互斥锁的操作函数以及线程创建和控制的函数。

当编译使用了 <pthread.h> 的程序时,通常需要链接线程库,这可以通过在编译命令中添加 -lpthread 选项来实现。例如:

gcc program.c -o program -lpthread

这条命令会编译 program.c 文件,并将POSIX线程库链接到生成的可执行文件 program 中。

(1)初始化条件变量

在POSIX线程(pthreads)库中,条件变量可以通过两种方式进行初始化:

  1. 静态初始化:使用预定义的宏 PTHREAD_COND_INITIALIZER 来初始化条件变量。这是在程序开始执行之前,即编译时期就已经完成的初始化。

    示例代码:

    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    
  2. 动态初始化:使用函数 pthread_cond_init() 在运行时动态地初始化条件变量。这种方式允许你指定条件变量的属性。

    示例代码:

    pthread_cond_t cond;
    int ret = pthread_cond_init(&cond, NULL); // 使用NULL作为属性参数,表示默认属性
    if (ret != 0) 
    {// 错误处理
    }
    

在动态初始化的情况下,如果你想要设置特定的条件变量属性,可以创建一个 pthread_condattr_t 类型的变量,并使用 pthread_condattr_init() 和相关函数来设置所需的属性。之后,将这个属性变量传递给 pthread_cond_init() 函数。

不论是静态还是动态初始化,初始化后的条件变量都处于未信号化的状态,等待被 pthread_cond_signal()pthread_cond_broadcast() 函数唤醒。

(2)等待条件满足

pthread_cond_wait 函数是POSIX线程库中用于等待条件变量的函数。它的作用是阻塞调用线程直到指定的条件变量被信号化。在等待期间,pthread_cond_wait 会自动释放与条件变量相关联的互斥锁,并且在条件变量被信号化后重新获取互斥锁。

pthread_cond_wait函数的原型:

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

参数解释

  • cond:指向需要等待的条件变量的指针。
  • mutex:指向当前线程已锁定的互斥锁的指针,必须在调用pthread_cond_wait之前由线程锁定。

返回值:

  • 如果函数成功,返回0。
  • 如果失败,将返回一个错误码(非零值)。

使用说明

  1. 线程在调用pthread_cond_wait之前,必须确保已经锁定了mutex互斥锁。
  2. 调用pthread_cond_wait后,线程会阻塞,并且mutex互斥锁被自动释放,以允许其他线程操作条件和互斥锁。
  3. 当其他线程对条件变量调用pthread_cond_signalpthread_cond_broadcast时,等待的线程会被唤醒。
  4. 被唤醒的线程在返回前将重新获取mutex互斥锁。这意味着当pthread_cond_wait返回时,线程已经再次锁定了mutex
  5. 因为可能有多个线程在等待同一个条件变量,所以即使线程被唤醒,也不能假设条件已经满足。通常需要在循环中调用pthread_cond_wait来重新检查条件。

示例代码

// 假设已经声明并初始化了cond和mutex
pthread_mutex_lock(&mutex);
while (condition_is_not_met) 
{pthread_cond_wait(&cond, &mutex);
}
// 此时condition_is_met为真,可以执行依赖于该条件的代码
pthread_mutex_unlock(&mutex);

在这个示例中,线程首先锁定互斥锁mutex,然后在一个循环中检查条件是否满足。如果条件不满足,线程调用pthread_cond_wait等待条件变量cond。当条件变量被其他线程信号化时,线程将被唤醒,并在重新获得互斥锁后继续执行。

(3)唤醒等待

pthread_cond_broadcastpthread_cond_signal 函数都是用来唤醒等待特定条件变量的线程。它们的区别在于唤醒等待线程的数量。

pthread_cond_broadcast()

pthread_cond_broadcast 函数唤醒所有等待特定条件变量的线程。如果没有线程在等待,调用此函数不会有任何效果。

函数原型如下

int pthread_cond_broadcast(pthread_cond_t *cond);

参数解释

  • cond:指向需要广播信号的条件变量的指针。

返回值

  • 如果函数成功,返回0。
  • 如果失败,将返回一个错误码(非零值)。
pthread_cond_signal()

pthread_cond_broadcast不同,pthread_cond_signal函数只唤醒一个正在等待特定条件变量的线程。如果有多个线程在等待,系统选择一个线程唤醒。选择哪个线程通常取决于线程调度策略,程序员无法控制。

函数原型如下

int pthread_cond_signal(pthread_cond_t *cond);

参数解释

  • cond:指向需要发送信号的条件变量的指针。

返回值

  • 如果函数成功,返回0。
  • 如果失败,将返回一个错误码(非零值)。

使用说明

  1. 在调用pthread_cond_signalpthread_cond_broadcast之前,通常需要锁定与条件变量相关联的互斥锁。
  2. 调用这些函数后,互斥锁可以被释放,以便唤醒的线程可以继续执行。
  3. 唤醒的线程将尝试重新获取互斥锁,一旦获取成功,它们就可以检查条件是否满足并继续执行。

示例代码

// 假设已经声明并初始化了cond和mutex
pthread_mutex_lock(&mutex);
// 更新条件并可能修改共享资源
condition_met = 1;
// 唤醒所有等待cond的线程
pthread_cond_broadcast(&cond);
// 或者,只唤醒至少一个等待cond的线程
// pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

在这个示例中,线程首先锁定互斥锁mutex,然后更新条件变量相关的条件。之后,使用pthread_cond_broadcastpthread_cond_signal来唤醒等待该条件变量的线程。最后,线程解锁互斥锁。

(4)销毁条件变量

销毁条件变量是指在条件变量不再需要时,释放它所占用的资源。在POSIX线程(pthreads)库中,可以使用 pthread_cond_destroy 函数来销毁一个条件变量。

pthread_cond_destroy 函数的原型

int pthread_cond_destroy(pthread_cond_t *cond);

参数解释

  • cond:指向需要销毁的条件变量的指针。

返回值

  • 如果函数成功,返回0。
  • 如果失败,将返回一个错误码(非零值)。

使用说明

  1. 在调用 pthread_cond_destroy 之前,必须确保没有线程正在等待或即将等待条件变量。否则,行为是未定义的,并且可能会导致程序崩溃或其他错误。
  2. 通常,在动态初始化的条件变量不再需要时调用 pthread_cond_destroy。对于静态初始化的条件变量,如果没有分配额外的资源,则可以不调用 pthread_cond_destroy
  3. 一旦条件变量被销毁,你应该避免再次使用它,除非它被重新初始化。

示例代码

// 假设 cond 是一个之前已经初始化的条件变量
int ret = pthread_cond_destroy(&cond);
if (ret != 0) 
{// 错误处理
}

在这个示例中,cond 是一个先前已经初始化并且现在不再需要的条件变量。通过调用 pthread_cond_destroy 来销毁它,从而释放可能分配的资源。如果销毁过程中出现错误,可以根据返回的错误码进行相应的错误处理。

2. 条件变量使用规范

条件变量的运行机制基于两个主要操作:等待(wait)和通知(signal/broadcast)

(1)条件变量的使用流程

  1. 等待条件(Waiting for a Condition):

    • 线程首先获取与条件变量关联的互斥锁。
    • 线程检查某个条件是否满足。如果条件不满足,线程将进入等待状态,并且原子地释放互斥锁,这样其他线程就可以获取互斥锁来更改条件。
    • 当条件变量收到通知后,线程被唤醒,重新尝试获取互斥锁。一旦获取到锁,线程将再次检查条件是否满足,以防在等待期间条件发生了变化。
  2. 通知等待线程(Notifying Waiting Threads):

    • 另一个线程在更改了条件后,会获取相同的互斥锁。
    • 在保持互斥锁的情况下,该线程更新条件。
    • 更新完毕后,线程通过条件变量发送通知,表示条件已经改变。通知操作有两种形式:
      1. signal:唤醒至少一个等待该条件变量的线程。
      2. broadcast:唤醒所有等待该条件变量的线程。
  3. 重新检查条件(Rechecking the Condition):

    • 被唤醒的线程在从等待状态返回时需要重新获得之前释放的互斥锁。
    • 一旦锁被重新获得,线程应该再次检查条件,因为在多个线程等待相同条件的情况下,条件可能已经再次变为假。

(2)条件变量的使用注意事项

  • 使用条件变量时,应当始终与互斥锁配合使用,以防止竞态条件。
  • 必须在修改条件之前获取互斥锁,并在修改完毕后释放互斥锁。
  • 在等待条件变量时,程序应该处于循环中检查条件,即使被signalbroadcast唤醒,也应重新检查条件是否真正满足。
  • 条件变量的等待和通知操作必须在同一个互斥锁保护下进行,以确保数据的一致性。

3. 使用条件变量的示例

#include <pthread.h>
#include <stdio.h>// 定义全局的条件变量和互斥锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *thread_function(void *arg) 
{// 获取互斥锁pthread_mutex_lock(&mutex);// 等待条件变量pthread_cond_wait(&cond, &mutex);// 做一些工作...printf("Received signal\n");// 释放互斥锁pthread_mutex_unlock(&mutex);return NULL;
}int main() 
{pthread_t thread_id;// 创建新线程pthread_create(&thread_id, NULL, thread_function, NULL);// 做一些工作...sleep(1); // 等待一段时间,模拟工作// 发送信号给等待的线程pthread_cond_signal(&cond);// 等待线程结束pthread_join(thread_id, NULL);// 清理资源pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;
}

这个示例中,主线程创建了一个新线程,并通过条件变量发送信号。新线程在接收到信号后开始执行打印操作。

三、线程安全

1. 概念

线程安全指的是当多个线程同时访问某个功能、对象或变量时,系统能够确保这个功能、对象或变量仍然能够如预期般正常工作。具体来说,一个线程安全的程序对于并发访问没有任何副作用,不会出现数据竞争、死锁等问题,可以正确地处理多线程同时访问的情况。

2. 常见的线程不安全的情况

线程不安全的情况通常发生在多个线程并发访问共享资源时,由于缺乏适当的同步或互斥机制,导致出现意外的结果。以下是一些常见的线程不安全的情况:

  1. 竞态条件(Race Conditions):当多个线程试图同时访问和修改共享的数据,而没有足够的同步保护时,会导致竞态条件。这可能导致数据损坏或不一致的结果。

  2. 数据竞争(Data Races):当至少两个线程同时访问相同的内存位置,其中至少一个是写操作时,且没有适当的同步时,就会发生数据竞争。这可能导致未定义的行为和程序崩溃。

  3. 死锁(Deadlock):当两个或多个线程互相持有对方所需的资源,并且在等待对方释放资源时都不释放自己的资源时,就会产生死锁。这将导致多个线程永远无法继续执行。

  4. 活锁(Livelock):类似于死锁,但线程们不断重试某个操作,却始终无法取得进展,导致系统无法正常工作。

  5. 非原子操作:当一个操作需要多个步骤完成,而这些步骤中间被其他线程打断,可能导致数据状态处于不一致的状态。

  6. 资源泄露:当线程在使用完资源后没有正确释放,导致资源泄露,可能最终耗尽系统资源。

  7. 不一致的状态:当多个线程并发修改共享状态时,由于缺乏同步机制,可能导致状态变得不一致,违反程序的预期行为。

以上情况都代表了典型的线程不安全问题,编写多线程程序时需要格外注意避免这些问题的发生。为了解决这些问题,可以使用锁、原子操作、条件变量等同步机制来确保线程安全,以及遵循良好的并发编程实践。

3. 常见的线程安全的情况

  1. 不可变对象(Immutable Objects):不可变对象在创建后无法被修改,因此多个线程同时访问不会引发线程安全问题。例如,Java中的String类就是不可变对象。

  2. 线程本地存储(Thread-Local Storage):每个线程都有自己独立的变量副本,不会被其他线程共享,从而避免了线程安全问题。可以使用ThreadLocal类来实现线程本地存储。

  3. 局部变量(Local Variables):局部变量是在每个线程的栈帧中创建的,每个线程拥有自己的副本,不存在线程安全问题。

  4. 同步容器(Synchronized Containers):某些容器类(如Vector、Hashtable)提供了内部同步机制,可以安全地在多线程环境下使用。这些容器会确保对它们的操作是原子的,并且提供了线程安全的迭代器。

  5. 并发容器(Concurrent Containers):Java中的ConcurrentHashMap、ConcurrentLinkedQueue等并发容器提供了高效的线程安全操作。它们使用了复杂的算法和数据结构来实现高性能的并发访问。

  6. 使用互斥锁(Mutex)或同步机制:通过在多个线程访问共享资源时使用互斥锁、读写锁等同步机制,可以保证线程安全。这样在任意时刻只有一个线程能够访问共享资源。

  7. 原子操作(Atomic Operations):某些编程语言提供了原子操作,这些操作是不可中断的,可以保证在多线程环境下的原子性。例如,Java中的AtomicInteger类提供了原子操作的整型变量。

  8. 使用并发编程库和框架:一些现代编程语言和框架提供了丰富的并发编程工具和库,如Java中的java.util.concurrent包,可以更方便地实现线程安全。

4. 可重入与线程安全的关系(八股文)

(1)可重入与线程安全的联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

(2)可重入与线程安全的区别

  • 可重入函数是线程安全函数的一种。
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

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

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

相关文章

Steampipe的安装部署及简单使用(附带AWS CLI的安装与使用)

介绍 Steampipe 将 API 和服务公开为高性能关系数据库&#xff0c;使您能够编写基于 SQL 的查询来探索动态数据。Mods 通过使用简单 HCL 构建的仪表板、报告和控件扩展了 Steampipe 的功能。 官网&#xff1a;https://steampipe.io/ steampipe的安装 下载脚本并执行 sudo /…

【Linux】cat 命令使用

cat 命令 cat&#xff08;英文全拼&#xff1a;concatenate&#xff09;命令用于连接文件并打印到标准输出设备上。 可以使用cat连接多个文件、创建新文件、将内容附加到现有文件、查看文件内容以及重定向终端或文件中的输出。 cat可用于在不同选项的帮助下格式化文件的输出…

LV.13 D1 嵌入式系统移植导学 学习笔记

一、嵌入式系统分层 操作系统&#xff1a;向下管理硬件、向上提供接口 操作系统为我们提供了&#xff1a; 1.进程管理 2.内存管理 3.网络接口 4.文件系统 5.设备管理 那系统移植是干什么呢&#xff1f; 就是将Linux操作系统移植到基于ARM处理器的开发板中。 那为什么要移植系…

【calcitonin ; 降钙素 ;降钙素原】

Parathyroid_Hormone -甲状旁腺激素 PTH &#xff1b; 特立帕肽&#xff1b;

【SQL开发实战技巧】系列(四十八):Oracle12C常用新特性☞多分区操作和管理

系列文章目录 【SQL开发实战技巧】系列&#xff08;一&#xff09;:关于SQL不得不说的那些事 【SQL开发实战技巧】系列&#xff08;二&#xff09;&#xff1a;简单单表查询 【SQL开发实战技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL开发实战技巧…

K8s构建的mysql无法远程连接

最近在写一个老师布置的大作业&#xff0c;都是老师写好的yaml文件&#xff0c;都是没问题的&#xff0c;但是构建的mysql无法远程连接。 尝试了网上的很多方法&#xff0c;都失败了&#xff0c;我的构建过程应该是没什么错误的&#xff0c;所以网上的方法并不奏效&#xff0c…

【小白专用】Sql Server 连接Mysql 更新23.12.09

目标 已知mysql连接参数&#xff08;地址和用户&#xff09;&#xff0c;期望通过Microsoft Sql Server Management Studio &#xff08;以下简称MSSSMS&#xff09;连接Mysql&#xff0c;在MSSSMS中直接查询或修改Mysql中的数据。 一般是选最新的版本下载。 选64位还是32位&a…

C++ 对象的初始化和清理:构造函数和析构函数

目录 构造函数和析构函数 构造函数 析构函数 构造函数的分类及调用 括号法 显示法 隐式转换法 拷贝构造函数的调用时机 使用一个已经创建完毕的对象来初始化一个新对象 值传递的方式给函数参数传值 以值方式返回局部对象 构造函数调用规则 初始化列表 类对象作…

【Java 基础】27 XML 解析

文章目录 1.SAX 解析器1&#xff09;什么是 SAX2&#xff09;SAX 工作流程初始化实现事件处理类解析 3&#xff09;示例代码 2.DOM 解析器1&#xff09;什么是 DOM2&#xff09;DOM 工作流程初始化解析 XML 文档操作 DOM 树 3&#xff09;示例代码 总结 在项目开发中&#xff0…

Jupyter notebook修改背景主题

打开Anaconda Prompt&#xff0c;输入以下内容 1. pip install --upgrade jupyterthemes 下载对应背景主题包 出现Successfully installed jupyterthemes-0.20.0 lesscpy-0.15.1时&#xff0c;说明已经下载安装完成 2. jt -l 查看背景主题列表 3. jt -t 主题名称&#xff08;…

mysql的BIT数值类型

MySQL :: MySQL 8.2 Reference Manual :: 11.1.5 Bit-Value Type - BIT MySQL :: MySQL 8.2 Reference Manual :: 9.1.5 Bit-Value Literals BIT类型用来存放bit值&#xff0c;每一位是0或者1&#xff0c;允许1-64位。 例如&#xff0c;下面表定义了new这列的类型为8位的BIT…

NestJS的微服务实现

1.1 基本概念 微服务基本概念&#xff1a;微服务就是将一个项目拆分成多个服务。举个简单的例子&#xff1a;将网站的登录功能可以拆分出来做成一个服务。 微服务分为提供者和消费者&#xff0c;如上“登录服务”就是一个服务提供者&#xff0c;“网站服务器”就是一个服务消…

Python如何实现数据驱动的接口自动化测试

大家在接口测试的过程中&#xff0c;很多时候会用到对CSV的读取操作&#xff0c;本文主要说明Python3对CSV的写入和读取。下面话不多说了&#xff0c;来一起看看详细的介绍吧。 1、需求 某API&#xff0c;GET方法&#xff0c;token,mobile,email三个参数 token为必填项mobil…

探索人工智能领域——每日20个名词详解【day13】

目录 前言 正文 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如需转载&#xff0c;请事先与我联系以…

Axure网页端高交互组件库, 下拉菜单文件上传穿梭框日期城市选择器

作品说明 组件数量&#xff1a;共 11 套 兼容软件&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;web端原型设计、桌面端原型设计 作品特色 本作品为「web端组件库」&#xff0c;高保真高交互 (带仿真功能效果)&#xff1b;运用了动态面板、中继…

UniGui使用CSSUniTreeMenu滚动条

有些人反应UniTreeMenu当菜单项目比较多的时候会超出但是没有出滚动条&#xff0c;只需要添加如下CSS 老规矩&#xff0c;unitreemeu的layout的componentcls里添加bbtreemenu&#xff0c;然后在css里添加 .bbtreemenu .x-box-item{ overflow-y: auto; } 然后当内容超出后就会…

【数据结构第 6 章 ②】- 用 C 语言实现邻接矩阵

目录 一、邻接矩阵表示法 二、AMGraph.h 三、AMGraph.c 四、Test.c 【数据结构第 6 章 ① 】- 图的定义和基本术语-CSDN博客 由于图的结构比较复杂&#xff0c;任意两个顶点之间都可能存在联系&#xff0c;因此无法以数据元素在存储区中的物理位置来表示元素之间的关系&…

SpringCloud网关介绍

一、Gateway简介 1、官网 上一代zuul 1.X&#xff1a;https://github.com/Netflix/zuul/wiki 当前gateway&#xff1a;https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/ 2、是什么 SpringCloud Gateway是SpringCloud的一个全…

.NET Core 依赖注入 Microsoft.Extensions.DependencyInjection

文章目录 前言什么是依赖注入C# 使用依赖注入框架介绍 Microsoft.Extensions.DependencyInjectionNuget安装简单单例使用打印结果 自动装配举例自动装配测试用例打印结果自动装配执行顺序测试用例有歧义构造函数渐进式构造函数循环依赖 自动装配结论 手动装配手动注入别名注入 …

调用win32 api获取电脑名字和系统目录

学习一下几个函数的功能&#xff0c;和调用方式&#xff1b; void CBasenameView::OnDraw(CDC* pDC) {CBasenameDoc* pDoc GetDocument();ASSERT_VALID(pDoc);// TODO: add draw code for native data hereCString str1;TCHAR myname1[50], myname2[50], mydirname1[50], myd…