【操作系统原理】—— 线程同步

简介: 【操作系统原理】—— 线程同步

实验相关知识

1.进程与线程

进程(Process):

  • 定义: 进程是操作系统中的一个独立执行单元。每个进程都有独立的内存空间、程序代码、数据和系统资源。
  • 资源独立性: 进程之间相互独立,一个进程的崩溃不会直接影响其他进程。
    切换代价: 进程切换的代价相对较高,因为切换时需要保存和恢复完整的上下文信息,包括内存、寄存器等。
  • 通信: 进程间通信相对复杂,通常需要通过进程间通信机制(IPC,Inter-Process Communication)来实现,如消息队列、信号量、管道等。
  • 创建: 进程的创建通常较为耗时,并且新的进程拥有自己的地址空间。

线程(Thread):

  • 定义: 线程是进程中的一个执行单元,是进程的一部分。一个进程可以包含多个线程,它们共享进程的地址空间和资源。
  • 资源共享: 线程之间共享相同的地址空间和文件描述符,它们之间的通信相对容易。
  • 切换代价: 线程切换的代价相对较低,因为线程共享相同的地址空间,切换时只需要保存和恢复寄存器等少量上下文信息。
  • 通信: 线程之间的通信相对容易,因为它们共享相同的内存空间。但也需要注意同步和互斥,以防止数据竞争等问题。
  • 创建: 线程的创建通常较为轻量,速度较快。

区别:

  • 资源独立性: 进程有独立的内存空间,而线程共享相同的内存空间。
  • 切换代价: 进程切换代价高,线程切换代价相对较低。
  • 通信: 进程间通信相对复杂,线程通信相对容易。
  • 创建: 进程创建代价较高,线程创建较为轻量。
  • 健壮性: 由于进程有独立的内存空间,一个进程的崩溃不会直接影响其他进程。在多线程中,一个线程的问题可能导致整个进程的崩溃。

2.线程同步

      线程同步是指多个线程在访问共享资源时采取的一种协调机制,以确保对共享资源的访问是有序和安全的。在多线程环境中,如果没有适当的同步机制,可能会导致竞争条件(Race Condition)、死锁(Deadlock)、数据不一致等问题。以下是一些常见的线程同步机制:

      互斥锁(Mutex): 互斥锁是最基本的同步机制之一。一次只允许一个线程持有互斥锁,其他线程必须等待锁的释放。这确保了对共享资源的独占式访问。

      信号量(Semaphore): 信号量是一种更为通用的同步机制,它可以允许多个线程同时访问临界区。信号量维护一个计数器,表示可同时访问的线程数量。

      条件变量(Condition Variable): 条件变量允许线程在某个条件发生或满足时等待,从而避免了忙等待。它通常与互斥锁一起使用,等待某个条件的线程会释放锁,然后进入阻塞状态。

      读写锁(Read-Write Lock): 读写锁允许多个线程同时读取共享资源,但在写操作时需要互斥。这样可以提高读取性能,因为多个线程可以同时读取,但写操作仍然是互斥的。

      原子操作: 原子操作是一种不可分割的操作,它可以保证在执行期间不会被其他线程中断。一些现代编程语言和库提供了原子操作,用于确保对共享数据的操作是原子的。

      屏障(Barrier): 屏障用于确保所有线程都达到某个点之后才能继续执行。它常用于同步多个线程的执行顺序。

      这些同步机制可以根据具体的应用场景和需求进行选择和组合。正确使用线程同步机制可以有效避免并发环境中的问题,确保多线程程序的正确性和稳定性。然而,不正确的同步可能导致难以调试和修复的问题,因此在设计和实现多线程程序时,需要仔细考虑同步机制的选择和使用。

3.多线程

     多线程是一种多任务并发的工作方式,在linux中线程包括内核线程和用户线程,内核线程有内核管理,不需要我们做更多的工作,我们这里讲的是用户线程,线程统一由用户线程来切换。

多线程的优势包括:

     并发执行: 多线程使得程序的不同部分可以同时执行,提高了程序的并发性。

     资源共享: 线程之间共享相同的地址空间和资源,简化了数据共享和通信。

     响应性: 多线程可以提高系统的响应性,因为其中一个线程的阻塞不会影响其他线程的执行。

     任务分解: 可以将复杂任务分解成多个线程,提高程序的结构性和可维护性。

     并行处理: 在多核处理器上,多线程可以实现真正的并行处理,充分利用硬件资源。

     

然而,多线程编程也带来了一些挑战,例如:

     同步和互斥: 多线程共享资源可能导致竞争条件,需要使用同步和互斥机制来确保数据的一致性。

     死锁: 不正确的同步可能导致死锁,使得线程无法继续执行。

     调试困难: 多线程程序的调试相对复杂,因为存在多个执行流。

     性能开销: 线程的创建和切换都有一定的性能开销。

4.线程相关函数

int pthread_create(pthread_t id,pthread_attr_t *attr, void *(*start_runtine)(void *), void *arg);//线程创建函数
获取线程ID(即上面创建的pthread_t id):pthread_t pthread_self();
退出线程:void pthread_exit(void *retval);
挂起线程:int pthread_join(pthread_t id,void **return);
线程同步:在POSIX中提供线程同步的方式有两种,条件变量和互斥锁

互斥锁:

pthread_mutex_t *mutex;//互斥锁变量
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_attr_t *attr);//初始化一个互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);//锁定互斥锁,这样子当一个线程锁定的话,另一个线程就会处于等待状态
int pthread_mutex_unlock(pthread_mutex_t  *mutex);//解锁互斥锁,如果解锁后,处于等待状态的线程就有机会访问临界区

条件变量:其实是对互斥锁的一种补充,因为线程可以在等待条件变量的时候同时解锁,这在生产者和消费者模式可以体现。

pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *cond, const pthread_cond_addr *attr);//初始化一个条件变量,后面参数attr是条件变量的属性
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//释放互斥量mutex,等待条件变量cond
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);//释放互斥量mutex,等待条件变量cond,与pthread_cond_wait函数不一样的是,该函数可以是线程在abstime时间内不阻塞。
int pthread_cond_signal(pthread_cond_t *cond);//释放条件变量
int pthread_cond_broadcast(pthread_cond_t *cond);//释放所有由cond阻塞的线程,这里要小心使用

5.线程属性

这些属性在使用前,必须调用相关的初始化函数pthread_xxx_init(xxx *);

线程属性:pthread_attr_t

上面的相关属性,POSIX大部分都提供了相应的接口来操作。如设置调度测略:
int pthread_attr_setschedpolicy(pthread_attr_t  *attr, int policy);
int pthread_attr_init(pthread_attr_t  *attr);//初始化线程属性对象
int pthread_attr_destroy(pthread_attr_t  *attr);//销毁线程属性对象

实验设备与软件环境

安装环境:分为软件环境和硬件环境

硬件环境:内存ddr3 4G及以上的x86架构主机一部

系统环境:windows 、linux或者mac os x

软件环境:运行vmware或者virtualbox

软件环境:Ubuntu操作系统

实验内容

题目要求

     在linux环境下,利用多线程及同步的方法,编写一个程序模拟火车售票系统,共3个窗口,卖10张票,程序输出结果类似(程序输出不唯一,可以是其他类似的结果)

     即有三点要求:

           1.创建三个线程

           2.使用互斥锁保证线程安全

           3.车票为0的时候停止卖票

我的思路

     通过使用pthread_create(pthread_t *tidp,const pthread_attr_t *attr,void *(start_rtn)(void),void *arg);函数创建线程,其中参数

     第一个参数为指向线程标识符的指针。

     第二个参数用来设置线程属性。

     第三个参数是线程运行函数的起始地址。

     最后一个参数是运行函数的参数。

     其中线程运行函数为自己编写的buyTicket()函数,作用是进行售票工作,车票为0的时候停止卖票。在函数内部使用pthread_mutex_lock(&mutex)对于互斥锁进行锁定。线程调用该函数让互斥锁上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用该线程将阻塞,直到该互斥锁变为可用为止(这样可以防止多人买票的时候计票数错误)。

     同时为了防止在售票函数中,当完成了售票,进行解锁,此时该线程所分配的cpu时间片还没有完,于是又继续循环上去加锁售票,以此往复导致只有一个线程售票,其他线程被卡在获取锁加锁的环节。(即防止只有一个窗口将票卖完)我在解锁后增加一个睡眠(usleep(1);)。

     最后通过使用pthread_join()函数等待线程的结束。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>   //互斥锁的头文件 
int tickets = 10;      //总票数 
pthread_mutex_t mutex;  //C语言多线程中互斥锁的初始化 
void *buyTicket(void *arg) {
  const char* name = (char*)arg; 
  while(tickets>0) {
    pthread_mutex_lock(&mutex);
    tickets--;
    printf("[%s]窗口卖出一张票,还剩%d张票\n",name,tickets);
    pthread_mutex_unlock(&mutex);
    //防止只有一个窗口将票卖完
    usleep(1);
  }
  printf("%s quit!\n",name);
  pthread_exit((void*)0);
//  return NULL;
}
int main() {
  
  pthread_mutex_init(&mutex, NULL);
  pthread_t t1,t2,t3;
  //创建线程 
  pthread_create(&t1, NULL, buyTicket, "thread 1");
  pthread_create(&t2, NULL, buyTicket, "thread 2");
  pthread_create(&t3, NULL, buyTicket, "thread 3");
  //等待线程执行结束 
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_join(t3, NULL);
  //注销互斥锁 
  pthread_mutex_destroy(&mutex);
  return 0; 
}

     我们可以看到这个程序成功的利用了多线程及同步的方法,实现了模拟火车售票系统,一共3个窗口,分别是thread1,thread2和thread3,一共卖10张票,车票为0的时候停止卖票。同时使用互斥锁保证了线程安全。

异常问题与解决方案

异常:

     编写好关于线程的程序后不能正常编译。

解决方法:

     在头文件的地方加入#include<pthread.h>(互斥锁的头文件)

异常:

     编译的时候出现“undefined reference to ‘pthread_create’”

解决方法:

     原因:pthread不是Linux下的默认的库,也就是在链接的时候,无法找到phread库中哥函数的入口地址,于是链接会失败。

     解决:在gcc编译的时候,附加要加 -lpthread参数即可解决。


相关文章
|
12天前
|
存储 算法 安全
深入理解操作系统内存管理:原理与实践
【5月更文挑战第25天】 随着计算机技术的不断发展,操作系统在硬件资源管理中扮演着越来越重要的角色。其中,内存管理作为操作系统的核心功能之一,对于提高系统性能和稳定性具有重要意义。本文将深入探讨操作系统内存管理的基本原理,包括内存分配、内存回收、内存保护等方面,并通过实例分析如何在实际开发中优化内存管理策略,以提高系统运行效率。
|
7天前
|
程序员 内存技术
深入理解操作系统内存管理:原理与实践
【5月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键。本文将深入探讨操作系统中内存管理的理论基础和实际应用,包括物理内存与虚拟内存的映射机制、分页与分段技术、以及内存分配策略等。通过对内存管理机制的分析与案例实践,旨在为读者提供一个清晰的内存管理概念框架和操作指南,帮助理解操作系统如何优化内存资源使用,提高系统性能。
|
7天前
|
存储 Linux 开发者
深入理解操作系统内存管理:原理与实践
【5月更文挑战第30天】 本文旨在探讨操作系统中的内存管理机制,其核心是内存的有效分配、使用与回收。我们将从内存管理的基本原理出发,逐步深入到具体的实现技术,如分页、分段和虚拟内存等。文章将详细阐述每种技术的工作原理、优势及其可能面临的问题,并通过实际案例来展示这些技术在现代操作系统中的应用。通过阅读本文,读者将对操作系统的内存管理有一个全面而深入的了解,并能够对常见的内存问题进行诊断和优化。
|
7天前
|
监控 Java 调度
Java并发编程:线程池的原理与实践
【5月更文挑战第30天】 在现代软件开发中,尤其是Java应用中,并发编程是一个不可忽视的领域。线程池作为提升应用性能和资源利用率的关键技术之一,其正确使用和优化对系统稳定性和效率至关重要。本文将深入探讨线程池的核心原理、常见类型以及在实际开发中的使用案例,旨在帮助开发者更好地理解和运用线程池技术,构建高性能的Java应用程序。
|
7天前
|
缓存 算法 Java
深入理解操作系统内存管理:原理与实践
【5月更文挑战第30天】 操作系统的心脏——内存管理,是确保系统高效稳定运行的关键。本文将深入剖析操作系统内存管理的基本原理,包括物理内存与虚拟内存的映射机制、分页与分段技术、以及内存分配策略等。同时,结合现代操作系统实例,探讨内存管理在多任务环境中的创新应用,如Linux内核的内存管理优化。文章旨在为读者提供一个全面、深入的视角,以理解并掌握操作系统中这一至关重要的组成部分。
|
7天前
|
存储 缓存 算法
深入理解操作系统的内存管理:原理与实践
【5月更文挑战第30天】 在现代计算机系统中,操作系统扮演着至关重要的角色,尤其是内存管理作为其核心功能之一。本文将详细探讨操作系统内存管理的基本原理、关键技术以及实际应用场景。我们将从内存层次结构出发,解析地址转换和分页机制,并探讨虚拟内存技术如何使得系统运行更加高效稳定。同时,通过分析具体案例,本文还将展示内存管理在提升系统性能和安全性方面的重要作用。
|
8天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化移动应用开发的未来:跨平台框架与原生操作系统的融合
【5月更文挑战第29天】在Java开发中,并发编程是一个重要的议题。随着多核处理器的普及,如何充分利用多核资源,提高程序的执行效率,同时保证数据的安全性和一致性,成为开发者必须面对的挑战。本文将从线程安全的基本概念出发,探讨Java中的线程安全问题,并介绍一些常见的解决方案,如同步机制、锁优化等。最后,我们将通过实例分析,展示如何在保证线程安全的前提下,进行性能优化。
|
8天前
|
算法 安全 调度
深入理解操作系统内存管理:原理与实践
【5月更文挑战第29天】 在现代计算机系统中,操作系统扮演着至关重要的角色,其中内存管理是其核心职能之一。本文旨在剖析操作系统中内存管理的基本原理和关键技术,以及它们如何在不同类型的操作系统中得以实现。我们将从内存的分配与回收机制入手,探讨分页、分段以及虚拟内存等概念,并分析它们如何共同作用以支持多任务处理和保护系统安全。通过实例演示和性能分析,本文为读者呈现一个全面而深入的操作系统内存管理视角。
|
8天前
|
监控 Java 开发者
深入理解Java并发编程:线程池的工作原理与实践
【5月更文挑战第29天】 在现代Java应用开发中,高效地管理并发任务是至关重要的。本文将深入探讨Java线程池的核心机制,揭示其背后的设计哲学和运作模式。通过分析线程池的优势、工作过程及关键参数,结合实例演示如何合理配置和优化线程池以提高应用程序的性能和响应能力。
|
8天前
|
Java 开发者
Java并发编程:理解线程池的工作原理与实践应用
【5月更文挑战第29天】在Java并发编程中,线程池是一种管理线程资源的重要工具。通过深入探讨线程池的工作原理和实践应用,本文将帮助开发者更好地理解和使用线程池,提高系统性能和稳定性。