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

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

实验相关知识

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参数即可解决。


目录
相关文章
|
6天前
|
存储 Java 调度
深入浅出Java线程池原理
本文深入分析了Java线程池的原理和实现,帮助读者更好地理解Java并发编程中线程池的创建、工作流程和性能优化。
|
5天前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
4天前
|
编解码 网络协议 API
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
|
26天前
|
存储 算法 调度
深入理解操作系统:从原理到实践
【7月更文挑战第24天】本文将深入探讨操作系统的基本原理和实践应用,包括进程管理、内存管理、文件系统和设备管理等方面。通过理论与实践相结合的方式,帮助读者更好地理解和掌握操作系统的相关知识。
|
10天前
|
存储 Java
线程池的底层工作原理是什么?
【8月更文挑战第8天】线程池的底层工作原理是什么?
40 8
|
17天前
|
存储 Linux C语言
深入浅出操作系统:从原理到实践
【8月更文挑战第2天】本文旨在通过浅显易懂的语言和丰富的代码示例,带领读者深入理解操作系统的基本原理和实践应用。我们将从操作系统的定义、功能、分类等基础概念入手,逐步深入到进程管理、内存管理、文件系统等核心模块,并通过代码示例展示这些原理在实际操作系统中的应用。无论你是计算机专业的学生,还是对操作系统感兴趣的开发者,这篇文章都将为你提供有价值的参考和启示。
13 2
|
20天前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
25天前
|
监控 Java 开发者
深入理解Java并发编程:线程池的原理与实践
【5月更文挑战第85天】 在现代Java应用开发中,高效地处理并发任务是提升性能和响应能力的关键。线程池作为一种管理线程的机制,其合理使用能够显著减少资源消耗并优化系统吞吐量。本文将详细探讨线程池的核心原理,包括其内部工作机制、优势以及如何在Java中正确实现和使用线程池。通过理论分析和实例演示,我们将揭示线程池对提升Java应用性能的重要性,并给出实践中的最佳策略。
|
26天前
|
存储 算法 Linux
操作系统中的内存管理:从原理到实践
本文深入探讨了操作系统中至关重要的内存管理机制,揭示了从理论到实现的复杂过程。通过分析内存分配、虚拟内存以及分页和交换等概念,本篇文章旨在为读者提供对现代操作系统内存管理技术的全面理解。结合最新的技术动态和研究成果,文章不仅阐述了内存管理的基本原理,还讨论了其在实际操作系统中的应用和优化策略。
25 1
|
28天前
|
人工智能 安全 调度
深入理解操作系统:从原理到实践
【7月更文挑战第22天】本文将带你深入了解操作系统的基本原理和实践应用。我们将从操作系统的定义和功能开始,探讨其核心组件如进程管理、内存管理、文件系统和设备管理。接着,我们将通过一个简化的实例来展示如何设计一个简单的操作系统,并讨论现代操作系统中的一些高级特性,如虚拟化技术、安全机制以及分布式操作系统。最后,我们将介绍操作系统的未来发展趋势,包括量子计算对操作系统的影响以及人工智能在操作系统中的应用。