一、线程的基本概念 及 创建
1、线程的概念(了解)
— 进程的关系
- 进程是 运行一段程序 系统给其分配资源的总称;
- 进程有独立的地址空间;每个进程互不影响,只能访问自己空间的数据;
- Linux为每个进程创建task_struct 结构体;
- 每个进程都参与内核调度,互不影响;
— 、为啥需要线程,线程的作用
- 进程再切换时对系统的开销很大;
以上如图:
- 进程(P1、P2)创建后 系统将数据放入内存(RAM)中,CPU去执行某一个进程时,需要去从进程中获取指令,要去取址、译码、执行,还要去访问数据,这些东西都是放在内存中的,cpu需要去内存中获取指令或数据区执行,处理。cpu频率很快,相对向内存读取要快的多,中间加一个Cache(高速缓存:静态RAM),每次运行进程前,将进程的部分数据放入cache中,cpu直接在cache中读取数据。
- 当进程间切换时,cache需要刷新,还有一种 tlb(页表缓存),每个进程的物理地址是不一样的。
- 很多操作系统引入轻量级进程LWP(线程)。
- 同一进程中的线程共享相同地址空间。
- Linux中不区分 进程、线程。
2、线程特点(了解)
- 通常线程指的是共享相同地址空间的多个任务。
- 使用多线程的好处:
- 大大提高了任务切换的效率。
- 避免了额外的TLB&cache的刷新。
一个进程中。
- 一个进程中的多个线程共享以下资源。
线程ID(TID)
PC(程序计数器)和相关寄存器。
堆栈
错误号(errno);
优先级;
执行状态和属性;
- pthread线程库中提供了如下基本操作。
创建线程;
回收线程;
结束线程;
- 同步和互斥机制
信号量;
互斥锁;
4、线程的创建(熟悉)
— 线程创建 — pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread , const pthread_attr_t *attr , void*(routine)(void*), void *arg);
- 成功返回0,失败返回错误码;
- thread 线程对象;
- attr :线程属性,NULL代表默认属性;
- routine : 线程执行的函数;
- arg : 传递给routine 的参数;
— 线程回收 — pthread_join
#include <pthread.h>
int pathread_join(pthread_t thread , void ** retval);
- 成功返回0,失败时返回错误码;
- thread 要回收的线程对象;
- 调用线程阻塞直到thread结束;
- *retval 接收线程thread的返回值;(一个二级指针返回的是一个地址)
— 结束一个线程 — pthread_exit
#include <pthread.h>
void pthread_exit(void*retval);
- 结束当前进程;
- retval可被其他线程通过pthread_join获取。(返回的retval地址不能是一个局部变量的地址,不然该空间被释放,会出错)
示例:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
char message[32] = "Hello World";
void *thread_func(void *arg);
int main(int argc, const char *argv[])
{
pthread_t a_thread;
void *result;
if(pthread_create(&a_thread,NULL,thread_func,NULL) != 0)
{
printf("fail to pthread_create");
exit(-1);
}
pthread_join(a_thread,&result); //线程回收
printf("result is %s \n",(char*)result);
printf("message is %s \n",message);
return 0;
}
void *thread_func(void *arg)
{
sleep(1); //
strcpy(message,"markey by thread");
pthread_exit("thank you for waiting for me"); //线程退出
}
- 线程时通过第三方的库函数实现的,在编译时要加入相应的库。
- 之前讲过 多进程中 父进程结束时,子进程还会再运行;但在多线程中:当进程中的主线程man返回结束时,还进程中的所有线程都会结束。
二、线程兼通信的问题
1、同步机制(了解)
- 线程共享同一进程的地址空间;
- 优点:线程间通信很容易
- 通过全局变量交换数据;
- 缺点:多个线程访问共享数据时需要同步或互斥机制。
2、线程通信 — 同步
- 同步(synchronization)指的是多个任务按照约定的先后次序相互配合完成一件事。(后完成的任务通常都需要等待 先完成的任务完成,先完成的任务完成后通知后任务)
- 1968年,Edsgar Dijktra基于信号量的概念 提出了一种 同步进制。
- 由信号量来决定线程是继续运行还是 阻塞等待;
3、信号量(理解)
- 信号量(灯):信号量代表某一类资源,其值表示系统中该资源的数量(没资源为0,有资源大于0)
- 信号量是一个受保护的变量,智能通过三种操作来访问。
- 初始化;
- P操作(申请资源):检查信号量的值是否大于0,大于0有资源,任务继续执行,去访问;等于0时没资源,线程阻塞,等待该资源。
- V操作(释放资源): 任务不需要访问资源,或者任务产生了一个资源,告诉其他需要该资源的任务。
4、对信号量基本操之 — P操作(熟悉)
- 信号量 --- P
P(S)含义如下:
if(信号量的值大于0)
{
申请资源的任务继续运行;
信号量的值减一;
}else
{
申请资源的任务阻塞;
}
5、对信号量的 V操作(熟悉)
- 信号量 ------ V
- V(S)含义如下:
信号量的值加一;
if(有任务在等待资源)
{
唤醒等待的任务,让其继续运行;
}
6、对应相应函数
— Posix信号量
- posix中定义了两种信号量;
- 无名信号量(基于内存的信号量):主要用于进程内部,线程之间的通信(同步);
- 有名信号量(既可以用于进程之间,也可以用于线程之间);
- pthread库常用的信号量操作函数如下:
int sem_init(sem_t *sem , int pshared , unsigned int value);
int sem_wait(sem_t *sem); P操作
int sem_post(sem_t *sem); V操作
2、 信号量初始化 -- sem_init
- #include <semaphore.h>
- int sem_init(sem_t *sem , int pshared , unsigned int value);
- 成功时返回0,失败时返回 EOF;
- sem 指向要初始化的信号量对象;
- pashared 0 — 线程间使用, 1— 进程间使用;
- val : 信号量初值;
3、信号量操作 — P/V操作
- #include <semaphore.h>
- int sem_wait(sem_t *sem); P操作
- int sem_post(sem_t *sem); V操作
- 成功时返回0,失败时返回EOF;
- sem指向要操作的信号量对象;
三、信号量同步示例1、2
1、信号量同步示例1 (熟练)
- 两个线程同步读写缓冲区(生产者/消费者问题)
— 写缓冲区线程 : writer 读缓冲区线程 : reader
— 写缓冲区对信号量进行一个P操作,并唤醒 读进程、都进程唤醒后对信号量进行一个V操作。
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
char buf[32];
sem_t sem;
void *function(void *arg);
int main(int argc, const char *argv[])
{
pthread_t a_thread;
if(sem_init(&sem,0,0) < 0)
{
perror("sem_init");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL) != 0)
{
perror("pthread_create");
exit(-1);
}
do
{
fgets(buf,32,stdin); //按行标准输入
sem_post(&sem); //对信号量进行V操作,获取/释放资源
}while(strncmp(buf,"quit",4) != 0);
return 0;
}
void *function(void *arg)
{
while(1)
{
sem_wait(&sem);//对信号量进行P操作,申请资源
printf("you enter %d characters \n",strlen(buf));
}
}
— ps aux –L |grep sem
2、信号量同步示例2 (熟练)
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
char buf[32];
sem_t sem_r;
sem_t sem_w;
void *function(void *arg);
int main(int argc, const char *argv[])
{
pthread_t a_thread;
if(sem_init(&sem_r,0,0) < 0)
{
perror("sem_init");
exit(-1);
}
if(sem_init(&sem_w,0,1) < 0)
{
perror("sem_init");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL) != 0)
{
perror("pthread_create");
exit(-1);
}
do
{
sem_wait(&sem_w);
fgets(buf,32,stdin);
sem_post(&sem_r); //对信号量进行V操作,获取/释放资源
}while(strncmp(buf,"quit",4) != 0);
return 0;
}
void *function(void *arg)
{
while(1)
{
sem_wait(&sem_r);//对信号量进行P操作,申请资源
printf("you enter %d characters \n",strlen(buf));
sem_post(&sem_w);
}
}
注:任何信号量都要进行初始化。
小结: sem_init
sem_wait
sem_post
四、进程间的互斥
1、临界资源(了解)
- 临界资源
- 通常是一个共享资源;
- 一次只能允许被一个任务(进程、线程)访问;
- 临界区
- 访问临界区的代码;(其他代码成为非临界代码)
- 互斥机制
- mutex互斥锁;
- 要不没有任务持有该锁,要不只有一个任务只有该锁。
- 任务访问临界资源前申请锁,访问完后释放锁;
2、互斥机制
- 互斥锁(熟悉)
- 互斥锁初始化 — pthread_mutex_init
#include <pthread.h>
int pathread_mutex_init(pathread_mutex_t *mutex , const pthread_mutexattr_t * attr);
- 成功时返回0,失败时返回错误码;
- mutex 指向要初始化 的互斥 锁对象;
- attr : 互斥锁的属性,NULL表示缺省属性;
- 申请锁 — pthread_mutex_lock
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
- 成功时返回0,失败时返回错误码;
- mutex 指向要 初始化的互斥锁对象;
- 如果无法获得锁,任务阻塞;
- 释放锁 — pthread_mutex_unlock
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t * mutex);
- 成功时返回0,失败时返回错误码;
- mutex 指向要 初始化的互斥锁对象;
- 执行完临界区要及时释放锁;
示例:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
unsigned int count,value1,value2;
pthread_mutex_t lock;
void *thread_func(void *arg);
int main(int argc, const char *argv[])
{
pthread_t a_thread;
if(pthread_mutex_init(&lock,NULL) != 0)
{
printf("fail to pthread_mutex_init \n");
exit(-1);
}
if(pthread_create(&a_thread,NULL,thread_func,NULL) != 0)
{
printf("fail to pthread_create");
exit(-1);
}
while(1)
{
count++;
#ifdef _LOCK_
pthread_mutex_lock(&lock);
#endif
value1 = count;
value2 = count;
#ifdef _LOCK_
pthread_mutex_unlock(&lock);
#endif
}
return 0;
}
void *thread_func(void *arg)
{
while(1)
{
#ifdef _LOCK_
pthread_mutex_lock(&lock);
#endif
if(value1 != value2)
{
printf("value1 =%u , value2 = %u \n",value1,value2);
usleep(100000);//将线程挂起一段时间,单位微妙
}
#ifdef _LOCK_
pthread_mutex_unlock(&lock);
#endif
//printf("value1 =%u , value2 = %u \n",value1,value2);
}
return NULL;
}
注:
不使用互斥锁运行:$gcc -o test test.c -Wall -lpthread
$./test
value1 = 198 , value2 = 199;
value1 = 2398 , value2 = 2399;
使用互斥锁运行:$gcc -o test test.c -Wall -lpthread _D_LOCK_
$./test
总结: (1) 进程与 线程量程在时间片的作用下 交互执行值,count的值一直在叠加的状态;
(2)在没有使用互斥锁的时候,进程中再给value1或者value2赋值的时候可能会遇到时间片使用完的情况,会有当次某 一个赋值失败的情况,导致跳入线程中后判定两值不相等,打印出对应数值;
(3)加了互斥锁以后,在申请锁成功以后,在跳入另一个进程中时,另一个进程也在申请锁时,会失败进入阻塞状态, 又会返回拥有锁的进程继续执行,直到进程释放锁,另一个进程得到执行,这是不是出现(2)中的情况;