在现代Linux操作系统中,一个程序在运行时,进程是分配资源的基本单位,Linux内核先fork一个子进程,分配物理内存,然后将要执行的可执行文件加载到内存。每个进程都是相互独立的,进程之间如果需要通信则需要借助第三方工具。
不同的进程在切换运行时,CPU需要不停地保存现场、恢复现场,因此进程上下切换的开销是很大的。所以如果程序要并行执行很多任务,通过创建很多的进程来实现,不是个很好的主意。在现代linux操作系统中引入了线程,它的目的是为了减少进程的开销,就是减少进程的切换。一个进程可能存在多个线程,但它至少有一个线程。多个线程之间的关系就像在一个家里一样,每个人都有自己的房间,但饭厅、客厅、卫生间都是公用的。多个线程都有各自独立的资源,如程序计数器、寄存器上下文和各自的栈空间;但是进程中的代码段、数据段、地址空间、打开的文件、信号处理程序等资源都是共享的。这些共享的资源为了保证程序的正确性符合逻辑,这共享资源会涉及安全访问和线程间同步的问题,一般通过互斥锁、条件锁、读写锁等锁机制来实现对资源的安全访问。这是很好理解的,就像在家里的厕所有人正在使用,其他人就要等。
在C语言中,使用pthread库来实现多线程对共享资源的互斥访问:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁
int shared_num = 0; // 共享资源
void* thread_task(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
shared_num++;
printf("共享变量值:%d\n", shared_num);
pthread_mutex_unlock(&mutex); // 解锁
pthread_exit(NULL); //退出程序
}
加锁也会增加系统开销,即当程序调用加锁函数时,操作系统会从用户态切换到内核态,并阻塞在内核态;当程序调用解锁时,也经历从用户态切换到内核态,再从内核态切换回用户态。这告诉我们如果减少此类开锁,就要减少加锁、解锁的操作。
我们再来说说线程池,虽然进程的多个线程可以共享进程的很多资源,如代码段、数据段、打开的文件,线程们也有自己的上下文环境,如各自的寄存器状态、栈、程序计数器等。在现代linux系统里,进程是资源分配的基本单元,而线程则是程序执行和调试的最小单元。线程的开锁除了线程上下文 切换带来的开销外,这里还有另一个开销,那就是线程的创建与销毁,尤其是频繁使用线程的场景下,这种开销会特别明显。
为了减少线程不断创建与销毁带来的开销,可以实现一个线程池。预先创建一些线程,没有任务时,这些线程就阻塞在池中,有任务时,通过管理线程将任务分配到指定的线程来执行。
一般来说实现一个线程池需要有管理线程、工作线程和任务接口等组成。管理线程是用于创建和管理工作线程的,将用户创建的不同任务分配给不同的工作线程去执行;工作线程是真正做事的线程,它负责执行给过来的伤,没有任务时,就阻塞中池中。线程池一般会提供一些任务接口供用户创建不同的任务,这些任务会分配到不同的线程中去执行。