多线程编程精髓(一)

(1)线程的基本概念和常见问题: 每个进程都有自己的独立进程地址空间和上下文堆栈,进程中实际执行单位为线程,每个进程至少有一个线程-主线程,线程是由操作系统安排调度的最小运行单元,进程中的线程可分为主线程和工作线程,实际使用中避免主线程退出,线程是独立运行的最小单元,拥有独立的上下文堆栈,正常来说一个线程奔溃不会影响其他线程,但会导致进程退出,从而其他线程也无法运行。

(2)线程的创建和使用:不同操作系统使用不同的API函数,

           linux创建线程 :int  pthread_create(pthread_t *thread,const pthread_attr_t * attr,void* (*start_routine) (void*),void* arg); //thread 线程ID,attr 线程属性,设置为NULL,start_routine 线程调用函数,arg 线程传入参数;,返回值为0 表示创建成功;

            windows创建线程:HANDLECreateThread( LPSECURITY_ATTRIBUTES  lpThreadAttributes,  SIZE_T  dwStackSize,  LPTHREAD_START_ROUTINE  lpStartAddress, LPVOID   lpParameter,DWORD   dwCreationFlags,  LPDWORD    lpThreadId);  // lpThreadAttributes 线程安全属性 设置NULL, dwStackSize 线程栈空间,设置0表示默认大小,lpStartAddress 线程函数指针,dwCreationFlags 32位无符号整形,设置为0表示线程创建后立即启动,pThreadId 线程创建成功返回的ID,返回为内核句柄的索引值,为NULL表示失败;

           注意:linux的线程函数调用方式是__cedel,这是C/C++ 中定义函数时默认的调用方式,而windows上调用方式createThread 定义线程函数时必须使用 __stdcall 调用方式,必须显示声明函数,如 DWORD __stdcall threadfunc(LPVOID lpThreadParameter) ,Windows 上的宏 WINAPICALLBACK 这两个宏的定义都是 __stdcall,因此也可以写成:

            //写法1  DWORD WINAPI threadfunc(LPVOID lpThreadParameter);

           //写法2  DWORD CALLBACK threadfunc(LPVOID lpThreadParameter);

            windows C函数库(对比上述,推荐使用):uintptr_t  _beginthreadex(  void *security,   unsigned stack_size,  unsigned ( __stdcall *start_address )( void * ),   void *arglist, unsigned initflag,   unsigned *thrdaddr );  //函数签名与上述一样;

             C11提供std::thread: 创建线程t1 std::threadt1(threadproc1);,创建线程t2    std::threadt2(threadproc2,1,2);  //对比上述的几种方式的固定格式函数,此方式便捷,函数签名更加多样化,但必须保证线程对象在线程运行期间有效不被销毁。

(3)排查linux进程占用CPU过高的方法:使用ps命令查看进程的ID,top -H 命令也可以显示每个进程的各个线程的运行状态。然后使用pstack命令查看进程中每个线程调用的堆栈,注意pstack是gdb调试器支持的,命令查看程序必须携带调试权限,而且用户必须有查看权限,对于pstack输出的各个线程中,对照源码逻辑排查进行修改优化,提高CPU资源的利用率。

(4)线程ID的用途以及原理:线程ID是用于标识一个线程的整形数值,熟练获取能帮助日志写入和后续问题排查。

           windows环境下可以在创建线程是直接获得,或者使用以下函数:

                      DWORD GetCurrentThreadId(); 

                     //注意 pthread_tDWORD 类型都是一个 32 位无符号整型值

           linux环境下有三种方式,在创建线程时获得、在需要获取 ID 的线程中调用 pthread_self() 函数获取、 通过系统调用获取线程 ID:

                    1.  #include<pthread.h>

                       pthread_t tid;

                       pthread_create(&tid, NULL, thread_proc, NULL);

                   2 .#include<pthread.h> 

                       pthread_t tid = pthread_self();

                    3.#include<sys/syscall.h>

                       #include<unistd.h>

                       int tid = syscall(SYS_gettid);

                      //注意方法一方法二获取的线程 ID 结果是一样的(转成16进制与pstack命令查询的线程ID一致),都是一个很大的数字,表示内存地址,全局并不唯一, 而方法三获取的线程 ID 是系统范围内全局唯一的,一般是一个不会太大的整数,这个数字也是就是所谓的 LWP (Light Weight Process,轻量级进程),早期的 Linux 系统的线程是通过进程来实现的,这种线程被称为轻量级线程)的 ID。

               C++11获取线程ID用两种方式: std::this_thread 类的 get_id 获取当前线程的 id(类静态方法)、std::threadget_id 获取指定线程的 ID(类实例化方法):

                         1. std::thread  t(worker_thread_func);

                             std::thread::id worker_thread_id = t.get_id();    //获取线程t的ID    

                         2.  std::thread::id main_thread_id = std::this_thread::get_id();  //获取主线程的线程 ID

                              std::ostringstream oss;   //先将std::thread::id转换成std::ostringstream对象   

                               oss << main_thread_id;

                               std::string str = oss.str();  //再将std::ostringstream对象转换成std::string 

                               int threadid = atol(str.c_str());//最后将 std::string 转换成整型值   

                                std::cout << "main thread id: " << threadid << std::endl;

(5)如何等待线程结束:一个线程需要等待另外一个线程执行完毕退出后再执行,windows和linux都有提供对应的操作系统API:

                 linux环境下:使用 pthread_join 函数,用来等待某线程的退出并接收它的返回值。这种操作被称为连接(joining),

                            int  pthread_join(pthread_tthread,void** retval);

                           //参数thread,需要等待的线程 id,参数retval,输出参数,用于接收等待退出的线程的退出码(Exit Code),线程退出码可以通过调用pthread_exit退出线程时指定,也可以在线程函数中通过return语句返回,可设置为NULL,此时线程会被挂起直到目前线程退出再被唤醒。

               windows环境下:使用 API WaitForSingleObjectWaitForMultipleObjects 函数,前者为等待一个线程结束,后者为等待多个线程结束,可选择控制等待时间,

                           DWORD  WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

                          //参数hHandle是需要等待的对象的句柄,等待线程退出,传入线程句柄。参数dwMilliseconds是需要等待的毫秒数,如果使用INFINITE宏,则表示无限等待下去,此时会挂起当前等待线程,直到等待的线程退出后,等待的线程才会被唤醒。

               C++11提供函数(windows和linux均使用):std::threadjoin 方法,为了防止等待线程已经退出出现奔溃,调用之前需判断是否可join,调用joinable 方法,

                           std::thread  t(FileThreadFunc);

                              if (t.joinable())

                                      t.join();

(6)线程函数传递C++类实例指针惯用法:除了C++11线程库提供的std::thread对线程函数签名没有特殊的要求之外,其余windows和linux对于函数签名都必须指定格式,此时线程函数

必须是静态方法,原因是C++编译器将函数翻译成全局函数时会将类的实例对象地址(也就是 this 指针)作为第一个参数传递给该方法,比如void *threadFunc(void* arg) 翻译之后就变成

了void* threadFunc(Thread*this,void* arg); 这样就不符合函数签名的要求了,如果是类的静态方法就没有办法访问类的实例方法了,因此实际开发中创建线程时将当前对象的地址(this

 指针)传递给线程函数,然后在线程函数中,将该指针转换成原来的类实例,再通过这个实例就可以访问类的所有方法了,实际上C++11的std::thread创建时要求传入this对象也是一样

的道理。

(7)整型变量的原子操作:多线程同时操作某个资源,以整型变量为例,同时对资源进行读和写,需要采用措施保护资源,避免冲突。举例说明:

        把一个变量的值赋值给另外一个变量,或者把一个表达式的值赋值给另外一个变量,如int a = b;从 C/C++ 语法的级别来看,这也是一条语句,是原子的;但是从实际执行的二进制

指令来看,由于现代计算机 CPU 架构体系的限制,数据不可以直接从内存搬运到另外一块内存,必须借助寄存器中断,这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄

存器如eax)中,再从该寄存器搬运到变量a的内存地址:move ax,dword ptr [b]  mov dword ptr [a], eax既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指

令执行完毕后被剥夺 CPU 时间片,切换到另外一个线程而产生不确定的情况。

          windows下提供整型原子操作API(Windows.h头文件),仅列出了与 32 位(bit)整型相关的 API 函数,Windows 还提供了对 8 位、16 位以及 64 位的整型变量进行原子操作的 API:

          函数名                                        函数说明

          InterlockedIncrement                 将 32 位整型变量自增 1

         InterlockedDecrement                 将 32 位整型变量自减 1

         InterlockedExchangeAdd           将 32 位整型值增加 n (n 可以是负值)

         InterlockedXor                             将 32 位整型值与 n 进行异或操作

         InterlockedCompareExchange    将 32 位整型值与 n1 进行比较,如果相等,则替换成 n2


          C++11提供的std::atomic模板类型  template<class T  >struct atomic,支持传入具体的整型类型(如 bool、char、short、int、uint 等)对模板进行实例化,比如 std::atomic<int> 

value;   value = 99; 还有大量方法可支持查询使用,注意在linux和windows环境不同用法的语法区别。



                     

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容