一、线程的概念
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- PCB(Process Control Block,进程控制块)是操作系统中用来存储和管理进程相关信息的数据结构。PCB 包含了进程的各种状态信息,如进程的标识号(PID)、程序计数器(PC)、寄存器值、调度信息、内存管理信息、打开文件的列表等等。PCB 的存在使得操作系统能够有效地管理进程,包括调度进程、恢复进程状态等操作。
- 一个 Linux 进程拥有自己的 PCB 时,意味着该进程在系统中有相应的数据结构,用于维护和管理该进程的状态以及各种信
-
在Linux系统中,在CPU眼中,看到的PCB()都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
下图为一个进程:
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表等的一些数据结构;当把磁盘中的数据和代码加载进内存中后,虚拟地址和物理地址就是通过页表建立映射的 。
进程:
通过上图,个进程它包含了进程地址空间、文件相关的属性、各种信号、页表等。
从内核角度来理解进程:
进程:它是承担分配系统资源的基本实体。
线程:它是CPU调度的基本单位,承担进程资源的一部分的基本实体。
换言之,当我们创建进程时是创建一个task_struct、创建地址空间、维护页表,然后在物理内存当中开辟空间、构建映射,打开进程默认打开的相关文件、注册信号对应的处理方案等等。而我们之前接触到的进程都只有一个task_struct,也就是该进程内部只有一个执行流。反之,内部有多个执行流的进程叫做多执行流进程。
CPU如何看待进程快的?
CPU不管有多少条执行流,只看task_struct,你task_struct有1条执行流就是单执行流的task_struct,有多执行流,你就是多执行流的task_struct。如下图:
Linux下并不存在真正的线程?而是用进程模拟的?
操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多,当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。
如果Linux实现真的线程,那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,搞一套与进程类似的线程管理模块,整个难度就比较大。
相对于其他操作系统,Linux系统内核只提供了轻量级进程的支持,并未实现线程模型。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。
进程是资源分配的单位,同一进程中的多个线程共享该进程的资源。Linux中所谓的“线程”只是在被创建时clone了父进程的资源,因此clone出来的进程表现为“线程”,这一点一定要弄清楚。因此,Linux“线程”这个概念只有在打引号的情况下才是最准确的。
二、创建多线程
1.创建多个线程,每个线程打印自己是第几个创建的。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUM 100
void *thread_inc(void *arg);
void *thread_des(void *arg);
long num = 0;
int main(int argc, char *argv[])
{
pthread_t thread_id[THREAD_NUM];
int i;
for (i = 0; i < THREAD_NUM; i++)
{
if (i % 2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
for (i = 0; i < THREAD_NUM; i++)
pthread_join(thread_id[i], NULL);
printf("result: %ld \n", num);
return 0;
}
void *thread_inc(void *arg)
{
for (int i = 0; i < 100000; i++)
num += 1;
return NULL;
}
void *thread_des(void *arg)
{
int i;
for (int i = 0; i < 100000; i++)
num -= 1;
return NULL;
}
执行后:
按理说应该执行后为0。
原因分析:线程的执行是随机的。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <pthread.h>
5 #include <unistd.h>
6
7 unsigned int value1,value2, count=0;
8 void *function(void *arg);
9 int main(int argc, char *argv[])
10 {
11 pthread_t a_thread;
12
13 if (pthread_create(&a_thread, NULL, function, NULL) < 0)
14 {
15 perror("fail to pthread_create");
16 exit(-1);
17 }
18 while ( 1 )
19 {
20 count++;
21 value1 = count;
22 value2 = count;
23 }
24 return 0;
25 }
26
27 void *function(void *arg)
28 {
29 while ( 1 )
30 {
31 if (value1 != value2)
32 {
33 printf("count=%d , value1=%d, value2=%d\n", count, value1, value2);
34 usleep(100000);
35 }
36 }
37 return NULL;
38 }
执行上述代码结果:
实际代码执行流程:
主线程和子线程是可以共享全局变量。我的理解:在子进程中出现了父进程的全局变量,内核就开始对线程进行了调度,执行子线程去判断 value1 != value2。
做个实验: 期待的功能程序:
- 主进程创建子线程,子线程函数function();
- 主线程count自加,并分别赋值给value1,value2;
- 时间片到了后切换到子线程,子线程判断value1、value2值是否相同,如果不同就打印信息value1,value2,count的值,但是因为主线程将count先后赋值给了value1,value2,所以value1,value2的值应该永远相同,所以不应该打印任何内容;
- 重复2、3步骤。
三、互斥锁、信号量
解决办法:常用的方法有信号量、互斥锁、条件变量等。
Linux中四种进程或线程同步互斥的控制方法
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
2、互斥量:为协调共同对一个共享资源的单独访问而设计的,互斥对象只有一个。
3、信号量:为控制一个具有有限数量用户资源而设计,只能在进程上下文中使用,适合长时间访问共享资源的情况
4、自旋锁:适合短时间访问共享资源的情况,如果锁被长时间持有,等待线程会消耗大量资源
5、事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
(2)互斥锁的方法解决:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define _LOCK_
unsigned int value1, value2, count = 0;
pthread_mutex_t mutex;
void *function(void *arg);
int main(int argc, char *argv[]) {
pthread_t a_thread;
if (pthread_mutex_init(&mutex, NULL) < 0) {
perror("fail to mutex_init");
exit(-1);
}
if (pthread_create(&a_thread, NULL, function, NULL) < 0) {
perror("fail to pthread_create");
exit(-1);
}
while (1) {
count++;
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
value1 = count;
value2 = count;
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
return 0;
}
void *function(void *arg) {
while (1) {
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
if (value1 != value2) {
printf("count=%d , value1=%d, value2=%d\n", count, value1, value2);
} else {
printf("count=%d , value1=%d, value2=%d\n", count, value1, value2);
}
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
return NULL;
}
执行结果:
流程分析:
如上图所示:
- 时刻n,主线程获得mutex,从而进入临界区(通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。);
- 时刻n+1,时间片到了,切换到子线程;
- n+2时刻子线程申请不到锁mutex,所以放弃cpu,进入休眠;
- n+3时刻,主线程释放mutex,离开临界区,并唤醒阻塞在mutex的子线程,子线程申请到mutex,进入临界区;
- n+4时刻,子线程离开临界区,释放mutex。
可以看到,加锁之后,即使主线程在value2 =count; 之前产生了调度,子线程由于获取不到mutex,会进入休眠,只有主线程出了临界区,子线程才能获得mutex,访问value1和value2,就永,就实现了我们预期的代码时序。
代码加锁的位置:主进程和子进程同时访问的资源的代码。
(3)用信号量来解决
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <semaphore.h>
#define _SIGNAL_
unsigned int value1, value2, count = 0;
pthread_mutex_t mutex;
sem_t g_sem;
void *function(void *arg);
int main(int argc, char *argv[])
{
pthread_t a_thread;
#ifdef _SIGNAL_
sem_init(&g_sem,0,1);
#endif
if (pthread_mutex_init(&mutex, NULL) < 0)
{
perror("fail to mutex_init");
exit(-1);
}
if (pthread_create(&a_thread, NULL, function, NULL) < 0)
{
perror("fail to pthread_create");
exit(-1);
}
while (1)
{
count++;
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
#ifdef _SIGNAL_
sem_wait(&g_sem);
#endif
value1 = count;
value2 = count;
#ifdef _SIGNAL_
sem_post(&g_sem);
#endif // DEBUG
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
sem_destroy(&g_sem);
return 0;
}
void *function(void *arg) {
while (1) {
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
#ifdef _SIGNAL_
sem_wait(&g_sem);
#endif
if (value1 != value2) {
printf("count=%d , value1=%d, value2=%d\n", count, value1, value2);
} else {
printf("count=%d , value1=%d, value2=%d\n", count, value1, value2);
}
#ifdef _SIGNAL_
sem_post(&g_sem);
#endif // DEBUG
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
return NULL;
}
运行结果:
小编的问题?为什么count在自增的时候,value1和value2的值是一定相等的,但是value1和value2的值是不等于count的,猜测可能和线程调度的时间片有关系。也可以一起来讨论哟
四、信号量(同步与互斥)
1.信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
2.编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
3.信号量的用法
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);
// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);
// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);
// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
// 销毁信号量
int sem_destroy(sem_t *sem);
4.信号量用于互斥
// 信号量用于互斥实例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem; //信号量
void printer(char *str)
{
sem_wait(&sem);//减一,p操作
while(*str) // 输出字符串(如果不用互斥,此处可能会被其他线程入侵)
{
putchar(*str);
fflush(stdout);
str++;
sleep(1);
}
printf("\n");
sem_post(&sem);//加一,v操作
}
void *thread_fun1(void *arg)
{
char *str1 = "hello";
printer(str1);
}
void *thread_fun2(void *arg)
{
char *str2 = "world";
printer(str2);
}
int main(void)
{
pthread_t tid1, tid2;
sem_init(&sem, 0, 1); //初始化信号量,初始值为 1
//创建 2 个线程
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
//等待线程结束,回收其资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem); //销毁信号量
return 0;
}
运行结果:
代码分析:线程2与线程1都要执行公共的函数prinfter在线程1,在printer中对信号量进行减1,待线程1中对临界区进行访问后,再对信号量进行加1。线程2就又可以对临界区进行访问了。注意:线程是同步执行,打印出来的字符是分两次而已。
5.信号量用于同步
// 信号量用于同步实例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem_g,sem_p; //定义两个信号量
char ch = 'a';
void *pthread_g(void *arg) //此线程改变字符ch的值
{
while(1)
{
sem_wait(&sem_g);
ch++;
sleep(1);
sem_post(&sem_p);
}
}
void *pthread_p(void *arg) //此线程打印ch的值
{
while(1)
{
sem_wait(&sem_p);
printf("%c",ch);
fflush(stdout);
sem_post(&sem_g);
}
}
int main(int argc, char *argv[])
{
pthread_t tid1,tid2;
sem_init(&sem_g, 0, 0); // 初始化信号量为0
sem_init(&sem_p, 0, 1); // 初始化信号量为1
// 创建两个线程
pthread_create(&tid1, NULL, pthread_g, NULL);
pthread_create(&tid2, NULL, pthread_p, NULL);
// 回收线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行结果:
代码分析:线程1对全局变量 ch = 'a'进行+1,线程2对字符进行打印是同步执行的。PV操作同步执行。
五、互斥锁(同步)
//互斥锁用于同步实例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 1.定义互斥量并初始化
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;
char ch = 'a';
void *pthread_g(void *arg) //此线程改变字符ch的值
{
while (1) {
pthread_mutex_lock(&g_tMutex);
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
ch++;
pthread_mutex_unlock(&g_tMutex);
sleep(1);
}
}
void *pthread_p(void *arg) //此线程打印ch的值
{
while (1) {
pthread_mutex_lock(&g_tMutex);
printf("%c", ch);
fflush(stdout);
pthread_cond_wait(&g_tConVar, &g_tMutex);
pthread_mutex_unlock(&g_tMutex);
}
}
int main(int argc, char *argv[]) {
pthread_t tid1, tid2;
// 创建两个线程
pthread_create(&tid1, NULL, pthread_g, NULL);
pthread_create(&tid2, NULL, pthread_p, NULL);
// 回收线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
代码运行结果:
代码分析:进程1中对临界区进行加锁,修改临界区后解锁,然后通知进程2打印临界区的值。
备注:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_wait
函数会释放传入的互斥锁mutex
,并且将调用线程挂起,等待条件变量cond
的变化。- 当另一个线程调用
pthread_cond_signal
或pthread_cond_broadcast
唤醒等待在条件变量上的线程时,被唤醒的线程会重新获取互斥锁mutex
并继续执行。 - 在等待条件变量的过程中,
pthread_cond_wait
函数会自动将互斥锁mutex
解锁,并且在线程被唤醒后重新加锁。