20155321 《信息安全系统设计》Linux多线程的深入学习

Posted 20155321

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了20155321 《信息安全系统设计》Linux多线程的深入学习相关的知识,希望对你有一定的参考价值。

再次学习之多线程

基本概念的再次学习

  • 线程是程序执行的最小单位(进程是资源管理的最小单位),线程隶属于某个进程中

  • 进程有自己的数据段、代码段和堆栈段。线程通常叫做轻型的进程,每个线程共享其所附属进程的所有资源,包括打开的文件、内存页面、信号标识及动态分配的内存等

  • 线程和进程比起来很小,因此线程花费更少的CPU资源

  • 在大并发、不断有客户端在请求服务器端的情况下,服务端则应该使用线程

  • 当进程退出时该进程所产生的线程都会被强制退出并清除。一个进程至少需要一个线程(主线程)作为它的指令执行体,进程管理着资源,将线程分配到某个CPU执行

  • 线程的分类

    • 用户级线程(由用户决定,主要解决上下文切换的问题)和内核级线程(由内核调度机制实现)
    • 用户级线程绑定内核级线程运行,一个进程中的内核级线程会分配到固定的时间片,用户级线程分配的时间片以内核级线程为准
    • 当CPU分配给线程的时间片用完后但线程没有执行完毕,此时线程会从运行状态返回到就绪状态,将CPU让给其它线程使用
  • 在Linux中,一般采用pthread线程库实现线程的访问与控制,因此在编译的时候要链接库pthread,即-lpthread

  • 线程的标识

    • 每个线程都有自己的唯一标识(标识是pthread_t数据类型)
    • 线程标识只在所属的进程环境中有效
    • 与线程标识相关的函数int pthread_equal(pthread_t,pthread_t):判断线程是否一致;pthread_t pthread_self(void):获得调用线程自己的ID

学习时遇到的理解问题

  • 问题1:对于CPU而言,每次均有一个进程抢到CPU,而进程的运行实质上是指进程中的线程在运行。但是一个进程中有多个线程(包括主线程和主线程创建的若干个为了满足客户需求的子线程),那么在某一时刻,到底是哪个线程获得CPU在运行呢?
  • 根据网上的辅助资料显示,具体哪个线程得到CPU并执行需要通过系统的调度,例如根据这些线程的优先级将某个线程从就绪状态调度为running状态。即系统是先把某个进程调度为running状态,后再根据某些规则(例如操作系统课上提到的优先级和调度算法)将某个线程调度为running状态。(当然这些线程之间也会根据某个算法进行切换,在进程拥有很多线程的时候,一般而言某个线程很难一直占有CPU)
  • 问题2:用户级线程与内核级线程是处于绑定运行的状态,也就是说用户级线程跟在内核级线程后进行运行。那么不会出现以下这个情况吗:某一个内核级线程与多个用户级线程绑定在一起,当这个内核级线程被调度的时候,与它所绑定的多个用户级线程也就被调度了,这样不会造成CPU的疑惑吗?(就是CPU怎么知道要执行哪个线程呢)
  • 默认情况下用户级线程和内核级线程是一对一,例如一个内核级线程被允许执行10ns,那么与它所绑定的这个用户级线程也就执行10ns的时间。虽然也可以多对一,但会导致实时性比较差

线程的创建

  • 调用函数:int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void*),void *restrict arg);成功则返回0

    • tidp:线程标识符指针
    • attr:线程属性指针(不要求则传空)
    • start_rtn:线程运行函数的起始地址,(需要用户自己定义)
    • arg:传递给线程运行函数的参数
  • 新创建线程从start_rtn函数的地址开始运行

  • 不能保证新线程和调用线程的执行顺序

  • 实践环节

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* th_fn(void *arg)
{
	
	int distance=(int)arg;
	int i;
	for(i=1;i<=distance;i++){
		printf("%lx run %d\\n",pthread_self(),i);
		sleep(1);
	}

	return (void*)0;
}


int main(void)
{
	int err;
	pthread_t rabbit,turtle;
	
	if((err = pthread_create(&rabbit,NULL,th_fn,(void*)50)) != 0)
	{
		printf("error!");
	}
	
	if((err = pthread_create(&turtle,NULL,th_fn,(void*)50)) != 0)
	{
		printf("error!");
	}

	printf("control thread id:%lx\\n",pthread_self());
	printf("finish!\\n");

	return 0;
}

运行结果如下所示:

从输出可以看到,程序并没有创建子线程成功,这个原因主要是因为当子线程被创建时,无法判断是主线程先执行还是子线程先执行,从这个结果可以看到,程序在创建完子线程后执行了主线程,因此当主线程执行完毕后,因为遇到了return 0,代表了整个进程的结束,因此新创建的子进程也被强制退出了。

由此可以得知若要令子线程也被执行,则应令主线程处于等待状态,即令主线程等待子线程执行完毕后再退出。在代码上只需在return 0;语句前加上sleep(10)语句,令主线程处于阻塞状态即可。


由运行结果可以看出,两个线程正在交替运行,运行结束后才执行主线程并退出

  • 问题3:从上面这个实践可以看出,在调用pthread_create()创建子线程的时候,是通过第四个参数进行参数传递,但是实际应用中可能向子线程传递多个参数,这个时候该怎么办呢?

  • 把所有要传的参数组成一个结构体,把这个结构体传给子线程以完成向子线程传递多个参数

  • 关于问题3的实践

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
	char name[20];
	int time;
	int start;
	int end;
}RaceArg;

void* th_fn(void *arg)
{
	RaceArg *r = (RaceArg*)arg;
	int i = r->start;

	for(; i <= r->end ; i++){
		printf("%s(%lx) running %d\\n",r->name,pthread_self(),i);
		usleep(r->time);
	}

	return (void*)0;
}


int main()
{
	int err;
	pthread_t rabbit,turtle;
	
	RaceArg r_a = {"rabbit",10,20,50};
	RaceArg t_a = {"turtle",20,10,60};

	if((err = pthread_create(&rabbit,NULL,th_fn,(void*)&r_a)) != 0)
	{
		printf("error!");
	}

	if((err = pthread_create(&turtle,NULL,th_fn,(void*)&t_a)) != 0)
	{
		printf("error!");
	}

	pthread_join(rabbit,NULL);
	pthread_join(turtle,NULL);
	printf("control thread id:%lx\\n",pthread_self());
	printf("finish!\\n");
	return 0;
}

即定义结构体,把需要传的参数放在结构体内,然后把指向结构体的指针传到子线程中。

  • 问题4:从上面两次的实践可以知道,通过线程创建函数创建了2个子线程,这些子线程在内存中是如何存储的,以保证他们内部存储的信息互不相干扰又能共同共享同一个进程?
  • 线程所拥有的局部变量是互不相干扰的,以上面实践的为例,在内存中的存储方式如下图所示:

    但对于全局变量则是存储在进程数据段当中,子线程则共享数据段中的数据(共享资源),但这样好像会对安全性有一定的影响

线程的终止

  • 主动终止

    • 线程的执行函数中调用return语句
    • 调用pthread_exit()
  • 被动终止

    • 被同一进程的其他线程取消:pthread_cancel(pthid),pthid为被终止的线程标识符。此函数类似进程中的kill函数
  • 注意,若在线程中调用exit(0)这类函数时,则是进程被终止了。另外,当进程还未结束时,退出线程所占用的资源并不会随线程结束而释放

  • pthread_join()函数

    • 原型为int pthread_join(pthread_t th,void **thread_return);th为被等待线程的标识符,thread_return为用户定义指针,用来存储被等待线程的返回值。成功则返回0
    • 调用pthread_join对资源的释放也有一定帮助,避免内存过于拥挤
  • 对于pthread_join()函数,我认为难点主要是第二个参数,它是一个二级指针变量,为了更好理解pthread_join()函数的第二个参数,可以使用下面这个例子(代码功能是将传入子线程的两个参数相加)

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

typedef struct
{
	int d1;
	int d2;
}Arg;

void* th_fn(void *arg)
{
	Arg *r = (Arg*)arg;
	return (void*)(r->d1 + r->d2);
}

int main(void)
{
	int err;
	pthread_t th;
	Arg r = {20,50};

	if((err = pthread_create(&th,NULL,th_fn,(void*)&r)) != 0){
		printf("error!");}

	int *result;

	pthread_join(th,(void**)&result);
	printf("result is %d\\n",(int)result);

	return 0;
}

运行结果如下:

  • 类似地,如果希望pthread_join()函数能返回多个参数,就把这多个参数组成一个结构体并令线程返回指向该结构体的指针变量即可。

线程的清理和控制

  • pthread_cleanup_push(void (*rtn)(void ),void arg)函数
  • pthread_cleanup_pop(int execute)函数,成功则返回0
  • 两者成对出现,即
while(execute){
    执行线程处理函数
}
  • 触发线程调用清理函数的动作

    • 调用pthread_exit
    • 响应取消请求
    • 用非零execute参数调用pthread_cleanup_pop时
  • 实践

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

void clean_fun(void *arg)
{
	char *s = (char*)arg;
	printf("clean_func:%s\\n",s);
}

void* th_fun(void *arg)
{
	int execute = (int)arg;
	pthread_cleanup_push(clean_fun,"first clean");
	pthread_cleanup_push(clean_fun,"second clean");

	printf("thread running %lx\\n",pthread_self());
	pthread_cleanup_pop(execute);
	pthread_cleanup_pop(execute);

	return (void*)0;
}

int main()
{
	int err;
	pthread_t th1,th2;
	if((err = pthread_create(&th1,NULL,th_fun,(void*)1)) != 0){
		printf("error");
	}

	pthread_join(th1,NULL);
	printf("th1(%lx) finished\\n",th1);

	if((err = pthread_create(&th2,NULL,th_fun,(void*)1)) != 0){
		printf("error");
	}

	pthread_join(th2,NULL);
	printf("th2(%lx) finished\\n",th2);

	return 0;
}


由运行结果也可以看出,栈的特点是后入先出,即先压入栈的先被处理。

进程和线程启动和终止方式的比较

线程的同步互斥

生活中的例子:若存在一个研发小组和一个测试小组都要对软件项目进行操作,那么这个软件项目就是一个共享资源,但是测试小组对软件项目进行操作的前提是研发小组已经开发完这个软件项目,因此研发小组和测试小组是一个互斥关系,同时研发小组和测试小组这两个线程是有先后顺序的,即先研发后测试(两者有相互约束的关系)

线程同步是建立在线程互斥的基础上又考虑了线程的先后执行顺序

  • 线程执行的相互排斥主要是为了不同线程在对共享资源操作时也是安全的。即某一个线程在对共享资源操作时阻塞其他线程对这个共享资源的操作
  • 解决同步方式
    • 条件变量
    • 线程信号量
  • 解决互斥方式
    • 互斥锁
    • 读写锁
    • 线程信号量

ATM机的实现(利用线程的同步和互斥)

  • 实践:对银行账户的操作(一个账户有一张主卡和副卡,A持主卡,B持副卡,两者都可以对账户进行存储或取钱操作)

    • 情景一:某一时候二者同时要取出账户的余额(10000元)
    • 分析:A、B看作是两个线程,这个账户看作是共享资源
    • 首先,先定义跟ATM机功能有关的函数(包括存钱、取钱、查看余额等)以及对应的头文件,方便写测试代码时直接调用。
    #ifndef _ACCOUNT_H_
    #define _ACCOUNT_H_
    typedef struct{
    	int code;
    	double balance;
    }Account;
    extern Account* create_account(int code,double balance);
    extern void destroy_account(Account *a);
    extern double withdraw(Account *a,double amt);
    extern double deposit(Account *a,double amt);
    extern double get_balance(Account *a);
    #endif
    
    • 其次,写这几个函数的具体实现形式
    #include "account.h"
    #include <malloc.h>
    #include <assert.h>
    #include <string.h>
    extern Account* create_account(int code,double balance){
    	Account *a = (Account*)malloc(sizeof(Account));
    	assert(a != NULL);
    	a->code = code;
    	a->balance = balance;
    	return a;
    }
    
    void destroy_account(Account *a){
    	assert(a != NULL);
    	free(a);
    }
    
    double withdraw(Account *a,double amt){
    	if(amt < 0 || amt > a->balance){
    		return 0.0;
    	}
    	double balance = a->balance;
    	
    	balance = balance - amt;
    	a->balance = balance;
    	return amt;
    }
    
    double deposit(Account *a,double amt){
    	assert(a != NULL);
    	if(amt < 0){
    		return 0.0;
    	}
    	double balance = a->balance;
    	balance = balance + amt;
    	a->balance = balance;
    	return amt;
    }
    double get_balance(Account *a){
    	assert(a != NULL);
    	double balance = a->balance;
    	return balance;
    }
    
    
    • 最后是测试代码的编写
    #include "account.h"
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct{
    	char name[20];
    	Account *account;
    	double amt;
    }OperArg;
    
    void* withdraw_fn(void *arg){
    	
    	OperArg *oa = (OperArg*)arg;
    	double amt = withdraw(oa->account,oa->amt);
    	printf("%8s(0x%lx) withdraw %f from account %d\\n",oa->name,pthread_self(),amt,oa->account->code);
    	return (void*)0;
    }
    
    void* deposit_fn(void *arg){
    	OperArg *oa = (OperArg*)arg;
    	double amt =deposit(oa->account,oa->amt);
    	printf("%8s(0x%lx) deposit %f from account %d\\n",oa->name,pthread_self(),amt,oa->account->code);
    	return (void*)0;
    }
    
    
    void* check_fn(void *arg){
    	return (void*)0;
    }
    
    int main(){
    	int err;
    	pthread_t boy,girl;
    	Account *a = create_account(100001,10000);
    
    	OperArg o1,o2;
    	strcpy(o1.name,"boy");
    	o1.account = a;
    	o1.amt = 10000;
    
    	strcpy(o2.name,"girl");
    	o2.account = a;
    	o2.amt = 10000;
    
    	if((err = pthread_create(&boy,NULL,withdraw_fn,(void*)&o1)) != 0){
    		printf("error!");
    	}
    
    	if((err = pthread_create(&girl,NULL,withdraw_fn,(void*)&o2)) != 0){
    		printf("error!");
    	}
    
    	pthread_join(boy,NULL);
    	pthread_join(girl,NULL);
    	
    	printf("account balance: %f\\n",get_balance(a));
    	destroy_account(a);
    
    	return 0;
    }
    
    
    • 运行结果如下

      从结果可以看到,虽然第二个用户也申请10000元的取出请求,但是因为此时账户的余额已是0元,因此它就取不出10000元了
  • 实践时遇到的问题

    • 实践时主要遇到的问题是因为刚开始编程的时候希望程序能够更加具备条理性,方便测试代码的编写,所以遇到了如何写头文件的问题、在实际调用函数的时候对参数不熟悉的问题以及函数指针类型强转的时候不太清晰的问题。对于这些问题,在实际编程的时候需要多尝试,遇到一个问题还是要停下来寻找网上的相关资料帮助理解,再进行编程调试就可以了。
  • 反思:实现代码仍存在的问题

    • 在实际的应用中,当第一个人正在打算取钱的时候,因为这个I/O操作可能要花费一定的时间,这就会导致CPU去执行其他进程,这个时候如果第二个人也提出取钱的操作,就会发生两个人都取出了10000元的怪事(因为此时数据段内的共享数据余额还没有被改写,导致第二个人也能取出10000元)
    • 解决方式:线程间的互斥,当一个线程在对一个共享数据进行操作的时候,禁止其他线程也对其进行操作
  • 互斥锁

    • 数据类型:pthread_mutex_t
    • 实现的思路:当第一个用户操作共享资源时,用一把互斥锁把该共享资源锁住,当第二个用户也想操作这个共享资源时,必须获得互斥锁才行,但因为互斥锁在第一个用户手上,第二个用户得不得便无法访问此共享数据,只能等第一个用户操作完毕释放互斥锁,因此得以实现线程间的互斥
    • 互斥锁的创建:int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutex_attr_t *mutexattr)
    • 互斥锁的销毁:int pthread_mutex_destroy(pthread_mutex_t *mutex)
    • 互斥锁的上锁:int pthread_mutex_lock(pthread_mutex_t *mutex)
    • 互斥锁的解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex)
    • 注意:以上四个函数成功均返回0
  • 对实践:ATM机的改进

    • 对于定义共享资源加上一把互斥锁,即
    typedef struct
    {
    	int code;
    	double balance;
    	pthread_mutex_t mutex;
    }Account;
    
    
    • 以取钱操作为例对账户进行上锁与解锁
  • 运行结果如下:

    虽然运行结果与刚刚并无差别,但是在实际应用中,加上互斥锁应该是更安全的做法

  • 读写锁

    • 倘若对共享数据同时有不同的线程进行操作,例如对于账户这个共享资源,同时有取款、存款、查看余额这三个线程进行操作,但是因为互斥锁的存在,在某一个时刻只有获得互斥锁的那一个线程可以操作,其他希望操作这个共享资源的线程均被阻塞。但是在实际的过程中,如果查看余额的操作比取款和存款的操作多,即多个线程希望同时查看余额的时候,互斥锁导致只能有一个线程可以查看,但是在这个数据本身不被修改的情况下,按逻辑应该允许多个线程同时查看这个共享数据
    • 因此当“读”的操作较多,“写”的操作较少的时候,使用互斥锁缺乏读的并发性,因此此时应该使用读写锁提高线程的并发性。即写的操作多时用互斥锁,读的操作多时用读写锁
    • 数据类型:pthread_rwlock_t
    • 读写锁创建:int pthread_rwlock_init(pthread_rwlock_t*restrict rwlock,const pthread_rwlockattr_t *restrict attr)
    • 读写锁的销毁:int pthread_rwlock_destroy(pthread_rwlock_t*restrict rwlock)
    • 读锁的上锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
    • 写锁的上锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
    • 读写锁的解锁:int pthread_mutex_unlock(pthread_mutex_t *rwlock)

线程的同步

  • 条件变量(用来实现线程同步)

    • 数据类型:pthread_cond_t
    • 内部是一个等待队列,线程在条件变量上等待和通知。互斥锁用来保护等待队列,一般与互斥锁一起使用,(此时等待队列就是一个共享资源)
    • 条件变量令线程等待特定条件发生,当条件不满足时,线程通常先进入阻塞状态,等待条件发生变化。具体的判断条件由用户自己给出
    • 条件变量的创建:int pthread_cond_init(pthread_cond_t*restrict cond,pthread_condattr_t *restrict attr);
    • 条件变量的销毁:int pthread_cond_destroy(pthread_cond_t* cond);
    • 条件变量的等待:int pthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutex_t *restrict mutex):线程阻塞自己,并把自己放于等待队列中
    • 条件变量的通知:int pthread_cond_signal(pthread_cond_t* cond);:通知单个线程和int pthread_cond_broadcast(pthread_cond_t* cond);:通知所有线程
  • 实践:一个线程进行计算操作,一个线程进行获取操作,获取线程必须在计算完成后才能被调度

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
	int res;
	int is_wait;
	pthread_cond_t cond;
	pthread_mutex_t mutex;
}Result;

void* set_fn(void *arg)
{
	int i=1,sum=0;
	for(;i<=100;i++){
		sum += i;
	}
	Result *r = (Result*)arg;
	r->res = sum;
	pthread_mutex_lock(&r->mutex);
	while(!r->is_wait){
		pthread_mutex_unlock(&r->mutex);
		sleep(1);
		pthread_mutex_lock(&r->mutex);
	}
	pthread_mutex_unlock(&r->mutex);
	pthread_cond_broadcast(&r->cond);
	return (void*)0;
}

void* get_fn(void *arg)
{
	Result *r = (Result*)arg;
	pthread_mutex_lock(&r->mutex);
	r->is_wait = 1; 
	pthread_cond_wait(&r->cond,&r->mutex);
	pthread_mutex_unlock(&r->mutex);
	int res = r->res;
	printf("0x%lx get sum is %d\\n",pthread_self(),res);

	return (void*)0;
}

int main()
{
	int err;
	pthread_t cal,get;
	Result r;
	r.is_wait=0;
	pthread_cond_init(&r.cond,NULL);
	pthread_mutex_init(&r.mutex,NULL);

	if((err = pthread_create(&get,NULL,get_fn,(void*)&r)) != 0){
		printf("error!");
	}

	if((err = pthread_create(&cal,NULL,set_fn,(void*)&r)) != 0){
		printf("error!");
	}

	pthread_join(cal,NULL);
	pthread_join(get,NULL);

	pthread_cond_destroy(&r.cond);
	pthread_mutex_destroy(&r.mutex);

	return 0;
}
  • 运行结果

实践中遇到的问题

  • 问题4:在得到计算结果的线程函数中,为什么要先等待后释放锁,即为什么要先执行pthread_cond_wait(&r->cond,&r->mutex);后执行pthread_mutex_unlock(&r->mutex);
  • 根据网上资料的学习,因为对于pthread_cond_wait()函数,其内部主要经历了以下几个过程:
    • 释放锁
    • 上锁,把线程自己插入到等待队列中(上锁为了避免让其他线程也访问这个等待队列)
    • 释放锁
    • 当前等待的线程被阻塞,等待唤醒
    • 在唤醒后上锁,从等待队列中删除线程自己(上锁原因与第二步一样)
      因此在调用完pthread_cond_wait()函数后要再释放锁,即这个unlock函数是与wait函数内部的上锁函数相对应,而不是与自己写的代码中的lock函数相对应

线程的状态转换

  • 线程状态转换图

线程——读者-写者问题的学习

  • 读者-写者问题的模型图主要如下:

  • 实践:

#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct{
	int value;

	pthread_cond_t rc;
	pthread_mutex_t rm;
	int r_wait;

	pthread_cond_t wc;
	pthread_mutex_t wm;
	int w_wait;
}Storage;
void set_data(Storage *s,int value){
	s->value = value;
}
int get_data(Storage *s){
	return s->value;
}
void* set_th(void *arg){
	Storage *s = (Storage*)arg;
	int i = 1;
	for(;i<=10;i++){
		set_data(s,i+100);
		printf("0x%lx write dadta:%d\\n",pthread_self(),i);
		pthread_mutex_lock(&s->rm);		
		while(!s->r_wait){
			pthread_mutex_unlock(&s->rm);
			sleep(1);
			pthread_mutex_lock(&s->rm);
		}
		s->r_wait=0;
		pthread_mutex_unlock(&s->rm);
		pthread_cond_broadcast(&s->rc);

		pthread_mutex_lock(&s->wm);
		s->w_wait=1;
		pthread_cond_wait(&s->wc,&s->wm);
		pthread_mutex_unlock(&s->wm);
	}
	return (void*)0;
}
void* get_th(void *arg)
{
	Storage *s = (Storage*)arg;
	int i = 1;
	for(;i<=10;i++){
		pthread_mutex_lock(&s->rm);
		s->r_wait = 1;
		pthread_cond_wait(&s->rc,&s->rm);
		pthread_mutex_unlock(&s->rm);
		int value = get_data(s);
		printf("0x%lx(%-5d) read data: %d\\n",pthread_self(),i,value);
		
		pthread_mutex_lock(&s->wm);
		while(!s->w_wait){
			pthread_mutex_unlock(&s->wm);
			sleep(1);
			pthread_mutex_lock(&s->wm);
		}
		s->w_wait=0;
		pthread_mutex_unlock(&s->wm);
		pthread_cond_broadcast(&s->wc);
	}
	return (void*)0;
}

int main(){
	int err;
	pthread_t rth,wth;

	Storage s;
	s.r_wait=0;
	s.w_wait=0;
	pthread_mutex_init(&s.rm,NULL);
	pthread_mutex_init(&s.wm,NULL);
	pthread_cond_init(&s.rc,NULL);
	pthread_cond_init(&s.wc,NULL);

	if((err = pthread_create(&rth,NULL,get_th,(void*)&s)) != 0){
		printf("error!");
	}
	if((err = pthread_create(&wth,NULL,set_th,(void*)&s)) != 0){
		printf("error!");
	}
	pthread_join(rth,NULL);
	pthread_join(wth,NULL);

	pthread_mutex_destroy(&s.rm);
	pthread_mutex_destroy(&s.wm);
	pthread_cond_destroy(&s.rc);
	pthread_cond_destroy(&s.wc);
	return 0;
}

  • 运行结果如下:

线程的信号量

  • 数据类型:sem_t
  • 本质上是一个非负整数计数器,是共享资源的数量
  • 相关函数
    • 信号量的创建:int sem_init(sem_t *sem ,int pshared,unsigned value);
    • 信号量的销毁:int sem_destroy(sem_t *sem);
    • 信号量的加:int sem_post(sem_t *sem);
    • 信号量的减:int sem_wait(sem_t *sem);:当信号量的值小于0时线程被阻塞,只有调用sem_post()是信号量的值非负时线程才能继续运行

PV原语

  • 上面使用到的信号的加、减函数可通过操作系统课上的PV原语进行理解,即
    • P操作:减,类似sem_wait()
    • V操作:加,类似sem_post()
  • 因此,在上面ATM机的实践中,以取款为例,加入PV原语,可以得到以下的伪代码:
void withdraw(){
    P(1);
    //取款
    V(1);
}
  • 实现代码如下(主要是线程执行函数的改变)
Account* create_account(int code,double balance){
	Account *a = (Account*)malloc(sizeof(Account));
	assert(a != NULL);
	a->code = code;
	a->balance = balance;
	sem_init(&a->sem,0,1);
	return a;
}
void destroy_account(Account *a){
	assert(a != NULL);
	sem_destroy(&a->sem);
	free(a);
}
double withdraw(Account *a,double amt){
	assert(a != NULL);
	sem_wait(&a->sem);
	if(amt < 0 || amt > a->balance){
		sem_post(&a->sem);		
		return 0.0;
	}
	double balance = a->balance;
	balance = balance - amt;
	a->balance = balance;
	sem_post(&a->sem);
	return amt;
}
double deposit(Account *a,double amt){
	assert(a != NULL);
	//P(1)
	sem_wait(&a->sem);
	if(amt < 0){
		//V(1)
		sem_post(&a->sem);
		return 0.0;
	}
	double balance = a->balance;
	balance = balance + amt;
	a->balance = balance;
	sem_post(&a->sem);
	return amt;
}
double get_balance(Account *a){
	assert(a != NULL);
	sem_wait(&a->sem);
	double balance = a->balance;
	sem_post(&a->sem);
	return balance;
}

  • 运行结果如下
  • 注:线程信号量除了实现线程互斥外,还可以实现线程的同步

死锁问题

  • 两个线程试图同时占有两个资源,并按照不同的次序锁定相应的共享资源
  • 解决思路
    • 按照相同的次序锁定相应的共享资源
    • 使用pthread_mutex_trylock()

学习总结

  • 通过对多线程的再次学习,明显感觉到自己对线程的理解比第一次接触时深入了许多,因为这部分本身包含了许多概念以及相关的知识海域操作系统课有关系,因此在当操作系统课上学习了相关基础知识后再进行学习多线程比之前轻松了许多。

以上是关于20155321 《信息安全系统设计》Linux多线程的深入学习的主要内容,如果未能解决你的问题,请参考以下文章

2017-2018-1 20155321 《信息安全系统设计基础》第十二周学习总结

2017-2018-1 20155321 20155330 《信息安全系统设计基础》实验四——外设驱动程序设计

2017-2018-1 20155321 《信息安全系统设计基础》实验五——实时系统

2017-2018-1 20155321 《信息安全系统设计基础》课堂实践——实现mypwd

2017-2018-1 20155321 《信息安全系统设计基础》第九周学习总结

2017-2018-1 20155321 《信息安全系统设计基础》第十一周学习总结