概念
进程:正在运行的程序,不仅指处于执行期的程序本身,还包括它所管理的资源,比如由它打开的窗口,地址的资源,进程状态等等
线程:CPU调度和分派的基本单位
进程好比工厂的车间,它代表CPU所能处理的单个任务,工厂给车间资源、线程,空间
线程好比车间里的工人
为什么使用多线程
1.如果只有一个车间,但是要完成多个任务,只要第一个任务完不成,其他任务都会被阻塞,所以要同时开展多个车间完成多个任务
2.CPU速度>内存速度>磁盘IO速度,当CPU执行完后,可能内存、磁盘还没有执行完,CPU这时处于空转状态,如果它没有去处理新的请求,就会导致程序性能很差劲
3.一个进程有4G的虚拟内存,多线程可以共享这个内存空间
函数详解
createthread
HANDLE CreateThread (
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId )
第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入
NULL 表示使用默认设置。
知识扩展*:attribute基本都表示内核对象
第二个参数 dwStackSize 表示线程栈空间大小。传入 0 表示使用默认大小(1MB)。
第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程
可以使用同一个函数地址。
第四个参数 lpParameter 是传给线程函数的参数。
第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为 0 表示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED 则表示线程创建后暂停运行,这样它就无法调度,直到调用 ResumeThread()。
第六个参数 lpThreadId 将返回线程的 ID 号,传入 NULL 表示不需要返回该线程 ID 号
beginthreadex
基本参数和createthread差不多,他是createthread的封装后的版本
代码实战
调用多线程函数要包含头文件 #include <process.h>
int arg1 = 100; int arg2 = 200; int arg3 = 300;
unsigned int one, two, three;
/包含头文件proces.h
/查看定义,第三个参数为指针类型
/第四个参数为函数参数,类型必须为void*
_beginthreadex(NULL, 0, &work1, (void*)&arg1, 0, &one);
_beginthreadex(NULL, 0, &work2, (void*)&arg2, 0, &two);
第三个参数必须为函数名的指针,而且这个函数的返回值必须是unsigned WINAPI ,WINAPI 也可以写成__stdcall,最后一个参数类型必须为unsigned*
unsigned WINAPI work1(void* arg1)
{
int count = *((int*)arg1);
for (int i=0;i<count;i++)
{
std::cout << "1 is work"<<std::endl;
Sleep(1000);
}
return 0;
}
unsigned WINAPI work2(void* arg2)
{
int count = *((int*)arg2);
for (int i = 0; i < count; i++)
{
std::cout << "2 is work" << std::endl;
Sleep(2000);
}
return 0;
}
多线程执行时,线程执行顺序不分先后,随机
子线程的麻烦
当main结束后,子线程也会被终止,要么我们加上system pause,要么我们使用延时函数,不让main结束,那还有其他办法吗?这里遇到的情况就要分为两种
1.子线程为单线程 2。子线程为多线程
我们先介绍一下内核对象
内核对象
内核对象就是我们创建的一块内存,而且只能由操作系统内核访问,它包括线程、进程、文件等,它就是为了方便管理线程、进程等资源而由操作系统创建的一个数据块,其创建的所有者肯定时操作系统
调用创建内核对象的函数后,会返回一个句柄,句柄标识了所创建的对象
1.子线程为单线程
解决方法waitForSingleObject
waitForSingleObject (
_In_ HANDLE hHandle, /指明一个内核对象的句柄
_In_ DWORD dwMilliseconds /等待时间
);
作用:等到内核对象变为已通知状态,只有内核对象,即本例中的多线程结束后,才处于通知状态
printf ("begin\n");
if ((wr = WaitForSingleObject (hThread, INFINITE )) == WAIT_FAILED )
{
puts ("thread wait error");
return -1;
}
printf (" end\n");
这里一开始会打印begin,只有hThread线程结束后,才会打印end,即waitforsingleobject函数会阻塞在这里
2.子线程为多线程
如果子线程有多个线程,那该怎么处理呢?我们可以用WaitForMultipleObjects函数
函数原型
WaitForMultipleObjects (
_In_ DWORD nCount, / 要监测的句柄的组的句柄的个数
_In_reads_ (nCount) CONST HANDLE * lpHandles, /要监测的句柄的组
_In_ BOOL bWaitAll,
/ TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号
_In_ DWORD dwMilliseconds /等待时间
);
第一个参数为多线程的句柄个数,第二个表示句柄数组,第三个true表示所有线程都发出信号,才不阻塞,第四个参数I为NFINITE时,表示直到线程结束时间才会结束
,代码实战
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);
long long num = 0;
int main(int argc, char* argv[])
{
HANDLE tHandles[NUM_THREAD]; /句柄数组
int i;
printf("sizeof long long: %d \n", sizeof(long long));
for (i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0,
NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0,
NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void* arg)
{
int i;
for (i = 0; i < 500000; i++)
num += 1;
return 0;
}
unsigned WINAPI threadDes(void* arg)
{
int i;
for (i = 0; i < 500000; i++)
num -= 1;
return 0;
}
这里表示有50个线程,第0个线程为num-=1,第1个线程为num+=1,最后的结果是什么呢“?不是0,如果每个线程轮流加减,最后的结果为0,但是结果却是一些很大的数,要么是很大的负数,要么是很大的正数,为什么呢
因为计算是通过CPU来计算的,比如计算第0 个线程时,CPU从内存中取得num的值,计算后暂时放入CPU中,此时num的值为x,而CPU中的num值为x-500000,因为线程是随机的,这时有可能会执行线程1,这时内存中值为x,执行线程1后,值为x+500000,然后又执行线程2,因为之前CPU里已经有值了,所以这时CPU的值又为x-500000,,但是如果按照正常逻辑,第0次,x-50000,第1次x-500000+500000,当执行第线程2时,x为x-5,虽然结果还是一样,但是原理却不同,而且,线程是随机的,就会导致结果有很大的出入。
用图表示
这是内存中实际的线程切换,图1表示从内存拿到数据后,没来得及返回值,就直接切换到了线程B,这样也算是执行了线程A一次
这时按照我们正常逻辑推断的图,可以看到索然执行结果虽然一样,但是原理却不一样
互斥对象
为了避免上述,线程没来得及就转移的情况,我们可以使用互斥对象
互斥对象属于内核对象,它能够确保线程对单个资源的互斥访问权
互斥对象包含一个使用数量,一个线程 ID 和一个计数器。其中线程 ID 用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
HANDLE WINAPI CreateMutexW (
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, /指向安全属性
_In_ BOOL bInitialOwner, /初始化互斥对象的所有者 TRUE 立即拥有互斥体
_In_opt_ LPCWSTR lpName /指向互斥对象名的指针,可以自己取名
)
首先创建一个互斥对象,
hMutex=CreateMutex(NULL, FALSE, NULL);
TRUE:立即占有该互斥量
FALSE,互斥量处于触发/有信号/已通知状态,不为任何线程所占用
然后在线程A和线程B都里加上互斥锁
unsigned WINAPI threadInc(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<500000; i++)
num+=1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<500000; i++)
num-=1;
ReleaseMutex(hMutex);
return 0;
}
如果运行线程A,等待互斥量B ReleaseMutex,意思是直到线程B完全执行后,才能执行线程A