线程的高级操作包括修改线程的属性和进行线程之间的同步操作。线程的同步有两种方式,一种是使用互斥量一种是使用读写锁。
线程共享进程空间内的资源,方便线程之间的通信,但是线程最大的优势在于并发执行,在并发执行的时候会因为资源是共享的造成操作冲突的情况,因此线程在访问共享资源的时候应该受到用户的控制,正常的完成任务。
互斥量是一种锁,在访问资源的共享资源的时候对其加锁,在访问结束的时候释放锁。这样可以保证在任意时间内,只有一个线程处于临界区内。任何想要进去临界区内的线程都要先进性测试,如果该锁被某一个线程所持有,测试线程就会阻塞,直到该锁被释放。
1初始化互斥量
Linux中用pthread_mutex_t数据类型表示互斥量,在使用之前要pthread_mutex_init()函数对互斥量进行初始化。
pthread_mutex_init(pthread_mutex_t *restrict mutex, contest pthread_mutexattr *restrict attr) 函数:
头文件: #include <pthread.h>
参数:第一个参数mutex是互斥量的指针,互斥量在该函数内进行初始化,并通过该函数将互斥量的指针返回给调用者。第二个参数attr是互斥量的属性,在此先设置为NULL。
返回值:如果初始化成功返回0,如果初始化失败返回错误号。
函数功能:该函数就是初始化一个互斥量,初始化成功之后,第一个参数mutex就是一个互斥量的指针,可供其他进程使用。
另外一种初始化互斥量的方法是: pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;
如果一个互斥量不在使用的时候通过pthread_mutex_destroy()函数销毁一个互斥量。
pthread_mutex_destroy(pthread_mutex_t *restrict mutex) 函数:
头文件: #include <pthread.h>
参数:要销毁的互斥量的互斥量指针。
返回值:如果初始化成功返回0,如果初始化失败返回错误号
函数功能:销毁不用的互斥量。
2.得到互斥量
互斥量对于用户来说是一个透明的数据结构,用户不能直接对其进行操作,应该使用系统提供的操作互斥量的函数接口。
pthread_mutex_lock(pthread_mutex_t *mutex) 函数:得到一个互斥量的锁也就是对临界区加锁,得不到互斥量的时候导致调用线程阻塞。
pthread_mutex_trylock(pthread_mutex_t *mutex) 函数:得到一个互斥量的锁也就是对临界区加锁,得不到互斥量的时候不会导致线程阻塞,返回一个错误编号EBUSY,表示申请的锁处于繁忙状态。
pthread_mutex_unlock(pthread_mutex_t *mutex) 函数:释放互斥量也就是解锁临界区。
3.使用互斥量实例
下面这个实例是维护一个任务队列,本质是一个链表。两个线程扫描链表,将属于自己线程的任务从任务列队中摘下,只有任务结点中所描述的线程ID与自己的线程ID相等的时候任务结点属于自己,每次最多取3个任务结点。链表就是临界区,任何对链表的操作均要得到互斥量的锁。主函数如下所示:
int main(void) { Job item; /* 任务队列是一个有头结点的链表 */ pthread_t tid1, tid2; int i,err; item = (struct job *)malloc(sizeof(struct job)); /* 设置头结点,该结点不 存储有效信息 */ item->next = NULL; item->val = 0; item->tid = -1; /* 创建2个线程,这2个线程会根据自己的线程ID取走不同的任务结点 */ if((err=pthread_create(&tid1, NULL, tfn7, item)) == -1){ /* 创建第1个线程,将 任务队列头作为线程体函数参数 */ printf("fail to create thread %s ", strerror(err)); exit(0); } if((err=pthread_create(&tid2, NULL, tfn7, item)) == -1){ /* 创建第2个线程,将 任务队列头作为线程体函数参数*/ printf("fail to create thread %s ", strerror(err)); exit(0); } printf("===the 1st put=== "); /* 第一次执行,由主线程将任务结点设置到任务队列中 */ pthread_mutex_lock(&q_lock); /*锁住任务队列 */ for(i = 0; i < 2; i++){ /* 共执行两次,每次放入2个结点,一个属于线程1,一个属于线程2 */ if(insert(item, i, tid1) == -1) exit(1); if(insert(item, i + 1, tid2) == -1) exit(1); } if(insert(item, 10, tid1) == -1) exit(1); pthread_mutex_unlock(&q_lock); /* 释放锁,当前任务队列中由5个任务结点,3个属于线程1,2个属于线程2 */ sleep(5); /* 休眠,保证线程可以取走任务结点,在这里不能使用pthread_join函数 * 因为队列中只有2个结点属于线程2,未取走3个结点线程是不会退出的 * 所以pthread_join函数会导致死锁 */ printf("===the 2nd put=== "); /*第二次输入,目前队列中已经没有任务结点了 */ pthread_mutex_lock(&q_lock); /* 再次锁住队列 */ if(insert(item, 9, tid2) == -1) exit(1); pthread_mutex_unlock(&q_lock); /* 释放锁 */ /* 这个时候可以使用pthred_join函数了 */ err = pthread_join(tid1, NULL); /* 得到线程1的退出信息 */ if(err != 0){ printf("can’t join thread %s ", strerror(err)); exit(1); } err = pthread_join(tid2, NULL);/* 得到线程2的退出信息 */ if(err != 0){ printf("can’t join thread %s ", strerror(err)); exit(1); } printf("main thread done "); /* 输出提示信息 */ if(item->next == NULL) /* 如果队列中没有任务结点了,则输出提示信息 */ printf("No job in the queue "); free(item); /* 释放头结点 */ return 0; }
上边是主程序的代码,我们先创建了两个线程,线程体函数都是取走属于自己进程的任务结点,每个线程取走3个结点退出线程体函数。两个线程创建之后,都在不断的进轮循队列,找自己的任务结点。
两个线程的操作都是要对队列进行的,队列就是临界区,接下来就是要向队列中插入结点,在插入结点前要锁住临界区,插入结点后再解锁队列,解锁队列之后两个线程就可以对队列进行操作,取出属于自己的任务结点。
第一次放入队列的五个结点中有3个属于线程1,2个属于线程2,因此线程1取到3个结点值之后就直接退出了,线程2还在继续的轮循队列。第二次向队列中再插入1个属于线程2的结点,线程2总共也取到3个结点,然后退出。
这就是一个维护队列的主要过程,从程序的使用我们要理解到两点
第一,线程的退出方式只有3中,线程体函数执行完之后自己退出,被其他进程kill或者调用exit函数自行退出。如果没有这三个条件,线程会一直存在。在这个程序中,如果没有取到3个结点线程体函数会一直循环查找队列。
第二,该程序中需要维护的就是一个链表,因此对该链表的所有操作都要加锁。包括在线程体函数中取结点的时候也要加互斥量锁住队列,线程体函数在下面会讲。
void * tfn7(void * arg) { int count; /* 每次取得链表结点数 */ Job task; /* 属于当前线程的任务结点的队列头 */ task = (struct job *)malloc(sizeof(struct job)); /* 设置头结点,该结点不 存储有效信息 */ task->next = NULL; task->val = 0; task->tid = -1; pthread_t tid; tid = pthread_self(); /* 得到当前线程ID,根据此线程ID判断任务是否归属于当前线程 */ count = 0; while(count < MAX_ITEM) if(pthread_mutex_trylock(&q_lock) == 0){ /* 将队列加锁 */ get_job(arg, task, &count,tid); pthread_mutex_unlock(&q_lock); /* 遍历链表结束,释放锁 */ } print(task); if(free_job(task) == -1) exit(1); return (void *)0; }
上面是线程体函数,线程体函数主要是取队列中属于自己进程的结点,需要注意的是在取结点的时候一定要对队列上锁。其他部分代码如下:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <pthread.h> #define MAX_ITEM 3 /* 每次最多取三个任务 */ typedef struct job * Job; /* 链表结点结构 */ struct job{ pthread_t tid; /* 线程ID */ Job next; /* 下一个链表结点 */ int val; /* 结点值 */ }; pthread_mutex_t q_lock = PTHREAD_MUTEX_INITIALIZER; /* 全局变量锁 */ int insert(Job head, int val, pthread_t tid) { Job p, q; p = head; /* 头指针 */ if(p != NULL){ /* 判断空链表的情况 */ while(p->next != NULL){ p = p->next; } } q = (Job)malloc(sizeof(struct job)); /* 为结点分配内存空间 */ if(q == NULL){ perror("fail to malloc"); return -1; } q->next = NULL; q->val = val; q->tid = tid; /* 设置结点的所有者,线程1 */ if(p == NULL){ /* 设置链表头指针 */ head = q; return 0; } p->next = q; /* 插入到队列中 */ return 0; } void get_job(Job head, Job task, int *count,pthread_t tid) { Job p,q; q = head; /* 参数是任务队列头 */ p = q->next; /* 指针p作为q的后继,两个指针共同前进 */ while(p != NULL){ /* 遍历链表,判断每个任务结点 */ if(tid == p->tid){ /* 找到属于当前线程的任务结点 */ q->next = p->next; p->next = NULL; /* 将该结点从原始的任务队列中摘下 */ while(task->next != NULL) task=task->next; task->next = p; /* 链入到新的当前线程的任务队列上去 */ p = q->next; (*count)++; /* 已取任务是递增 */ }else{ q = p; p = p->next; } } } int free_job(Job head) { Job p,q; for(p = head; p != NULL; p = p->next){ /* 线程退出时释放所有的任务结点 */ q = p; free(q); } return 0; } void print(Job head) { Job p; for(p = head->next; p != NULL; p = p->next) /* 输出取得的任务列表 */ printf("thread %u: %d ", (unsigned int)p->tid, p->val); }
总结:互斥量使用这个程序的代码不仅仅是互斥量的使用,也是对链表知识的一个回顾,基本的链表操作要熟练掌握。互斥量的使用不难,不过这只是一个简单的例子,以后的编程中肯定会用到互斥量,通过编程调试了解了互斥量的使用办法、互斥量的作用之后在以后的程序中如果又类似需求的时候能想起来去灵活的运用就可以了。这个程序中只有一个需要维护的队列,如果编程中需要维护的队列有很多,不同的线程中有不同的队列需要维护,这就需要根据实际情况去进行编程了。