July, 2005

  • The Old New Thing

    Does Windows have a limit of 2000 threads per process?

    • 30 Comments

    Often I see people asking why they can't create more than around 2000 threads in a process. The reason is not that there is any particular limit inherent in Windows. Rather, the programmer failed to take into account the amount of address space each thread uses.

    A thread consists of some memory in kernel mode (kernel stacks and object management), some memory in user mode (the thread environment block, thread-local storage, that sort of thing), plus its stack. (Or stacks if you're on an Itanium system.)

    Usually, the limiting factor is the stack size.

    #include <stdio.h>
    #include <windows.h>
    
    DWORD CALLBACK ThreadProc(void*)
    {
     Sleep(INFINITE);
     return 0;
    }
    
    int __cdecl main(int argc, const char* argv[])
    {
    int i;
     for (i = 0; i < 100000; i++) {
      DWORD id;
      HANDLE h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &id);
      if (!h) break;
      CloseHandle(h);
     }
     printf("Created %d threads\n", i);
     return 0;
    }
    

    This program will typically print a value around 2000 for the number of threads.

    Why does it give up at around 2000?

    Because the default stack size assigned by the linker is 1MB, and 2000 stacks times 1MB per stack equals around 2GB, which is how much address space is available to user-mode programs.

    You can try to squeeze more threads into your process by reducing your stack size, which can be done either by tweaking linker options or manually overriding the stack size passed to the CreateThread functions as described in MSDN.

      HANDLE h = CreateThread(NULL, 4096, ThreadProc, NULL,
                   STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
    

    With this change, I was able to squeak in around 13000 threads. While that's certainly better than 2000, it's short of the naive expectation of 500,000 threads. (A thread is using 4KB of stack in 2GB address space.) But you're forgetting the other overhead. Address space allocation granularity is 64KB, so each thread's stack occupies 64KB of address space even if only 4KB of it is used. Plus of course you don't have free reign over all 2GB of the address space; there are system DLLs and other things occupying it.

    But the real question that is raised whenever somebody asks, "What's the maximum number of threads that a process can create?" is "Why are you creating so many threads that this even becomes an issue?"

    The "one thread per client" model is well-known not to scale beyond a dozen clients or so. If you're going to be handling more than that many clients simultaneously, you should move to a model where instead of dedicating a thread to a client, you instead allocate an object. (Someday I'll muse on the duality between threads and objects.) Windows provides I/O completion ports and a thread pool to help you convert from a thread-based model to a work-item-based model.

    Note that fibers do not help much here, because a fiber has a stack, and it is the address space required by the stack that is the limiting factor nearly all of the time.

  • The Old New Thing

    What is this "web site" thing you are talking about?

    • 29 Comments

    One reaction I've seen when people learn about all the compatibility work done in the Windows 95 kernel is to say,

    Why not add code to the installer wizard [alas, page is now 404] which checks to see if you're installing SimCity and, if so, informs you of a known design flaw, then asks you to visit Electronic Arts' webpage for a patch?

    Let's ignore the issue of the "installer wizard"; most people do not go through the Add and Remove Programs control panel to install programs, so any changes to that control panel wouldn't have helped anyway.

    But what about detecting that you're running SimCity and telling you to get a patch from Electronic Arts' web site?

    Remember, this was 1993. Almost nobody had web sites. The big thing was the "Information Superhighway". (Remember that? I don't think it ever got built; the Internet sort of stole its thunder.) If you told somebody, "Go to Electronic Arts' web site and download a patch", you'd get a blank stare. What's a "web site"? How do I access that from Prodigy? I don't have a modem. Can you mail me their web site?

    In Windows XP, when Windows detects that you're running a program with which it is fundamentally incompatible, you do get a pop-up window directing you to the company's web site. But that's because it's now 2005 and even hermits living in caves have email addresses.

    In 1993, things were a little different.

    (Heck, even by 1995 things most people did not have Internet access and those few that did used modems. Requiring users to obtain Internet access in order to set the computer clock via NTP would have been rather presumptuous.)

  • The Old New Thing

    When the normal window destruction messages are thrown for a loop

    • 17 Comments

    Last time, I alluded to weirdness that can result in the normal cycle of destruction messages being thrown out of kilter.

    Commenter Adrian noted that the WM_GETMINMAXINFO message arrives before WM_NCCREATE for top-level windows. This is indeed unfortunate but (mistake or not) it's been that way for over a decade and changing it now would introduce serious compatibility risk.

    But that's not the weirdness I had in mind.

    Some time ago I was helping to debug a problem with a program that was using the ListView control, and the problem was traced to the program subclassing the ListView control and, through a complicated chain of C++ objects, ending up attempting to destroy the ListView control while it was already in the process of being destroyed.

    Let's take our new scratch program and illustrate what happens in a more obvious manner.

    class RootWindow : public Window
    {
    public:
     RootWindow() : m_cRecurse(0) { }
     ...
    private:
     void CheckWindow(LPCTSTR pszMessage) {
      OutputDebugString(pszMessage);
      if (IsWindow(m_hwnd)) {
       OutputDebugString(TEXT(" - window still exists\r\n"));
      } else {
       OutputDebugString(TEXT(" - window no longer exists\r\n"));
      }
     }
    private:
     HWND m_hwndChild;
     UINT m_cRecurse;
     ...
    };
    
    LRESULT RootWindow::HandleMessage(
                              UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     ...
      case WM_NCDESTROY:
       CheckWindow(TEXT("WM_NCDESTROY received"));
       if (m_cRecurse < 2) {
        m_cRecurse++;
        CheckWindow(TEXT("WM_NCDESTROY recursing"));
        DestroyWindow(m_hwnd);
        CheckWindow(TEXT("WM_NCDESTROY recursion returned"));
       }
       PostQuitMessage(0);
       break;
    
      case WM_DESTROY:
       CheckWindow(TEXT("WM_DESTROY received"));
       if (m_cRecurse < 1) {
        m_cRecurse++;
        CheckWindow(TEXT("WM_DESTROY recursing"));
        DestroyWindow(m_hwnd);
        CheckWindow(TEXT("WM_DESTROY recursion returned"));
       }
       break;
      ...
    }
    

    We add some debug traces to make it easier to see what is going on. Run the program, then close it, and watch what happens.

    WM_DESTROY received - window still exists
    WM_DESTROY recursing - window still exists
    WM_DESTROY received - window still exists
    WM_NCDESTROY received - window still exists
    WM_NCDESTROY recursing - window still exists
    WM_DESTROY received - window still exists
    WM_NCDESTROY received - window still exists
    WM_NCDESTROY recursion returned - window no longer exists
    Access violation - code c0000005
    eax=00267160 ebx=00000000 ecx=00263f40 edx=7c90eb94 esi=00263f40 edi=00000000
    eip=0003008f esp=0006f72c ebp=0006f73c iopl=0         nv up ei ng nz na pe cy
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000283
    0003008f ??               ???
    

    Yikes! What happened?

    When you clicked the "X" button, this started the window destruction process. As is to be expected, the window received a WM_DESTROY message, but the program responds to this by attempting to destroy the window again. Notice that IsWindow reported that the window still exists at this point. This is true: The window does still exist, although it happens to be in the process of being destroyed. In the original scenario, the code that destroyed the window went something like

    if (IsWindow(hwndToDestroy)) {
     DestroyWindow(hwndToDestroy);
    }
    

    At any rate, the recursive call to DestroyWindow caused a new window destruction cycle to begin, nested inside the first one. This generates a new WM_DESTROY message, followed by a WM_NCDESTROY message. (Notice that this window has now received two WM_DESTROY messages!) Our bizarro code then makes yet another recursive call to DestroyWindow, which starts a third window destruction cycle. The window gets its third WM_DESTROY message, then its second WM_NCDESTROY message, at which point the second recursive call to DestroyWindow returns. At this point, the window no longer exists: DestroyWindow has destroyed the window.

    And that's why we crash. The base Window class handles the WM_NCDESTROY message by destroying the instance variables associated with the window. Therefore, when the innermost DestroyWindow returns, the instance variables have been thrown away. Execution then resumes with the base class's WM_NCDESTROY handler, which tries to access the instance variables and gets heap garbage, and then makes the even worse no-no of freeing memory that is already freed, thereby corrupting the heap. It is here that we crash, attempting to call the virtual destructor on an already-destructed object.

    I intentionally chose to use the new scratch program (which uses C++ objects) instead of the classic scratch program (which uses global variables) to highlight the fact that after the recursive DestroyWindow call, all the instance variables are gone and you are operating on freed memory.

    Moral of the story: Understand your window lifetimes and don't destroy a window that you know already to be in the process of destruction.

  • The Old New Thing

    What is the difference between WM_DESTROY and WM_NCDESTROY?

    • 16 Comments

    There are two window messages closely-associated with window destruction, the WM_DESTROY message and the WM_NCDESTROY message. What's the difference?

    The difference is that the WM_DESTROY message is sent at the start of the window destruction sequence, whereas the WM_NCDESTROY message is sent at the end. This is an important distinction when you have child windows. If you have a parent window with a child window, then the message traffic (in the absence of weirdness) will go like this:

    hwnd = parent, uMsg = WM_DESTROY
    hwnd = child, uMsg = WM_DESTROY
    hwnd = child, uMsg = WM_NCDESTROY
    hwnd = parent, uMsg = WM_NCDESTROY
    

    Notice that the parent receives the WM_DESTROY before the child windows are destroyed, and it receives the WM_NCDESTROY message after they have been destroyed.

    Having two destruction messages, one sent top-down and the other bottom-up, means that you can perform clean-up appropriate to a particular model when handling the corresponding message. If there is something that must be cleaned up top-down, then you can use the WM_DESTROY message, for example.

    The WM_NCDESTROY is the last message your window will receive (in the absence of weirdness), and it is therefore the best place to do "final cleanup". This is why our new scratch program waits until WM_NCDESTROY to destroy its instance variables.

    These two destruction messages are paired with the analogous WM_CREATE and WM_NCCREATE messages. Just as WM_NCDESTROY is the last message your window receives, the WM_NCCREATE message is the first message, so that's a good place to create your instance variables. Note also that if you cause the WM_NCCREATE message to return failure, then all you will get is WM_NCDESTROY; there will be no WM_DESTROY since you never got the corresponding WM_CREATE.

    What's this "absence of weirdness" I keep alluding to? We'll look at that next time.

    [Typos corrected, 9:30am]

  • The Old New Thing

    Dinner at the Herbfarm in Woodinville

    • 9 Comments

    As part of the going-away festivities for my friend, a group of us went to The Herbfarm, the local restaurant referenced in Clue I of Puzzle #3.

    The restaurant is nestled in the Sammamish Valley, right next to the Willows Lodge resort and its restaurant, The Barking Frog. Less than a kilometer down the road is the Chateau Ste. Michelle Winery. All the high-falutin' wine-snob destinations in one convenient location. (Right behind is the Redhook Brewery, if you tire of the whole urbane ambience and just want a beer and a sandwich.)

    Actually, the word "restaurant" doesn't do the Herbfarm justice. It's really a "total dining experience". The evening begins at 4pm with a tour of the herb garden. After heading inside, you are introduced to the evening's menu and the restaurant staff. (This restaurant has a staff mushroom forager!) Once the introductions are complete, the nine-course five-hour meal begins.

    For posterity, I record the menu below.

    A Fete for the Sun
    The Herbfarm • Sunday, July 24, 2005

    From the Water's Edge
    Paddlefish Caviar on Cucumber Gelée
    Westcott Bay Mussel Skewer
    Razor Clams with Sea Beans
    1999 Argyle Oregon Brut

    Spice-Crusted Pacific Albacore
    On a Salad of Beets, Wasabi, and Dill
    2003 Rulo Winery Viognier, Walla Walla

    Dungeness Crab, Chanterelle, & Lamb's Quarter Lasagna
    2004 Dr. Loosen/Chateau Ste. Michelle “Eroica” Riesling

    Troll-Caught Sockeye Salmon
    Slow Roasted in Squash Blossoms
    With Zucchini Strands and Sorrel-Lemon Thyme Sauce
    2004 Chinook Wines Rosé of Cabernet Franc

    Tarragon Ice & Apricot Float

    Three Tastes of Muscovy Duck
    Lavender-Crusted Breast on Onion Pudding
    With Black Currant Sauce
    Duck Confit with Just-Dug Potatoes
    Slow-Braised Leg on Morel Risotto
    2002 Cougar Crest Reserve Syrah, Stellar Vineyard

    An Herbfarm Garden Salad
    With Currants & Sally Jackson Guernsey Cow Cheese

    Sonata of Summer Desserts
    Anise Hyssop Panna Cotta with Raspberries
    Marionberry-Rose Geranium Ice Cream Cone
    Almond-Filled Donut Peach with Lemon Verbena Yogurt Ice Cream

    Brewed Coffees, Teas & Infusions

    A Selection of Small Treats
    Vintage 1916 Barbeito Malvazia Madeira

    Multigrain Rolls & Herbed Foccacia

    Between the duck and salad courses, we stepped outside to feed the pigs.

    The preliminary tour of the herb garden primed me to appreciate the extensive range of herbs employed in the various dishes. (We also learned herb trivia—daylilies are edible, as are zucchini and chive blossoms. I found this part most fascinating, learning about herbs and how the various elements were chosen for the dishes to come.) The garden salad, it seemed, didn't have two leaves from the same plant!

    If you're considering paying the Herbfarm a visit, be warned: You need to make reservations months in advance, and the evening will set you back a pretty penny.

    And we did pay the bill before we left.

  • The Old New Thing

    On paying for your meal upon leaving a restaurant

    • 14 Comments

    Robert Scoble's embarrassment over forgetting to pay a restaurant bill reminds me of an even more embarrassing incident experienced by a component team from the Windows 95 team.

    To celebrate something or other, their team went to lunch at The Salish Lodge, a fine dining establishment. At the end of the meal, everybody thought somebody else was going to handle the bill, and they all walked out as a group. The administrative assistant who made the reservation received a somewhat concerned telephone call from the restaurant when they discovered that a large party just skipped the check. Profuse apologies were extended and the bill was settled over the phone (with what I assume was a very generous tip). I just happened to be in the hallway when this whole thing happened and got to hear the story from the very exasperated administrative assistant shortly after it transpired.

    So remember, folks: Pay the bill before leaving the restaurant. It saves everybody a lot of grief.

  • The Old New Thing

    Marin Alsop to be music director of the Baltimore Symphony Orchestra

    • 16 Comments

    Marin Alsop has been selected to lead the Baltimore Symphony Orchestra. The development has gotten a lot of attention, presumably, because this makes Maestra Alsop (as she prefers to be called) the first woman to be named music director of a major U.S. orchestra. I saw her perform in Seattle earlier this year and thought she did a fine job. If I'd known she'd become a big newsmaker a few months later, I would have taken notes!

    The focus on her "breakthrough accomplishment" just strikes me as odd. I thought we as a society were beyond being concerned about the gender of the best person for the job. I remember somebody asking, "What it like being from the first state in the United States to have your governor and both senators all women?" It never even occurred to me that all three positions were held by women. It struck me as silly as the question, "What it like being from the first state in the United States to have your governor and both senators all have blonde hair?" What kind of answer were they looking for?

    (Curiously, if you ask Google Who is the governor of Washington?, it says the answer is Gary Locke. Don't use Google to cheat on your civics exam yet.)

  • The Old New Thing

    The importance of passing the WT_EXECUTELONGFUNCTION flag to QueueUserWorkItem

    • 17 Comments

    One of the flags to the QueueUserWorkItem function is WT_EXECUTELONGFUNCTION. The documentation for that flag reads

    The callback function can perform a long wait. This flag helps the system to decide if it should create a new thread.

    As noted in the documentation, the thread pool uses this flag to decide whether it should create a new thread or wait for an existing work item to finish. If all the current thread pool threads are busy running work items and there is another work item to dispatch, it will tend to wait for one of the existing work items to complete if they are "short", because the expectation is that some work item will finish quickly and its thread will become available to run a new work item. On the other hand, if the work items are marked WT_EXECUTELONGFUNCTION, then the thread pool knows that waiting for the running work item to complete is not going to be very productive, so it is more likely to create a new thread.

    If you fail to mark a long work item with the WT_EXECUTELONGFUNCTION flag, then the thread pool ends up waiting for that work item to complete, when it really should be kicking off a new thread. Eventually, the thread pool gets impatient and figures out that you lied to it, and it creates a new thread anyway. But it often takes a while before the thread pool realizes that it's been waiting in vain.

    Let's illustrate this with a simple console program.

    #include <windows.h>
    #include <stdio.h>
    
    DWORD g_dwLastTick;
    
    void CALLBACK Tick(void *, BOOLEAN)
    {
     DWORD dwTick = GetTickCount();
     printf("%5d\n", dwTick - g_dwLastTick);
    }
    
    DWORD CALLBACK Clog(void *)
    {
     Sleep(4000);
     return 0;
    }
    
    int __cdecl
    main(int argc, char* argv[])
    {
     g_dwLastTick = GetTickCount();
     switch (argc) {
     case 2: QueueUserWorkItem(Clog, NULL, 0); break;
     case 3: QueueUserWorkItem(Clog, NULL, WT_EXECUTELONGFUNCTION); break;
     }
     HANDLE hTimer;
     CreateTimerQueueTimer(&hTimer, NULL, Tick, NULL, 250, 250, 0);
     Sleep(INFINITE);
     return 0;
    }
    

    This program creates a periodic thread pool work item that fires every 250ms, and which merely prints how much time has elapsed since the timer was started. As a baseline, run the program with no parameters, and observe that the callbacks occur at roughly 250ms intervals, as expected.

      251
      501
      751
     1012
    ^C
    

    Next, run the program with a single command line parameter. This causes the "case 2" to be taken, where the "Clog" work item is queued. The "Clog" does what its names does: It clogs up the work item queue by taking a long time (four seconds) to complete. Notice that the first callback doesn't occur for a whole second.

     1001
     1011
     1021
     1021
     1252
     1502
     1752
    ^C
    

    That's because we queued the "Clog" work item without the WT_EXECUTELONGFUNCTION flag. In other words, we told the thread pool, "Oh, don't worry about this guy, he'll be finished soon." The thread pool wanted to run the Tick event, and since the Clog work item was marked as "fast", the thread pool decided to wait for it and recycle its thread rather than create a new one. After about a second, the thread pool got impatient and spun up a new thread to service the now-long-overdue Tick events.

    Notice that as soon as the first Tick event was processed, three more were fired in rapid succession. That's because the thread pool realized that it had fallen four events behind (thanks to the clog) and had to fire the next three immediately just to clear its backlog. The fifth and subsequent events fire roughly on time because the thread pool has figured out that the Clog really is a clog and should be treated as a long-running event.

    Finally, run the program with two command line parameters. This causes the "case 3" to be taken, where we queue up the Clog but also pass the WT_EXECUTELONGFUNCTION flag.

      251
      511
      761
     1012
    ^C
    

    Notice that with this hint, the thread pool no longer gets fooled by the Clog and knows to spin up a new thread to handle the Tick events.

    Moral of the story: If you're going to go wading into the thread pool, make sure you play friendly with other kids and let the thread pool know ahead of time whether you're going to take a long time. This allows the thread pool to keep the number of worker threads low (thus reaping the benefits of thread pooling) while still creating enough threads to keep the events flowing smoothly.

    Exercise: What are the consequences to the thread pool if you create a thread pool timer whose callback takes longer to complete than its timer period?

  • The Old New Thing

    News flash: Going for a walk on a nice day makes you feel better

    • 10 Comments

    Scientists continue to amaze us with their research breakthroughs, such as a discovery late last year that going for a walk on a nice day makes you feel better. What would we do without science?

  • The Old New Thing

    FindFirstFile is not a SQL query

    • 15 Comments

    The FindFirstFile function is not a SQL query. It's a very simple directory enumerator. There is a slightly fancier version called FindFirstFileEx, but even that function doesn't add much at present beyond filtering for directories or devices.

    You don't get to pass it sort criteria like or "return the files/directories sorted smallest file first", or "return the files/directories in reverse order of creation" The only filtering you can provide are the wildcard pattern and (if you use FindFirstFileEx) a directory filter. The wildcard language is very limited as well; it can't express queries like "files whose extension are either .exe or .dll" or "all files whose extension is .c plus all directories regardless of extension". You also can't ask it questions like, "Tell me the total size of the files in this directory", for as we saw earlier, this question is underspecified.

    If you want to do any of those advanced queries, you'll have to code it up yourself. Or as Hippie Tim is fond of saying, "Start typing!"

Page 1 of 4 (37 items) 1234