多线程编程精髓(一)

(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_t tid;

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

                   2 .#include 

                       pthread_t tid = pthread_self();

                    3.#include

                       #include

                       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模板类型  templatestruct atomic,支持传入具体的整型类型(如 bool、char、short、int、uint 等)对模板进行实例化,比如 std::atomic 

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



                     

你可能感兴趣的:(多线程编程精髓(一))