湖北高企达建设有限公司网站,中国铁路总公司建设管理部网站,软件外包企业排名,网站关键词符号阅读导航 引言一、线程同步1. 竞态条件的概念2. 线程同步的概念 二、条件变量1. 条件变量函数⭕使用前提#xff08;1#xff09;初始化条件变量#xff08;2#xff09;等待条件满足#xff08;3#xff09;唤醒等待pthread_cond_broadcast()pthread_cond_signal() 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.hpthread.h 头文件中定义了所有与POSIX线程相关的数据类型、函数原型和宏。这包括了条件变量的操作函数、互斥锁的操作函数以及线程创建和控制的函数。
当编译使用了 pthread.h 的程序时通常需要链接线程库这可以通过在编译命令中添加 -lpthread 选项来实现。例如
gcc program.c -o program -lpthread这条命令会编译 program.c 文件并将POSIX线程库链接到生成的可执行文件 program 中。
1初始化条件变量
在POSIX线程pthreads库中条件变量可以通过两种方式进行初始化 静态初始化使用预定义的宏 PTHREAD_COND_INITIALIZER 来初始化条件变量。这是在程序开始执行之前即编译时期就已经完成的初始化。 示例代码 pthread_cond_t cond PTHREAD_COND_INITIALIZER;动态初始化使用函数 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。如果失败将返回一个错误码非零值。
使用说明
线程在调用pthread_cond_wait之前必须确保已经锁定了mutex互斥锁。调用pthread_cond_wait后线程会阻塞并且mutex互斥锁被自动释放以允许其他线程操作条件和互斥锁。当其他线程对条件变量调用pthread_cond_signal或pthread_cond_broadcast时等待的线程会被唤醒。被唤醒的线程在返回前将重新获取mutex互斥锁。这意味着当pthread_cond_wait返回时线程已经再次锁定了mutex。因为可能有多个线程在等待同一个条件变量所以即使线程被唤醒也不能假设条件已经满足。通常需要在循环中调用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_broadcast 和 pthread_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。如果失败将返回一个错误码非零值。
使用说明
在调用pthread_cond_signal或pthread_cond_broadcast之前通常需要锁定与条件变量相关联的互斥锁。调用这些函数后互斥锁可以被释放以便唤醒的线程可以继续执行。唤醒的线程将尝试重新获取互斥锁一旦获取成功它们就可以检查条件是否满足并继续执行。
示例代码
// 假设已经声明并初始化了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_broadcast或pthread_cond_signal来唤醒等待该条件变量的线程。最后线程解锁互斥锁。
4销毁条件变量
销毁条件变量是指在条件变量不再需要时释放它所占用的资源。在POSIX线程pthreads库中可以使用 pthread_cond_destroy 函数来销毁一个条件变量。
pthread_cond_destroy 函数的原型
int pthread_cond_destroy(pthread_cond_t *cond);参数解释
cond指向需要销毁的条件变量的指针。
返回值
如果函数成功返回0。如果失败将返回一个错误码非零值。
使用说明
在调用 pthread_cond_destroy 之前必须确保没有线程正在等待或即将等待条件变量。否则行为是未定义的并且可能会导致程序崩溃或其他错误。通常在动态初始化的条件变量不再需要时调用 pthread_cond_destroy。对于静态初始化的条件变量如果没有分配额外的资源则可以不调用 pthread_cond_destroy。一旦条件变量被销毁你应该避免再次使用它除非它被重新初始化。
示例代码
// 假设 cond 是一个之前已经初始化的条件变量
int ret pthread_cond_destroy(cond);
if (ret ! 0)
{// 错误处理
}在这个示例中cond 是一个先前已经初始化并且现在不再需要的条件变量。通过调用 pthread_cond_destroy 来销毁它从而释放可能分配的资源。如果销毁过程中出现错误可以根据返回的错误码进行相应的错误处理。
2. 条件变量使用规范
条件变量的运行机制基于两个主要操作等待wait和通知signal/broadcast。
1条件变量的使用流程 等待条件Waiting for a Condition: 线程首先获取与条件变量关联的互斥锁。线程检查某个条件是否满足。如果条件不满足线程将进入等待状态并且原子地释放互斥锁这样其他线程就可以获取互斥锁来更改条件。当条件变量收到通知后线程被唤醒重新尝试获取互斥锁。一旦获取到锁线程将再次检查条件是否满足以防在等待期间条件发生了变化。 通知等待线程Notifying Waiting Threads: 另一个线程在更改了条件后会获取相同的互斥锁。在保持互斥锁的情况下该线程更新条件。更新完毕后线程通过条件变量发送通知表示条件已经改变。通知操作有两种形式 signal唤醒至少一个等待该条件变量的线程。broadcast唤醒所有等待该条件变量的线程。 重新检查条件Rechecking the Condition: 被唤醒的线程在从等待状态返回时需要重新获得之前释放的互斥锁。一旦锁被重新获得线程应该再次检查条件因为在多个线程等待相同条件的情况下条件可能已经再次变为假。
2条件变量的使用注意事项
使用条件变量时应当始终与互斥锁配合使用以防止竞态条件。必须在修改条件之前获取互斥锁并在修改完毕后释放互斥锁。在等待条件变量时程序应该处于循环中检查条件即使被signal或broadcast唤醒也应重新检查条件是否真正满足。条件变量的等待和通知操作必须在同一个互斥锁保护下进行以确保数据的一致性。
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. 常见的线程不安全的情况
线程不安全的情况通常发生在多个线程并发访问共享资源时由于缺乏适当的同步或互斥机制导致出现意外的结果。以下是一些常见的线程不安全的情况 竞态条件Race Conditions当多个线程试图同时访问和修改共享的数据而没有足够的同步保护时会导致竞态条件。这可能导致数据损坏或不一致的结果。 数据竞争Data Races当至少两个线程同时访问相同的内存位置其中至少一个是写操作时且没有适当的同步时就会发生数据竞争。这可能导致未定义的行为和程序崩溃。 死锁Deadlock当两个或多个线程互相持有对方所需的资源并且在等待对方释放资源时都不释放自己的资源时就会产生死锁。这将导致多个线程永远无法继续执行。 活锁Livelock类似于死锁但线程们不断重试某个操作却始终无法取得进展导致系统无法正常工作。 非原子操作当一个操作需要多个步骤完成而这些步骤中间被其他线程打断可能导致数据状态处于不一致的状态。 资源泄露当线程在使用完资源后没有正确释放导致资源泄露可能最终耗尽系统资源。 不一致的状态当多个线程并发修改共享状态时由于缺乏同步机制可能导致状态变得不一致违反程序的预期行为。
以上情况都代表了典型的线程不安全问题编写多线程程序时需要格外注意避免这些问题的发生。为了解决这些问题可以使用锁、原子操作、条件变量等同步机制来确保线程安全以及遵循良好的并发编程实践。
3. 常见的线程安全的情况 不可变对象Immutable Objects不可变对象在创建后无法被修改因此多个线程同时访问不会引发线程安全问题。例如Java中的String类就是不可变对象。 线程本地存储Thread-Local Storage每个线程都有自己独立的变量副本不会被其他线程共享从而避免了线程安全问题。可以使用ThreadLocal类来实现线程本地存储。 局部变量Local Variables局部变量是在每个线程的栈帧中创建的每个线程拥有自己的副本不存在线程安全问题。 同步容器Synchronized Containers某些容器类如Vector、Hashtable提供了内部同步机制可以安全地在多线程环境下使用。这些容器会确保对它们的操作是原子的并且提供了线程安全的迭代器。 并发容器Concurrent ContainersJava中的ConcurrentHashMap、ConcurrentLinkedQueue等并发容器提供了高效的线程安全操作。它们使用了复杂的算法和数据结构来实现高性能的并发访问。 使用互斥锁Mutex或同步机制通过在多个线程访问共享资源时使用互斥锁、读写锁等同步机制可以保证线程安全。这样在任意时刻只有一个线程能够访问共享资源。 原子操作Atomic Operations某些编程语言提供了原子操作这些操作是不可中断的可以保证在多线程环境下的原子性。例如Java中的AtomicInteger类提供了原子操作的整型变量。 使用并发编程库和框架一些现代编程语言和框架提供了丰富的并发编程工具和库如Java中的java.util.concurrent包可以更方便地实现线程安全。
4. 可重入与线程安全的关系八股文
1可重入与线程安全的联系
函数是可重入的那就是线程安全的。函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题。如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
2可重入与线程安全的区别
可重入函数是线程安全函数的一种。线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
温馨提示
感谢您对博主文章的关注与支持如果您喜欢这篇文章可以点赞、评论和分享给您的同学这将对我提供巨大的鼓励和支持。另外我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新不要错过任何精彩内容
再次感谢您的支持和关注。我们期待与您建立更紧密的互动共同探索Linux、C、算法和编程的奥秘。祝您生活愉快排便顺畅