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

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

实验相关知识

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


相关文章
|
4月前
|
存储 安全 Shell
深入浅出操作系统:从原理到实践
【9月更文挑战第21天】在数字时代的浪潮中,操作系统扮演着至关重要的角色。本文将深入探究操作系统的奥秘,从其基本概念和核心原理出发,逐步引导读者理解操作系统的工作机制。我们将通过生动的例子和实用的代码片段,揭示操作系统如何管理计算机硬件资源、提供用户接口以及确保系统安全与性能优化。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇通往操作系统深层世界的大门。准备好跟随我们的脚步,一起探索这个让计算机变得生动起来的神奇软件吧!
93 8
|
1月前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
5天前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
24 6
|
1月前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
2月前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
2月前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
49 2
|
2月前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
4月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
178 29
|
4月前
|
存储 消息中间件 资源调度
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
该文章总结了操作系统基础知识中的十个关键知识点,涵盖了进程与线程的概念及区别、进程间通信方式、线程同步机制、死锁现象及其预防方法、进程状态等内容,并通过具体实例帮助理解这些概念。
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
|
3月前
|
算法 安全 调度
深入理解操作系统:进程与线程的管理
【10月更文挑战第9天】在数字世界的心脏跳动着的,不是别的,正是操作系统。它如同一位无形的指挥家,协调着硬件与软件的和谐合作。本文将揭开操作系统中进程与线程管理的神秘面纱,通过浅显易懂的语言和生动的比喻,带你走进这一复杂而又精妙的世界。我们将从进程的诞生讲起,探索线程的微妙关系,直至深入内核,理解调度算法的智慧。让我们一起跟随代码的脚步,解锁操作系统的更多秘密。
51 1