线程 协程

线程

基本概念

定义

  线程是计算机程序并发执行的最小单位。它是进程内的一个独立执行流,拥有独立的程序计数器、栈和一组寄存器。与进程不同,线程在同一个进程内共享相同的内存空间和系统资源。
  线程可以被视为在进程内部创建的轻量级执行单元(轻量级进程)。一个进程可以包含多个线程,这些线程可以独立执行不同的任务,同时共享进程的内存和资源。

特点

  1. 提高程序的性能和响应能力:线程是一个执行路径,将程序划分为多个线程,是并发执行的单元。多个线程可以在同一时间段内并发执行,就像将大型任务分成小块并同时执行。每个线程都有自己的指令序列,可以独立执行指令。
  2. 线程共享同一内存空间:在同一个进程内,所有线程共享相同的内存空间。这意味着它们可以直接访问相同的变量和数据结构,从而实现线程间的数据共享和通信。
  3. 线程可以协同工作:不同的线程可以协同工作,执行不同的任务,以实现更复杂的功能。线程可以通过互斥锁、条件变量等同步机制来协调和通信。
  4. 线程具有较低的开销:相对于创建和管理进程,创建和切换线程的开销较低。因为线程共享进程的资源,所以线程的创建和销毁相对较快。
  5. 线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。、
  6. 线程不属于任何操作语言,属于操作系统级别。

线程的级别

  1. 内核级线程(Kernel-level threads)
    • 内核级线程由操作系统内核直接管理和调度。操作系统能够感知和控制内核级线程的状态和执行。
    • 内核级线程的调度由操作系统的调度器负责,可以使用抢占式调度方式,即操作系统可主动剥夺线程的CPU时间片,切换到其他线程执行。
    • 内核级线程的优点是并发性和可靠性高,能够实现真正的并行执行,适用于多核处理器。但是,由于涉及系统调用和内核切换,内核级线程的创建和切换开销较大。
  2. 用户级线程(User-level threads)
    • 用户级线程完全由用户空间的线程库管理,操作系统对其一无所知。线程的创建、调度和同步等操作都在用户空间完成。
    • 用户级线程的调度由线程库实现,通常采用协作式调度方式,即线程主动让出CPU控制权。这种调度方式可以减少上下文切换的开销,但也可能导致某个线程长时间占用CPU,影响其他线程的执行。
    • 用户级线程的优点是轻量级和灵活性高,线程切换的开销较小。但是,如果一个用户级线程阻塞,可能会导致整个进程的阻塞,因为操作系统无法感知和调度用户级线程的阻塞状态。
  3. 混合级线程(Hybrid threads)
    • 混合级线程是用户级线程和内核级线程的结合,既可以在用户空间进行线程管理,也可以由操作系统内核进行调度和执行。
    • 在混合级线程模型中,用户级线程和内核级线程可以一对一、多对一或多对多的关系,具体取决于线程库和操作系统的设计。
    • 混合级线程的优点是结合了用户级线程和内核级线程的优势。用户级线程可以提供轻量级和灵活性,而内核级线程可以提供并发性和可靠性。

线程的调度

由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
  (1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态。
在这里插入图片描述

  (2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。

线程状态

  线程状态在不同的系统或平台分类各有不同
操作系统线程状态如下:
在这里插入图片描述
java线程状态如下:
在这里插入图片描述

守护线程

  守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
  守护线程通常用于执行一些辅助性任务,如垃圾回收、缓存清理等,它们不需要等待所有的任务完成后再退出。
  简单的来说,这个线程不会影响别的线程或者整个程序的运行,主程序或者其他线程不需要等待守护线程完成才退出。
在多任务计算机操作系统中,守护进程(/ˈdiːmən/ 或 /ˈdeɪmən/)是作为后台进程运行的计算机程序,而不是在交互式用户的直接控制下。 传统上,守护进程的进程名称以字母 d 结尾,以澄清该进程实际上是一个守护进程,以及区分守护进程和普通计算机程序。 例如,syslogd 是一个实现系统日志功能的守护进程,而 sshd 是一个为传入的 SSH 连接提供服务的守护进程。
  在 Unix 环境中,守护进程的父进程通常(但不总是)是 init 进程。 守护进程通常是由一个进程派生一个子进程然后立即退出,从而导致 init 采用子进程,或者由 init 进程直接启动守护进程来创建的。 此外,通过分叉和退出启动的守护进程通常必须执行其他操作,例如将进程与任何控制终端 (tty) 分离。 此类过程通常在各种便利例程中实现,例如 Unix 中的 daemon(3)。
  系统通常会在启动时启动守护进程,这些守护进程将通过执行某些任务来响应网络请求、硬件活动或其他程序。 诸如 cron 之类的守护进程也可以在预定的时间执行定义的任务。

守护线程分类

  1. 系统守护进程:syslogd、login、crond、at等。
  2. 网络守护进程:sendmail、httpd、xinetd、等。
  3. 独立启动的守护进程:httpd、named、xinetd等。
  4. 被动守护进程(由xinetd启动):telnet、finger、ktalk等。

线程池

  线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。提高响应速度,提高线程的可管理性等问题。
  线程池的实现原理是通过池化思想来管理计算机中的资源,将已经创建并初始化的资源放到池中,使用时直接从池中获取,跳过创建了及初始化的过程,提高了响应速度。线程池的底层原理是通过Worker对象来实现的,Worker对象和工作线程可以近似地画上等号。线程池的参数包括核心数、顶点、队列长度等,可以通过动态化线程池实现线程池参数可动态配置和即时生效。线程池的大小需要合理设置,可以通过线程池主动度来让用户在发生Reject异常之前能够采集线程池负载问题
  Java : Java提供了Executor框架来启动线程,通过线程池实现,节省开支,易于管理,效率更高。Java线程池具备可拓展性,允许开发人员向其中增加更多的功能,比如延迟定时线程池ScheduledThreadPoolExecutor。
参考文章:

  1. 美团:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
  2. javaguid:https://javaguide.cn/java/concurrent/java-thread-pool-summary.html
  3. 博客园: https://www.cnblogs.com/wyz1994/p/17121950.html

  C++:C++是一种广泛应用的编程语言,其强大的性能和灵活性使其成为许多高性能和实时系统的首选。C++中实现多线程的方法有不同:继承Thread类、实现Runnable接口、实现可调用接口。

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back(
                [this] {
                    for (;;) {
                        std::function<void()> task;
                        {
                            std::unique_lock<std::mutex> lock(this->queue_mutex);
                            this->condition.wait(lock,
                                [this] { return this->stop || !this->tasks.empty(); });
                            if (this->stop && this->tasks.empty())
                                return;
                            task = std::move(this->tasks.front());
                            this->tasks.pop();
                        }
                        task();
                    }
                }
            );
        }
    }

    template<class F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& worker : workers)
            worker.join();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

int main() {
    ThreadPool pool(4);

    std::vector<std::future<int>> results;

    for (int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i * i;
            })
        );
    }

    for (auto && result : results)
        std::cout << "result:" << result.get() << std::endl;
    std::cout << std::endl;

    return 0;
}

参考文章:
4. https://blog.csdn.net/RabbitTuzi/article/details/131415196

线程并发

  线程并发是指多个线程同时执行,这些线程之间相互独立,互不干扰,各自执行各自的任务。在多线程并发执行的过程中,由于多个线程可能同时访问或修改共享数据,会导致数据的不一致性,主要防止导致错误,线程同步机制就是用于防止错误发生的机制.

线程同步

  线程同步是指在多线程编程中,为了保证共享资源的访问顺序和数据一致性,需要协调多个线程的执行顺序以及对共享资源的互斥访问。当多个线程同时访问一个共享资源时,会导致数据的不一致和程序的错误行为。 线程同步通过协调线程的执行顺序,以及对共享资源的互斥访问,确保数据的正确性。线程同步的主要目的是解决竞争状态和数据一致竞争条件是指多个线程同时访问一个共享资源,并且对资源进行读写操作时,由于线程执行顺序的不确定性,可能会导致数据的不一致和程序的错误行为。修改一致性是指当多个线程同时共享一个资源时,由于线程执行顺序和执行速度的不确定性,可能会导致数据不一致。为了解决这些问题,需要使用线程同步技术来协调线程的执行顺序和对共享资源的互斥访问。常用的线程同步机制包括锁、易失性关键字、CAS机制、信号量机制和队列机制等提高机制。为了避免数据冲突和程序的并发性能,需要注意以下几点:

  • 尽量减少共享资源的数量,避免多个线程同时访问同一个资源。
  • 尽量缩小同步代码块的范围,避免锁的粒度过大。
  • 使用非阻塞算法和无锁算法,例如CAS机制。
  • 使用线程池来管理线程,避免线程的间隙创建和思考。
  • 使用容器来代替同步容器,例如ConcurrentHashMap和ConcurrentLinkedQueue等。

  规避措施如下:

  1. 锁机制:通过加锁来保证同一时刻只有一个线程可以访问共享资源。Java中的synchronized关键字和ReentrantLock类都是锁机制的实现。锁机制可以保证线程安全,但是会降低程序的并发性能。
  2. 易失性关键字:易失性是Java虚拟机提供的轻量级的同步,可以保证持续性和群体性机制,但不能保证原子性。易失性关键字适用于只有一个线程写入,多个线程读取的场景。
  3. CAS:CAS(Compare And Swap)是一种乐观锁定,通过比较内存中的值与期望值是否一致的机制来判断是否没有发生冲突,如果发生冲突,则将新值写入内存。Java中的Atomic类是基于CAS机制实现的。
  4. 信号量机制:信号量是一个计数器,用于控制多个线程对共享资源的访问。当信号量的计数器为0时,线程需要等待;当计数器大于0时,线程可以访问共享资源。
  5. 队列机制:通过队列来实现线程间的协作,例如Java中的BlockingQueue类。当队列为空时,消费者线程需要等待;当队列满时,生产者线程需要等待。

示例:

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

void *thread_func(void *arg) {
    printf("线程ID:%lu,开始执行\n", (unsigned long)pthread_self());
    return NULL;
}

int main() {
    pthread_t thread_id;
    int err = pthread_create(&thread_id, NULL, thread_func, NULL);
    if (err != 0) {
        perror("创建线程失败:");
        return 1;
    }
    pthread_join(thread_id, NULL);
    printf("主线程继续执行\n");
    return 0;
}

线程异步

  线程异步是指在多线程编程中,为了提高程序的运行性能和响应速度,将一些运行的操作放在另一个线程中执行,不会阻塞主线程。 异步编程可以提高程序的性能和用户体验,但同时也会带来一些问题,例如代码的复杂性和调试的困难等。以下是一些常见的异步编程技术和注意事项:

  • Task和async/await:Task是在ThreadPool的基础上推出的,可以将任务放在线程池队列中异步执行。async/await是.net5.0推出的异步编程方式,可以让异步编程更加方便。使用Task和async/await可以避免手动创建维护线程和管理线程池,提高程序的区别性和可性。
  • 回调函数:回调函数是一种常见的异步编程方式,可以在异步操作完成后调用指定的回调函数来处理结果。回调函数可以避免阻塞主线程,但是会带来代码的复杂性和强制性问题。
  • 事件驱动编程:事件驱动编程是一种常见的异步编程方式,可以通过事件来触发异步操作的执行。事件驱动编程可以提高程序的响应速度和并发性能,但是需要注意事件的订阅和取消订阅等问题。
  • 异步编程的注意事项:异步编程需要注意线程安全问题和数据一致性问题,需要使用线程同步技术来协调线程的执行顺序和对共享资源的互斥访问。同时,需要注意异步操作的取消和异常处理等问题,巴勒斯坦节目的错误行为和资源泄露等问题。

  规避措施如下:

  1. 使用线程池:线程池可以避免线程间隙创建和思考的问题,提高程序的性能和可维护性。
  2. 使用异步编程技术:异步编程可以避免主线程,提高程序的并发性能和响应速度。常用的异步编程技术包括Task和async/await、回调函数、事件驱动编程等。
  3. 使用线程同步技术:线程同步可以协调线程的执行顺序和对共享资源的互斥访问,避免数据冲突和不一致性问题。常用的线程同步技术包括锁机制、易失性关键字、CAS机制、信号量机制和队列等。
  4. 注意线程安全问题:多个线程同时访问同一个共享资源时,可能会发生数据冲突问题。需要使用线程同步技术来协调线程的执行顺序和对共享资源的相互访问,造成数据冲突和不一致性问题。
  5. 注意异常处理问题:异步编程可能会带来异常处理的问题,需要注意异常的捕获和处理,厦门程序的错误行为和资源泄露等问题。

示例:

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

void *thread_func(void *arg) {
    printf("线程ID:%lu,开始执行\n", (unsigned long)pthread_self());
    return NULL;
}

int main() {
    pthread_t thread_id;
    int err = pthread_create(&thread_id, NULL, thread_func, NULL);
    if (err != 0) {
        perror("创建线程失败:");
        return 1;
    }
    printf("主线程继续执行\n");
    return 0;
}

区别

  • 线程同步:多个线程相互是有联系的,执行是有先后顺序的,要不然会出问题
  • 线程异步:多个线程各干各的,线程之间谁也不管谁

协程

   协程是一种轻量级的线程,可以在单线程中实现多个协程的并发执行,避免了线程切换的开销和资源占用。协程可以在执行过程中可以暂停和恢复,在不同的情况下协程之间切换执行,从而实现异步编程。协程的实现方式有很多种,例如C++中的boost库、Python中的asyncio库、Kotlin中的协程等。
  协程的定义和特点:协程是一个特殊的函数,它可以在函数调用过程中暂停和恢复执行。协程的主要特点包括可以在函数调用过程中暂停和恢复执行,以及在不同的情况下协程之间切换执行。
  协程的实现原理:协程的实现原理包括协程的调度、上下文切换、同步和异常处理等方面。协程的实现可以使用X语言或C语言的setjmp和longjmp函数来实现。协程之间的同步需要使用线程同步技术来协调协调进程的执行顺序和对共享资源的互斥访问。协调进程的异常处理需要注意异常的捕获和处理,程序的错误行为和资源泄漏等问题。
协程的优点包括:

  1. 轻量级:协程的创建和开销的消耗,可以在单线程中实现多个协程的负载执行,避免线程切换的开销和资源占用。
  2. 高效性:协程的执行过程中可以暂停和恢复,可以在不同的协程之间切换执行,从而实现异步编程,提高程序的并行性能和响应速度。
  3. 灵活:协程的执行过程中可以暂停和恢复,可以在不同的协程之间切换执行,从而实现异步编程,可以更灵活地处理复杂的业务逻辑。

为了解决协调进程的问题,需要注意以下几点:

  • 协程的调度:协程的调度需要考虑协程的优先级和调度算法,自治区协程的饥饿和死锁等问题。
  • 协程的同步:协程之间的同步需要使用线程同步技术来协调协程的执行顺序以及对共享资源的互斥访问,队列数据冲突和不一致性问题。
  • 协程的异常处理:协程的异常处理需要注意异常的捕获和处理,程序的错误行为和资源泄漏等问题。
#include <pthread.h>
#include <stdio.h>
#include <ucontext.h>

void *thread_func(void *arg) {
    printf("协程开始执行\n");
    printf("协程执行完毕\n");
    swapcontext(&child_ctx, &main_ctx);
}

int main() {
    char *stack = (char*)malloc(STACK_SIZE);
    if (stack == NULL) {
        perror("分配栈空间失败");
        return 1;
    }
    getcontext(&child_ctx);
    child_ctx.uc_stack.ss_sp = stack;
    child_ctx.uc_stack.ss_size = STACK_SIZE;
    child_ctx.uc_link = &main_ctx;
    makecontext(&child_ctx, thread_func, 0);
    printf("主函数开始执行\n");
    swapcontext(&main_ctx, &child_ctx);
    printf("主函数执行完毕\n");
    free(stack);
    return 0;
}

实现要点及难点

  1. 协程的创建和推理:创建和协程需要使用和等函数。协程需要分配和初始化栈空间,并设置上下文信息。pthread_create、pthread_join
  2. 协程的上下文切换:协程的上下文切换需要使用库中的函数实现。这些函数可以用于保存和恢复协程的上下文信息,如寄存器、栈指针等。ucontext
  3. 协程的同步和异常处理:协程的同步之间需要使用线程同步技术来协程的执行顺序和对共享资源的互斥访问。协程的异常处理需要注意异常的捕获和处理,队列程序的错误行为和资源泄漏等问题。
  4. 协程的通信和信号:协程之间可以使用信号、信号量、消息队列等方式进行通信。这些通信方式可以帮助协程在执行过程中共享数据和资源。
  5. 协程的优先级和调度:协程的优先级和调度需要考虑协程的执行时间、资源占用和依赖关系。协程的调度可以采用优先队列、时间片轮询等算法来实现。
  6. 协程的监控和管理:协程需要进行监控和管理,以保证协程的正常运行。这可以通过使用锁、互斥量、条件变量等线程同步技术来实现。

  为实现这些功能,可能需要使用更高级的协程库,例如libco、Boost.Coroutine和Coroutine TS等。这些库可以提供更复杂的协程功能,例如协程的创建、内在、上下文切换、同步、异常处理、通信、信号、优先级和调度等。

参考原文

  1. https://blog.csdn.net/qq_58216564/article/details/131986161
  2. https://www.cnblogs.com/liang1101/p/12777831.html(荐)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值