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

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

实验相关知识

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


目录
相关文章
|
17天前
|
UED 开发者 Python
探索操作系统的心脏:理解进程与线程
【8月更文挑战第31天】在数字世界的海洋中,操作系统犹如一艘巨轮,其稳定航行依赖于精密的进程与线程机制。本文将揭开这一机制的神秘面纱,通过深入浅出的语言和直观的代码示例,引领读者从理论到实践,体验进程与线程的魅力。我们将从基础概念出发,逐步深入到它们之间的联系与区别,最后探讨如何在编程实践中高效运用这些知识。无论你是初学者还是有经验的开发者,这篇文章都将为你的技术之旅增添新的航标。
|
3天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
3天前
|
开发者 Python
深入浅出操作系统:进程与线程的奥秘
【8月更文挑战第46天】在数字世界的幕后,操作系统扮演着至关重要的角色。本文将揭开进程与线程这两个核心概念的神秘面纱,通过生动的比喻和实际代码示例,带领读者理解它们的定义、区别以及如何在编程中运用这些知识来优化软件的性能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技巧。
|
3天前
|
存储 安全 Linux
探索操作系统:从原理到实践
【9月更文挑战第14天】本文深入探讨了操作系统的核心概念,通过分析其设计原则和功能,揭示了操作系统如何管理计算机硬件资源、提供用户接口并确保系统安全。文章不仅阐述了操作系统的基本原理,还通过实际代码示例展示了如何在操作系统上进行编程,旨在帮助读者更好地理解并应用操作系统知识。
|
9天前
|
算法 调度 UED
操作系统中的进程管理:原理与实践
在数字世界的心脏跳动着无数进程,它们如同细胞一般构成了操作系统的生命体。本文将深入探讨进程管理的奥秘,从进程的诞生到成长,再到最终的消亡,揭示操作系统如何协调这些看似杂乱无章却又井然有序的活动。通过浅显易懂的语言和直观的比喻,我们将一起探索进程调度的策略、同步机制的重要性以及死锁问题的解决之道。准备好跟随我们的脚步,一起走进操作系统的微观世界,解锁进程管理的秘密吧!
20 6
|
21天前
|
存储 缓存 Linux
深度探索Linux操作系统 —— Linux图形原理探讨3
深度探索Linux操作系统 —— Linux图形原理探讨
28 9
|
21天前
|
存储 Linux 图形学
深度探索Linux操作系统 —— Linux图形原理探讨1
深度探索Linux操作系统 —— Linux图形原理探讨
26 7
|
21天前
|
存储 算法 网络协议
了解操作系统的基本原理和常见操作,提高计算机使用效率
了解操作系统的基本原理和常见操作,提高计算机使用效率
23 4
|
20天前
|
调度
深入理解操作系统:进程与线程的管理
【8月更文挑战第29天】在数字世界的每一次点击和滑动背后,都隐藏着操作系统的精妙运作。本文将带你探索操作系统的核心概念之一——进程与线程的管理。我们将从基础定义出发,逐步深入到它们在内存中的表示、状态变迁以及它们之间错综复杂的关系。通过简洁明了的语言和直观的比喻,即便是没有计算机背景的读者也能轻松理解这一主题。准备好了吗?让我们一起揭开操作系统神秘的面纱,探索那些看似晦涩却无比精彩的知识吧!
|
21天前
|
Linux API 图形学
深度探索Linux操作系统 —— Linux图形原理探讨2
深度探索Linux操作系统 —— Linux图形原理探讨
22 3