linux线程间同步(一)

1. 线程同步概念

假设有4个线程A、B、C、D,当前一个线程A对内存中的共享资源进行访问的时候,其他线程B, C, D都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,B,C,D中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

1.1 为什么要同步

线程需要分时复用CPU时间片,并且如果测试程序中线程的CPU时间片没用完就被迫挂起了,这样就能让CPU的上下文切换(保存当前状态, 下一次继续运行的时候需要加载保存的状态)更加频繁,更容易再现数据混乱的这个现象。

1.2 同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁读写锁条件变量信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源

找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区。确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:
在临界区代码的上边,添加加锁函数,对临界区加锁。

  • 哪个线程调用这句代码,就会把这把锁锁上,其他线程就只能阻塞在锁上了。
  • 在临界区代码的下边,添加解锁函数,对临界区解锁。
  • 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入到临界区了。
  • 通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问了。

2. 互斥锁

2.1 互斥锁函数

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块, 被锁定的这个代码块, 所有的线程只能顺序执行(不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
在Linux中互斥锁的类型为pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:pthread_mutex_t mutex;

在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关

Linux 提供的互斥锁操作函数如下,如果函数调用成功会返回0,调用失败会返回相应的错误号:

// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源            
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:

  • mutex: 互斥锁变量的地址
  • attr: 互斥锁的属性, 一般使用默认属性即可, 这个参数指定为NULL
    修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
  • int pthread_mutex_lock(pthread_mutex_t *mutex);

这个函数被调用, 首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:

  • 没有被锁定, 是打开的, 这个线程可以加锁成功, 这个这个锁中会记录是哪个线程加锁成功了
  • 如果被锁定了, 其他线程加锁就失败了, 这些线程都会阻塞在这把锁上
  • 当这把锁被解开之后, 这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

调用这个函数对互斥锁变量加锁还是有两种情况:

  • 如果这把锁没有被锁定是打开的,线程加锁成功
  • 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

不是所有的线程都可以对互斥锁解锁,哪个线程加的锁, 哪个线程才能解锁成功。

2.1 互斥锁使用

我们可以将上面多线程交替数数的例子修改一下,使用互斥锁进行线程同步。两个线程一共操作了同一个全局变量,因此需要添加一互斥锁,来控制这两个线程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 100
// 全局变量
int number;

// 创建一把互斥锁
// 全局变量, 多个线程共享
pthread_mutex_t mutex;

// 线程处理函数
void* funcA_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        // 如果线程A加锁成功, 不阻塞
        // 如果B加锁成功, 线程A阻塞
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        pthread_mutex_unlock(&mutex);
        printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
    }

    return NULL;
}

void* funcB_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        // a加锁成功, b线程访问这把锁的时候是锁定的
        // 线程B先阻塞, a线程解锁之后阻塞解除
        // 线程B加锁成功了
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        pthread_mutex_unlock(&mutex);
        printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
        usleep(5);
    }

    return NULL;
}

int main(int argc, const char* argv[])
{
    pthread_t p1, p2;

    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建两个子线程
    pthread_create(&p1, NULL, funcA_num, NULL);
    pthread_create(&p2, NULL, funcB_num, NULL);

    // 阻塞,资源回收
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // 销毁互斥锁
    // 线程销毁之后, 再去释放互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

3. 死锁

当多个线程访问共享资源, 需要加锁, 如果锁使用不当, 就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。
造成死锁的场景有如下几种:

  • 加锁之后忘记解锁
// 场景1
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        // 忘记解锁
    }
}

// 场景2
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        if(xxx)
        {
            // 函数退出, 没有解锁(解锁函数无法被执行了)
            return ;
        }
        
        pthread_mutex_lock(&mutex);
    }
}

重复加锁, 造成死锁

void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A线程阻塞
        pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

// 隐藏的比较深的情况
void funcA()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

void funcB()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        funcA();		// 重复加锁
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

在程序中有多个共享资源, 因此有很多把锁,随意加锁,导致相互被阻塞
场景描述:

  1. 有两个共享资源:X, Y,X对应锁A, Y对应锁B
    • 线程A访问资源X, 加锁A
    • 线程B访问资源Y, 加锁B
  2. 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
    • 线程A被锁B阻塞了, 无法打开A锁
    • 线程B被锁A阻塞了, 无法打开B锁
      在使用多线程编程的时候,如何避免死锁呢?
  • 避免多次锁定, 多检查
  • 对共享资源访问完毕之后, 一定要解锁,或者在加锁的使用 trylock
  • 如果程序中有多把锁, 可以控制对锁的访问顺序(顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。
  • 项目程序中可以引入一些专门用于死锁检测的模块
### 回答1: 线程同步是指多个线程的协调和合作,以保证它们能够正确地访问共享资源。在Linux系统中,线程同步可以通过各种机制来实现,如互斥锁、条件变量、信号量等。这些机制可以确保多个线程的访问顺序和互斥性,从而避免了数据竞争和死锁等问题。线程同步Linux系统中非常重要的一个概念,它对于保证程序的正确性和性能都有着至关重要的作用。 ### 回答2: 在Linux中,线程同步是确保多个线程在访问共享资源时无冲突、无竞争的机制。为了实现线程同步Linux提供了多个同步机制,其中最常用的包括互斥锁、条件变量、读写锁等。 互斥锁是最常用的线程同步机制之一,用于控制对共享资源的访问。在使用互斥锁时,多个线程必须竞争获得锁的访问权,只有获得锁的线程才能访问共享资源。一旦线程完成对共享资源的访问,它就必须释放锁,以便其他线程可以获得访问权。互斥锁的使用可以有效避免多个线程同时访问共享资源而导致的数据不一致的问题。 条件变量也是Linux中用于线程同步的机制之一。条件变量通常与互斥锁一起使用,它可以让线程在特定条件下等待。例如,如果多个线程需要访问共享资源,但只有在该资源可用时才能进行操作,这时就可以使用条件变量来让线程在资源没有准备好时等待。一旦资源可用,条件变量将通知等待的线程来执行相应的操作。 另一个常用的线程同步机制是读写锁。读写锁可以同时允许多个线程在共享资源中读取数据,但在有线程在写入资源时,其他线程都不能同时访问该资源。这种机制的好处是在共享资源中大量读取操作的情况下可以提高并发效率。 总之,在Linux中,线程同步是多线程编程的重要组成部分。为了提高程序的效率和稳定性,开发者要灵活运用不同的线程同步机制,根据不同的应用场景选择合适的机制。 ### 回答3: Linux线程同步机制是指多个线程的协调与保护,使它们能够正确地同时运行而不会互相干扰或产生冲突。在Linux中,有多种线程同步的机制,包括互斥锁、条件变量、读写锁和信号量等。 互斥锁是最基本的一种线程同步机制,它是一种保护共享资源的方法。在任何时候只有一个线程可访问被锁定的资源,其他线程必须等待锁的释放才能访问该资源。互斥锁的实现可以使用pthread_mutex_t结构体,通过pthread_mutex_init、pthread_mutex_lock和pthread_mutex_unlock等函数对互斥锁进行初始化、加锁和解锁操作。 条件变量是另一种线程同步机制,它被用来协调线程的执行。一个条件变量提供了一个线程“等待”的位置,如果条件不满足,线程就会被阻塞,直到其他线程通知该变量满足条件,即pthread_cond_signal或pthread_cond_broadcast函数的调用。条件变量的实现可以使用pthread_cond_t结构体,通过pthread_cond_init、pthread_cond_wait和pthread_cond_signal等函数对条件变量进行初始化、等待和通知操作。 读写锁是一种常用的线程同步机制,它可以提高读取性能。当某个资源被多个线程同时读取时,读写锁允许所有读取该资源的线程同时访问,而当有线程尝试写该资源时,它必须等待所有读取该资源的线程完成后才能获得该资源的写入权限。读写锁的实现可以使用pthread_rwlock_t结构体,通过pthread_rwlock_init、pthread_rwlock_rdlock、pthread_rwlock_wrlock和pthread_rwlock_unlock等函数对读写锁进行初始化、加读锁、加写锁和解锁操作。 信号量是一种更为复杂的线程同步机制,它可以控制多个线程对共享资源的访问。信号量有两种类型,分别是二进制信号量(只能取0或1)和计数信号量(可以取0以上的值),使用信号量时,每个线程必须在访问共享资源前先请求信号量,如果资源已被占用,则线程会被阻塞,直到其他线程释放信号量。信号量的实现可以使用sem_t结构体,通过sem_init、sem_wait、sem_post和sem_destroy等函数对信号量进行初始化、等待、增加和销毁操作。 总之,Linux提供了多种线程同步机制,可以根据实际需要选择合适的机制来协调和保护多个线程的执行,确保程序能够正确地运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值