引言
本文所有内容均摘抄自《LinuxC编程直通车》,仅作为学习交流使用。
进程是系统中程序执行和资源分配的基本单位,在进程调度时涉及较复杂的上下文切换。
线程是进程内独立的一条运行路线, 是处理机调度的最小单元,也称“轻量级进程”。
线程可对进程的内存空间和资源进行访问,并与统一进程中的其他线程共享这些资源。
1. 线程概述
线程是一个进程内的基本调度单位,线程是在进程的共享内存中并发的多道执行路径,它们共享一个进程的资源, 如文件描述和信号处理, 因此,进程可以有一个或多个线程, 即有一个或多个线程控制表及堆栈寄存器, 但却共享一个用户地址空间。同时, 线程也有其私有的数据信息, 包括线程号、寄存器、堆栈、信号掩码等。
为什么有了进程的概念后, 还要引入线程的概念呢?
速度快
在Linux系统下, 启动一个新的进程必须分配给它独立的地址空间, 建立众多的数据表来维护它的代码段、堆栈段和数据段, 是一种“昂贵” 的多任务工作方式。而线程公用相同的地址空间,共享大部分数据, 启动一个线程所花费的空间远远小于启动一个进程所花费的时间, 而且线程间彼此切换所需的时间也远远小于进程间切换所需的时间。 据统计, 总的来说, 一个进程的开销大约是线程开销的30倍左右(在不同的系统上,这个数据可能会有较大的区别)。
通信便捷
对于不同的进程来说, 它们具有独立的数据空间, 要进行数据的传递只能通过通信的方式进行, 这种方式不仅费时,而且很不方便。线程则不然,由于同一线程之间共享数据空间, 所以一个线程的数据可以直接为其他线程所用,这不仅快捷而且方便。但是有的变量不能同时被两个线程所修改,有的子程序中声明为static 的数据改有可能给多线程程序带来灾难性的打击。
此外线程还有以下优点:
- 提高应用程序响应速度
- 使多CPU系统更加有效
- 改善程序结构
- … …
目前Linux中最流行的线程机制是POSIX 1003.1c "pthread"标准借口。在程序中需包含头文件“pthread.h” , 在编译链接时需要使用“-lpthread” 选项,-lpthread意味着链接库目录下的libpthread.a 或 libpthread.so 文件。
2. 线程的基本操作
线程的基本操作包括创建线程、 线程等待、 线程终止, 这些过程很像进程的常见操作。这些函数接口在/usr/include/pthread.h头文件中都进行了引用声明。
2.1 创建线程
线程的创建通过函数pthread_create来完成,函数原型如下
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
第一个参数为只想线程标识符的指针。pthread_t是一个无符号长整型数值,在pthread线程库中的定义如下
typedef unsigned long int pthread_t;
第二个参数用来设置线程的属性,常被设为空指针,这样将生成默认属性的线程
第三个参数是线程运行函数的起始地址,最后一个参数是传递给运行函数的参数,若不需要则将该参数设为空指针。
当创建线程成功时, 函数返回0。常见的错误返回代码为EAGAIN(系统限制创建新的线程)和EINVA(线程属性值非法)。
每个线程都有自己的线程ID, 线程ID 在pthread_create调用时返回给创建线程的调用者;一个线程也可以在创建后使用pthread_self函数调用来获取自己的线程ID。
创建新线程示例代码1如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int *thread_function(void *args)
{
pthread_t newthid;
newthid = pthread_self();
printf("New thread, thread ID = %u \n", newthid);
return NULL;
}
int main(void)
{
int ret;
pthread_t thid;
printf("Main thread, thread ID = %u \n", pthread_self());
ret = pthread_create(&thid, NULL, (void *)thread_function, NULL);
if(ret != 0){
printf("Create thread error ! \n");
exit(1);
}
printf("Return new thread ID is %u\n", thid);
sleep(1);
return 0;
}
2.2 线程等待
在示例1中,主线程调用sleep()函数而特意使新的线程能够抢占到CPU, 事实上POSIX pthread标准提供了专门使一个线程等待另一个线程返回的函数调用pthread_join, 其函数原型如下
int pthread_join(pthread_t thread, void **thread_return);
该函数的作用是将调用pthread_join的线程挂起, 直到参数thread所代表的线程终止。pthread_join相当于进程用来等待子进程的wait函数。
第一个参数指定了将要等待的进程——pthread_create返回的线程标识符;
第二个参数是一个指针, 它指向另一个指针,之后在指向线程的返回值,等待线程的资源被回收。
进程等待代码示例2如下
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void thread_function(void)
{
int i;
printf("New thread, thred ID = %u \n", pthread_self());
for(i = 0; i < 500; i++)
printf("This is the new thread.\n");
}
int main(void)
{
pthread_t thread_id;
int i, ret;
printf("Main thread, thread ID = %u\n", pthread_self());
ret = pthread_create(&thread_id, NULL, (void *)thread_function, NULL);
if(ret != 0){
printf("Create thread error! \n");
exit(1);
}
for(i = 0; i < 500; i++)
printf("This is the main thread.\n");
pthread_join(thread_id, NULL);
return 0;
}
从运行结果可以看出:
- 主线程在执行完语句后并没有直接执行"return 0", 而是等待创建线程的返回。
- 两个线程的顺序是不一定的, 更有可能是交替运行
注意,一个线程不能被多个线程等待,否则第一个接到信号的线程成功返回, 其余调用pthread_join的线程则返回错误代码ESRCH
2.3 线程终止
一个进程的终止一般有三种途径,
- 线程执行完毕后,通过return() 或exit() 从线程函数主动返回;
- 通过调用pthread_exit()函数使线程退出;
- 被其他线程调用pthread_cancel()函数终止。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
char buffer[] = "Hello World";
void thread_function(void *arg)
{
int i;
printf("New thread is running, argument is %s\n", (char *)arg);
strcpy(buffer, "Bye!");
pthread_exit("I like Linux C program!");
}
int main(void)
{
pthread_t thread_id;
int ret;
void *thread_result;
ret = pthread_create(&thread_id, NULL, (void *)thread_function, (void *)buffer);
if(ret != 0){
printf("Create thread error! \n");
exit(1);
}
printf("Main thread is running. \n");
printf("Before new thread running, the buffer content is %s \n", buffer);
ret = pthread_join(thread_id, &thread_result);
if(ret != 0){
printf("Threat join error! \n");
exit(1);
}
printf("Main waiting new thread, it returns %s \n", (char*)thread_result);
printf("New thread returned, now the buffer is %s\n",buffer);
return 0;
}
示例代码中pthread_exit函数唯一的参数是函数的返回代码,返回给pthread_join的第二个参数(当第二个参数不为NULL时)。
2.4. 线程属性
咳咳,这些内容先不写…
3. 线程同步
线程最大的特点就是资源共享性,但资源共享中的同步问题是多线程编程的难点。
所谓同步,即线程等待某个时间的发生,只有当等待的事件发生后线程才能继续执行,否则线程被挂起并放弃处理器。
Linux下提供了多种方式来处理线程的同步, 最常见的机制是互斥锁、条件变量和信号量。
互斥锁(mutex) 通过锁机制实现线程间同步,同一时刻只允许一个线程执行一个关键部分代码;
条件变量(condition variable) 是利用线程间共享的全局变量进行同步的一种机制;
3.1 互斥锁(mutex)
互斥锁用来保证一段时间内只有一个线程在执行一段代码,它只有“上锁”、“解锁”两种状态。
同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作,若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,知道上锁的线程释放互斥锁为止。
无论是软件资源还是硬件资源,多个线程必须互斥地对它进行访问。每个线程中方位临界资源的代码称为临界区(critical section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只允许一个线程进入临界区,进入后不允许其他线程进入。
3.3.1 创建和销毁
在Linux Threads中有两种方式创建互斥锁:静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,该方法很简单:
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init()函数动态地初始化一个互斥锁,函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
销毁操作函数原型如下
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3.3.2 互斥锁属性
3.3.3 锁操作
锁操作主要包括加锁、解锁和测试三个,函数原型如下
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
3.2 条件变量
3.3 信号量
信号量可以分为二值信号量和多值信号量。前者只有两种状态,0为灯灭,资源不可用,1为灯亮,资源可用。多值信号量本质是一个非负的整数计数器,当共享资源被释放时信号量+1,当资源被占用时信号量-1.
3.3.1创建和注销
信号量的数据类型为sem_t,实际上是一个长整型的数。函数sem_init()用来创建一个信号量,并初始化信号量的值,其函数原型为:
int sem_init(sem_t *sem, int pshared, unsigned int value);
指针sem表示要创建的信号量, pshared表示是否再多进程间共享信号量而不是仅在一个进程中的所有线程间共享,该参数为0时表示只能在当前进程的所有线程间共享,否则此信号量在多个进程间共享,value为信号量的初始值。
函数sem_destroy()用来释放信号量sem, 函数原型如下
int sem_destroy(sem_t *sem);
3.3.2类P操作
函数sem_wait()用来阻塞当前线程直到信号量sem的值>0,解除阻塞后将sem的值-1,表明公共资源经占用后数目减少,与P操作有着异曲同工之妙,函数原型如下
int sem_wait(sem_t *sem);
3.3.3类V操作
函数sem_post()用来增加信号量的值,当有线程阻塞在这个信号量上时,调用该函数会使其中的一个线程不在阻塞,而选择哪个线程获得资源则是由线程的调度策略决定的,函数原型如下:
int sem_post(sem_t *sem);