第1节   背景

为了更好的理解多线程的概念,先对进程,线程的概念背景做一下简单介绍。

早期的计算机系统都只允许一个程序独占系统资源,一次只能执行一个程序。在大型机年代,计算能力是一种宝贵资源。对于资源拥有方来说,最好的生财之道自然是将同一资源同时租售给尽可能多的用户。最理想的情况是垄断全球计算市场。所以不难理解为何当年IBM预测“全球只要有4台计算机就够了”。

这种背景下,一个计算机能够支持多个程序并发执行的需求变得十分迫切。由此产生了进程的概念。进程在多数早期多任务操作系统中是执行工作的基本单元。进程是包含程序指令和相关资源的集合。每个进程和其他进程一起参与调度,竞争CPU,内存等系统资源。每次进程切换,都存在进程资源的保存和恢复动作,这称为上下文切换。

进程的引入可以解决支持多用户的问题,但是多进程系统也在如下方面产生了新的问题:
      进程频繁切换引起的额外开销可能会严重影响系统性能。
      进程间通信要求复杂的系统级实现。

在程序功能日趋复杂的情况下,上述缺陷也就凸现出来。比如,一个简单的GUI程序,为了有更好的交互性,通常用一个任务支持界面交互,另一个任务支持后台运算。如果每个任务均由一个进程来实现,那会相当低效。对每个进程来说,系统资源看上去都是其独占的。比如内存空间,每个进程认为自己的内存空间是独有的。一次切换,这些独立资源都需要切换。

由此就演化出了利用分配给同一个进程的资源,尽量实现多个任务的方法。这也就引入了线程的概念。同一个进程内部的多个线程,共享的是同一个进程的所有资源。

比如,与每个进程独有自己的内存空间不同,同属一个进程的多个线程共享该进程的内存空间。例如在进程地址空间中有一个全局变量globalVar,若A线程将其赋值为1,则另一线程B可以看到该变量值为1。两个线程看到的全局变量globalVar是同一个变量。

通过线程可以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信也更简单。

目前多线程应用主要用于两大领域:网络应用和嵌入式应用。为什么在这两个领域应用较多呢因为多线程应用能够解决两大问题:
      并发。网络程序具有天生的并发性。比如网络数据库可能需要同时处理数以千计的请求。而由于网络连接的时延不确定性和不可靠性,一旦等待一次网络交互,可以让当前线程进入睡眠,退出调度,处理其他线程。这样就能够有效利用系统资源,充分发挥系统处理能力。
      实时。线程的切换是轻量级的,所以可以保证足够快。每当有事件发生,状态改变,都能有线程及时响应,而且每次线程内部处理的计算强度和复杂度都不大。在这种情况下,多线程实现的模型也是高效的。

在有些语言中,对多线程或者并发的支持是直接内建在语言中的,比如Ada和VHDL。在C++里面,对多线程的支持由具体操作系统提供的函数接口支持。不同的系统中具体实现方法不同。后面所有例子只给出windows和Unix/Linux的实现。

在后面的实现中,考虑的是尽量封装隔离底层的多线程函数接口,屏蔽操作系统底层的线程实现具体细节,介绍的重点是多线程编程中较通用的概念。同时也尽量体现C++面向对象的一面。

最后,由于空闲时间有限,我只求示例代码能够明确表达自己的意思即可。至于代码的尽善尽美就只能有劳各位尽力以为之了。

第2节 线程的创建

在一个线程的生存期内,可以在多种状态之间切换,不同的操作系统可以实现不同的线程模型。定义许多不同的线程状态,每个状态可以包含多个子状态,但大体来说,如下几种状态是通用的:

就绪,参与调度,等待别执行,一旦被选中,立即执行。

运行,占用CPU,正在运行中。

休眠, 暂不参与调度,等待特定事件发生。

中止,已经运行完毕,等待回收线程资源。

线程存在于进程之中,进程内所有全局资源对于内部的每个线程都是可见的。

静态存储区,全局变流量,静态变量

动态存储区,也就是堆空间。

线程内典型得 局部资源有:

本地栈空间,存放线程的函数调用栈,函数内部局部变量等。

部分寄存器变量,例如本线程要执行下一步要执行的代码的指针偏移量。

一个进程发起之后,首先会生成一个缺省的线程,通常成为主线程。C/C++程序中主线程是通过main函数进入线程。

由主线程衍生的线程为从线程,从线程也可以由自己的入口函数,相当于主线程的main函数。

这个函数由用户自己制定,pthread和winapi中都是通过传入函数指针实现的,在制定线程入口函数时,也可以制定入口函数的参数。就像卖弄函数由固定的格式要求一样。线程入口函数一般也有固定的格式要求,参数通常是void*类型,返回类型在pthread中是void*。winapi是unsigned int,而且都需要是全局函数。

最常见的线程模型,除了主线程也弄个较为特殊之外,其他线程一旦被创建,相互之间就是对等关系(peer to peer)

,不存在隐含层次关系,每个进程可以被创建的最大线程数有具体实现决定。

为了更好的理解通过具体代码来详细说明。

线程类接口定义:

一个线程类无论具体执行什么任务,其基本的共性无非就是

穿件并启动线程

停止线程

另外还有就是能睡,能等,能分离执行

将线程的概念佳宜抽象,可以其定义如下的类。

文件thread.h

#ifndef _THREAD_H_

#define _THREAD_H

class Thread

{

public:

Thread(){}

virtual ~ Thread();

int start(void * =NULL);

void stop();

void sleep(int);

void detach():

void *wait();

protected virtual void*run(void*)=0;

private:

//这部分win和unix略有不同,先不定义,后面再实现

};

#endif

Thread::start(){}函数是线程启动函数,其输入参数是无类型的指针。

Thread::stop()函数中止当前线程。

Thread::sleep(int)函数让当前线程休眠给定的时间,单位为毫秒。

Thread::run()函数是用于实现线程类的线程函数调用。

Thread::detach()和Thread::wait()函数设计的概念略有复杂一些,稍后解释。

示例程序

代码写得不够精致,暴力转型比较多。

  1. //文件create.h
  2. #ifndef __CREATOR__H_
  3. #define __CREATOR__H_
  4. #include <stdio.h>
  5. #include "thread.h"
  6. class Create: public Thread{
  7. protected:
  8. void * run(void * param)
  9. {
  10. char * msg = (char*) param;
  11. printf ("%s\n", msg);
  12. //sleep(100); 可以试着取消这行注释,看看结果有什么不同。
  13. printf("One day past.\n");
  14. return NULL;
  15. }
  16. };
  17. #endif

然后,实现一个main函数,来看看具体的效果

  1. //文件Genesis.cpp
  2. #include<stdio.h>
  3. #include"create.h"
  4. using namespace std;
  5. int main(int argc,char** argv)
  6. {
  7. Create monday;
  8. Create tuesday;
  9. monday.start("Naming the light,Day,and the dark, Night,the first day");
  10. tuesday.start("Gave the arch the name of heavern ,the second day.");
  11. cout <<"these are the genrations of the heaven and the earth.\n";
  12. return 0;
  13. }

编译运行,程序输出如下:
At the first God made the heaven and the earth.
These are the generations of the heaven and the earth.
令人惊奇的是,由周一和周二对象创建的子线程似乎并没有执行!这是为什么呢别急,在最后的printf语句之前加上如下语句:
monday.wait();
tuesday.wait();
重新编译运行,新的输出如下:
At the first God made the heaven and the earth.
Naming the light, Day, and the dark, Night, the first day.
One day past.
Gave the arch the name of Heaven, the second day.
One day past.
These are the generations of the heaven and the earth.

为了说明这个问题,需要了解前面没有解释的Thread::detach()和Thread::wait()两个函数的含义无论在windows中

还是posix中,主线程和子线程的默认关系式:

无论子线程执行是否完毕,一旦主线程执行完毕退出,所有的子线程都会终止。这时,整个进程结束或僵死(部分线程保持一种终止执行单还未撤销的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态),在第一个例子中的输出,可以看淡子线程还来不及执行完毕。主线程main函数已经执行结束了,从而所有的子线程终止。

需要强调的是线程函数执行完毕退出后,或其他非常方式终止,线程进入终止态。进入终止态后,为线程分配的系统资源并不一定已经释放,而且可能在系统重启之前一直不能释放。终止态的线程,仍旧作为一个线程实体与操作系统中存在。而什么时候释放呢?取决于线程属性。

通常,这种终止方式并非我们所期望的结果,而且一个潜在的问题是未执行玩就终止的子线程,除非作为线程实体占用系统资源外,其线程函数所拥有的资源也不一定能释放,所以针对这个问题,主线程和自相成之间通常定义两种关系:

可会和(joinable),这种关系下,主线程需要名且执行等待操作,在子线程结束后,主线程等待的操作执行完毕,子线程和主线程会合。这是主线程执行等待操作之后的下一步操作,主线程必须会合可以会合的子线程,Thread类中,这个操作通过主线程的线程函数内部调用子线程对象的wait函数实现。必须强调的是,及时子线程能够再逐项成执行完毕,进入终止状态,也必须显示执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也不会释放。

相分离(detached)顾名思义,这表示子线程无需和主线程会合,也就是相分离的。这种情况下,子线程一旦进入终止态,系统立即销毁线程,回收资源,无需再主线程内调用wait实现会后。Thread类中,调用detach使线程进入detach状态,这种方式常在线程数量较多的情况下,有事让主线程诸葛等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难的,或者是不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

缺省情况下,创建的线程都是可会合的。可会合的线程可以通过调用detach()方法变成相分离的线程。但反向则不行。
UNIX实现

  1. //文件thread.h
  2. #ifndef __THREAD__H_
  3. #define __THREAD__H_
  4. class Thread{
  5. public:
  6. Thread();
  7. virtual ~Thread();
  8. int start (void * = NULL);
  9. void stop();
  10. void sleep (int);
  11. void detach();
  12. void * wait();
  13. protected:
  14. virtual void * run(void *) = 0;
  15. private:
  16. pthread_t handle;
  17. bool started;
  18. bool detached;
  19. void * threadFuncParam;
  20. friend void * threadFunc(void *);
  21. };
  22. //pthread中线程函数必须是一个全局函数,为了解决这个问题
  23. //将其声明为静态,以防止此文件之外的代码直接调用这个函数。
  24. //此处实现采用了称为Virtual friend function idiom 的方法。
  25. Static void * threadFunc(void *);
  26. #endif
  1. //文件thread.cpp
  2. #include <pthread.h>
  3. #include <sys/time.h>
  4. #include “thread.h”
  5. static void * threadFunc (void * threadObject)
  6. {
  7. Thread * thread = (Thread *) threadObject;
  8. return thread->run(thread->threadFuncParam);
  9. }
  10. Thread::Thread(){ started = detached = false; }
  11. Thread::~Thread(){ stop(); }
  12. bool Thread::start(void * param)
  13. {
  14. pthread_attr_t attributes;
  15. pthread_attr_init(&attributes);
  16. if (detached){
  17. pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
  18. }
  19. threadFuncParam = param;
  20. //create
  21. if (pthread_create(&handle, &attributes, threadFunc, this) == 0)
  22. {
  23. started = true;
  24. }
  25. pthread_attr_destroy(&attribute);
  26. }
  27. void Thread::detach()
  28. {
  29. if (started && !detached)   {
  30. pthread_detach(handle);
  31. }
  32. detached = true;
  33. }
  34. void * Thread::wait()
  35. {
  36. void * status = NULL;
  37. if (started && !detached)   {
  38. pthread_join(handle, &status);
  39. }
  40. return status;
  41. }
  42. void Thread::stop()
  43. {
  44. if (started && !detached){
  45. pthread_cancel(handle);//顺序ok?
  46. pthread_detach(handle);//
  47. detached = true;
  48. }
  49. }
  50. void Thread::sleep(unsigned int milliSeconds)
  51. {
  52. timeval timeout = { milliSeconds/1000, millisecond%1000};
  53. select(0, NULL, NULL, NULL, &timeout);
  54. }

Windows实现

文件thread.h
#ifndef _THREAD_SPECIFICAL_H__
#define _THREAD_SPECIFICAL_H__

#include <windows.h>

static unsigned int __stdcall threadFunction(void *);

class Thread {
        friend unsigned int __stdcall threadFunction(void *);
public:
        Thread();
        virtual ~Thread();
        int start(void * = NULL);
        void * wait();
        void stop();
        void detach();
        static void sleep(unsigned int);

protected:
        virtual void * run(void *) = 0;

private:
        HANDLE threadHandle;
        bool started;
        bool detached;
        void * param;
        unsigned int threadID;
};

#endif

文件thread.cpp
#include "stdafx.h"
#include <process.h>
#include "thread.h"

unsigned int __stdcall threadFunction(void * object)
{
        Thread * thread = (Thread *) object;
        return  (unsigned int ) thread->run(thread->param);
}

Thread::Thread()
{
        started = false;
        detached = false;
}

Thread::~Thread()
{
        stop();
}

int Thread::start(void* pra)
{
        if (!started)
        {
                param = pra;
                if (threadHandle = (HANDLE)_beginthreadex(NULL, 0, threadFunction, this, 0, &threadID))
                {
                        if (detached)
                        {
                                CloseHandle(threadHandle);
                        }
                        started = true;
                }
        }
        return started;
}

//wait for current thread to end.
void * Thread::wait()
{
        DWORD status = (DWORD) NULL;
        if (started && !detached)
        {
                WaitForSingleObject(threadHandle, INFINITE);
                GetExitCodeThread(threadHandle, &status);       
                CloseHandle(threadHandle);
                detached = true;
        }

return (void *)status;
}

void Thread::detach()
{
  if (started && !detached)
  {
    CloseHandle(threadHandle);
  }
  detached = true;
}

void Thread::stop()
{
        if (started && !detached)
        {
                TerminateThread(threadHandle, 0);

//Closing a thread handle does not terminate 
                //the associated thread. 
                //To remove a thread object, you must terminate the thread, 
                //then close all handles to the thread.
                //The thread object remains in the system until 
                //the thread has terminated and all handles to it have been 
                //closed through a call to CloseHandle
                CloseHandle(threadHandle);
                detached = true;
        }
}

void Thread::sleep(unsigned int delay)
{
        ::Sleep(delay);
}

小结:

本节的主要目的是帮助入门者建立基本的线程概念,以此为基础,抽象出一个最小接口的通用线程类。在示例程序部分,初学者可以体会到并行和串行程序执行的差异。有兴趣的话,大家可以在现有线程类的基础上,做进一步的扩展和尝试。如果觉得对线程的概念需要进一步细化,大家可以进一步扩展和完善现有Thread类。

想更进一步了解的话,一个建议是,可以去看看其他语言,其他平台的线程库中,线程类抽象了哪些概念。比如Java, perl等跨平台语言中是如何定义的,微软从winapi到dotnet中是如何支持多线程的,其线程类是如何定义的。这样有助于更好的理解线程的模型和基础概念。

另外,也鼓励大家多动手写写代码,在此基础上尝试写一些代码,也会有助于更好的理解多线程程序的特点。比如,先开始的线程不一定先结束。线程的执行可能会交替进行。把printf替换为cout可能会有新的发现,等等。

每个子线程一旦被创建,就被赋予了自己的生命。管理不好的话,一只特例独行的猪是非常让人头痛的。

对于初学者而言,编写多线程程序可能会遇到很多令人手足无措的bug。往往还没到考虑效率,避免死锁等阶段就问题百出,而且很难理解和调试。这是非常正常的,请不要气馁,后续文章会尽量解释各种常见问题的原因,引导大家避免常见错误。目前能想到入门阶段常遇到的问题是:
      内存泄漏,系统资源泄漏。
      程序执行结果混乱,但是在某些点插入sleep语句后结果又正确了。
      程序crash, 但移除或添加部分无关语句后,整个程序正常运行(假相)。
      多线程程序执行结果完全不合逻辑,出于预期。

本文至此,如果自己动手改改,试一些例子,对多线程程序应该多少有一些感性认识了。刚开始只要把基本概念弄懂了,后面可以一步一步搭建出很复杂的类。不过刚开始不要贪多,否则会欲速则不达,越弄越糊涂。

最后,大家见仁见智吧,我在此起到抛砖引玉的作用就很开心了,呵呵。另外文本编辑器的原因,代码如果编译不过,可能需要把标点符号从中文换成英文。

转载出处:http://blog.163.com/zhaojingong@126/blog/static/799089922010814104312911/

C++面向对象多线程入门的更多相关文章

  1. JavaScript面向对象轻松入门之封装(demo by ES5、ES6、TypeScript)

    本章默认大家已经看过作者的前一篇文章 <JavaScript面向对象轻松入门之抽象> 为什么要封装? 封装(Encapsulation)就是把对象的内部属性和方法隐藏起来,外部代码访问该对 ...

  2. java多线程入门学习(一)

    java多线程入门学习(一) 一.java多线程之前 进程:每一个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销.一个进程包括1--n个线程.     线程:同一类线程共享代码 ...

  3. Java多线程学习(一)Java多线程入门

    转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79640870 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...

  4. 《挑战30天C++入门极限》C++面向对象编程入门:构造函数与析构函数

        C++面向对象编程入门:构造函数与析构函数 请注意,这一节内容是c++的重点,要特别注意! 我们先说一下什么是构造函数. 上一个教程我们简单说了关于类的一些基本内容,对于类对象成员的初始化我们 ...

  5. 《挑战30天C++入门极限》C++面向对象编程入门:类(class)

        C++面向对象编程入门:类(class) 上两篇内容我们着重说了结构体相关知识的操作. 以后的内容我们将逐步完全以c++作为主体了,这也意味着我们的教程正式进入面向对象的编程了. 前面的教程我 ...

  6. JavaSE_多线程入门 线程安全 死锁 状态 通讯 线程池

    1 多线程入门 1.1 多线程相关的概念 并发与并行 并行:在同一时刻,有多个任务在多个CPU上同时执行. 并发:在同一时刻,有多个任务在单个CPU上交替执行. 进程与线程 进程:就是操作系统中正在运 ...

  7. JavaScript面向对象轻松入门之多态(demo by ES5、ES6、TypeScript)

    多态(Polymorphism)按字面的意思就是"多种状态",同样的行为(方法)在不同对象上有不同的状态. 在OOP中很多地方都要用到多态的特性,比如同样是点击鼠标右键,点击快捷方 ...

  8. (转载)Java多线程入门理解

    转载出处http://blog.csdn.net/evankaka 写在前面的话:此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更 ...

  9. c++标准库多线程入门

    从c++ 11开始,语言核心和标准库开始引入了对多线程的原生支持.如下所示: int doSth(char c) { default_random_engine dre(c); uniform_int ...

随机推荐

  1. 将DevExpress.Utils.ImageCollection变量的image导出

    private void tspBtnExportExcel_Click(object sender, EventArgs e) { //暂时用来导出图片 string filePath = Syst ...

  2. POJ 2965 The Pilots Brothers' refrigerator (枚举+BFS+位压缩运算)

    http://poj.org/problem?id=2965 题意: 一个4*4的矩形,有'+'和'-'两种符号,每次可以转换一个坐标的符号,同时该列和该行上的其他符号也要随之改变.最少需要几次才能全 ...

  3. Docker operation

    Docker 容器镜像删除 1.停止所有的container,这样才能够删除其中的images: docker stop $(docker ps -a -q) 如果想要删除所有container的话再 ...

  4. MongoDB(课时15 数据排序)

    3.4.2.10 数据排序 在MongoDB里数据排序操作使用“sort()”函数,在进行排序的时候可以有两个顺序:升序(1),降序(-1). 范例:排序 db.students.find().sor ...

  5. mysql 超大数据/表管理技巧

    如果你对长篇大论没有兴趣,也可以直接看看结果,或许你对结果感兴趣.在实际应用中经过存储.优化可以做到在超过9千万数据中的查询响应速度控制在1到20毫秒.看上去是个不错的成绩,不过优化这条路没有终点,当 ...

  6. RabbitMQ入门_04_Exchange & Binding

    如果你比较细心,你会发现 HelloWorld 例子中的 Sender 只申明了一个 hello 队列,然后就开始向默认 Exchange 发送路由键为 hello 的消息.按照之前 AMQP 基本概 ...

  7. Python -- Scrapy 框架简单介绍(Scrapy 安装及项目创建)

    Python -- Scrapy 框架简单介绍 最近在学习python 爬虫,先后了解学习urllib.urllib2.requests等,后来发现爬虫也有很多框架,而推荐学习最多就是Scrapy框架 ...

  8. WPF中的Style(风格,样式)

    作者: 周银辉  来源: 博客园  发布时间: 2009-02-27 15:04  阅读: 6698 次  推荐: 0   原文链接   [收藏]   在WPF中我们可以使用Style来设置控件的某些 ...

  9. fzu1901 kmp

    For each prefix with length P of a given string S,if S[i]=S[i+P] for i in [0..SIZE(S)-p-1], then the ...

  10. 关于InputStream类的available()方法

    要一次读取多个字节时,经常用到InputStream.available()方法,这个方法可以在读写操作前先得知数据流里有多少个字节可以读取.需要注意的是,如果这个方法用在从本地文件读取数据时,一般不 ...