C++多线程编程

一个进程指的是一个正在执行的应用程序,线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。

每个进程执行前,操作系统都会为其分配所需的资源,包括要执行的程序代码、数据、内存空间、文件资源等。一个进程至少包含 1 个线程,可以包含多个线程,所有线程共享进程的资源,各个线程也可以拥有属于自己的私有资源。

进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。


图 1 进程和线程的关系

如图所示,所有线程共享的进程资源有:

  • 代码:即应用程序的代码;
  • 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
  • 进程空间:操作系统分配给进程的内存空间;
  • 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。

各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等

所谓多线程,即一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序

进程:进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。

线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。
多线程:多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这多个步骤同时执行。
C++多线程:(简单情况下)C++多线程使用多个函数实现各自功能,然后将不同函数生成不同线程,并同时执行这些线程(不同线程可能存在一定程度的执行先后顺序,但总体上可以看做同时执行)。

进程与线程的区别

1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;

4. 调度和切换:线程上下文切换比进程上下文切换要快得多

/*
创建线程
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
各个参数的含义是:
1) pthread_t *thread:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。
2) const pthread_attr_t *attr:用于手动设置新建线程的属性,例如线程的调用策略、线程所能使用的栈内存的大小等。大部分场景中,我们都不需要手动修改线程的属性,将 attr 参数赋值为 NULL,pthread_create() 函数会采用系统默认的属性值创建线程。
3) void *(*start_routine) (void *):以函数指针的方式指明新建线程需要执行的函数,该函数的参数最多有 1 个(可以省略不写),形参和返回值的类型都必须为 void* 类型。void* 类型又称空指针类型,表明指针所指数据的类型是未知的。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。
如果该函数有返回值,则线程执行完函数后,函数的返回值可以由 pthread_join() 函数接收。
4) void *arg:指定传递给 start_routine 函数的实参,当不需要传递任何数据时,将 arg 赋值为 NULL 即可。
5) 如果成功创建线程,pthread_create() 函数返回数字 0,反之返回非零值。各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:
EAGAIN:系统资源不足,无法提供创建线程所需的资源。
EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
以上这些宏都声明在 <errno.h> 头文件中,如果程序中想使用这些宏,需提前引入此头文件。
*/
/*
线程阻塞方法join()与detach(),
阻塞线程的目的是调节各线程的先后执行顺序,这里重点讲join()方法,不推荐使用detach(),detach()使用不当会发生引用对象失效的错误。当线程启动后,一定要在和线程相关联的thread对象销毁前,对线程运用join()或者detach()
join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续。th1.join(),即该语句所在的线程(该语句写在main()函数里面,即主线程内部)暂停,等待指定线程(指定线程为th1)执行结束后,主线程再继续执行
整个过程就相当于你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。
等待线程执行结束,获取某个线程执行结束时返回的数据
pthread_join() 函数声明在<pthread.h>头文件中,语法格式如下:
int pthread_join(pthread_t thread, void ** retval);
thread 参数用于指定接收哪个线程的返回值;retval 参数表示接收到的返回值,如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL
如果 pthread_join() 函数成功等到了目标线程执行结束(成功获取到目标线程的返回值),返回值为数字 0;反之如果执行失败,函数会根据失败原因返回相应的非零值,每个非零值都对应着不同的宏,例如:
EDEADLK:检测到线程发生了死锁。关于线程发生死锁,我们会在《Linux如何避免线程发生死锁?》一节中做详细讲解。
EINVAL:分为两种情况,要么目标线程本身不允许其它线程获取它的返回值,要么事先就已经有线程调用 pthread_join() 函数获取到了目标线程的返回值。
ESRCH:找不到指定的 thread 线程。
以上这些宏都声明在 <errno.h> 头文件中,如果程序中想使用这些宏,需提前引入此头文件。
*/
C语言实现
#include <stdio.h>
#include <iostream.h>
#include <assert.h>
#include <pthread.h>
//!bug1 不允许返回一个局部变量,static修饰后局部变量的生命周期持续整个程序运行阶段
/*void * Thread1(void *arg){
  printf("http://c.biancheng.net\n");
  char str[] = "Thread1成功执行";
  return str;
  }*/
//!bug2 不允许,"Thread1成功执行"是const char[20],invalid conversion from ‘const void*’ to ‘void*
/*void * Thread1(void *arg){
  printf("http://c.biancheng.net\n");
  return "Thread1成功执行";
  }*/
 //!bug3 testFunc.cpp:(.text+0x72):对‘pthread_create’未定义的引用
 /*由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,在编译中要加-lpthread参数:
gcc testFunc.cpp -o thread.exe -lpthread(常用这种,-l添加静态库)  或者 gcc -o thread.exe -pthread testFunc.cpp*/
//!bug4 Assertion `res != 0' failed.
/*res == 0时说明线程成功创建,assert(expection),expection为假时会输出错误*/
void * Thread1(void *arg){
  printf("http://c.biancheng.net\n");
  static char str[] = "Thread1成功执行";
  return str;
}
void * Thread2(void *arg){
  printf("C语言中文网\n");
  static char str[] = "Thread2成功执行";
  return str;
}
int main()
{
  cout << "我是主线程"<<endl;
  int res;
  pthread_t mythread1,mythread2;
  void* thread_result;
 res = pthread_create(&mythread1, NULL, Thread1, NULL);
 assert (res == 0) ;
 res = pthread_create(&mythread2, NULL, Thread2, NULL);
assert(res == 0);
res = pthread_join(mythread1, &thread_result);
    //输出线程执行完毕后返回的数据
    printf("%s\n", (char*)thread_result);
    res = pthread_join(mythread2, &thread_result);
    printf("%s\n", (char*)thread_result);
    printf("主线程执行完毕");
    return 0;
}

C++ 实现

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
/*编译命令g++ -o thread.exe testFunc.cpp  -lpthread */
void proc(int a)
{
  cout << "我是子线程,传入参数为" << a << endl;
  //* 命名空间std::this_thread提供了一组关于当前线程的函数
  cout << "子线程中显示子线程id为" << this_thread::get_id() << endl;
}
int main()
{
  cout << "我是主线程" << endl;
  int a = 9;
    thread th2(proc, a); //创建线程的同时初始化线程,并开始执行
    //第一个参数为函数名,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。
    cout << "主线程中显示子线程id为" << th2.get_id() << endl;
    th2.join() ; //此时主线程main被阻塞直至子线程th2执行结束。
  return 0;
}

互斥锁:C++具有多线程并发特性,即多个线程同时执行,一个进程可以有多个线程,线程之间可以共享数据,当多个线程同时对共享资源进行操作时,会导致数据混乱

为避免共享资源混乱,C++提出互斥锁的概念:每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

通过“锁”就将资源的访问变成互斥操作,因为同一时刻,只能有一个线程持有该锁。

mutex一般用于为一段代码加锁,以保证这段代码的原子(atomic)操作(指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断),

可能被多个线程修改的数据,我们一般用互斥量(mutex)来保护。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a){
  m.lock();
  cout << "proc1函数正在改写a" << endl;
  cout << "原始a为" << a << endl;
  cout << "现在a为" << a + 2 << endl;
 m.unlock();
}
void proc2(int a){
  m.lock();
  cout << "proc2函数正在改写a" << endl;
  cout << "原始a为" << a << endl;
  cout << "现在a为" << a + 1 << endl;
  m.unlock();
}
int main(){
  int a = 0;
  /* 在教程中把线程名与函数名重名了,导致编译不过,在编程中一定注意不要重名
      thread proc1(proc1, a);
    thread proc2(proc2, a);
    */
  thread th1(proc1,a);
  thread th2(proc2,a);
  th1.join();
  th2.join();
  return 0;
}/*编译命令g++ -o thread.exe testFunc.cpp  -lpthread */

我们期望的结果是

proc1函数正在改写a
原始a为0
现在a为2
proc2函数正在改写a
原始a为0
现在a为1

而如果不加入lock和unlock,两个线程可能会对a同时操作,最后出来的结果每次都不一样,如下:

hx@hx-Precision-3551:~/桌面/Test/src$ ./thread.exe
proc1函数正在改写aproc2函数正在改写a
原始a为0
现在a为2

原始a为0
现在a为1
hx@hx-Precision-3551:~/桌面/Test/src$ ./thread.exe
proc1函数正在改写a
原始a为0
proc2函数正在改写a
现在a为原始a为0
现在a为1
2

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)
project (demo)
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11")
#设置可执行文件的输出目录,EXECUTABLE_OUT_PATH和PROJECT_SOURCE_DIR是CMake自带的预定义变量,其意义如下,
#EXECUTABLE_OUTPUT_PATH :目标二进制可执行文件的存放位置 PROJECT_SOURCE_DIR:工程的根目录
#所以,这里set的意思是把存放可执行elf文件的位置设置为工程根目录下的bin目录。
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
set (SRC_LIST ${PROJECT_SOURCE_DIR}/src/main.cpp)
add_executable(main ${SRC_LIST})
#找到库
find_package(Threads REQUIRED)
#把库文件与通过add_executable生成的目标文件连接
target_link_libraries(main ${CMAKE_THREAD_LIBS_INIT})

 

 

posted @ 2022-01-28 10:29  帝企鹅日记  阅读(605)  评论(0)    收藏  举报