背景
多线程概述:应用层的多线程的目的就是让每一个任务(例如一系列函数调用)都认为自己独占CPU资源,即宏观上,多个任务可以同时执行(实际可能是轮转的串行执行)。
代码实现:线程库可以由编程语言的标准库或者操作系统的库实现,具体包含的头文件如下:
- C/C++ : < thread >
- POSIX(Portable Operating System Interface of UNIX, Linux环境使用较多) :< pthread.h >
- Windows OS : < windows.h >
具体环境使用哪个库,有不同的观点,参考
c++多线程编程主要用pthread还是c++11中的thread类?
即使是同一环境,也有不同封装层次的API
CreateThread()与_beginthread()的区别详细解析
主线程与工作线程:
一般应用程序都有主要的执行流程,例如C/C++的main入口函数,主要执行流程是在进程中执行的,也可以认为main是线程,独占了进程的全部资源,称为主线程。如果在该进程执行时,创建多个线程,用于并行处理其他任务,称为工作线程。
本文讲不同风格的线程创建\销毁,和访问共享数据的锁操作
本系列源码:cursorhu/SimpleMultiThread
Windows风格多线程
(1)双线程打印
#include <iostream>
#include <windows.h>
using namespace std;
DWORD WINAPI Print(LPVOID lpParamter)
{
std::string s = (char*)lpParamter;
for (int i = 0; i < 10; i++)
cout << s << endl;
return 0;
}
int main()
{
std::string s1 = "Work thread";
std::string s2 = "Main thread";
HANDLE hThread = CreateThread(NULL, 0, Print, (LPVOID)s1.c_str(), 0, NULL);
Print((LPVOID)s2.c_str());
CloseHandle(hThread);
return 0;
}
主线程和工作线程都运行Print(),各线程的栈空间保存自己的局部数据。
windows API使用CreateThread和CloseHandle创建线程、释放线程句柄,说明如下
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD:线程安全相关的属性,常置为NULL
SIZE_T dwStackSize,//initialstacksize:新线程的初始化栈的大小,可设置为0
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction:被线程执行的回调函数,也称为线程函数
LPVOID lpParameter,//threadargument:传入线程函数的参数,不需传递参数时为NULL
DWORD dwCreationFlags,//creationoption:控制线程创建的标志
LPDWORD lpThreadId//threadidentifier:传出参数,用于获得线程ID,如果为NULL则不返回线程ID
)
/*
lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。
dwStackSize:设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。
任何情况下,Windows根据需要动态延长堆栈的大小。
lpStartAddress:指向线程函数的指针,函数名称没有限制,但是必须以下列形式声明:
DWORD WINAPI 函数名 (LPVOID lpParam) ,格式不正确将无法调用成功。
lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
dwCreationFlags:控制线程创建的标志,可取值如下:
(1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程(就绪状态),直到线程被唤醒时才调用
(2)0:表示创建后立即激活。
(3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize参数指定初始的保留堆栈的大小,
如果STACK_SIZE_PARAM_IS_A_RESERVATION标志未指定,dwStackSize将会设为系统预留的值
lpThreadId:保存新线程的id
返回值:函数成功,返回线程句柄,否则返回NULL。如果线程创建失败,可通过GetLastError函数获得错误信息。
*/
BOOL WINAPI CloseHandle(HANDLE hObject); //关闭一个被打开的对象句柄
/*可用这个函数关闭创建的线程句柄,如果函数执行成功则返回true(非0),如果失败则返回false(0),
如果执行失败可调用GetLastError.函数获得错误信息。
*/
LPVOID 与 std::string类型的转换,需要用char*类型作中介,LPVOID接受buffer数组类型的转换
注意CloseHandle只是释放句柄资源,线程的资源释放是其函数执行完毕自动销毁的。
2次的运行结果
可见,两个线程是随机切换的,导致如下现象:
- Print()内的
cout << s
和<<endl
之间线程被切换,导致没有换行+双重换行。 - 存在工作线程没执行完,主线程就执行完导致main return,整个进程销毁的情况。
改进如下:
#include <iostream>
#include <windows.h>
using namespace std;
DWORD WINAPI Print(LPVOID lpParamter)
{
std::string s = (char*)lpParamter;
for (int i = 0; i < 10; i++)
cout << s;
return 0;
}
int main()
{
std::string s1 = "Work thread\n";
std::string s2 = "Main thread\n";
HANDLE hThread = CreateThread(NULL, 0, Print, (LPVOID)s1.c_str(), 0, NULL);
Print((LPVOID)s2.c_str());
CloseHandle(hThread);
Sleep(100);
return 0;
}
使用以下方法解决上述问题
主线程完成Print后,休眠100s,这个时间足够工作线程完成,Sleep结束后,main进程执行完毕
把换行放到字符串中,使该字符串的完整打印成为不可被中途切换的操作,即原子操作
输出如下:
如果Print有很多句打印,又不希望中途切换线程,如何做?
- 互斥锁可以实现“大块代码的原子操作”
- 锁是全局变量,因为主线程main和工作线程Print都能看到全局变量,而看不到对方的局部变量
代码如下:
#include <iostream>
#include <windows.h>
using namespace std;
HANDLE hMutex = NULL;//互斥锁的句柄
DWORD WINAPI Print(LPVOID lpParamter)
{
std::string s = (char*)lpParamter;
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(hMutex, INFINITE);//请求锁
cout << s << endl;
ReleaseMutex(hMutex);//释放锁
}
return 0;
}
int main()
{
std::string s1 = "Work thread";
std::string s2 = "Main thread";
hMutex = CreateMutex(NULL, FALSE, NULL); //创建互斥锁
HANDLE hThread = CreateThread(NULL, 0, Print, (LPVOID)s1.c_str(), 0, NULL);
Print((LPVOID)s2.c_str());
CloseHandle(hThread);
CloseHandle(hMutex);//销毁互斥锁
return 0;
}
运行结果:
关于windows的互斥锁:
互斥量:
采用互斥对象机制。互斥锁,像一个物件,这个物件只能同时被一个线程持有。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。
一、创建 创建互斥锁的方法是调用函数CreateMutex: CreateMutex(&sa, bInitialOwner, szName);第一个参数是一个指向SECURITY_ATTRIBUTES结构体的指针,一般的情况下,可以是nullptr。 第二个参数类型为BOOL,表示互斥锁创建出来后是否被当前线程持有。 第三个参数类型为字符串(const TCHAR*),是这个互斥锁的名字,如果是nullptr,则互斥锁是匿名的。 例: HANDLE hMutex = CreateMutex(nullptr, FALSE, nullptr);上面的代码创建了一个匿名的互斥锁,创建出来后,当前线程不持有这个互斥锁。
二、持有 WaitForSingleObject函数可以让一个线程持有互斥锁。用法: WaitForSingleObject(hMutex, dwTimeout);这个函数的作用比较多。这里只介绍第一个参数为互斥锁句柄时的作用。 它的作用是等待,直到一定时间之后,或者,其他线程均不持有hMutex。第二个参数是等待的时间(单位:毫秒),如果该参数为INFINITE,则该函数会一直等待下去。
三、释放 用ReleaseMutex函数可以让当前线程“放开”一个互斥锁(不持有它了),以便让其他线程可以持有它。用法 ReleaseMutex(hMutex)
四、销毁 当程序不再需要互斥锁时,要销毁它。 CloseHandle(hMutex)
五、命名互斥锁 如果CreateMutex函数的第三个参数传入一个字符串,那么所创建的锁就是命名的。当一个命名的锁被创建出来以后,当前进程和其他进程如果试图创建相同名字的锁,CreateMutex会返回原来那把锁的句柄,并且GetLastError函数会返回ERROR_ALREADY_EXISTS。这个特点可以使一个程序在同一时刻最多运行一个实例
C++风格多线程
双线程分别实现计算和打印
#include <thread>
#include <chrono>
#include <mutex>
#include <iostream>
int g_mydata = 1;
std::mutex g_mutex;
void thread_func1()
{
while (g_mydata < INT_MAX)
{
g_mutex.lock();
++g_mydata;
g_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void thread_func2()
{
while (g_mydata < INT_MAX)
{
g_mutex.lock();
std::cout << "g_mydata = " << g_mydata << ", ThreadID = " << std::this_thread::get_id() << std::endl;
g_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(thread_func1);
std::thread t2(thread_func2);
t1.join();
t2.join();
return 0;
}
几点说明:
- C++使用< thread >调用线程库
- std::thread t(thread_func)创建一个thread对象,传入参数为thread_fun,即线程内执行的函数
- t.join()的含义是,线程t执行完毕后,join函数才能返回,主线程才能继续向后执行,宏观上就是,主线程被t线程阻塞在join函数处,这也许就是join的含义,t线程“加入”主线程的队伍,主线程必须原地等待t准备好了(执行完了)才能继续向后走。
- 由于全局数据g_mydata和打印语句都不是原子操作,要保证完整操作,需要加锁,库定义在< mutex >
- 为什么要sleep? 注意两个工作线程都while循环操作,sleep是手动使当前线程休眠,操作系统会轮换到其他active状态的线程执行,如果不sleep, 一个线程一直执行再被OS切换,间隔可能很久。< chrono >库用于时间
- INT_MAX是C++定义的int类最大值,2^31-1
运行结果:
POSIX/Linux风格
逻辑同上节,代码如下
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <limits.h> //for INT_MAX
int g_mydata = 1;
pthread_mutex_t m;
void* thread_function1(void* args)
{
while (g_mydata < INT_MAX)
{
pthread_mutex_lock(&m);
++g_mydata;
pthread_mutex_unlock(&m);
sleep(1);
}
return NULL;
}
void* thread_function2(void* args)
{
while (g_mydata < INT_MAX)
{
pthread_mutex_lock(&m);
std::cout << "g_mydata = " << g_mydata << ", ThreadID: " << pthread_self() << std::endl;
pthread_mutex_unlock(&m);
sleep(1);
}
return NULL;
}
int main()
{
pthread_mutex_init(&m, NULL);
pthread_t threadIDs[2];
pthread_create(&threadIDs[0], NULL, thread_function1, NULL);
pthread_create(&threadIDs[1], NULL, thread_function2, NULL);
for(int i = 0; i < 2; ++i)
{
pthread_join(threadIDs[i], NULL);
}
pthread_mutex_destroy(&m);
return 0;
}
win32应用程序使用pthread,需要配置pthread dll库,下载地址和配置方法:
pthreads-win32
VS2013 配置pthread
pthread的几个锁,参考:
linux线程互斥量pthread_mutex_t使用简介