在C++编程中,尤其是在服务器端,多线程几乎是必备技能了。.net中自有微软为大家封装好的线程池与线程等等完备的方案,C++中,大部分资料都是集中在windows.h所支持的多线程操作,当然也有boost开源库之类的。而今天我们要谈的是在Linux与Unix中广泛使用的pthread库,当然比较简略,笔者还是缺乏更多的实际经验,毕竟也就刚开始接触Linux服务器端。
依照惯例先推荐个资料:杨沙洲博士在2001年发表的《Posix线程编程指南》,这本可以说是深入人心,而且一些概念都有提及,不过依然需要大家多多实践和探索。
无论是pthread库还是windows API,本质上都是一些小的模块,而我们的工作就是设计模块的结合,来达到我们的目的。
这篇文章只讲三个最基础的部分:基础的pthread操作,互斥锁,信号机制。
I 基础的Pthread操作
总共只涉及四个基本元素:pthread_t,pthread_attr_t, pthread_create 和 pthread_join。
先来看一段程序,创建一个线程。
#include <iostream>
#include <pthread.h>
using namespace std;
void* child_thread(void *arg)
{
pthread_t tid = pthread_self();
unsigned long int tidint = (unsigned long int) tid;
cout << " child_Thread ID : " << tidint << endl;
}
int main()
{
pthread_t tid;//Id for Threads
pthread_create(&tid, NULL, child_thread, NULL);
cout << "Main thread exit" << endl;
return 0;
}
如代码所示,创建一个线程就是如此的简单,而线程要做的工作就是由我们的指针函数child_thread来承担对应的工作。
如果你还不了解何为指针函数,请去《C++ Primer》或是《Effective C++》之类的经典读本了解下相关知识。
如主程序main中所提到的,创建一个线程,我们只需要两样东西:创建动作与唯一标识。
其中唯一标识就是pthread_t的工作,它用于显示线程的ID,无论去哪都不好改变。
而pthread_create这个函数,就是做了创建这个动作。
那么一个子线程是如何才能创建的呢?读过操作系统之类书籍的一定知道,进程的创建实质上是一个PCB控制块的建立,而一个PCB控制块一般而言都包含了唯一标识、状态、相应的程序等等,类似的子线程也是由这些部分组成的:标识符、线程属性(状态)、控制程序的起始地址以及需要的参数。而它的流程是这样的:
分配一个唯一标识给即将创建的线程
调用创建动作并传递对应需要的参数。
这也就是我们上面程序所做的所有工作,一个最基本的线程就创建完成了。
前面有提到线程属性这个概念,其实属性也就是规定了这个线程是个怎样的类型,那也就是我们第三个元素pthread_attr_t的作用了。
一般而言,属性的设置都是用pthread的自带库的设置函数来完成的。
pthread_attr_t attr;//Attribute for threads
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
其中第一句定义了一个属性实例,第二步通过默认初始化完成初始化,第三步则是调用了属性设置允许线程同步。
刚刚提到了一个很有意思的名字叫作:线程同步。这也是最麻烦的地方。
何为同步?
假设我们的主线程main函数完成所有工作需要10s,其中我们有两个子线程叫作吃饭,睡觉。吃饭完成需要5s,睡觉完成需要20s。
然后的我们假设主线程在运行的第一秒内创建了子线程吃饭和子线程睡觉。
那么会发生什么?
在主线程10s完成所有工作的时候,子线程吃饭已经完成了,但子线程睡觉仅完成了不到一半,这下子问题来了,主线程都结束工作了,自然不允许你继续睡下去,于是乎,全部结束。
而同步就是为了解决这类的问题。
何为同步,我们规定,必须先吃饭,后睡觉,睡觉完了才能干别的事情。
那么主线程创建好吃饭这个子线程,必须等待它完成,再去创建睡觉这个子线程,等睡觉完成,再做别的工作随后结束。
没有同步时:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* Fan(void *arg)
{
pthread_t tid = pthread_self();
unsigned long int tidint = (unsigned long int) tid;
cout << " Fan Start " << tidint << endl;
sleep(5);
cout << " Fan Over " << tidint << endl;
}
void* Sleep(void *arg)
{
pthread_t tid = pthread_self();
unsigned long int tidint = (unsigned long int) tid;
cout << " Sleep Start " << tidint << endl;
sleep(20);
cout << " Sleep Over " << tidint << endl;
}
int main()
{
pthread_t tid1,tid2;//Id for Threads
pthread_create(&tid1, NULL, Fan, NULL);
pthread_create(&tid2, NULL, Sleep, NULL);
sleep(10);
cout << "Main thread exit" << endl;
return 0;
}
运行结果
如何解决同步问题?这时候,我们的第四要素pthread_join登场,它会阻塞主线程直到子线程完成。
仅对main函数进行修改
int main()
{
pthread_t tid1,tid2;//Id for Threads
void *status;
pthread_create(&tid1, NULL, Fan, NULL);
pthread_join(tid1, &status);
pthread_create(&tid2, NULL, Sleep, NULL);
pthread_join(tid2, &status);
sleep(10);
cout << "Main thread exit" << endl;
return 0;
}
运行结果:
至此,我们已经讲述了基本的线程操作以及如何进行同步。
而接下来的互斥锁与信号机制实质上也是为了解决同步问题的两类方法,它们解决了临界资源的同步。
II 互斥锁
互斥锁怎么实现?首先,互斥锁不是我们使用的变量,可以把它当成某个变量的标志位,比如我们有一个临界资源CD,它对应的标志位就是CD_mutex。假设这样一种场景,一盘CD每次只能由一个人借走,而我们记录它总共借出的次数,可以这一定义:
int cd_quantity = 0;
pthread_mutex_t cd_quantity_mutex;
一个是本身的变量,一个则是它的标志位也叫锁。那么怎么使用它呢?笔者在这里仅给出伪代码,请自行尝试。
void* 子线程(void *arg)
{
pthread_mutex_lock(&cd_quantity_mutex);//先加锁,若取不到锁则阻塞,知道拿到锁
//对于变量的操作,比如计数加一
cd_quantity++;
pthread_mutex_unlock(&cd_quantity_mutex);//释放锁,允许其他线程取得锁
}
int main()
{
pthread_t tid1,tid2;//Id for Threads
pthread_mutex_init (&cd_quantity_mutex, NULL);//初始化锁
//不同的子线程同时修改cd_quantity的值
//请自行设计
pthread_mutex_destroy(&cd_quantity_mutex);//消除锁
cout << "Main thread exit" << endl;
return 0;
}
这样,通过互斥锁,我们可以发现变量cd_quantity可以被有序修改。
当然根据设计的不同,有可能线程是混乱的。
III 基础信号量机制
如果说互斥锁可以解决临界资源问题,信号机制,在笔者看来更多的是解决执行顺序问题,一般而言都是与互斥锁搭配使用。
在这一部分,由于笔者自认为还不能很好完善地解释,因而我给出了一个基本示例,来自其它博客:http://blog.csdn.net/hitwengqi/article/details/8015646
也是笔者在找资料时发现的。
对代码进行了精简
pthread_mutex_t tasks_mutex; //互斥锁
pthread_cond_t tasks_cond; //条件信号变量,处理两个线程间的条件关系,当task>5,hello2处理,反之hello1处理,直到task减为0
void* say_hello2( void* args )
{
bool is_signaled = false; //sign
while(1)
{
pthread_mutex_lock( &tasks_mutex ); //加锁
if( tasks > BOUNDARY )
{
cout << "[" << pid << "] take task: " << tasks << " in thread " << *( (int*)args ) << endl;
--tasks; //modify
}
else if( !is_signaled )
{
cout << "[" << pid << "] pthread_cond_signal in thread " << *( ( int* )args ) << endl;
pthread_cond_signal( &tasks_cond ); //signal:向hello1发送信号,表明已经>5
is_signaled = true; //表明信号已发送,退出此线程
}
pthread_mutex_unlock( &tasks_mutex ); //解锁
if( tasks == 0 )
break;
}
}
void* say_hello1( void* args )
{
while(1)
{
pthread_mutex_lock( &tasks_mutex ); //加锁
if( tasks > BOUNDARY )
{
cout << "[" << pid << "] pthread_cond_signal in thread " << *( ( int* )args ) << endl;
pthread_cond_wait( &tasks_cond, &tasks_mutex ); //wait:等待信号量生效,接收到信号,向hello2发出信号,跳出wait,执行后续
}
else
{
cout << "[" << pid << "] take task: " << tasks << " in thread " << *( (int*)args ) << endl;
--tasks;
}
pthread_mutex_unlock( &tasks_mutex ); //解锁
if( tasks == 0 )
break;
}
}
int main()
{
pthread_cond_init( &tasks_cond, NULL ); //初始化条件信号量
pthread_mutex_init( &tasks_mutex, NULL ); //初始化互斥量
//创建并连接两个线程
pthread_mutex_destroy( &tasks_mutex ); //注销锁
pthread_cond_destroy( &tasks_cond ); //正常退出
}
很多人看了还是会有点模糊,笔者来简单解释下:
pthread_cond_wait用于阻塞当前的线程,在阻塞的同时释放锁,等待pthread_cond_signal或pthread_cond_broadcast来唤醒。
pthread_cond_signal是发送一个信号量给某个阻塞的进程,并唤醒它。
那么在本例中is_signaled是用来干嘛的?其实在用pthread_cond_signal进行唤醒阻塞进程的操作时,当前线程并没有被阻塞或者强制结束,依然在运行!所以is_signaled相当于标志位,避免该线程继续争夺资源。
详细描述可以参考另一篇博文
http://blog.csdn.net/hudashi/article/details/7709421
至此,笔者简单向大家介绍了多线程编程,但路漫漫,还有很多操作我们在这并没有实现,而实际应用中也不是上面这些例子可以比拟的。希望大家和笔者一起进步,不断学习。
等笔者感觉有所精益,再来和大家说道说道多线程吧~
The End.
转载请注明出处。