信号量线程池读者写者模型

2024-05-07 05:20

本文主要是介绍信号量线程池读者写者模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

大家好,我是jiantaoyab,本篇文章接着给大家介绍线程有关的信号量及线程池的基本理解。

信号量

在计算机中,信号量就是个 0 以上的整数值,当为 0 时表示己无可用信号 ,或者说条件不再允许,因此它表示某种信号的累积“ 量飞故称为信号量。

信号量是种同步机制。同步一般是指合作单位之间为协作完成某项工作而共同遵守的工作步调,强调的是配合时序,就像十字路口的红绿灯,只有在绿灯亮起的情况下司机才能踩油门把车往前开,这就是一种同步,同步简单来说就是不能随时随意工作,工作必须在某种条件具备的情况下才能开始,工作条件具备的时间顺序就是时序。

信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。

既然信号量是计数也必然要有对计数增减的方法。P、V 操作来表示信号量的减、增,这两个都是荷兰语中的单词的缩写。P是指Proberen表示减少, V是指单词 Verhogen,表示增加。

V操作

  1. 将信号量的值加 1
  2. 唤醒在此信号量上等待的线程

P操作:

  1. 判断信号量是否大于 0 。
  2. 若信号量大于 0,则将信号量减 1 。
  3. 若信号量等于 0,当前线程将自己阻塞,以在此信号量上等待。

信号量是个全局共享变量,up 和 down 又都是读写这个全局变量的操作,而且它们都包含一系列的子操作,因此它们必须都是原子操作。
信号量的初值代表是信号资源的累积量,也就是剩余量,若初值为1的话,它的取值就只能为0和1,这便称为二元信号量,我们可以利用二元信号量来实现锁。

在二元信号量中,down 操作就是获得锁,up操作就是释放锁。我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥。

大致流程为:

  1. 线程 A 进入临界区前先通过 down 操作获得锁(我们有强制通过锁进入临界区的手段),此时信号量的值便为 0 。
  2. 后续线程 B 再进入临界区时也通过 down 操作获得锁,由于信号量为 0,线程 B 便在此信号量上等待,也就是相当于线程 B 进入了睡眠态
  3. 当线程 A 从临界区出来后执行 up 操作释放锁,此时信号量的值重新变成 1 ,之后线程 A 将线程 B唤醒 。
  4. 线程 B 醒来后获得了锁,进入临界区 。

信号量接口

初始化

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值

基本操作

//销毁
int sem_destroy(sem_t *sem);
//等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
//发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);//V()

基于环形队列的生产者消费者模型

ring_queue.hpp

#pragma once
#include <vector>
#include <pthread.h>
#include <iostream>
#include <semaphore.h>using namespace std;
namespace ns_ring_queue
{template <class T>class RingQueue{private:vector<T> _ring_queue; // 环形队列int _cap;              // 容量sem_t _black_sem;      // 生产者关系空位置资源sem_t _data_sem;       // 消费者关心数据资源int _c_step;           // 消费者走到哪int _p_step;pthread_mutex_t _c_mtx;pthread_mutex_t _p_mtx;public:RingQueue(int cap = 3): _ring_queue(cap), _cap(cap){sem_init(&_black_sem, 0, cap);sem_init(&_data_sem, 0, 0);_c_step = _p_step = 0; // 从0开始走pthread_mutex_init(&_c_mtx, nullptr);pthread_mutex_init(&_p_mtx, nullptr);}~RingQueue(){sem_destroy(&_black_sem);sem_destroy(&_data_sem);pthread_mutex_destroy(&_c_mtx);pthread_mutex_destroy(&_p_mtx);}public:void Push(const T& in){//生产接口sem_wait(&_black_sem); //并行的先分配好信号量pthread_mutex_lock(&_p_mtx);//再分配锁_ring_queue[_p_step]=in;//生产到p_step位置上_p_step++;_p_step%=_cap;pthread_mutex_unlock(&_p_mtx);sem_post(&_data_sem);}void Pop(T* out){//消费接口sem_wait(&_data_sem);pthread_mutex_lock(&_c_mtx);*out=_ring_queue[_c_step]; //拿c_step上的数据_c_step++;_c_step%=_cap;pthread_mutex_unlock(&_c_mtx);sem_post(&_black_sem);}};
}

ring_cp.cc

#include"ring_queue.hpp"
#include<pthread.h>
#include<time.h>
#include<unistd.h>using namespace ns_ring_queue;void* consumer(void* args)
{RingQueue<int>* rq = (RingQueue<int>*)args;while(true){int data = 0;rq->Pop(&data);std::cout << "消费数据是: " << data << std::endl;//  sleep(1);}
}void* producter(void* args)
{RingQueue<int>* rq = (RingQueue<int>*)args;while(true){int data = rand()%20 + 1;std::cout << "生产数据是:  " << data << std::endl;rq->Push(data);sleep(1);}
}
int main()
{srand((long long)time(nullptr));RingQueue<int>* rq = new RingQueue<int>();pthread_t c0,c1,c2,c3,p0,p1,p2;pthread_create(&c0, nullptr, consumer, (void*)rq);pthread_create(&c1, nullptr, consumer, (void*)rq);pthread_create(&c2, nullptr, consumer, (void*)rq);pthread_create(&c3, nullptr, consumer, (void*)rq);pthread_create(&p0, nullptr, producter, (void*)rq);pthread_create(&p1, nullptr, producter, (void*)rq);pthread_create(&p2, nullptr, producter, (void*)rq);pthread_join(c0, nullptr);pthread_join(c1, nullptr);pthread_join(c2, nullptr);pthread_join(c3, nullptr);pthread_join(p0, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);return 0;
}

线程的阻塞和唤醒

线程能运行是因为调度器将线程从就绪队列中摘出来放到处理器上,如果不让线程在就绪队列出现就能实现线程的阻塞。阻塞是线程自己发出的动作,也就是线程自己阻塞自己,并不是被别人阻塞的,阻塞是线程主动的行为。己阻塞的钱程是由别人来唤醒的,唤醒是被动的。

当线程被换上处理器运行后,在其时间片内,线程将主宰自己的命运。阻塞是一种意愿,表达的是线程运行中发生了一些事情,这些事情通常是由于缺乏了某些运行条件造成的,以至于线程不得不暂时停下来,必须等到运行的条件再次具备时才能上处理器继续运行。因此,阻塞发生的时间是在线程自己的运行过程中,是线程自己阻塞自己,并不是被谁阻塞。

己被阻塞的线程是无法运行的,属于睡梦中,因此它只能让别人唤醒它,否则它永远没有运行的机会。这个别人便是锁的持有者,它释放了锁之后便去唤醒在它后面因获取该锁而阻塞的线程。

因此唤醒己阻塞的线程是由别的线程,通常是锁的持有者来做的。值得注意的是线程阻塞是线程执行时的“动作”,因此线程的时间片还没用完,在唤醒之后,线程会继续在剩余的时间片内运行,调度器并不会将该线程的时间片“充满”,也就是不会再用线程的优先级priority 为时间片 ticks 赋磕。

因为阻塞是线程主动的意愿,它也是“迫于无奈”才“慷慨”地让出处理器资源给其他线程,所以调度器没必要为其“大方”而“赏赐”它完整的时间片。

线程池

线程池就是事先创建若干个可执行的线程放入一个池中,需要的时候从池中取线程而不用自行创建,使用完毕后不用销毁线程而是返回池中,从而减少线程对象创建和销毁的开销。这种做法可以大大提高服务器的性能,因为线程的创建和销毁成本相对较高,而线程池通过复用线程来降低这种开销。

线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池的种类

  1. FixedThreadPool(定长线程池)
    • 线程数量固定,不会随着任务的增加而增加。
    • 任务队列为链表结果的有界队列。
    • 适用于任务量较稳定、预计执行时间较长的情况。
  2. CachedThreadPool(可缓存线程池)
    • 线程数量不固定,根据任务的数量和执行时间自动调整。
    • 任务队列为不存储元素的阻塞队列。
    • 适用于执行大量、耗时少的任务。
    • 线程闲置超过60秒会被回收。
  3. SingleThreadExecutor(单线程化线程池)
    • 只有一个核心线程,无非核心线程。
    • 任务队列为链表结果的有界队列。
    • 适用于需要保证任务顺序执行的场景。
  4. ScheduledThreadPool(定时线程池)
    • 核心线程数量固定,非核心线程数量无限。
    • 任务队列为延时阻塞队列。
    • 执行定时或周期性任务。
    • 线程闲置超过10秒会被回收。
  5. ForkJoinPool(分治任务线程池)
    • 将任务拆分成更小的子任务,并行执行,并合并结果。
    • 适用于处理大规模的计算任务。
  6. WorkStealingPool(工作窃取线程池)
    • 每个线程都有自己的工作队列。
    • 当某个线程完成自己的任务后,会从其他线程的队列中偷取任务来执行。
    • 这种机制可以提高任务执行效率。

普通版本的线程池

thread_pool.hpp

#pragma once
#include<iostream>
#include<string>
#include<queue>
#include<unistd.h>
#include<pthread.h>namespace ns_threadpool
{template<class T>class ThreadPool{private:int _num;std::queue<T> _task_queue; //临界资源pthread_mutex_t _mtx;pthread_cond_t _cond;public:void Lock(){pthread_mutex_lock(&_mtx);}void Unlock(){pthread_mutex_unlock(&_mtx);}void Wait(){pthread_cond_wait(&_cond,&_mtx);}void Wakeup(){pthread_cond_signal(&_cond);}bool IsEmpty(){return _task_queue.empty();}public:ThreadPool(int num=3):_num(num){pthread_mutex_init(&_mtx,nullptr);pthread_cond_init(&_cond,nullptr);} //想要线程在类中执行类中的函数是不可以行的,因为只能传一个参数//要设置成static,让线程执行静态方法static void* Rountine(void* args){pthread_detach(pthread_self());//分离不用等ThreadPool<T> *tp=(ThreadPool<T> *)args; //this指针while(true){tp->Lock();while(tp->IsEmpty()){tp->Wait(); //没任务等待}int data = 0;tp->PopTask(&data);tp->Unlock();//处理任务std::cout << "消费数据是: " << data << std::endl;}}void InitThreadPool(){pthread_t id;for(int i=0;i<3;i++){pthread_create(&id,nullptr,Rountine,(void*)this);}}void PushTask(const T& in){Lock();_task_queue.push(in);Unlock();Wakeup();}void PopTask(T* out){*out=_task_queue.front();_task_queue.pop(); }~ThreadPool(){pthread_mutex_destroy(&_mtx);pthread_cond_destroy(&_cond);}};
}

main.cc

#include"thread_pool.hpp"
#include<ctime>
#include<cstdlib>using namespace ns_threadpool;int main()
{ThreadPool<int>* tp= new ThreadPool<int>(3);tp->InitThreadPool();srand((long long)time(nullptr));while(true){sleep(1);tp->PushTask(rand()%5);}
}

单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

一般我们有2种方法实现单例模式,饿汉模式和懒汉模式。

饿汉式:全局的单例实例在类装载时构建,急切实例化。这种方式比较简单,因为单例的实例被声明为static和final变量,第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

template <typename T>
class Singleton {static T data;
public:static T* GetInstance(){return &data;}
};

懒汉式:懒汉式是类加载进内存的时候,并不立即初始化这个单例,只有在第一次调用getInstance()方法时才初始化。需要加上双重检查锁定保证线程安全。

//存在线程安全问题
template <typename T>
class Singleton {static T* inst;
public:static T* GetInstance() {if (inst == NULL) {inst = new T();}return inst;}
};

懒汉下单例模式线程池

thread_pool.hpp

#pragma once#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>namespace ns_threadpool
{const int g_num = 5;template <class T>class ThreadPool{private:int num_;std::queue<T> task_queue_; //该成员是一个临界资源pthread_mutex_t mtx_;pthread_cond_t cond_;static ThreadPool<T> *ins;private:// 构造函数必须得实现,但是必须的私有化ThreadPool(int num = g_num) : num_(num){pthread_mutex_init(&mtx_, nullptr);pthread_cond_init(&cond_, nullptr);}ThreadPool(const ThreadPool<T> &tp) = delete;//赋值语句ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;public:static ThreadPool<T> *GetInstance(){static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 当前单例对象还没有被创建if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!{pthread_mutex_lock(&lock);if (ins == nullptr){ins = new ThreadPool<T>();ins->InitThreadPool();std::cout << "首次加载对象" << std::endl;}pthread_mutex_unlock(&lock);}return ins;}void Lock(){pthread_mutex_lock(&mtx_);}void Unlock(){pthread_mutex_unlock(&mtx_);}void Wait(){pthread_cond_wait(&cond_, &mtx_);}void Wakeup(){pthread_cond_signal(&cond_);}bool IsEmpey(){return task_queue_.empty();}public:// 在类中要让线程执行类内成员方法,是不可行的!// 必须让线程执行静态方法static void *Rountine(void *args){pthread_detach(pthread_self());ThreadPool<T> *tp = (ThreadPool<T> *)args;while (true){tp->Lock();while (tp->IsEmpey()){tp->Wait();}//该任务队列中一定有任务了T t;tp->PopTask(&t);tp->Unlock();t();}}void InitThreadPool(){pthread_t tid;for (int i = 0; i < num_; i++){pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);}}void PushTask(const T &in){Lock();task_queue_.push(in);Unlock();Wakeup();}void PopTask(T *out){*out = task_queue_.front();task_queue_.pop();}~ThreadPool(){pthread_mutex_destroy(&mtx_);pthread_cond_destroy(&cond_);}};template <class T>ThreadPool<T> *ThreadPool<T>::ins = nullptr;
} 

main.cc

#include "thread_pool.hpp"
#include "Task.hpp"#include <ctime>
#include <cstdlib>using namespace ns_threadpool;
using namespace ns_task;int main()
{std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;std::cout << "当前正在运行我的进程其他代码..." << std::endl;sleep(5);srand((long long)time(nullptr));while(true){sleep(1);//网络Task t(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);ThreadPool<Task>::GetInstance()->PushTask(t);//单例本身会在任何场景,任何环境下被调用//GetInstance():被多线程重入,进而导致线程安全的问题std::cout << ThreadPool<Task>::GetInstance() << std::endl;}return 0;
}

读者写者模型

读者写者模型是操作系统中的一种同步与互斥机制,它与消费者和生产者模型有相似之处,但也有其独特的特点。在读者写者模型中,主要涉及到两种角色:读者和写者。

读者:在读者写者模型中,读者是指那些只需要读取数据的角色。多个读者之间可以同时读取数据,不会发生冲突,因此读者之间是并行的关系。

写者:写者是指那些需要修改数据的角色。由于数据在修改时不能被其他写者或读者访问,因此写者之间、以及写者与读者之间是互斥的关系。

读者写者模型的特点

  1. 多读少写:在多数应用中,读者的数量通常远多于写者。读者写者模型适用于这种多读少写的情况,能够有效地提高数据的并发访问性能。
  2. 读者并行:多个读者可以同时访问数据,实现并行读取,提高了数据的访问效率。
  3. 写者互斥:当有写者需要修改数据时,其他写者和读者都不能访问数据,保证了数据的一致性和完整性。
  4. 优先级策略:根据不同的应用场景,可以设定不同的优先级策略,如读者优先、写者优先或公平策略等。

读写锁接口

设置读写优先

默认是读锁优先级高

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

和生产者消费者的区别

读者写者模型

读者:只读取数据,不进行写操作。

写者:对数据进行修改或写入。

生产者消费者模型

  • 生产者:负责生成数据并放入缓冲区。
  • 消费者:从缓冲区取出数据进行处理。

读者和消费者最大的区别就是读者只是读并不会对数据进行取走处理。读者写者模型主要关注于如何协调读者和写者对共享数据的并发访问,而生产者消费者模型则主要解决生产者与消费者之间的数据传递和同步问题。

这篇关于信号量线程池读者写者模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/966432

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

图神经网络模型介绍(1)

我们将图神经网络分为基于谱域的模型和基于空域的模型,并按照发展顺序详解每个类别中的重要模型。 1.1基于谱域的图神经网络         谱域上的图卷积在图学习迈向深度学习的发展历程中起到了关键的作用。本节主要介绍三个具有代表性的谱域图神经网络:谱图卷积网络、切比雪夫网络和图卷积网络。 (1)谱图卷积网络 卷积定理:函数卷积的傅里叶变换是函数傅里叶变换的乘积,即F{f*g}

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

AI Toolkit + H100 GPU,一小时内微调最新热门文生图模型 FLUX

上个月,FLUX 席卷了互联网,这并非没有原因。他们声称优于 DALLE 3、Ideogram 和 Stable Diffusion 3 等模型,而这一点已被证明是有依据的。随着越来越多的流行图像生成工具(如 Stable Diffusion Web UI Forge 和 ComyUI)开始支持这些模型,FLUX 在 Stable Diffusion 领域的扩展将会持续下去。 自 FLU

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号