C 语言多线程2024 年 8 月 28 日 | 阅读 13 分钟 引言在 C 语言中,**“多线程”** 这个术语指的是**并发**地使用**多个线程**。每个线程执行**不同的任务**。由于多线程的并发特性,可以同时执行许多任务。此外,**多线程**还可以减少**CPU的资源使用**。多任务处理分为两类:**基于进程**和**基于线程**。当描述某事物为多线程时,意味着至少有两个甚至更多的线程在同一进程中同时运行。为了理解 C 语言中的多线程,我们首先必须了解什么是线程和进程。让我们深入探讨这些主题以获得更好的理解。 什么是进程和线程?**线程**是任何进程执行的**基本构建块**。一个程序由多个进程组成,每个进程又由线程组成,线程是更基本的单元。因此,线程可以被认为是进程的基本构建块,或者共同决定 CPU 利用率的更简单的单元。 线程包含以下几项 线程 ID 这是一个在线程创建时生成并为该特定线程保留的**唯一线程 ID**。 程序计数器 这是**硬件加载**的一个值。 一组寄存器 这是一组**通用寄存器**。 堆栈 这是**特定线程**的残留物。 此外,如果两个线程在同一进程中同时工作,它们将共享**代码**、**数据段**以及其他操作系统资源,如文件**打开**和**信号**。传统的进程类型,即重量级进程,可以控制一个线程。然而,多线程的控制能力可以同时打开和执行多个任务。使用线程可以大大提高系统效率,这就是它们有用的原因。 这里解释了 C 语言中**单线程**和**多线程**的区别。首先,这是一个**单线程进程**。因此,整个块 - 包括**代码、数据**等 - 被视为一个进程,该进程只有一个线程。这意味着该技术一次只能完成一项任务。但是,**多线程进程**与之相对。它包含了**代码、堆栈、数据**和**文件**等活动,但它们由多个线程执行,每个线程都有自己的堆栈和寄存器。由于在这种情况下可以同时完成多项任务,因此该进程被称为**多线程进程**。 线程有两种类型 用户级线程 顾名思义,它是在用户级别。内核无法访问其数据。 内核级线程 这种类型的线程是指线程与系统内核和操作系统的关系。 **进程** - 执行程序的步骤序列可以称为**进程**。程序运行时不会立即执行。它被分解成几个基本步骤,这些步骤按顺序以有组织的方式执行,最终导致进程的执行。 分解成较小步骤的进程称为**“克隆进程或子进程”**,而原始进程称为**“父进程”**。在内存中,每个进程使用一定量的空间,该空间不与其他进程共享。 一个过程在执行前会经历一些阶段。 新建 - 在这种情况下,会**生成**一个新的进程。 就绪 - 当一个进程准备好并等待分配处理器时,它处于此状态。 运行 - 当进程正在运行时,它处于此状态。 等待 - 当进程处于此状态时,正在**等待**某个事件发生。 终止 - 这是进程正在执行的状态。 为什么 C 语言是多线程的?C 语言中的**多线程**概念可以通过并行性来增强**应用程序的功能**。考虑在浏览器窗口中打开多个标签页的情况。那么,每个标签页都会并发运行,并可称为**线程**。假设我们使用**Microsoft Excel**,一个线程将处理**文本格式化**,另一个线程将**处理输入**。因此,C 语言的多线程功能可以轻松地一次执行多个任务。线程的创建速度大大加快。线程之间的上下文切换速度更快。此外,线程之间的通信速度更快,并且线程的终止也很简单。 如何编写 C 语言多线程程序?虽然多线程应用程序不是 C 语言的内置功能,但根据操作系统可以实现。**threads.h 标准库**用于在**C 语言**中实现多线程概念。但是,目前没有编译器可以做到这一点。如果我们想在 C 语言中使用多线程,我们必须使用特定于平台的实现,例如**“POSIX”**线程库,通过使用头文件**pthread.h**。**“Pthreads”**是它的另一个名称。可以使用以下方法创建 POSIX 线程: 在这种情况下,**Pthread_create** 创建一个新线程以使其可执行。它允许您在代码中根据需要多次实现 C 语言中的多线程。这里列出了来自早期(的参数及其描述。 线程 这是**子进程返回**的**唯一标识符**。 attr 当我们想设置线程属性时,我们使用这个**不透明的属性**。 start_routine 当**start_routine**被生成时,线程将运行一个例程。 arg **start_routine**接收的参数。如果没有给出参数,将使用**NULL**。 一些 C 语言多线程示例以下是 C 语言中多线程问题的一些示例。 1. 读写者问题一个常见的操作系统进程同步问题是**读者/写者问题**。假设我们有一个数据库,**读者**和**写者**两个不同的用户类别可以访问。**读者**只能**读取**数据库,而**写者**可以读取数据库并更新它。让我们以**IRCTC**为例。如果我们想检查特定的**火车号**的状态,只需在系统中输入火车号即可查看相关的火车信息。此处仅显示网站上存在的信息。这是读操作。但是,如果我们想预订车票,我们必须填写车票预订表,其中包含姓名、年龄等详细信息。因此,我们将在此处执行写操作。IRCTC 数据库将进行一些修改。 问题在于,多人同时尝试访问**IRCTC 数据库**。他们可能是**写者**或**读者**。如果一个读者正在使用数据库,而一个写者同时访问该数据库以处理相同的数据,则会产生问题。当一个写者使用数据库,而一个读者访问与数据库相同的信息时,也会产生另一个问题。第三个问题发生在当一个写者更新数据库,而另一个写者试图更新同一数据库上的数据时。第四种情况发生在两个读者试图检索相同信息时。如果读者和写者使用相同的数据库数据,则所有这些问题都会发生。 信号量是一种用于解决此问题的方法。让我们来看一个如何使用此问题的示例。 读者进程输出 Reader 1 reads data: 0 Reader 2 reads data: 0 Reader 3 reads data: 0 Reader 4 reads data: 0 Reader 5 reads data: 0 说明 在此代码中,我们有共享变量 data 和**读者计数 rc**。**wrt 条件变量**用于限制**写者进程**的访问,而**互斥锁**用于确保对共享数据访问的互斥。 **reader() 函数**代表读者进程。在获取**互斥锁**之前,**读者计数 (rc)** 会增加。如果它是**第一个读者 (rc == 1)**,它会使用 **pthread_cond_wait()** 在 **wrt 条件变量**上等待。因此,在所有读者完成之前,将阻止写者写入。 读者进程在读取共享数据后,会检查它是否是**最后一个读者 (rc == 0)** 并减少读者**计数 (rc--)**。如果是,**pthread_cond_signal()** 会向**wrt 条件变量**发送信号,让等待的写者进程继续。 在**main() 函数**中,我们使用**pthread_create()** 和 **pthread_join() 函数****创建**和**连接**多个读者线程。为每个读者线程分配一个唯一的 ID 以供标识。 写者进程在上一个示例中,与**读者进程**相同,当用户希望访问数据或对象时,会执行一个称为等待操作的操作。之后,新用户将无法访问该对象。并且在用户完成写入后,将对 wrt 执行另一个信号操作。 2. 锁和解锁问题在 C 语言的多线程中,利用**互斥锁**的概念来确保**线程**之间不会发生**竞态条件**。当多个线程同时开始处理相同的数据时,这种情况被称为**竞态**。但是,如果存在这些情况,我们必须这样做。我们使用**互斥锁的 lock()** 和 **unlock() 函数**来为特定线程锁定代码的特定部分。这样,另一个线程就不能开始执行相同的操作。这个受保护的代码区域称为**“临界区/区域”**。在使用共享资源之前,我们在特定区域设置了很多东西,用完之后,我们会再次解锁。 让我们通过一个例子来看看互斥锁在 C 语言多线程中加锁和解锁的操作。 示例 输出 Thread 1: Shared data modified. New value: 1 Thread 2: Shared data modified. New value: 2 Thread 3: Shared data modified. New value: 3 Thread 4: Shared data modified. New value: 4 Thread 5: Shared data modified. New value: 5 说明 在此上述示例中,我们解释了如何**锁定**和**解锁**一段代码,以保护我们免受竞态情况的影响。**'pthread_mutex_t'** 用作上述示例中的**初始化器**。然后,在我们要锁定的代码开始之前**写入** **'pthread_mutex_lock'**。然后完成我们要锁定的代码。之后,使用**'pthread_mutex_unlock'** 结束代码的锁定;此后,将没有任何代码处于锁定模式。 餐桌上的哲学家问题同步的经典问题之一是**哲学家就餐问题**。需要对多个进程进行简单的资源分配,但不应导致**死锁**或**饥饿**。**哲学家就餐问题**可以看作是对多个进程的简单表示,每个进程都要求资源。由于每个进程都需要资源分配,因此有必要将这些资源分配给所有进程,以免任何一个进程卡住或停止工作。 假设有五个哲学家围坐在一个**圆形桌子**旁。他们有时吃饭,有时思考。哲学家们均匀地分布在圆桌旁的椅子上。此外,桌子中间有一碗米饭和五双筷子给每个哲学家。当哲学家觉得她无法与坐在她旁边的同事互动时。 哲学家饿的时候偶尔会拿起两双筷子。她从她**左边**和**右边**的邻居那里拿起筷子,这些筷子就在她附近。但哲学家一次最多只能拿起一双筷子。她显然无法拿起邻居正在使用的筷子。 示例 让我们用一个例子来演示如何在 C 语言中实现这一点。 输出 Philosopher 0 is thinking. Philosopher 1 is thinking. Philosopher 2 is thinking. Philosopher 3 is thinking. Philosopher 4 is thinking. Philosopher 0 is eating. Philosopher 1 is eating. Philosopher 2 is eating. Philosopher 3 is eating. Philosopher 4 is eating. Philosopher 0 Finished eating Philosopher 1 Finished eating Philosopher 2 Finished eating Philosopher 3 Finished eating Philosopher 4 Finished eating 说明 **筷子**可以用信号量表示。由于桌子上有**筷子**,没有哲学家选择筷子,所有筷子的分量都初始化为**1**。现在,**chopstick[i]** 被选作第一双**筷子**。**chopstick[i]** 和 **chopstick[(i+1)%5]** 都将执行第一个等待操作。这些**筷子的等待操作**表示哲学家已经拿起它们。一旦哲学家选择了他的**筷子**,就开始进食。当哲学家吃完后,现在对**chopsticks [i]** 和 **[(i+1)%5]** 执行信号操作。然后哲学家继续睡觉。 为了确定**子线程**是否已加入主线程,我们使用了**pthread_join 函数**。同样,我们使用 **pthread_mutex_init** 方法检查了**互斥锁**是否已初始化。 为了初始化并检查新线程是否已创建,我们使用了 **pthread_create 函数**。类似地,我们使用 **pthread_mutex_destroy** 函数销毁了**互斥锁**。 生产者-消费者问题多线程进程同步中的一个常见问题是**生产者-消费者问题**。其中有两个进程:第一个是**生产者进程**,第二个是**消费者进程**。此外,假设这两个操作都在并发并行地进行。此外,它们是协作过程,这意味着它们在彼此之间共享某些东西。重要的是,当缓冲区**满**时,生产者不能添加数据。当缓冲区为空时,消费者不能从缓冲区提取数据,因为生产者和消费者之间的公共缓冲区大小是**固定的**。问题就是这样陈述的。因此,为了实现并解决生产者-消费者问题,我们将采用并行编程的思路。 示例 输出 1. producer 2. consumer 3. for exit Please enter your choice: 说明 我们执行两项任务。**consumer()** 和 **producer()** 函数指示**消费者**和**生产者**的状态和操作。调用**producer() 方法**时,它将创建**互斥锁**并检查缓冲区是否**已满**。当缓冲区已满时,将不会生产任何东西。如果不是,它将**创建**,然后,在**生产**之后,它将使自己休眠以解锁**互斥锁**。与**生产者**一样,消费者首先创建**互斥锁**,检查**缓冲区**,消费**产品**,然后释放锁后再进入休眠状态。 在生产过程中将使用**计数器 (x)**,并且它将持续增长,直到制造商生产出产品。然而,消费者将减少相同制造的**产品 (x)** 的数量。 结论使用**两个**或**多个线程**执行程序的思想在 C 编程语言中称为**多线程**。**多线程**允许同时执行多个任务。程序最简单的可执行组件是**线程**。进程是将任务分解成多个**子进程**来完成任务的思想。 为了在 C 语言中实现多线程,需要 **pthread.h** 头文件,因为不能直接实现。 下一个主题C语言中的数字模式程序 |
我们请求您订阅我们的新闻通讯以获取最新更新。