1.进程
在讲到线程之前,我们应该先了解一下进程的概念。进程(Process)是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在程序中我们使用fork函数创建进程。实际上在每一个CPU上同一时刻只能运行一个进程。但为什么我们的PC能同时开多个软件呢。
这是因为CPU的运行速度很快,操作系统为了运行多个程序,CPU会把它的执行时间划分成很多段,比如每一段是0.1秒,那么就可以这样A程序运行0.1秒,然后B程序运行0.1,然后C程序运行0.2秒,因为这个切换很快,所以我们感觉程序是同时运行的。
再一个程序与进程是不一样的概念,程序实际上就是一堆指令和数据的集合,这个集合反映在了一个静态可执行文件和相关的配置文件等。程序是静态的,但进程是动态的,从操作系统上看运行中的程序就是指一个进程。同一个程序可由多个进程执行。而且还有可能共享地址空间等资源。Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中。
2.线程
如果我们要生产一种商品,我们需要生产该商品的工厂,如果一个工厂不够,我们就需要更多的工厂。每一个工厂都类似于一个进程,商品就类似于我们需要执行的程序,增加工厂就类似于创建更多的进程。每一个进程都是独立的,互不干扰的,拥有自己的进程空间。
那么什么是线程呢?
通俗的讲,线程就是工厂(进程)中的流水线,一个工厂中可以有很多的流水线,每一条流水线可以使用该工厂中的共享资源(同一个进程内的各个线程,能够共享整个进程的全局变量,除了线程的局部变量外,其他资源都共享)。
从严谨的定义来说, 线程,是进程内部的一个控制序列,是可由调度器独立管理的最小程序指令序列。通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
线程的优点、缺点
优点:
缺点:
3.线程的使用
1)线程的创建
int pthread_create (
pthread_t *thread,
pthread_attr_t *attr,
void *(*start_routine)(void*),
void *arg);
/*参数:thread, 指向新线程的标识符。
通过该指针返回所创建线程的标识符。
attr, 用来设置新线程的属性。
一般取默认属性,即该参数取NULL
start_routine, 该线程的处理函数
该函数的返回类型和参数类型都是void*
arg, 线程处理函数start_routine的参数
功能:创建一个新线程,
同时指定该线程的属性、执行函数、执行函数的参数
通过参数1返回该线程的标识符。
返回值:成功,返回0
失败,返回错误代码
注意:大部分pthread_开头的函数成功时返回0,失败时返回错误码(而不是-1)*/
注意:
使用fork创建进程后,进程马上就启动,但是是和父进程同时执行fork后的代码。
使用pthread_create创建线程后,新线程马上就启动,即执行对应的线程处理函数。
2)线程的终止
void pthread_exit (void *retval)
/*功能:在线程函数内部调用该函数。
终止该线程,并通过参数retval返回一个指针。
该指针不能指向该线程的局部变量。*/
3)等待指定线程结束
int pthread_join (pthread_t th,
void ** thread_return);
/*参数:th, 指定等待的线程
thread_return, 指向该线程函数的返回值
线程函数的返回值类型为void*,故该参数的类型为void**
功能:类似与进程中的waitpid
等待指定的线程结束,并使参数指向该线程函数的返回值(用pthread_exit返回的值)
*/
注意:在编译创建线程的程序时,要带上-lpthread。比如:gcc text.c -o text.exe -lpthread.
4)线程的简单实例
编写一个程序,创建一个线程,线程与主进程都对程序的一个全局变量进行减减操作,并输出该变量的值。
#include
#include
#include
int my_count=10;
void text(){
int count=0;
while(count<5){
my_count--;
printf("my_count的值是:%d\n",my_count);
sleep(1);
count++;
}
}
int main(void){
pthread_t my_thread;
int ret=pthread_create(&my_thread,0,text,0);
if(ret!=0){
printf("线程创建失败!\n");
exit(1);
}
int count=0;
while(count<5){
my_count--;
printf("my_count的值是:%d\n",my_count);
sleep(1);
count++;
}
return 0;
}
1.线程的互斥 - 指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
2.线程的同步 - 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
为什么要进行线程间的同步互斥?之前我们讲过,线程类似于工厂里的流水线,当某条流水线在生产时使用了工厂的公共资源时,在使用完之前,我们肯定不想其它流水线占用这个公共资源。所有我们一般在使用时都会将公共资源上锁。在使用完之后再释放。
所谓的“公共资源”,严格来讲称为临界区。线程锁的使用就是为了实现对临界区的有序访问。也就是实现线程的同步互斥。
1.什么是信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
通俗的说就是当创建一个信号量,在临界区加上该限制,当代码执行到此处时,会对信号量进行判断,如果信号量大于0,则继续执行,如果等于0,就阻塞在这里。直至信号量大于1且被获取。
信号量通过P(减一操作),V(加一操作)操作对信号量进行加减操作。具体通过函数实现。
2.信号量操作函数
1 信号量的初始化
int sem_init (sem_t *sem, int pshared, unsigned int value);
/*功能:对信号量进行初始化
参数:sem, 指向被初始化的信号量
pshared, 0:表示该信号量是该进程内使用的“局部信号量”, 不再被其它进程共享。非0:该信号量可被其他进程共享,Linux不支持这种信号量
value, 信号量的初值。>= 0
返回值:成功,返回0
失败, 返回错误码 */
2 信号量的P操作
int sem_wait (sem_t *sem);
/*
对信号量减一
返回值:成功,返回0
失败, 返回错误码*/
3 信号量的V操作
int sem_post (sem_t *sem);
/*
对信号量加一
返回值:成功,返回0
失败, 返回错误码*/
4 信号量的删除
int sem_destroy (sem_t *sem);
/*返回值:成功,返回0
失败, 返回错误码*/
3.实例
主线程循环输入字符串,把字符串存放到一个全局缓存中。新线程从全局缓存中读取字符串,统计该字符串的长度。直到用户输入end
简易版:
#include
#include
#include
#include
#include
#define BUFF_SIZE 64
char buff[BUFF_SIZE];
sem_t sem;
void* text(){
while(1){
if(sem_wait(&sem)!=0){
printf("sem_wait failed!\n");
exit(1);
}
printf("字符串是:%s",buff);
printf("字符长度为:%d\n",strlen(buff));
}
}
int main(void){
strcpy(buff,"Hello!");
pthread_t my_pthread;
int ret=pthread_create(&my_pthread,0,text,0);
if(ret!=0){
printf("进程创建失败!\n");
exit(1);
}
ret=sem_init(&sem,0,0);
if(ret!=0){
printf("信号量创建失败!\n");
}
while(1){
fgets(buff,sizeof(buff),stdin);
sem_post(&sem);
if(strncmp(buff,"end",3)==0){
break;
}
}
return 0;
}
升级版:
#include
#include
#include
#include
#include
#define BUFF_SIZE 80
char buff[BUFF_SIZE];
sem_t sem;
static void* str_thread_handle(void *arg)
{
while(1) {
//P(sem)
if (sem_wait(&sem) != 0) {
printf("sem_wait failed!\n");
exit(1);
}
printf("string is: %slen=%d\n", buff, strlen(buff));
if (strncmp(buff, "end", 3) == 0) {
break;
}
}
}
int main(void)
{
int ret;
pthread_t str_thread;
void *thread_return;
ret = sem_init(&sem, 0, 0);
if (ret != 0) {
printf("sem_init failed!\n");
exit(1);
}
ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
if (ret != 0) {
printf("pthread_create failed!\n");
exit(1);
}
while (1) {
fgets(buff, sizeof(buff), stdin);
//V(sem)
if (sem_post(&sem) != 0) {
printf("sem_post failed!\n");
exit(1);
}
if (strncmp(buff, "end", 3) == 0) {
break;
}
}
ret = pthread_join(str_thread, &thread_return);
if (ret != 0) {
printf("pthread_join failed!\n");
exit(1);
}
ret = sem_destroy(&sem);
if (ret != 0) {
printf("sem_destroy failed!\n");
exit(1);
}
return 0;
}
4.练习
创建2个线程(共有主线程、线程1、线程2共3个线程)
1.主线程阻塞式等待用户输入字符串
2.主线程每接收到一个字符串之后, 线程1就马上对该字符串进行处理。
3.线程1的处理逻辑为:统计该字符串的个数并输出。线程1把该字符串处理完后,线程2输出该字符串,直到用户输入exit.
1.什么是互斥量
效果上等同于初值为1的信号量,类型为 pthread_mutex_t。
2.函数实现
1 互斥量的初始化
int pthread_mutex_init(pthread_mutex_t *mutex,
pthread_mutexattr_t *attr);
/*参数:mutex, 指向被初始化的互斥量
attr, 指向互斥量的属性
一般取默认属性(当一个线程已获取互斥量后,该线程再
获取该信号量,将导致死锁!)*/
2 互斥量的获取
int pthread_mutex_lock (pthread_mutex_t *mutex);
3 互斥量的释放
int pthread_mutex_unlock (pthread_mutex_t *mutex);
4 互斥量的删除
int pthread_mutex_destroy (pthread_mutex_t *mutex);
3.实例
用户循环输入字符串,线程输出该字符串,直至输入end结束。
#include
#include
#include
#include
#include
#include
void *thread_function(void *arg);
pthread_mutex_t work_mutex; /* protects both work_area and time_to_exit */
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
int main() {
int res;
pthread_t a_thread;
void *thread_result;
res = pthread_mutex_init(&work_mutex, NULL);
if (res != 0) {
perror("Mutex initialization failed");
exit(EXIT_FAILURE);
}
res = pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
pthread_mutex_lock(&work_mutex);
printf("Input some text. Enter 'end' to finish\n");
while(!time_to_exit) {
fgets(work_area, WORK_SIZE, stdin);
pthread_mutex_unlock(&work_mutex);
while(1) {
pthread_mutex_lock(&work_mutex);
if (work_area[0] != '\0') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
}
else {
break;
}
}
}
pthread_mutex_unlock(&work_mutex);
printf("\nWaiting for thread to finish...\n");
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
pthread_mutex_destroy(&work_mutex);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
sleep(1);
pthread_mutex_lock(&work_mutex);
while(strncmp("end", work_area, 3) != 0) {
printf("You input %d characters\n", strlen(work_area) -1);
work_area[0] = '\0';
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
while (work_area[0] == '\0' ) {
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
}
}
time_to_exit = 1;
work_area[0] = '\0';
pthread_mutex_unlock(&work_mutex);
pthread_exit(0);
}
4.总结
为了实现对临界区的访问同步。一个有用的小技巧是设置标记,因为我们在某一线程的工作完成后需要等待另一线程获取锁,以实现线程同步。设置标记并判断是否进入等待,如果进入等待,就让锁释放,获取循化进行,中间休眠一段时间,这是为了让另一线程获取到锁,完成工作。再修改该标记,让第一个线程循化退出。这样就完成了线程的同步访问。在上述代码中表现在下图中:
这里加一版简单一点的代码,以便理解,同时也是信号量练习的答案。
#include
#include
#include
#include
#include
#define BUFF_SIZE 64
char buff[BUFF_SIZE];
sem_t sem;
int count=1; //用来做标记的全局变量
int text(){
while(1){
sem_wait(&sem);
printf("字符串长度为:%d个字节\n",strlen(buff));
//sem_post(&sem);
while(1){
if(count==1){ //判断条件
sem_post(&sem);
sleep(1);
sem_wait(&sem);
}
else{
count++;
break;
}
}
}
return 0;
}
int text1(){
while(1){
if(count==1){
sem_wait(&sem);
printf("字符串是:%s\n",buff);
count--;
sem_post(&sem);
}
}
return 0;
}
int main(void){
pthread_t my_thread;
pthread_t my_thread1;
pthread_create(&my_thread,0,text,0);
pthread_create(&my_thread1,0,text1,0);
sem_init(&sem,0,0);
while(1){
printf("请输入字符串!");
fgets(buff,sizeof(buff),stdin);
sem_post(&sem);
if(strncmp(buff,"exit",4)==0){
break;
}
}
return 0;
}
1.什么是条件变量
1.与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
2 条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
3 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
2.函数实现
int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
/*参数:cond, 条件变量指针
attr 条件变量高级属性*/
int pthread_cond_signal (pthread_cond_t *cond);
/*参数:cond, 条件变量指针*/
int pthread_cond_broadcast (pthread_cond_t *cond);
/*参数:cond, 条件变量指针*/
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
/*参数:cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
const struct timespec *abstime 等待被唤醒的绝对超时时间*/
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
/*参数:cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
常见错误码: [EINVAL] cond或mutex无效,
[EINVAL] 同时等待不同的互斥量
[EINVAL] 主调线程没有占有互斥量 */
int pthread_cond_destroy (pthread_cond_t *cond);
/*参数:cond, 条件变量指针*/
3.实例
#include
#include
pthread_mutex_t mutex;
pthread_cond_t cond;
void* thread1(void *arg)
{
while (1) {
printf("thread1 is running\n");
pthread_mutex_lock(&mutex);
printf("thread1 lock..\n");
pthread_cond_wait(&cond, &mutex);
printf("thread1 applied the condition\n");
printf("thread1 unlock..\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
}
void* thread2(void *arg)
{
while (1) {
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
printf("thread2 lock..\n");
pthread_cond_wait(&cond, &mutex);
printf("thread2 applied the condition\n");
printf("thread2 unlock..\n");
pthread_mutex_unlock(&mutex);
sleep(2);
}
}
int main()
{
pthread_t thid1, thid2;
printf("condition variable study!\n");
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&thid1, NULL, (void *)thread1, NULL);
pthread_create(&thid2, NULL, (void *)thread2, NULL);
do {
sleep(10);
pthread_cond_signal(&cond);
} while (1);
return 0;
}
4.条件变量执行过程
1.pthread_cond_wait(&cond, &mutex)在代码运行到这个函数时它会有两个操作,第一个是将自己挂起直至收到唤醒信号,第二个是将传入的互斥量mutex解锁,也就是执行V操作。
2.在它收到唤醒信号后被唤醒后他会将互斥量mutex减一,即执行P操作。这些动作也是为了保证线程临界区的同步访问。