【操作系统】线程的使用

简介: 【操作系统】线程的使用

线程

为什么使用线程?

  • 使用fork创建进程以执行新的任务,该方式的代价很高——子进程将父进程的所有资源都复制一遍。
  • 多个进程之间不会直接共享内存。
  • 进程是系统分配资源的基本单位线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程想要执行任务,必须得有线程,进程至少要有一条线程,程序启动会默认开启一条线程,这条线程被称为主线程或UI线程

什么是线程?

  • 线程是进程内部的一个控制序列。

    • 类比:

      • 创建一个进程,就类似于克隆一个家庭,新的"家庭"与原来的家庭完全相同,但是新"家庭"和原来的家庭完全独立。
      • 进程包含一个或多个线程,就像一个家庭中包含一个或多个家庭成员。
      • 家庭内的各个成员同时做各自的事情,而对于家庭外部的人来说,就是这个家庭同时在做多件事情。
      • 家庭内的每个成员,就是一个线程。每个家庭成员都有自己的个人资源,即线程有自己的局部变量。
      • 所有的家庭成员都能共享这个家庭的资源,即同一个进程内的各个线程,都能共享当前这个进程中的全局变量,除了线程自己的局部变量外,其它资源都共享。
    • 注意:

      • 在单核处理器上,同一个时刻,只能运行一个线程。但是对于用户而言,感觉像是执行了多个线程一样,是因为各个线程在单核CPU上不断进行切换。

线程的优缺点

  • 优点:创建线程比创建进程开销要小
  • 缺点:

    • 多线程编程要多加小心,很容易发生错误。
    • 多线程调试很困难。
  • 补充:

    • 把一个任务划分为两部分,如果在单核处理器上运行,速度不一定更快。除非能确定这个任务运行在多核处理器上,即两部分可以同时执行。

线程的应用场合

  • 需要让用户感觉同时在做多件事情时。

    • 比如:处理文档的进程,一个线程处理用户编辑,一个线程同时统计用户字数。
  • 当一个应用程序,需要同时处理输入、计算、输出时。

    • 可开3个线程,分别处理输入、计算、输出。
  • 综上所述,即高并发编程

线程的使用

线程的创建

  • pthread_create
  • 功能:创建一个新线程。

    • 同时指定该线程的属性、执行函数、执行函数的参数。
  • 函数原型:
int  pthread_create (
  pthread_t *thread,
  pthread_attr_t *attr,
  void *(*start_routine)(void*), 
  void *arg
);       
  • 参数:

    • thread:指向新的线程标识符。
    • attr:用来设置新线程的属性。

      • 一般取默认属性,即该参数取NULL。
    • start_routine:该线程的处理函数。

      • 该函数的返回类型和参数类型都是void*。
    • arg:线程处理函数start_routine的参数。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误代码。(错误代码,这里略过,其它函数也是如此。)
  • 注意:

    • 使用fork创建进程后,进程马上就启动,执行的是fork后面的代码
    • 使用pthread_create创建线程后,新线程马上就启动,执行对应的线程处理函数

线程的终止

  • pthread_exit
  • 功能:在线程函数内部调用该函数,对本线程进行终止,返回值通过参数retval指定。
  • 函数原型:void pthread_exit (void *retval)
  • 参数:

    • retval:它指向的数据为线程退出时的返回值,如果不需要接收该线程的返回值,设置NULL即可。
  • 参考补充:


等待指定线程结束

  • pthread_join
  • 功能:等待指定线程结束,并获取该线程的返回值。
  • 函数原型:int pthread_join (pthread_t th,void ** thread_return);
  • 参数:

    • th:线程标识符,指定要的等待的线程。
    • thread_return:指向(接收)当前线程的返回值。参数类型为void**。
  • 返回值

    • 成功:返回0。
    • 失败:返回错误号。

使用线程程序的编译

  • 定义宏:_REENTRANT——可重入

    • 即:gcc -D_REENTRANT
  • 功能:告诉编译器,编译时需要可重用功能。

    • 即,在编译时,编译部分函数的可重入版本。将共享的资源给本线程独享。
  • 注意:

    • 在单线程程序中,整个程序都是顺序执行的,一个函数在同一时刻只能被一个函数调用,但是在多线程中,由于并发性,一个函数可能同时被多个函数调用,此时这个函数就成了临界资源,很容易造成调用函数时,处理结果的相互影响
    • 如果一个函数在多线程并发的环境中,每次被调用产生的结果是不确定的,我们就说这个函数是不可重入的/线程不安全的
  • 编译时,指定线程库。

    • 即:gcc xxx -lpthread

      • 功能:使用系统默认的NPTL线程库。

        • 即,在默认路径中寻找库文件libpthread.so。默认路径为/usr/lib和usr/local/lib
  • 一般使用如下形式即可:

    • gcc mythread.c -o mythread -D_REENTRANT -lpthread

示例:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

int my_global;
void* my_thread_handle(void* arg){
    int val;
    val =  *((int*)arg);
    printf("new thread begin,arg=%d\n",val);
    my_global += val;
    sleep(3);
    pthread_exit(&my_global);
    //下面一行不再执行
    printf("new thread end\n");
}

int main(void){
    pthread_t mythread;
    int arg;
    int ret;
    void *thread_return;
    
    arg = 100;
    my_global = 1000;
    printf("my_global=%d\n", my_global);
    printf("ready create thread...\n");
    ret = pthread_create(&mythread,0,my_thread_handle,&arg);
    if (ret != 0) {
        printf("create thread failed!\n");
        exit(1);
    }
    printf("wait thread finished...\n");
    ret = pthread_join(mythread,&thread_return);
    if (ret != 0) {
        printf("pthread_join failed!\n");
        exit(1);
    }
    printf("wait thread end, return value is %d\n", *((int*)thread_return));
    printf("my_global=%d\n", my_global);
    printf("create thread finished!\n");
    return 0;
}

image-20220827105611636


线程的同步与互斥

  • 线程的互斥:

    • 指某一资源同一时间只允许一个访问者对其进行访问,具有唯一性和排它性。
    • 但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 线程的同步:

    • 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

  • 问题 :同一个进程内的各个线程,共享该进程的全局变量,如果多个线程同时对某个全局变量进行访问时,就可能导致竞态。

  • 解决办法:对临界区使用信号量、或互斥量。
  • 信号量和互斥量的选择:对于同步和互斥,使用信号量和互斥量都可以实现。使用时选择更符合情况的:

    • 如果要求最多只允许一个线程进入临界区,则使用互斥量。
    • 如果要求多个线程之间的执行顺序满足某个约束,则使用信号量。

信号量

什么是信号量?
  • 此时所指的"信号量"是指用于同一个进程内多个线程之间的信号量。即POSIX信号量,而不是System V信号量。(用于进程之间的信号量)。
  • 用于线程的信号量原理与用于进程之间的信号量原理相同。都有P、V操作。
  • 信号量的表示:sem_t类型。
信号量的初始化
  • sem_init
  • 功能:对信号量进行初始化。
  • 函数原型:int sem_init (sem_t *sem, int pshared, unsigned int value);
  • 参数:

    • sem:指向要被初始化的信号量。
    • pshared:

      • 0表示该信号量是该进程内使用的"局部信号量",不再被其它进程共享。
      • 非0表示该信号量可被其它进程共享,Linux不支持这种信号量。
    • value:信号量的初值。>=0
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

信号量的P操作
  • sem_wait
  • 功能:信号量的P操作。-1
  • 函数原型:int sem_wait (sem_t *sem);
  • 参数:

    • sem:要操作的信号量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

信号量的V操作
  • sem_post
  • 功能:信号量的V操作。+1
  • 函数原型:int sem_post (sem_t *sem);
  • 参数:

    • sem:要操作的信号量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

信号量的删除
  • sem_destroy
  • 功能:删除信号量。
  • 函数原型:int sem_destroy (sem_t *sem);
  • 参数:

    • sem:要操作的信号量。
  • 返回值:

    • 成功: 返回0。
    • 失败:返回错误码。

示例:

#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define BUFF_SIZE 80

char buff[BUFF_SIZE];
sem_t sem;

static void* str_thread_handle(void *arg) 
{
    while(1) {
        //P(sem) -1
        if (sem_wait(&sem) != 0) {
            printf("sem_wait failed!\n");
            exit(1);
        }
        
        printf("string is: %slen=%d\n", buff, strlen(buff));
        if (strncmp(buff, "end", 3) == 0) {
            break;
        }
    }
}

int main(void)
{
    int ret;
    pthread_t  str_thread;
    void *thread_return;


    ret = sem_init(&sem, 0, 0);
    if (ret != 0) {
        printf("sem_init failed!\n");
        exit(1);
    }

    ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
    if (ret != 0) {
        printf("pthread_create failed!\n");
        exit(1);
    }

    while (1) {
        fgets(buff, sizeof(buff), stdin);

        //V(sem) +1 
        if (sem_post(&sem) != 0) {
            printf("sem_post failed!\n");
            exit(1);
        }
        
        if (strncmp(buff, "end", 3) == 0) {
            break;
        }
    }

    ret = pthread_join(str_thread, &thread_return);
    if (ret != 0) {
        printf("pthread_join failed!\n");
        exit(1);
    }

    ret = sem_destroy(&sem);
    if (ret != 0) {
        printf("sem_destroy failed!\n");
        exit(1);
    }

    return 0;
}

image-20220827160636350


互斥量

什么是互斥量?
效果等同于初始值为1的信号量。

互斥量的初始化
  • pthread_mutex_init
  • 功能:初始化互斥量。
  • 函数原型:int pthread_mutex_init(pthread_mutex_t mutex,pthread_mutexattr_t attr);
  • 参数:

    • mutex:指向被初始化的互斥量。
    • attr:互斥量的属性,一般取默认属性。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

互斥量的获取
  • pthread_mutex_lock
  • 功能:获取互斥量。
  • 函数原型:int pthread_mutex_lock (pthread_mutex_t *mutex);
  • 参数:

    • mutex:指向要操作的互斥量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

互斥量的删除
  • pthread_mutex_destroy
  • 功能:删除互斥量。
  • 函数原型:int pthread_mutex_destroy (pthread_mutex_t *mutex);
  • 参数:

    • mutex:指向要操作的互斥量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

示例:

#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define BUFF_SIZE 80

int global_value = 1000;
pthread_mutex_t  lock;

static void* str_thread_handle(void *arg) 
{
    int i = 0;

    for (i=0; i<10; i++) {
        pthread_mutex_lock(&lock);

        if (global_value  > 0) {
            // work
            sleep(1);
            printf("soled ticket(%d) to ChildStation(%d)\n",
                global_value, i+1);
        }
        global_value--;
        
        pthread_mutex_unlock(&lock);
        sleep(1);
    }
}

int main(void)
{
    int ret;
    pthread_t  str_thread;
    void *thread_return;
    int i;

    

    ret = pthread_mutex_init(&lock, 0);
    if (ret != 0) {
        printf("pthread_mutex_init failed!\n");
        exit(1);
    }

    ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
    if (ret != 0) {
        printf("pthread_create failed!\n");
        exit(1);
    }

    for (i=0; i<10; i++) {
        pthread_mutex_lock(&lock);
        
        if (global_value  > 0) {
            // work
            sleep(1);
            printf("soled ticket(%d) to MainStation(%d)\n",
                global_value, i+1);
        }
        global_value--;
        
        
        pthread_mutex_unlock(&lock);
        sleep(1);
    }

    ret = pthread_join(str_thread, &thread_return);
    if (ret != 0) {
        printf("pthread_join failed!\n");
        exit(1);
    }

    ret = pthread_mutex_destroy(&lock);
    if (ret != 0) {
        printf("pthread_mutex_destroy failed!\n");
        exit(1);
    }

    return 0;
}

image-20220827170736489


线程的条件变量

什么是条件变量?

  • 与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某种情况发生为止,通常条件变量和互斥锁一起使用
  • 条件变量使我们可以睡眠来等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

    • 线程因等待"条件变量的条件成立"而被挂起;
    • 线程使"条件成立"(给出条件成立信号)。
  • 条件的检测是在互斥锁的保护下进行的。如果条件为假,一个线程自动阻塞(挂起),并释放等待状态改变的互斥锁。
  • 如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。
  • 如果两个进程共享可读写的内存,条件变量可以被用来实现这两个进程间的线程同步。

条件变量初始化

  • pthread_cond_init
  • 功能:初始化条件变量
  • 函数原型:int pthread_cond_init (pthread_cond_t cond, const pthread_condattr_t attr);
  • 参数:

    • cond:要操作的条件变量。
    • attr:设置条件变量属性。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

唤醒一个等待线程

  • pthread_cond_signal
  • 功能:通知条件变量,唤醒一个等待者。
  • 函数原型: int pthread_cond_signal (pthread_cond_t *cond);
  • 参数:

    • cond:要操作的条件变量(条件变量指针)。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

唤醒所有等待该条件变量的线程

  • pthread_cond_broadcast
  • 功能:广播条件变量。
  • 函数原型: int pthread_cond_broadcast (pthread_cond_t *cond);
  • 参数:

    • cond:要操作的条件变量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

等待条件变量/超时被唤醒

  • pthread_cond_timedwait
  • 功能:等待条件变量cond被唤醒,直到由一个信号或广播,或到绝对超时时间abstime,才唤醒该线程。
  • 函数原型:int pthread_cond_timedwait (pthread_cond_t cond, pthread_mutex_t mutex, const struct timespec *abstime);
  • 参数:

    • cond:要操作的条件变量:
    • mutex:互斥量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

等待条件变量被唤醒

  • pthread_cond_wait
  • 功能:等待条件变量cond被唤醒(由一个信号或广播)。
  • 函数原型:int pthread_cond_wait (pthread_cond_t cond, pthread_mutex_t mutex);
  • 参数:

    • cond:要操作的条件变量。
    • mutex:互斥量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

释放/销毁条件变量

  • pthread_cond_destroy
  • 功能:销毁条件变量。
  • 函数原型:int pthread_cond_destroy (pthread_cond_t *cond);
  • 参数:

    • cond:要销毁的条件变量。
  • 返回值:

    • 成功:返回0。
    • 失败:返回错误码。

示例

  • 个人理解,条件变量会为临界资源创建两道防线,第一道防线为进入等待唤醒前,第二道为进入临界区前。
  • 条件变量与互斥量的结合。
  • 进入到第一道防线时,pthread_mutex_lock对互斥量加锁(P操作)。
  • 进入到第一道防线后,也就是等待被唤醒前,pthread_cond_wait首先会先将当前线程挂起,然后解锁互斥量(V操作)。
  • 被唤醒时,pthread_cond_wait首先对互斥量加锁,然后线程才被唤醒(第二道防线也突破),执行完临界区中的代码后,再次解锁。
  • 如下图示中注意:

    • 我们默认该进程有两个额外创建的线程,线程1首先执行。
    • 图中仅示例线程1和线程2分别执行一次。

image-20220827205151298

#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;//互斥量
pthread_cond_t cond;//条件变量
void *thread1(void *arg){

    while (1) {
        printf("thread1 is running\n");
        pthread_mutex_lock(&mutex);//加锁
        pthread_cond_wait(&cond, &mutex);//等待时挂起,然后开锁,被唤醒时,先加锁,然后再唤醒(即执行下方代码)。。
        printf("thread1 applied the condition\n");
        pthread_mutex_unlock(&mutex);//开锁
        sleep(4);
    }
}
void *thread2(void *arg){
    while (1) {
        printf("thread2 is running\n");
        pthread_mutex_lock(&mutex);//加锁
        pthread_cond_wait(&cond, &mutex);//等待时挂起,然后开锁,被唤醒时,先加锁,然后再唤醒(即执行下方代码)。
        printf("thread2 applied the condition\n");
        pthread_mutex_unlock(&mutex);//开锁
        sleep(2);
    }
}
int main(){
    pthread_t thid1, thid2;
    printf("condition variable study!\n");
    pthread_mutex_init(&mutex, NULL);//互斥量初始化
    pthread_cond_init(&cond, NULL);//条件变量初始化
    pthread_create(&thid1, NULL, (void *)thread1, NULL);//线程1创建
    pthread_create(&thid2, NULL, (void *)thread2, NULL);//线程2创建
    do {
        pthread_cond_signal(&cond);//唤醒一个等待线程
       sleep(1);
    } while (1);
    return 0;
}

image-20220828095243264


相关文章
|
3月前
|
UED 开发者 Python
探索操作系统的心脏:理解进程与线程
【8月更文挑战第31天】在数字世界的海洋中,操作系统犹如一艘巨轮,其稳定航行依赖于精密的进程与线程机制。本文将揭开这一机制的神秘面纱,通过深入浅出的语言和直观的代码示例,引领读者从理论到实践,体验进程与线程的魅力。我们将从基础概念出发,逐步深入到它们之间的联系与区别,最后探讨如何在编程实践中高效运用这些知识。无论你是初学者还是有经验的开发者,这篇文章都将为你的技术之旅增添新的航标。
|
10天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
12天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
27 2
|
14天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
2月前
|
存储 消息中间件 资源调度
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
该文章总结了操作系统基础知识中的十个关键知识点,涵盖了进程与线程的概念及区别、进程间通信方式、线程同步机制、死锁现象及其预防方法、进程状态等内容,并通过具体实例帮助理解这些概念。
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
|
1月前
|
算法 安全 调度
深入理解操作系统:进程与线程的管理
【10月更文挑战第9天】在数字世界的心脏跳动着的,不是别的,正是操作系统。它如同一位无形的指挥家,协调着硬件与软件的和谐合作。本文将揭开操作系统中进程与线程管理的神秘面纱,通过浅显易懂的语言和生动的比喻,带你走进这一复杂而又精妙的世界。我们将从进程的诞生讲起,探索线程的微妙关系,直至深入内核,理解调度算法的智慧。让我们一起跟随代码的脚步,解锁操作系统的更多秘密。
37 1
|
18天前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。
|
2月前
|
资源调度 算法 调度
深入浅出操作系统之进程与线程管理
【9月更文挑战第29天】在数字世界的庞大舞台上,操作系统扮演着不可或缺的角色,它如同一位精通多门艺术的导演,精心指挥着每一个进程和线程的演出。本文将通过浅显的语言,带你走进操作系统的内心世界,探索进程和线程的管理奥秘,让你对这位幕后英雄有更深的了解。
|
1月前
|
算法 调度 UED
探索操作系统中的多线程编程
【8月更文挑战第78天】在数字世界的复杂迷宫中,操作系统扮演着至关重要的角色。本文旨在揭开操作系统中多线程编程的神秘面纱,引导读者理解其概念、实现及应用。通过深入浅出的方式,我们将探讨如何在程序设计中运用多线程,以及这一技术如何优化软件性能和提升用户体验。文章将结合具体代码示例,展示多线程在实际应用中的魔力。无论你是编程新手还是资深开发者,这篇文章都将为你提供新的视角和思考路径。
|
2月前
|
开发者 Python
深入浅出操作系统:进程与线程的奥秘
【8月更文挑战第46天】在数字世界的幕后,操作系统扮演着至关重要的角色。本文将揭开进程与线程这两个核心概念的神秘面纱,通过生动的比喻和实际代码示例,带领读者理解它们的定义、区别以及如何在编程中运用这些知识来优化软件的性能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技巧。