线程基础
- 每个用户进程有自己的地址空间
- 系统为每个用户进程创建一个 task_struct来描述该进程
- 该结构体中包含了一个指针指向该进 程的虚拟地址空间映射表
- 实际上task_struct 和地址空间映射表一起用来表示一个进程
- 由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大
- 为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程
- 在同一个进程中创建的线程共享该进程的地址空间
- Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度
- 通常线程指的是共享相同地址
空间的多个任务
使用多线程的好处 - 大大提高了任务切换的效率
避免了额外的TLB & cache的刷新
一个进程中的多个线程共享以下资源
- 可执行的指令
- 静态数据
- 进程中打开的文件描述符
- 信号处理函数
- 当前工作目录
- 用户ID
- 用户组ID
每个线程私有的资源如下
- 每个线程私有的资源如下
- 线程ID (TID)
- PC(程序计数器)和相关寄存器
- 堆栈
局部变量
返回地址 - 错误号 (errno)
- 信号掩码和优先级
- 执行状态和属性
多线程编程
pthread线程库中提供了如下基本操作
创建线程 pthread_create()
回收线程 pthread_join()
结束线程 pthread_detach()
这将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
线程 – 示例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
char message[32] = "Hello World";
void *thread_func(void *arg);
int main(void)
{
pthread_t a_thread;
void *result;
//创建线程
if (pthread_create(&a_thread, NULL, thread_func, NULL) != 0)
{
printf("fail to pthread_create\n");
exit(-1);
}
//回收线程
pthread_join(&a_thread, &result);
printf("result is %s\n", result);
printf("message is %s\n", message);
return 0;
}
void *thread_func(void *arg)
{
sleep(1);
strcpy(message, "marked by thread");
pthread_exit("thank you for waiting for me");
}
result is thank you for waiting for me
message is marked by thread
线程间同步和互斥机制
- 线程共享同一进程的地址空间
- 优点:线程间通信很容易通过全局变量交换数据
- 缺点:多个线程访问共享数据时需要同步或互斥机制
线程间同步
- 同步(synchronization)指的是多个任务按照约定的先后次序相互配合完成一件事情
- 1968年,Edsgar Dijkstra基于信号量的概念提出了一种同步机制
- 由信号量来决定线程是继续运行还是阻塞等待
信号量(灯)
- 信号量代表某一类资源,其值表示系统中该资源的数量
- 信号量是一个受保护的变量,只能通过三种操作来访问
·初始化
·P操作(申请资源):当任务(比如线程)要访问某个资源的时候,因为任务不知道当前系统中有没有这个资源,所以该任务对代表此资源的信号量进行P操作(检查信号量的值):如果信号量的值大于0,任务继续执行,访问资源,如果当前信号量的值等于0,就代表没有资源,则任务阻塞,直到有资源为止。
·V操作(释放资源):如果当前任务不需要访问资源了,或者任务产生了一个资源,就要执行V操作(告诉系统,资源数增加了,系统就可以唤醒等待这些资源的任务了)
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操作
信号量初始化sem_init()
成功时返回0,失败时EOF
sem 指向要初始化的信号量对象
pshared 0 – 线程间 1 – 进程间
val 信号量初值
信号量–P/V操作sem_wait()/sem_post()
- 由信号量来决定线程是继续运行还是阻塞等待
P(S) 含义如下:
if (信号量的值大于0) { 申请资源的任务继续运行; 信号量的值减一;} else { 申请资源的任务阻塞; }
V(S) 含义如下:
信号量的值加一; if (有任务在等待资源) { 唤醒等待的任务,让其继续运行 }
int sem_wait(sem_t *sem); P操作
int sem_post(sem_t *sem); V操作
线程同步示例1
两个线程同步读写缓冲区(生产者/消费者问题)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
char buf[32];
sem_t sem;
void *function(void *arg);
int main(void)
{
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) //先初始化信号量,再创建线程
{
printf("fail to pthread_create");
exit(-1);
}
printf("input ‘quit’ to exit\n");
do {
fgets(buf,32,stdin);
sem_post(&sem);
}while (strncmp(buf,"quit",4) != 0);
return 0;
}
void *function(void *arg)
{
while (1)
{
sem_wait(&sem);
printf("you enter %d characters\n", strlen(buf));
}
}
线程同步示例2
读线程在读缓冲区前,P操作检查缓冲区中有没有数据,没有的话阻塞,有的话才读数据
实际上对于写线程来说,也要如此。当缓冲区为空的时候,才能去写数据。
如果读线程数据处理过程比较长,读线程还没读完,写线程又把新数据覆盖上去了,这样就破坏了数据,是不合理的
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <string.h> #include <semaphore.h> char buf[32]; sem_t sem_r,sem_w; void *function(void *arg); int main(void) { pthread_t a_thread; if (sem_init(&sem_r,0,0) < 0) //刚开始缓冲区是空的,不可读 { perror("sem_r_init"); exit(-1); } if (sem_init(&sem_w,0,1) < 0) //刚开始缓冲区是空的,可写 { perror("sem_r_init"); exit(-1); } if (pthread_create(&a_thread,NULL,function,NULL) != 0) //先初始化信号量,再创建线程 { printf("fail to pthread_create"); exit(-1); } printf("input ‘quit’ to exit\n"); do { sem_wait(&sem_w);//写之前对可写信号量进行P操作,缓冲区非空则阻塞 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); printf("you enter %d characters\n",strlen(buf)); sem_post(&sem_w); } }
线程间互斥
- 临界资源
· 一次只允许一个任务(进程、线程)访问的共享资源 - 临界区
·访问临界资源的代码
·访问非临界资源的代码叫非临界区 - 互斥机制
·临界区互斥:当一个任务在访问临界区的时候,其他任务不能访问该临界区(相同的临界资源)
·mutex互斥锁:互斥锁只能被一个任务所持有。互斥锁有两种状态,空闲/只被一个任务所持有。
·任务访问临界资源前申请锁,访问完后释放锁。没有申请到锁的任务需要阻塞等待,直到持有这个锁为止
互斥锁初始化 – pthread_mutex_init
申请锁 – pthread_mutex_lock
释放锁 – pthread_mutex_unlock
- 成功时返回0,失败时返回错误码
- mutex 指向要初始化的互斥锁对象
- 执行完临界区要及时释放锁
- 如果有多个临界资源,比如一个任务每次都要同时访问两个临界资源,那这两个临界资源只需要一个互斥锁保护就行。如果这两个临界资源访问不是同时的,就需要两个互斥锁来分别保护
线程互斥 – 示例
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
unsigned int count, value1, value2;
pthread_mutex_t lock;
void *function(void *arg);
int main(void)
{
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, function, 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 *function(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
}
return NULL;
}
- 主线程中count++,之后会先把count赋给value1,再把count赋给value2;另外一个线程每隔100ms执行一次判断value1!=value2,如果不等于,分别打印value1和value2;
- 如果不上锁,主线程把count赋给value1后,如果还没来得及赋给value2,主线程的时间片用完了,系统自动调度到另外 一个线程,就会出现value1!=value2的情况
- 如果上了互斥锁,就能让两个线程中的临界区代码严格互斥
不使用互斥锁
$ gcc –o test test.c -lpthread
$ ./test
value1 = 19821, value2 = 19820
value1 = 14553456, value2 = 14553455
value1 = 29196032, value2 = 29196031
……
使用互斥锁
$ gcc –o test test.c –lpthread –D_LOCK_
$ ./test
版权声明:本文为Set_Mode原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。