一.进程和线程的概念
1.进程(最小的资源单位):
进程:就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。
程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成;
数据集;则是程序在执行过程中所需要使用的资源;
进程控制块:用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
进程比线程更早出现,计算机早期处理代码时,使用的是串行的方法,假设计算机在运行A,B,C三个软件,需要A运行完了再运行B,B运营完再运行C。这就造成了一个问题,如果A执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。你是不是已经想到在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停。聪明,这当然没问题,但这里有一个关键词:切换。
既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B分别需要什么资源,怎样去识别程序A和程序B等等(比如读书)。
由于进程之间的资源不能共享,所以假如我们有个编辑软件,一共有三个进程:输入文件内容进程,显示文件内容进程,修改文件内容进程。这时候,三个进程都必须有一份相同的文件内容,这就造成了cpu在切换进程时,记录进程状态开销太大,导致CPU处理效率低。
2.线程(最小的执行单位)
线程:线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。
像刚刚所举例子一样,线程的出现,可以使任务入文件内容,显示文件内容,修改文件内容共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗。
线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。
3.线程和进程的关系
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。
多线程和多进程的区别:
二.并行和并发
并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集。由于python存在GIL的原因,所以同一进程下不可能实现并行。
三.同步和异步
在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。举个例子,打电话时就是同步通信,发短息时就是异步通信。
四.线程
1.线程的创建
c语言中创建线程的方式如下:
#include <pthread.h>
pthread_create (thread, attr, start_routine, arg)
其中,thread为线程指针;attr为线程属性,默认NULL;start_routine为线程运行函数起始地址;arg为运行函数的参数。无参数时使用NULL。
如果线程成功创建,函数返回0,否则说明创建失败。
当线程完成工作后,若无需继续存在,则会调用pthread_exit函数。也就是说,这个函数可以显式地退出一个线程。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg)
{
// 线程执行的逻辑
printf("Thread function executed\n");
pthread_exit(NULL);
}
int main()
{
pthread_t thread_id;
int result;
// 创建线程,并执行函数 thread_function
result = pthread_create(&thread_id, NULL, thread_function, NULL);
if (result != 0) {
fprintf(stderr, "Failed to create a thread: %d\n", result);
return 1;
}
// 线程创建成功
printf("Thread created successfully\n");
// ...
return 0;
}
根据ret的返回值判断线程是否创建成功,创建的一个简单线程输出结果为Thread created successfully。
当线程完成工作后,若无需继续存在,则会调用pthread_exit函数。也就是说,这个函数可以显式地退出一个线程。
2.多线程
pthread_exit (status)
下面做一个简单的示例,通过调用多线程来打印数字
//test1.c
#include<stdio.h>
#include<pthread.h>
#define NUM_THREADS 5 //线程数
void *PrintTh(void *th)
{
int i = *((int*)th);
printf("Hello, I'm thread %d\n", i);
return 0;
}
int main(){
int i,ret;
pthread_t p;
for(i=0; i<NUM_THREADS; i++){
printf("create th %d\n",i);
ret = pthread_create(&p,NULL, PrintTh, (void*)&i);
if(ret != 0)
printf("th %d error, code = \n",i);
}
pthread_exit(NULL);
return 0;
}
如果在Linux中使用pthread,需要使用库libpthread.a, 编译时要加-lpthread参数:
gcc test1.c -lpthread -o test1.o ./test1.
运行结果如下
create th 0
create th 1
Hello, I'm thread 1
Hello, I'm thread 2
create th 2
Hello, I'm thread 3
create th 3
create th 4
Hello, I'm thread 4
Hello, I'm thread 4
之所以main函数和printTh输出的值不同,是因为我们传入printTh的参数是一个空指针,这个空指针直接指向了用于循环的i的地址,所以printTh调用i的时候,有时i的取值刚好发生变化,有时尚未发生变化。
由于函数pthread_create接收的传输参数必须为void*,而且只有一个,故当需要传递多个参数时,需要用到结构体。
// test2.c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define NUM_THREADS 5
typedef struct PERSON{
int id;
char name;
}Person, *pPerson;
void * PrintTh(void * p)
{
pPerson tid = (pPerson)p;
printf("I'm %c ,th %d\n", tid->name,tid->id);
return 0;
}
int main(void)
{
pthread_t Pthread[NUM_THREADS];
Person persons[NUM_THREADS];
int i, ret;
for (i = 0; i < NUM_THREADS; i++)
{
printf("create th %d \n",i);
persons[i].id = i;
persons[i].name = 'A'+i%10;
ret = pthread_create(&Pthread[i], NULL, PrintTh, (void *)&persons[i]);
if (0 != ret)
{
printf("Error: 创建线程失败!\n");
exit(-1);
}
}
pthread_exit(NULL);
return 0;
}
输出结果为
create th 0
create th 1
I'm A ,th 0
I'm B ,th 1
create th 2
create th 3
I'm C ,th 2
I'm D ,th 3
create th 4
I'm E ,th 4
五.线程间的通信
前面介绍了线程的基本知识与创建回收,但是在实际过程中需要线程之间配合来完成同一个任务,这就涉及到线程之间的通信,线程之间的通信方式主要分为3种,共享内存、消息队列、管道。
每种方式有不同的方法来实现。
共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
volatile关键字共享内存
消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显式的进行通信
wait()/notify()等待通知方式
join()方法
管道流
管道输入/输出流的形式
共享内存:(代码由gpt创建示例)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 共享数据结构
typedef struct {
int value;
pthread_mutex_t mutex;
} SharedData;
void *producer_thread(void *arg)
{
SharedData *shared_data = (SharedData *)arg;
for (int i = 1; i <= 10; i++) {
// 使用互斥锁保证对共享数据的原子操作
pthread_mutex_lock(&shared_data->mutex);
// 修改共享数据
shared_data->value = i;
printf("Producer thread: Produced value %d\n", i);
pthread_mutex_unlock(&shared_data->mutex);
// 短暂休眠
usleep(200000);
}
pthread_exit(NULL);
}
void *consumer_thread(void *arg)
{
SharedData *shared_data = (SharedData *)arg;
for (int i = 1; i <= 10; i++) {
pthread_mutex_lock(&shared_data->mutex);
// 读取共享数据
int value = shared_data->value;
printf("Consumer thread: Consumed value %d\n", value);
pthread_mutex_unlock(&shared_data->mutex);
// 短暂休眠
usleep(200000);
}
pthread_exit(NULL);
}
int main()
{
pthread_t producer_thread_id, consumer_thread_id;
SharedData shared_data;
// 初始化共享数据和互斥锁
shared_data.value = 0;
pthread_mutex_init(&shared_data.mutex, NULL);
// 创建生产者线程
if (pthread_create(&producer_thread_id, NULL, producer_thread, &shared_data) != 0)
{
fprintf(stderr, "Failed to create a producer thread\n");
return 1;
}
// 创建消费者线程
if (pthread_create(&consumer_thread_id, NULL, consumer_thread, &shared_data) != 0)
{
fprintf(stderr, "Failed to create a consumer thread\n");
return 1;
}
// 等待线程执行完毕
if (pthread_join(producer_thread_id, NULL) != 0)
{
fprintf(stderr, "Failed to join the producer thread\n");
return 1;
}
if (pthread_join(consumer_thread_id, NULL) != 0)
{
fprintf(stderr, "Failed to join the consumer thread\n");
return 1;
}
// 销毁互斥锁
pthread_mutex_destroy(&shared_data.mutex);
return 0;
}
在上述示例中,我们创建了一个共享数据结构SharedData,其中包含一个整型变量value和一个互斥锁mutex。这个共享数据结构被传递给生产者线程和消费者线程。
生产者线程通过修改value,生产出一系列的值,然后进行休眠。消费者线程通过读取value,消费这些值,并进行休眠。为了避免对共享数据的冲突访问,我们使用互斥锁进行保护。
在main函数中,我们创建了生产者线程和消费者线程,并使用pthread_join函数等待线程的执行完毕。最后,我们销毁互斥锁。
需要注意的是,这个示例中的线程通信方式使用了共享内存,但并未进行数据同步。在实际应用中,需要根据具体情况使用适当的同步机制(如互斥锁、条件变量等)来保证线程间的数据同步和互斥访问。
消息队列
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <mqueue.h>
#define MAX_MSG_SIZE 1024
#define MSG_QUEUE_NAME "/my_message_queue"
typedef struct {
int value;
} Message;
void *producer_thread(void *arg)
{
mqd_t mq;
char buffer[MAX_MSG_SIZE];
Message message;
// 打开消息队列
mq = mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, 0666, NULL);
if (mq == (mqd_t)-1) {
fprintf(stderr, "Failed to open the message queue\n");
return NULL;
}
for (int i = 1; i <= 10; i++) {
// 构造消息
message.value = i;
// 发送消息到队列
if (mq_send(mq, (const char *)&message, sizeof(Message), 0) == -1) {
fprintf(stderr, "Failed to send message\n");
} else {
printf("Producer thread: Produced value %d\n", i);
}
// 短暂休眠
usleep(200000);
}
// 关闭消息队列
mq_close(mq);
pthread_exit(NULL);
}
void *consumer_thread(void *arg)
{
mqd_t mq;
char buffer[MAX_MSG_SIZE];
unsigned int prio;
ssize_t received_bytes;
Message message;
// 打开消息队列
mq = mq_open(MSG_QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
fprintf(stderr, "Failed to open the message queue\n");
return NULL;
}
while (1) {
// 接收消息
received_bytes = mq_receive(mq, buffer, MAX_MSG_SIZE, &prio);
if (received_bytes == -1) {
fprintf(stderr, "Failed to receive message\n");
break;
}
// 处理接收到的消息
if (received_bytes == sizeof(Message)) {
message = *((Message *)buffer);
printf("Consumer thread: Consumed value %d\n", message.value);
}
// 短暂休眠
usleep(200000);
}
// 关闭消息队列
mq_close(mq);
pthread_exit(NULL);
}
int main()
{
pthread_t producer_thread_id, consumer_thread_id;
struct mq_attr attr;
// 初始化消息队列属性
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = sizeof(Message);
attr.mq_curmsgs = 0;
// 创建消息队列
mqd_t mq = mq_open(MSG_QUEUE_NAME, O_CREAT | O_RDWR, 0666, &attr);
if (mq == (mqd_t)-1) {
fprintf(stderr, "Failed to create the message queue\n");
return 1;
}
// 创建生产者线程
if (pthread_create(&producer_thread_id, NULL, producer_thread, NULL) != 0) {
fprintf(stderr, "Failed to create a producer thread\n");
return 1;
}
// 创建消费者线程
if (pthread_create(&consumer_thread_id, NULL, consumer_thread, NULL) != 0) {
fprintf(stderr, "Failed to create a consumer thread\n");
return 1;
}
// 等待线程执行完毕
if (pthread_join(producer_thread_id, NULL) != 0) {
fprintf(stderr, "Failed to join the producer thread\n");
return 1;
}
if (pthread_join(consumer_thread_id, NULL) != 0) {
fprintf(stderr, "Failed to join the consumer thread\n");
return 1;
}
// 关闭消息队列
mq_close(mq);
// 删除消息队列
mq_unlink(MSG_QUEUE_NAME);
return 0;
}
在上述示例中,我们使用了 <mqueue.h> 头文件提供的消息队列相关函数和数据类型。首先,我们通过调用 mq_open 函数创建一个消息队列,并指定消息队列的属性(包括最大消息数和每个消息的大小等)。
生产者线程通过调用 mq_send 函数将消息发送到消息队列中,消息内容是一个包含整型变量的结构。
管道
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define READ_END 0
#define WRITE_END 1
typedef struct {
int value;
} Message;
void *producer_thread(void *arg)
{
int pipe_fd[2];
Message message;
// 创建管道
if (pipe(pipe_fd) == -1) {
fprintf(stderr, "Failed to create the pipe\n");
return NULL;
}
for (int i = 1; i <= 10; i++) {
// 构造消息
message.value = i;
// 在写端写入消息
if (write(pipe_fd[WRITE_END], &message, sizeof(Message)) == -1) {
fprintf(stderr, "Failed to write to the pipe\n");
} else {
printf("Producer thread: Produced value %d\n", i);
}
// 短暂休眠
usleep(200000);
}
// 关闭写端
close(pipe_fd[WRITE_END]);
pthread_exit(NULL);
}
void *consumer_thread(void *arg)
{
int pipe_fd[2];
Message message;
// 创建管道
if (pipe(pipe_fd) == -1) {
fprintf(stderr, "Failed to create the pipe\n");
return NULL;
}
while (1) {
// 从读端读取消息
ssize_t read_bytes = read(pipe_fd[READ_END], &message, sizeof(Message));
if (read_bytes == -1) {
fprintf(stderr, "Failed to read from the pipe\n");
break;
} else if (read_bytes == 0) {
// 管道中没有数据,结束循环
break;
}
// 处理读取到的消息
printf("Consumer thread: Consumed value %d\n", message.value);
// 短暂休眠
usleep(200000);
}
// 关闭读端
close(pipe_fd[READ_END]);
pthread_exit(NULL);
}
int main()
{
pthread_t producer_thread_id, consumer_thread_id;
// 创建生产者线程
if (pthread_create(&producer_thread_id, NULL, producer_thread, NULL) != 0) {
fprintf(stderr, "Failed to create a producer thread\n");
return 1;
}
// 创建消费者线程
if (pthread_create(&consumer_thread_id, NULL, consumer_thread, NULL) != 0) {
fprintf(stderr, "Failed to create a consumer thread\n");
return 1;
}
// 等待线程执行完毕
if (pthread_join(producer_thread_id, NULL) != 0) {
fprintf(stderr, "Failed to join the producer thread\n");
return 1;
}
if (pthread_join(consumer_thread_id, NULL) != 0) {
fprintf(stderr, "Failed to join the consumer thread\n");
return 1;
}
return 0;
}
在上述示例中,我们使用pipe函数创建了一个管道,并使用文件描述符数组pipe_fd来保存管道的读端和写端。
生产者线程通过调用write函数将消息写入管道的写端,消费者线程通过调用read函数从管道的读端读取消息。注意,当管道的写端被关闭时,读取操作会返回0,表示管道中没有数据可读。
在main函数中,我们创建了生产者线程和消费者线程,并使用pthread_join函数等待线程的执行完毕。
需要注意的是,管道有一定的缓冲区大小,如果写入的数据量超过了缓冲区的大小,写操作可能会阻塞。同样地,如果读取的数据量超过了缓冲区中已有的大小,读操作可能会阻塞。在实际使用中,需要根据具体需求和数据量进行合理的处理。