Linux 线程同步

本文介绍了线程同步的三种常用机制:互斥量、读写锁和条件变量。详细阐述了这些机制的工作原理、应用场景及使用方法,并提供了具体的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程 的同步, 发生在多个线程共享相同内存的时候, 这时要保证每个线程在每个时刻看到的共享数据是一致的. 如果每个线程使用的变量都是其他线程不会使用的(read & write), 或者变量是只读的, 就不存在一致性问题. 但是, 如果两个或两个以上的线程可以read/write一个变量时, 就需要对线程进行同步, 以确保它们在访问该变量时, 不会得到无效的值, 同时也可以唯一地修改该变量并使它生效.

    以上就是我们所说的线程同步.

    线程同步有三种常用的机制: 互斥量(mutex), 读写锁(rwlock)和条件变量(cond).

    互斥量有两种状态: lock和unlock, 它确保同一时间只有一个线程访问数据;

    读写锁有三种状态: 读加锁, 写加锁, 不加锁, 只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁.

    条件变量则给多个线程提供了一个会合的场所, 与互斥量一起使用时, 允许线程以无竞争的方式等待特定条件的发生.

互斥量

    互斥量从本质上说就是一把锁, 提供对共享资源的保护访问.

1. 初始化:

    在 Linux 下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:

对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init.

对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.

  • 原型:
    • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
    • int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 头文件: <pthread.h>
  • 返回值: 成功则返回0, 出错则返回错误编号.
  • 说明: 如果使用默认的属性初始化互斥量, 只需把attr设为NULL. 其他值在以后讲解.

2. 互斥操作:

    对共享资源的访问, 要对互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁. 在完成了对共享资源的访问后, 要对互斥量进行解锁.

首先说一下加锁函数:

  • 头文件: <pthread.h>
  • 原型:
    • int pthread_mutex_lock(pthread_mutex_t *mutex);
    • int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 返回值: 成功则返回0, 出错则返回错误编号.
  • 说 明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态.

再说一下解所函数:

  • 头文件: <pthread.h>
  • 原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 返回值: 成功则返回0, 出错则返回错误编号.

3. 死锁:

    死锁主要发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生. 如何避免死锁是使用互斥量应该格外注意的东西.

    总体来讲, 有几个不成文的基本原则:

  • 对共享资源操作前一定要获得锁.
  • 完成操作以后一定要释放锁.
  • 尽量短时间地占用锁.
  • 如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC.
  • 线程错误返回时应该释放它所获得的锁.

信号量
  信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加 信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数 pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。
  信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。

  函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
  函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
  函数sem_destroy(sem_t *sem)用来释放信号量sem。
  下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}

/*从文件2.dat读取数据*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}

void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}

int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序过早退出,让它在此无限期等待*/
pthread_join(t1,NULL);
}

  在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11


  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

写锁

    在线程同步系列的第一篇文章里已经说过, 读写锁是因为有3种状态, 所以可以有更高的并行性.

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);

成功则返回0, 出错则返回错误编号.

同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.

 

4. 读和写:

#include < pthread.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);

成功则返回0, 出错则返回错误编号.
这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作, 同样, 非阻塞的函数为:
#include < pthread.h >

int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *
rwlock);

成功则返回0, 出错则返回错误编号.
非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.


条件变量


     条件变量分为两部分: 条件和变量. 条件本身是由互斥量保护的. 线程在改变条件状态前先要锁住互斥量.

1. 初始化:

    条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:

  • 静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量.
  • 动态: pthread_cond_init函数, 是释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.
#include < pthread.h >

int pthread_cond_init(pthread_cond_t * restrict cond, pthread_condattr_t * restrict attr);
int pthread_cond_destroy(pthread_cond_t *
cond);

成功则返回0, 出错则返回错误编号.

    当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量; 非默认情况以后讨论.

 

2. 等待条件:

#include < pthread.h >

int pthread_cond_wait(pthread_cond_t * restrict cond, pthread_mutex_t * restric mutex);
int pthread_cond_timedwait(pthread_cond_t * restrict cond, pthread_mutex_t * restrict mutex, const struct timespec *
restrict timeout);

成功则返回0, 出错则返回错误编号.

    这两个函数分别是阻塞等待和超时等待.

    等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.

    当pthread_cond_wait返回时, 互斥量再次被锁住.

 

3. 通知条件:

#include < pthread.h >

int pthread_cond_signal(pthread_cond_t * cond);
int pthread_cond_broadcast(pthread_cond_t *
cond);

成功则返回0, 出错则返回错误编号.

    这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.

<think>好的,我现在需要回答用户关于Linux环境下线程同步的方法机制的问题。首先,我得回忆一下Linux提供的线程同步机制有哪些。根据之前提供的引用内容,用户提到的引用[1][4]都提到了条件变量,而引用[2][3]还提到了互斥锁、信号量、读写锁等。需要把这些机制都涵盖进去。 首先,我应该列出主要的同步方法,比如互斥锁、条件变量、信号量、读写锁屏障。然后,每个方法需要给出简要的解释,并附上示例代码,这样用户能更直观地理解。同时,根据用户的要求,行内数学公式用$...$,独立公式用$$...$$,但这里可能数学公式不多,主要是代码示例。需要确保代码语法正确,使用中文回答,并在适当的地方添加引用标识。 接下来,我需要检查每个同步机制的正确用法。例如,互斥锁使用pthread_mutex_t,条件变量配合互斥锁使用,信号量有POSIXSystem V两种,但通常推荐POSIX信号量。读写锁用pthread_rwlock_t,屏障用pthread_barrier_t。每个示例代码的结构应该正确,比如初始化、加锁解锁、销毁等步骤。 然后,要注意引用对应的来源。比如,互斥锁条件变量在引用[1]、[2]、[3]、[4]中都有提及,需要合理分配引用标记。例如,条件变量在引用[1][3]中都有提到,所以可能在解释条件变量时加上[^1][^3]。 可能遇到的问题包括如何简明扼要地解释每个机制,同时保持示例代码的正确性。例如,信号量的示例可能需要展示sem_waitsem_post的使用,而屏障则需要展示pthread_barrier_wait的用法。此外,还需要注意线程安全函数的提及,如引用[2]中的可重入函数,但用户的问题主要关注同步机制,所以可能不需要深入讨论线程安全函数。 最后,生成的相关问题需要围绕线程同步的方法展开,比如性能比较、死锁避免、适用场景等。确保问题与用户的问题相关,并且能够引导进一步的学习。 总结步骤: 1. 列出Linux下的线程同步机制:互斥锁、条件变量、信号量、读写锁、屏障。 2. 对每个机制进行简要说明,给出代码示例。 3. 添加对应的引用标记。 4. 生成相关问题。</think>### Linux 环境下线程同步的方法与示例 Linux 提供了多种线程同步机制,用于解决多线程并发访问共享资源时的互斥协作问题。以下是常见方法及其实现示例: --- #### 1. **互斥锁(Mutex)** 通过锁机制保证同一时间只有一个线程访问共享资源。 **示例代码**: ```c #include <pthread.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* thread_func(void* arg) { pthread_mutex_lock(&mutex); // 加锁 // 访问共享资源 pthread_mutex_unlock(&mutex); // 解锁 return NULL; } ``` **特点**:简单高效,但可能引发死锁[^2][^4]。 --- #### 2. **条件变量(Condition Variable)** 用于线程间通信,当某个条件满足时唤醒等待的线程。需与互斥锁配合使用。 **示例代码**: ```c #include <pthread.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; bool flag = false; void* consumer(void* arg) { pthread_mutex_lock(&mutex); while (!flag) { pthread_cond_wait(&cond, &mutex); // 等待条件成立 } // 条件满足后执行操作 pthread_mutex_unlock(&mutex); return NULL; } void* producer(void* arg) { pthread_mutex_lock(&mutex); flag = true; pthread_cond_signal(&cond); // 通知等待线程 pthread_mutex_unlock(&mutex); return NULL; } ``` **应用场景**:生产者-消费者模型。 --- #### 3. **信号量(Semaphore)** 通过计数器控制对共享资源的访问,支持多线程同时访问(但数量受限)。 **示例代码**: ```c #include <semaphore.h> sem_t sem; void* thread_func(void* arg) { sem_wait(&sem); // P操作(计数器减1) // 访问共享资源 sem_post(&sem); // V操作(计数器加1) return NULL; } // 初始化信号量(初始值为2) sem_init(&sem, 0, 2); ``` **特点**:适用于限流场景,如连接池管理。 --- #### 4. **读写锁(Read-Write Lock)** 允许多个读线程同时访问,但写线程独占资源。 **示例代码**: ```c #include <pthread.h> pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; void* reader(void* arg) { pthread_rwlock_rdlock(&rwlock); // 读锁 // 读取共享资源 pthread_rwlock_unlock(&rwlock); return NULL; } void* writer(void* arg) { pthread_rwlock_wrlock(&rwlock); // 写锁 // 修改共享资源 pthread_rwlock_unlock(&rwlock); return NULL; } ``` **适用场景**:读多写少的场景(如缓存系统)。 --- #### 5. **屏障(Barrier)** 同步多个线程,直到所有线程到达某个点后才继续执行。 **示例代码**: ```c #include <pthread.h> pthread_barrier_t barrier; void* thread_func(void* arg) { // 执行部分任务 pthread_barrier_wait(&barrier); // 等待其他线程 // 继续后续任务 return NULL; } // 初始化屏障(等待3个线程) pthread_barrier_init(&barrier, NULL, 3); ``` **应用场景**:并行计算中多阶段任务同步[^4]。 --- ### 对比与选择 | 机制 | 适用场景 | 复杂度 | |--------------|----------------------------|--------| | 互斥锁 | 简单互斥访问 | 低 | | 条件变量 | 线程间条件协作 | 中 | | 信号量 | 限流或资源池管理 | 中 | | 读写锁 | 读多写少场景 | 中 | | 屏障 | 多线程分阶段同步 | 高 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值