🔍前言:在当今这个数据驱动、高性能计算盛行的时代,多线程编程已经成为软件开发中不可或缺的一部分。Linux,作为开源世界的领头羊,其强大的多线程支持为开发者提供了广阔的舞台,让高并发、高性能的应用得以实现。然而,多线程编程也是一把双刃剑,它在带来性能提升的同时,也引入了线程安全、资源竞争等复杂问题
线程互斥与同步,正是解决这些问题、确保多线程程序正确运行的关键技术。它们如同一道坚固的防线,守护着程序的并发性,防止数据被意外篡改,确保资源被公平、高效地利用
本文旨在深入探讨Linux多线程编程中的线程互斥与同步机制。我们将从基本概念出发,逐步揭示互斥锁、条件变量、信号量等同步原语的工作原理和实际应用。通过生动的示例和详实的分析,帮助读者理解这些技术背后的原理,掌握如何在Linux环境下正确使用它们来构建健壮、高效的多线程应用
让我们一同踏上这段探索之旅,揭开Linux多线程编程中线程互斥与同步的神秘面纱!
线程封装是指将线程相关的操作和功能封装成一个独立的实体或模块,以便更方便地管理和使用线程,在学习完上节课的线程控制后,我们其实已经可以自己封装一下线程,来对线程进行简单的封装并不算难,我就不细讲嘞
使用线程类:
线程互斥(Thread Mutex,或称为互斥锁,Mutex)是多线程编程中用于防止多个线程同时访问共享资源的一种机制。通过互斥锁,可以确保同一时刻只有一个线程能够操作某个共享资源,从而避免数据竞争和不一致性问题
临界资源
:多线程执行流共享的资源就叫做临界资源临界区
:每个线程内部,访问临界资源的代码,就叫做临界区互斥
:任何时刻,互斥保证有且只有一个执行流进入临界区
,访问临界资源,通常对临界资源起保护作用原子性
:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成代码示例:
// 获取名字
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 10000;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 )
{
if ( ticket > 0 )
{
usleep(1000);
printf("%s get a ticket: %d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
本来我们是让4个线程一起去抢票,但是这里就出现问题了,我们把票抢成了负数,为什么导致这样的结果,我们来一探究竟
--ticket
操作本身就不是一个原子操作我们在--ticket
的时候,编译器并不是一步完成的,转到汇编后我们可以发现,这个动作其实是有三条汇编代码的
// 取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
--
操作并不是原子操作,而是对应三条汇编指令:
load
:将共享变量ticket从内存加载到寄存器中update
: 更新寄存器里面的值,执行-1操作store
:将新值,从寄存器写回共享变量ticket的内存地址解决方案:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量!
互斥量用于多线程编程中的同步机制,用于防止多个线程同时访问共享资源,从而避免数据竞争和不一致性。其主要目的是保证在任何时刻,只有一个线程可以访问特定的资源或代码段
互斥锁的工作原理:
初始化
:在互斥锁被使用之前,需要对其进行初始化。这通常是在创建线程之前完成的加锁(Lock)
:当一个线程需要访问共享资源时,它会尝试获取互斥锁。如果锁当前未被其他线程持有,则该线程将成功获取锁,并继续执行其后续代码。如果锁已被其他线程持有,则该线程将被阻塞,直到锁被释放为止临界区
:持有互斥锁的线程可以安全地访问共享资源或执行临界区代码。临界区是指那些需要同步访问的代码段解锁(Unlock)
:当线程完成对共享资源的访问后,它会释放互斥锁。这允许其他被阻塞的线程获取锁并访问共享资源销毁
:在不再需要互斥锁时,可以将其销毁。这通常是在程序结束或线程终止时进行的初始化互斥量:
方法1 -> 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER // 全部变量,并且已经初始化
方法2 -> 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
constpthread_mutexattr_t *restrict attr);
//参数:
//mutex:要初始化的互斥量
//attr:NULL
销毁互斥量:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量不需要销毁互斥量加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
代码示例:(改进后)
// 获取名字
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 10000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 )
{
pthread_mutex_lock(&mutex); // 加锁
if ( ticket > 0 )
{
usleep(1000);
printf("%s get a 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_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);
return 0;
}
注意:在改进之后,centos
系统下可能会出现一个线程将全部票抢完的情况,而ubuntu
则是雨露均沾,每个线程都会抢一部分票,这些情况都是正常的
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
死锁是计算机操作系统和并发编程中的一个重要概念,它指的是两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用,这些进程都将无法继续执行下去,此时称系统处于死锁状态,这些永远在互相等待的进程则被称为死锁进程
代码示例:(简单死锁)
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 )
{
pthread_mutex_lock(&mutex); // 加锁
if ( ticket > 0 )
{
usleep(1000);
printf("%s get a ticket: %d\n", id, ticket);
ticket--;
pthread_mutex_lock(&mutex); // 加锁
}
else
{
pthread_mutex_lock(&mutex); // 加锁
break;
}
}
}
死锁四个必要条件:
互斥条件
:一个资源每次只能被一个执行流使用请求与保持条件
:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件
:一个执行流已获得的资源,在末使用完之前,不能强行剥夺循环等待条件
:若干执行流之间形成一种头尾相接的循环等待资源的关系避免死锁:
概念:
线程安全
:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题重入
:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数常见的线程不安全的情况:
常见的线程安全的情况:
常见不可重入的情况:
常见可重入的情况:
可重入与线程安全联系与区别:
联系
区别
线程同步的核心目的是保证多个线程能够按照某种预定顺序或条件来访问和操作共享资源,从而避免数据竞争、死锁和优先级反转等问题,确保程序的一致性和正确性
同步概念与竞态条件:
同步
:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步竞态条件
:因为时序问题,而导致程序异常,我们称之为竞态条件。条件变量:
条件变量(Condition Variable)是线程同步中的一种机制,用于协调多个线程之间的执行顺序。它通常与互斥锁(Mutex)一起使用,以实现对共享资源的有效访问和控制。条件变量的核心作用是允许一个或多个线程在某些条件未满足时等待,并在条件满足时被唤醒继续执行
条件变量的主要特性:
初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//参数:
//cond:要初始化的条件变量
//attr:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//参数:
//cond:要在这个条件变量上等待
//mutex:互斥量
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{
while ( 1 )
{
pthread_cond_wait(&cond, &mutex);
printf("活动\n");
}
}
void *r2(void *arg )
{
while ( 1 )
{
pthread_cond_signal(&cond);
sleep(1);
}
}
int main( void )
{
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, r1, NULL);
pthread_create(&t2, NULL, r2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
在探索Linux多线程编程的旅程中,我们深入了解了线程互斥与同步的重要性及其实现机制。从互斥锁的基本使用,到条件变量和信号量的灵活运用,每一步都见证了我们对并发控制技术的深刻理解和实践能力的提升
回顾这段学习经历,我们不难发现,线程互斥与同步不仅是多线程编程中的核心难点,更是确保程序稳定性和性能的关键所在。通过合理的互斥控制和精确的同步协调,我们能够有效地避免数据竞争和死锁等并发问题,从而构建出更加健壮、高效的多线程应用
然而,学习之路永无止境。随着技术的不断进步和需求的不断变化,Linux多线程编程领域也将持续演进。因此,我们不仅要掌握现有的互斥与同步技术,更要保持对新技术和新方法的敏锐洞察,以便在未来的挑战中立于不败之地
愿你在未来的编程实践中,能够灵活运用这些技术,创造出更加精彩、更加高效的并发应用。让我们携手共进,共同迎接更加辉煌的编程未来!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行! 谢谢大家支持本篇到这里就结束了,祝大家天天开心