Linux系统编程(线程同步 互斥锁)

简介: Linux系统编程(线程同步 互斥锁)

前言

本篇文章带大家学习线程的同步。

一、什么是线程同步

线程同步是指协调多个线程之间的执行顺序,以确保共享资源的正确访问和数据的一致性。当多个线程同时操作共享数据时,如果没有适当的同步机制,就会出现数据竞争和不一致的情况。

线程同步的目的是为了保证共享资源在多线程环境下的安全访问,避免数据冲突和并发缺陷。通过使用同步机制,可以使得多个线程按照一定的顺序来访问共享资源,避免出现竞态条件(Race Condition)和不确定性的结果。

常用的线程同步机制包括:

1.互斥锁(Mutex):互斥锁是一种常见的同步机制,用于保护共享资源,一次只允许一个线程访问共享资源。当一个线程获取到互斥锁之后,其他线程必须等待该线程释放锁之后才能继续访问。

2.信号量(Semaphore):信号量是一种用于控制并发访问数量的同步机制。通过设置一个计数器,限制同时访问某个资源的线程数量。当计数器大于0时,线程可以获取许可进行访问,访问后计数器减少;当计数器为0时,线程等待其他线程释放许可。

3.条件变量(Condition Variable):条件变量用于线程之间的条件等待与信号通知。线程可以在某个条件成立时等待条件变量,而其他线程在满足条件时发出信号通知等待线程继续执行。

4.栅栏(Barrier):栅栏用于线程的同步和屏障等待。多个线程在某个位置等待,直到所有线程都到达屏障位置,然后才能继续执行后面的操作。

`线程同步的正确使用可以避免数据竞争和不一致性问题,提高并发程序的正确性和可靠性。然而,不正确或过度的同步机制也可能导致性能问题,因此需要根据具体的应用场景进行合理的同步策略和设计。``

二、不使用线程同步访问共享资源可能出现的问题

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int i = 0;
void* Thread1(void *arg)
{
    while (1)
    {
        i++;
        printf("Thread1 i = %d\n", i);
        sleep(1);//休眠1s
    }
}
void* Thread2(void *arg)
{
    while (1)
    {
        i++;
        printf("Thread2 i = %d\n", i);
        sleep(1);//休眠1s
    }
}
int main(void)
{
    pthread_t tid1;//线程ID
    pthread_t tid2;//线程ID
    int err = -1;
    err = pthread_create(&tid1, NULL, Thread1, NULL);
    if(err < 0)
    {
        printf("create thread1 is err\n");
    }
    err = pthread_create(&tid2, NULL, Thread2, NULL);
    if(err < 0)
    {
        printf("create thread2 is err\n");
    }
    while (1)
    {
        sleep(1);//休眠1s
    }
    return 0;
}

运行结果:

由运行结果可以看出不使用线程同步访问共享资源的话可能导致数据的不一致性。

三、互斥锁概念

互斥锁(Mutex Lock)是一种并发编程中的同步机制,用于保护共享资源的访问,确保在任何给定时间只有一个线程可以执行受保护的代码段。

互斥锁是二进制锁,它有两个状态:锁定(locked)和解锁(unlocked)。当一个线程获取到互斥锁时,它将锁定状态设置为锁定,其他线程尝试获取锁时将被阻塞,直到锁被释放。

互斥锁主要用于解决并发环境下的竞争条件,例如多个线程试图同时访问和修改同一共享资源的情况。通过使用互斥锁,我们可以确保同一时间只有一个线程能够进入临界区(Critical Section),执行对共享资源的访问和操作,避免数据竞争和不一致的结果。

使用互斥锁的基本流程如下:

1.初始化互斥锁:在使用互斥锁前,需要对其进行初始化。

2.获取互斥锁:线程通过调用互斥锁的"锁定"操作,尝试获取互斥锁。如果互斥锁当前处于解锁状态,线程将获取到锁,并将其状态设置为锁定;否则,线程将被阻塞,直到锁被释放。

3.执行临界区代码:一旦线程成功获取到互斥锁,它就可以进入临界区,执行需要保护的代码,访问共享资源。

4.释放互斥锁:线程执行完临界区代码后,应该调用互斥锁的"解锁"操作来释放锁,将互斥锁的状态设置为解锁,以允许其他线程获取锁并访问共享资源。

互斥锁是一种常见的线程同步机制,用于确保共享资源在并发访问时的正确性和一致性。然而,不正确地使用互斥锁可能导致死锁和性能问题,因此,合理的设计和使用是很重要的。

四、互斥锁使用

1.初始化线程锁的方式

静态方法:

静态创建互斥锁意味着在编译时为互斥锁分配静态的内存,并在定义时进行初始化。

在C语言中,可以使用静态初始化宏PTHREAD_MUTEX_INITIALIZER来静态创建并初始化互斥锁。

pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;

动态方法:

动态创建互斥锁意味着在运行时动态分配互斥锁的内存,并使用相关函数进行初始化。

在C语言中,可以使用pthread_mutex_init函数进行动态创建和初始化互斥锁。

示例代码如下:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);

2.使用代码

示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
/*共享变量*/
int val = 0;
pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
void* Thread1(void* arg)
{
    while (1)
    {
        /*加锁*/
        pthread_mutex_lock(&m_mutex);
        val++;
        /*解锁*/
        pthread_mutex_unlock(&m_mutex);
        printf("thread1 val : %d\n", val);
        sleep(1);
    }
}
void* Thread2(void* arg)
{
    while (1)
    {
        /*加锁*/
        pthread_mutex_lock(&m_mutex);
        val++;
        /*解锁*/
        pthread_mutex_unlock(&m_mutex);
        printf("thread2 val : %d\n", val);
        sleep(1);
    }
}
int main(void)
{
    pthread_t tid1;
    pthread_t tid2;   
    pthread_create(&tid1, NULL, Thread1, NULL);
    pthread_create(&tid2, NULL, Thread1, NULL);
    while(1)
    {
        sleep(1);
    }
    return 0;
}

五、死锁的产生和解决方法

1.什么是死锁

死锁是指在并发程序中,两个或多个线程或进程因为互相等待对方释放资源而无法继续执行的情况。在死锁中,每个线程都在等待其他线程释放资源,导致所有线程都被阻塞,无法进行下一步操作。

死锁通常发生在多线程或多进程环境中,涉及共享资源的竞争。当多个线程需要访问共享资源时,如果每个线程都持有一个资源并且等待其他线程释放它所需要的资源,就可能发生死锁。

2.为什么会产生死锁

死锁的产生需要满足以下四个必要条件,也被称为死锁的条件:

1.互斥条件(Mutual Exclusion):一个资源一次只能被一个线程或进程占用,即当一个线程或进程访问资源时,其他线程或进程必须等待。

2.请求与保持条件(Hold and Wait):一个线程或进程可以在保持已经获得的资源的同时请求新的资源。

3.不可抢占条件(No Preemption):已经分配给一个线程或进程的资源不能被强制性地抢占,只能在使用完之后自愿释放。

4.循环等待条件(Circular Wait):存在一种等待资源的循环链,即若干个线程或进程之间形成一种头尾相接的循环等待资源的关系。

要解决死锁问题,可以采用死锁预防、死锁避免、死锁检测和死锁恢复等策略。这些策略的目标是打破死锁产生的必要条件,从而避免或解决死锁的发生。

3.怎么解决死锁问题

1.资源有序分配:为了避免循环等待条件,可以对资源进行排序,并确保线程按照相同的顺序请求资源,从而避免产生循环等待。下面是一个示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread1: Holding mutex1...\n");
    // 线程1等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread1: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread1: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}
void *thread2(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread2: Holding mutex1...\n");
    // 线程2等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread2: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread2: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}
int main() {
    pthread_t tid1, tid2;
    // 创建两个线程
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    // 等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("Program completed.\n");
    return 0;
}

在这个示例中,我们确保线程1和线程2都按照相同的顺序访问互斥锁mutex1和mutex2,从而避免了循环等待的情况。

2.避免持有并等待:一种解决死锁问题的方法是要求线程在申请资源时,释放已持有的资源。这样,当线程请求新的资源时,它没有任何资源保持,从而避免了持有并等待的情况。以下是示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread1: Holding mutex1...\n");
    pthread_mutex_unlock(&mutex1);
    // 线程1等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread1: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread1: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}
void *thread2(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread2: Holding mutex1...\n");
    pthread_mutex_unlock(&mutex1);
    // 线程2等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread2: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread2: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}
int main() {
    pthread_t tid1, tid2;
    // 创建两个线程
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    // 等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("Program completed.\n");
    return 0;
}

在这个示例中,线程1和线程2在获取并释放mutex1之后立即释放它,然后才尝试获取mutex2。这种方法确保了线程在请求新资源之前不保持任何资源,从而避免了持有并等待的情况。

总结

本篇文章主要讲解了线程同步的概念和互斥锁的使用,以及死锁的产生和解决方法。


相关文章
|
15天前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
7天前
|
安全 程序员 API
|
4天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
5天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
23 4
|
5天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
25 3
|
6天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
18 1
|
10天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
17 3
|
12天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
|
10天前
|
缓存 安全 Java
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文将深入探讨Java中的多线程编程,包括其基本原理、实现方式以及常见问题。我们将从简单的线程创建开始,逐步深入了解线程的生命周期、同步机制、并发工具类等高级主题。通过实际案例和代码示例,帮助读者掌握多线程编程的核心概念和技术,提高程序的性能和可靠性。
10 2
|
11天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。