您的位置:首页 > 理论基础 > 计算机网络

网络编程—线程

2016-05-25 23:46 218 查看
之前的博客介绍了管道通信,进程间的通信,今天来介绍线程。

介绍线程前先要搞清楚线程与进程的区别。进程可以理解为正在运行的程序,进程管理和调度资源,而线程就是受进程调度的最小单位,也就是说进程给一个或多个线程资源并调度线程的运行。

要运用线程,要包含头文件 pthread.h 。线程的属性标识符:pthread_attr_t。

线程的属性结构如下:

typedef struct
{
int                   etachstate;      //线程的分离状态
int                   schedpolicy;     //线程调度策略
structsched_param     schedparam;      //线程的调度参数
int                   inheritsched;    //线程的继承性
int                   scope;           //线程的作用域
size_t                guardsize;       //线程栈末尾的警戒缓冲区大小
int                   stackaddr_set;   //线程的栈设置
void*                 stackaddr;       //线程栈的位置
size_t                stacksize;       //线程栈的大小
}pthread_attr_t;


属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。

一,进程的作用域(scope)

作用域属性描述特定线程将于哪些线程竞争资源。线程可以在两种竞争域内竞争资源:

1. 进程域(process scope):与同一进程内的其他线程。

2. 系统域(system scope):与系统中的所有线程。一个具有系统域的线程将与整个系统中所有具有系统域的线程按照优先级竞争处理器资源,进行调度。

3. Solaris系统,实际上,从 Solaris 9 发行版开始,系统就不再区分这两个范围。

二、线程的绑定状态(binding state)

轻进程(LWP:Light Weight Process)关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process):轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制 一个或多个线程。

1. 非绑定状态

默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。

2. 绑定状态

绑定状况下,则顾名思义,即某个线程固定的”绑”在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

三、线程的分离状态(detached state)

1. 线程的分离状态决定线程以什么样的方式终止自己。

2. 非分离状态

线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

3. 分离状态

分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

线程分离状态的函数:pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。

第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就 可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

四,线程的优先级(priority)

1. 新线程的优先级为默认为0。

2. 新线程不继承父线程调度优先级(PTHREAD_EXPLICIT_SCHED)

3. 仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。

五、线程的栈地址(stack address)

1. POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。

2. 也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。

3. 当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和 pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。

六、线程的栈大小(stack size)

1. 当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。

2. 当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

3. 函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。

七、线程的栈保护区大小(stack guard size)

1. 在线程栈顶留出一段空间,防止栈溢出。

2. 当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程。

3. 该属性默认值是PAGESIZE大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。

4. 当改变栈地址属性时,栈保护区大小通常清零。

八、线程的调度策略(schedpolicy)

POSIX标准指定了三种调度策略:先入先出策略 (SCHED_FIFO)、循环策略 (SCHED_RR) 和自定义策略 (SCHED_OTHER)。SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。SCHED_FIFO 和 SCHED_RR 是对 POSIX Realtime 的扩展。SCHED_OTHER 是缺省的调度策略。

1. 新线程默认使用 SCHED_OTHER 调度策略。线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。

2. SCHED_FIFO

如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用 SCHED_FIFO,SCHED_FIFO 基于 TS 调度类。

3. SCHED_RR

如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR(基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 0。

九,线程间的互斥锁

多线程编程能提升程序运行效率,但多线程中对于共有资源的调用必须要运用互斥锁来保证安全。原因在于,在一个时间片中一个线程对资源正在调用时,如被剥夺优先权变成就绪态,另个线程使用并改变了共有资源,那么当之前的线程重新调用时就将会出错。

我们采用加锁的方式,对某一使用的共有资源进行互斥量的加锁,当其他线程要使用这一共有资源时,需先进行加锁,而上一个线程正在使用这一资源处于对此资源的加锁状态,这个线程就只能阻塞 等待上一个线程使用完解锁后才能够加锁使用。这就保证了某一线程使用时不被剥夺资源而导致的数据错误。

注意对资源的互斥量加锁,一定要进行必要的解锁,否则该资源一直处于被加锁状态,其他线程将不能访问该资源,这种状况称为——死锁

十,线程的应用—生产者和消费者

生产者和消费者问题是线程中的一个经典问题。场景设计为,生产者线程只负责生产资源,将生产的资源放在存放区,消费者线程只消费资源。要求是当存放区满时,生产者不能在生产了,同理当存放区空时,消费者也不能在消费。这样的场景在生活中非常的多,比如银行的存钱取钱等等,

列出实现代码如下:

#include<unistd.h>
#include<stdio.h>
#include<pthread.h>  //调用线程所需的头文件

#define MAX_SIZE 20  //总共生产20个数据
#define BUFFER_SIZE 8  //存放区大小为8

struct
{
pthread_mutex_t mutex;    //定义互斥量
pthread_cond_t  nofull;   //信号量 不满
pthread_cond_t  noempty;  //信号量 不空
int buff[BUFFER_SIZE];    //定义存放区
int nput;                 //下标
int nval;                 //存的值
int count;                //BUFF_SIZE的占有量
}shared = {PTHREAD_MUTEX_INITIALIZER,PTHREAD_COND_INITIALIZER,PTHREAD_COND_INITIALIZER};                //结构体的初始化


生产者的实现如下:

void* produce(void *arg)
{
for(;;)
{
pthread_mutex_lock(&shared.mutex);  //通过互斥量上锁
if(shared.nval >= 20)
{
pthread_mutex_unlock(&shared.mutex); //满20个解锁退出
printf("produce finish.\n");
break;
}
if(shared.count < BUFFER_SIZE)
{
shared.buff[shared.nput] = shared.nval;
shared.nput++;
if(shared.nput >= BUFFER_SIZE)
shared.nput = 0;
shared.nval++;
shared.count++;
pthread_cond_signal(&shared.noempty);
}
else
pthread_cond_wait(&shared.nofull, &shared.mutex);  //当存放区满时,阻塞生产者线程,等到nofull才能继续
pthread_mutex_unlock(&shared.mutex);
}
}


消费者实现代码:

void* consume(void *arg)
{
int j = 0;
for(int i=0; i<MAX_SIZE;)
{
if(shared.count > 0)
{
printf("value = %d\n", shared.buff[j]);
i++;
sleep(1);
j++;
if(j >= BUFFER_SIZE)  //取到8后循环在接着从0取数据
j = 0;
shared.count--;
pthread_cond_signal(&shared.nofull);
}
else
pthread_cond_wait(&shared.noempty, &shared.mutex);  //当存放区空时,阻塞消费者进程,直到等到信号noempty
}
}


int main()
{
pthread_t ptid;
pthread_t ctid;

pthread_create(&ptid, NULL, produce, NULL);
pthread_create(&ctid, NULL, consume, NULL);

pthread_join(ptid,NULL);
pthread_join(ctid,NULL);

return 0;
}


程序的结果是生产者生产的数据最后都被消费者输出,注意并不是生产者一次生产存放区的大小的数据,然后由消费者使用空后,接着再生产存放区大小…这样的循环。线程间的运行是随系统的调度,也就说线程原则上根据优先度随机执行的。也可能消费一个立马使用一个,也可能生产3个,8个在使用。

生产者和消费者的问题还涉及 , 多生产者—单消费者,单生产者—多消费者,多生产者—多消费者。这些情况请大家可以思考下。

推荐< unix网络编程 >,对线程有详细的讲解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  网络编程